Senior: питання на співбесіді з теми «Основи й щоденна робота»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
4 питання
Зазвичай HEAD вказує на гілку, а гілка - на коміт:
cat .git/HEAD
# ref: refs/heads/main
Новий коміт пересуває гілку main, а HEAD іде слідом.
Detached HEAD - HEAD вказує напряму на коміт, оминаючи гілку:
git switch --detach v2.4.0 # чи git checkout v2.4.0, git checkout a1b2c3d
cat .git/HEAD
# 9fceb02d0ae598e95dc970b74767f19372d61af8
Коли це відбувається:
- перехід на тег чи конкретний коміт, щоб подивитися старий стан;
git bisectперемикає коміти саме так;- під час
rebaseз зупинками й розв'язання конфліктів; - CI зазвичай клонує конкретний коміт у detached HEAD;
- підмодулі - завжди на конкретному коміті.
Небезпека: у цьому стані можна комітити, але на нові коміти не вказує жодна гілка. Після перемикання на main вони стають недосяжними:
Warning: you are leaving 2 commits behind, not connected to
any of your branches:
3e1f2a4 Try new cache driver
Через якийсь час (за замовчуванням недосяжні записи reflog живуть 30 днів) git gc їх видалить.
Як зберегти роботу:
# ще в detached HEAD - створити гілку на поточному коміті
git switch -c experiment/cache-driver
# уже перемкнулися - знайти коміт і створити гілку
git reflog
git branch experiment/cache-driver 3e1f2a4
Як помітити стан: git status пише HEAD detached at v2.4.0, а більшість промптів оболонки показують хеш замість назви гілки.
Практичні поради:
- для експериментів зі старою версією одразу створюйте гілку:
git switch -c try-fix v2.4.0; git switchбез--detachне перейде на тег - захист від випадкового detached HEAD;- у скриптах CI, які щось комітять (наприклад, оновлення changelog), явно створюйте чи перемикайте гілку перед комітом, інакше
git pushне матиме що відправити.
~ і ^ рухаються по батьках коміту, але по-різному:
~n- n кроків назад по першому батькові:HEAD~1- батько,HEAD~3- прапрадід;^n- n-й батько коміту. Має сенс для merge-комітів, у яких батьків два:HEAD^1- гілка, у яку зливали,HEAD^2- гілка, яку зливали.
D---E (feature)
/ \
A---B---C---M (main, HEAD)
HEAD~1 = HEAD^ = HEAD^1 = C
HEAD^2 = E
HEAD~2 = B
HEAD^2~1 = D
Для звичайного коміту з одним батьком HEAD~1 і HEAD^ однакові - різниця видна лише на merge-комітах.
Інші способи назвати коміт:
main@{yesterday} # де була main вчора (за reflog)
@{-1} # попередня гілка
@{u} # upstream поточної гілки
HEAD^{tree} # дерево коміту
:/fix invoice # останній коміт з таким текстом у повідомленні
v2.4.0^{commit} # коміт, на який вказує тег
Діапазони:
| Запис | Що означає |
|---|---|
A..B |
коміти, досяжні з B, але не з A |
A...B |
коміти, досяжні з A або B, але не з обох (симетрична різниця) |
^A B |
те саме, що A..B |
git log main..feature # що є у feature і ще не злито в main
git log feature..main # що з'явилося в main, поки ви працювали
git log --left-right --oneline main...feature # обидві сторони з позначками < і >
git log origin/main..HEAD # що ви ще не запушили
Пастка: .. і ... у git diff означають інше:
git diff A..B- те саме, щоgit diff A B: порівняння двох знімків;git diff A...B- зміни вBвід спільного предка зA. Саме так GitHub показує diff pull request-у: лише зміни гілки, без того, що за цей час з'явилося вmain.
Тому git diff main...feature - правильний спосіб побачити «що змінює моя гілка», а git diff main feature покаже ще й чужі зміни в main з протилежним знаком.
Запис stash - це звичайні коміти, на які вказує ref refs/stash, а список stash@{n} - це reflog цього ref-а.
Структура одного запису:
.----W ← робочий каталог (сам запис stash@{0})
/ /|
H----I | H - HEAD на момент stash, I - стан індексу
U U - невідстежувані файли (лише з -u чи -a)
- W - merge-коміт з батьками
H,Iі, якщо був-u, -U; - окремий коміт для індексу дозволяє
git stash apply --indexвідновити, що саме було в індексі.
git cat-file -p stash@{0}
# tree ...
# parent 84ba035... ← H
# parent 48c2a34... ← I
# parent 84d625d... ← U (untracked files on main)
git show stash@{0}^3 # невідстежувані файли зі stash
Що з цього випливає:
git stash dropі успішнийpopвидаляють лише запис у reflogrefs/stash. Самі коміти лишаються в базі об'єктів, доки їх не прибереgit gc;- до stash можна звертатися як до будь-якого коміту:
git diff stash@{0}^1 stash@{0},git restore --source=stash@{0} -- file.php.
Відновлення видаленого stash:
Якщо щойно виконали pop чи drop, Git друкує хеш:
Dropped refs/stash@{0} (5c3e8a7f...)
git stash apply 5c3e8a7f
Якщо хеш загублено - шукати недосяжні коміти-сироти:
git fsck --no-reflog --unreachable | awk '/commit/ {print $3}' \
| xargs git log --no-walk --merges --format='%h %ci %s' | grep 'WIP on\|On '
Коміти stash - merge-коміти з повідомленнями «WIP on main» чи «On main: опис». Знайдений хеш - git stash apply <хеш> чи git branch rescue <хеш>.
Обмеження: після git gc (Git запускає його й автоматично) недосяжні об'єкти старші за gc.pruneExpire (за замовчуванням 2 тижні) видаляються остаточно.
git stash branch name stash@{1} - корисний для старих записів: створює гілку від коміту H, застосовує stash і видаляє запис, тож конфліктів з новим кодом не буде.
Псевдоніми (aliases) скорочують часті команди:
# ~/.gitconfig
[alias]
st = status -sb
co = switch
lg = log --graph --oneline --decorate --all
last = log -1 HEAD --stat
unstage = restore --staged
amend = commit --amend --no-edit
fixup = "!f() { git commit --fixup=$1 && GIT_SEQUENCE_EDITOR=: git rebase -i --autosquash $1~1; }; f"
gone = "!git fetch -p && git branch -vv | awk '/: gone]/ {print $1}' | xargs -r git branch -D"
- звичайний псевдонім підставляє аргументи команди
git; !на початку - виконати команду оболонки, з функцією для аргументів. Такі команди виконуються з кореня репозиторію;git goneвище видаляє локальні гілки, чиї віддалені версії вже видалено.
Власні команди git-*: будь-який виконуваний файл git-назва у PATH стає підкомандою git назва. Для складної логіки це краще за довгі рядки в конфігурації: скрипт можна версіонувати, тестувати й поширювати в команді.
# ~/bin/git-pr-checkout
#!/bin/sh
git fetch origin "pull/$1/head:pr-$1" && git switch "pr-$1"
Налаштування, що заощаджують час:
[pull]
rebase = true # pull без merge-комітів
[push]
autoSetupRemote = true # перший push сам створює upstream
[rebase]
autoStash = true # stash/unstash навколо rebase
autoSquash = true # fixup! коміти автоматично стають на місце
updateRefs = true # оновлювати залежні гілки в стеку
[rerere]
enabled = true # пам'ятати розв'язання конфліктів
[fetch]
prune = true # прибирати видалені віддалені гілки
[diff]
algorithm = histogram # зрозуміліші diff-и
colorMoved = default # підсвічувати переміщені рядки
[merge]
conflictStyle = zdiff3 # показувати базову версію в конфліктах
[branch]
sort = -committerdate # свіжі гілки першими
[help]
autocorrect = prompt # пропонувати виправлення опечаток
Що враховувати:
- псевдоніми й налаштування персональні: в інструкціях для команди і в скриптах CI використовуйте повні стандартні команди;
zdiff3,updateRefs,autoSetupRemoteз'явилися у відносно нових версіях Git - перевірте версію на всіх машинах команди;- dotfiles-репозиторій з
~/.gitconfigдає однакове середовище на всіх ваших машинах.