Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

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 не матиме що відправити.

Докладніше в документації: git checkout: detached HEAD

~ і ^ рухаються по батьках коміту, але по-різному:

  • ~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 з протилежним знаком.

Докладніше в документації: gitrevisions

Запис 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 видаляють лише запис у reflog refs/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 і видаляє запис, тож конфліктів з новим кодом не буде.

Докладніше в документації: git 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 дає однакове середовище на всіх ваших машинах.

Докладніше в документації: Pro Git: псевдоніми