Перейти до вмісту

Git

Як Git зберігає історію та синхронізує її із сервером, чим merge відрізняється від rebase, яку модель розгалуження обрати команді та як рятувати «втрачені» коміти. Git питають на будь-якій співбесіді: з відповідей видно, чи працював кандидат у команді з pull request, чи лише натискав кнопки в IDE.

Junior+

Як влаштований Git і які команди потрібні щодня?

Section titled “Як влаштований Git і які команди потрібні щодня?”
Відповідь

Що це

Коміт — знімок усіх файлів з автором, повідомленням і посиланням на попередній коміт. Зміна проходить три області:

  1. Робоча копія — файли на диску.
  2. Індекс — те, що увійде в наступний коміт.
  3. Репозиторій — тека .git з історією.

Як користуватися

Команда Що робить
git clone <url> копіює репозиторій
git status що змінено і що в індексі
git add <файл> кладе правки в індекс
git commit -m "..." створює коміт з індексу
git switch -c <гілка> створює гілку й переходить у неї
git fetch завантажує чужі коміти
git pull fetch і злиття в поточну гілку
git push надсилає коміти на сервер

Типова помилка

Вважати, як у SVN, що коміт уже в колег. До git push він тільки у вас.

Як влаштовано

Ознака SVN Git
Історія на центральному сервері повна копія в кожному клоні
Коміт одразу на сервері локально, на сервер — через push
Гілка копія каталогу вказівник на коміт
Без мережі майже нічого усе, крім обміну із сервером

fetch проти pull

  • git fetch оновлює тільки гілки віддаленого відстеження origin/*. Ваші гілки й файли не змінюються.
  • git pull робить fetch, а потім вливає origin/<гілка> у поточну гілку злиттям або rebase.

Пастки

  • Несподіваний merge-коміт. Якщо ви й колега закомітили в одну гілку, git pull зі злиттям створить коміт злиття. Історія обростає «Merge branch ‘main’ of …».
  • Зайве в коміті. git add . додасть усе підряд, зокрема bin/, obj/ і локальні налаштування. Їх виключають у .gitignore.
  • Відмова pull. Без налаштування стратегії Git зупиняється з «Need to specify how to reconcile divergent branches». Інсталятор Git for Windows зазвичай прописує pull.rebase false, тому у Windows цієї помилки можна не побачити.

Правило

Спершу git fetch і git log main..origin/main, потім свідомо merge або rebase. Стратегію pull налаштуйте явно.

Під капотом

  • git clone копіює всі об’єкти й створює гілки віддаленого відстеження origin/*. Локальна main отримує upstream — гілку на сервері, з якою порівнюється.
  • git status -sb показує розбіжність з upstream: [ahead 2, behind 1]. Це порівняння з останнім fetch, а не із сервером просто зараз.
  • Віддалених репозиторіїв може бути кілька: origin для свого форку й upstream для вихідного проєкту.

Ціна

  • Клон містить усю історію. Для CI вистачає git clone --depth 1 (shallow clone), для великих репозиторіїв — --filter=blob:none (partial clone): вміст старих файлів завантажується на вимогу.
  • Бінарні файли не зливаються, а блокувань, як svn lock, у самому Git немає: конфлікт у них розв’язують лише вибором однієї версії.

Компроміси

  • pull.rebase true тримає локальну історію лінійною, але конфлікти розв’язуються покомітно.
  • pull.rebase false простіше, але засмічує історію комітами злиття.
  • pull.ff only найбезпечніше: у разі розбіжності Git зупиняється й чекає рішення людини.

Як перевірити

  • git remote -v — куди дивляться віддалені репозиторії.
  • git log --oneline --graph --all — граф локальних і віддалених гілок.
  • git config --show-origin --get-regexp pull — звідки взялася стратегія pull.

Підсумок: у Git дві історії — ваша й серверна, і між ними немає нічого автоматичного, крім того, що ви явно налаштували.

Приклад
Окно терминала
git clone https://github.com/acme/shop.git # копія репозиторію з усією історією
cd shop
git switch -c feature/discount # нова гілка від поточного коміту
git status # що змінено і що вже в індексі
git add src/Discount.cs # покласти файл в індекс
git commit -m "Add discount calculation" # локальний коміт: сервер про нього не знає
git fetch origin # оновити origin/*, свої гілки не чіпати
git log --oneline main..origin/main # які коміти прийшли на сервер у main
git push -u origin feature/discount # опублікувати гілку й запам'ятати upstream
Middle+

Чим merge відрізняється від rebase і коли що використовувати?

Section titled “Чим merge відрізняється від rebase і коли що використовувати?”
Відповідь

Що це

Два способи перенести зміни однієї гілки в іншу.

  • Merge з’єднує гілки. Якщо обидві пішли вперед, з’являється коміт злиття з двома батьками.
  • Rebase застосовує ваші коміти заново поверх іншої гілки. Виходять нові коміти з новими хешами й пряма історія.

Як користуватися

  1. Оновити гілку: git fetch, потім git rebase origin/main або git merge origin/main.
  2. У разі конфлікту Git зупиняється й позначає спірні місця: <<<<<<<, =======, >>>>>>>.
  3. Виправте файл, git add <файл>, потім git rebase --continue (після merge — git commit).
  4. Скасувати — git rebase --abort або git merge --abort.

Типова помилка

Rebase гілки, яку вже забрали колеги, і git push --force: їхня історія розійдеться із сервером.

Як влаштовано

  1. Fast-forward. Якщо в цільовій гілці немає нових комітів, merge просто зсуває її вказівник уперед. Нового коміту немає.
  2. Тристороннє злиття. Інакше Git порівнює спільного предка й дві вершини та створює коміт злиття. --no-ff створює його навіть там, де можливий fast-forward.
  3. Rebase. Git знаходить спільного предка, по одному застосовує ваші коміти поверх нової бази й переносить на результат вказівник гілки.

Коли що обирати

Ситуація Що використовувати
Оновити свою гілку від main rebase
Почистити свої коміти перед PR rebase -i
Гілка спільна для кількох людей merge
Влити PR у main метод, заданий у репозиторії

Пастки

  • Rebase опублікованого. Золоте правило з Pro Git: не переписувати коміти, які існують поза вашим репозиторієм і на яких інші могли заснувати роботу.
  • Force push. --force затирає чужі коміти. --force-with-lease відмовить, якщо гілка на сервері змінилася з вашого останнього fetch. Після fetch без інтеграції захист зникає — для цього є --force-if-includes (Git 2.30+).
  • Squash merge. Після нього коміти гілки не потрапляють у main. Продовжувати ту саму гілку не можна — її видаляють і починають нову.

Правило

Свою гілку — rebase і --force-with-lease, спільну — лише merge.

Під капотом

  • Rebase — серія cherry-pick: кожен коміт застосовується заново як окремий патч. Тому конфлікт може виникнути на кожному коміті, а не один раз.
  • Старі коміти не видаляються одразу: на них вказують ORIG_HEAD і reflog, доки їх не прибере збирання сміття.
  • З Git 2.34 стратегія злиття за замовчуванням — ort, помітно швидша заміна recursive.

Компроміси

Метод злиття PR Плюс Мінус
Merge commit повна історія, злиття відкочується одним revert -m 1 граф з паралельними гілками
Squash один коміт на PR, читабельний main губляться проміжні коміти та їхні автори
Rebase merge лінійна історія з окремими комітами нові хеші, підписи комітів губляться

Чищення своїх комітів

  • git commit --fixup <hash> і git rebase -i --autosquash main вклеюють виправлення в потрібний коміт.
  • git config rerere.enabled true запам’ятовує розв’язані конфлікти й застосовує їх повторно під час наступного rebase.

Як перевірити в продакшені

  • git log --first-parent main показує історію main по одному коміту на PR, якщо зливали через merge commit.
  • git range-diff порівнює гілку до й після rebase і показує, що змінилося під час розв’язання конфліктів.

Підсумок: merge зберігає правду про те, як ішла робота, rebase — зручну розповідь про неї; переписують лише те, чого ще ніхто не забрав.

Приклад
Окно терминала
git switch feature/discount
git fetch origin
git rebase origin/main # переносимо свої коміти поверх свіжого main
# CONFLICT (content): Merge conflict in price.txt
git status --short # UU price.txt — файл з конфліктом
# правимо price.txt і прибираємо маркери <<<<<<< ======= >>>>>>>
git add price.txt
git rebase --continue # застосовуємо решту комітів
git push --force-with-lease # відмовить, якщо гілку на сервері хтось змінив
Історія: до, після merge і після rebase
# До: гілки розійшлися
$ git log --oneline --graph --all
* d331889 Add tests
* 19aa729 Apply discount
| * 2767ee0 Update notes
|/
* 2ccb685 Add price
# Варіант 1: git merge feature/discount у main
$ git log --oneline --graph
* 6371798 Merge branch 'feature/discount'
|\
| * d331889 Add tests
| * 19aa729 Apply discount
* | 2767ee0 Update notes
|/
* 2ccb685 Add price
# Варіант 2: git rebase main у гілці, потім git merge feature/discount у main
$ git log --oneline --graph
* 37980f0 Add tests
* e677de9 Apply discount
* 2767ee0 Update notes
* 2ccb685 Add price

Після rebase у комітів гілки нові хеші (19aa729 → e677de9), а злиття в main пройшло fast-forward.

Middle+

Які бувають моделі розгалуження і як обрати?

Section titled “Які бувають моделі розгалуження і як обрати?”
Відповідь

Що це

Модель розгалуження відповідає на запитання: від якої гілки створювати свою, куди її вливати і що викладається в прод.

Як користуватися: GitHub Flow

  1. Створіть гілку від свіжого main: git switch -c feature/discount origin/main.
  2. Робіть невеликі коміти й надсилайте гілку на сервер.
  3. Відкрийте pull request. CI перевіряє збірку й тести, колеги роблять рев’ю.
  4. Після схвалення PR вливають у main, а гілку видаляють.

Типова помилка

Тримати гілку тижнями. PR виростає до сотень файлів, рев’ю перетворюється на формальність, а під час злиття накопичуються конфлікти.

Як влаштовано

Модель Гілки Кому підходить
Git Flow main, develop, feature/*, release/*, hotfix/* продукт з версіями, рідкісні релізи
GitHub Flow main і короткі гілки задач вебсервіс з частими викладаннями
Trunk-based trunk (main), гілки на день-два, релізні за потреби часта інтеграція і CI/CD

Коли що обирати

  • Викладаєте кілька разів на тиждень і підтримуєте одну версію — GitHub Flow або trunk-based.
  • Постачаєте коробкові версії й лагодите старі — релізні гілки: trunk-based з ними або Git Flow.
  • Незавершену функцію не тримають у гілці: її вливають вимкненою за feature flag.

Пастки

  • Hotfix у Git Flow. Виправлення вливають і в main, і в develop. Забули друге — помилка повернеться в наступному релізі.
  • Прапорці назавжди. Кожен прапорець — дві гілки коду й два набори тестів. Після ввімкнення для всіх прапорець видаляють.
  • Захист без перевірок. Правило «лише через PR» без обов’язкових перевірок і рев’ю нічого не захищає. Які перевірки робити обов’язковими — у темі CI/CD і якість коду.

Правило

Що частіше ви викладаєте, то коротше мають жити гілки.

Ціна довгих гілок

  • Конфлікти ростуть разом з розбіжністю гілок, а інтеграція відкладається до злиття. Помилки інтеграції знаходять наприкінці, коли їх найдорожче лагодити.
  • Великий PR рев’юють поверхово: обмежена увага рецензента, а не кількість рядків.

Релізні гілки

  1. Гілку release/2.4 відрізають від main, коли версія готова, і позначають тегом.
  2. Нові функції в неї не потрапляють — лише виправлення.
  3. Виправлення роблять у main і переносять через cherry-pick -x: у повідомленні лишається посилання на вихідний коміт.

Зворотний порядок — спершу в реліз — ризикує загубити виправлення в main, і помилка повернеться в наступній версії.

Компроміси

  • Feature flags відокремлюють викладання від релізу, але додають розгалуження в коді й комбінації для тестів.
  • Великий рефакторинг без довгої гілки роблять через branch by abstraction: нова реалізація росте поруч зі старою за спільною абстракцією.
  • Коли в main вливають десятки PR на день, перевірки на застарілій базі пропускають поломки. Merge queue в GitHub перевіряє кожен PR поверх тих, що вже стоять у черзі.

Як перевірити в продакшені

Метрики моделі: час життя гілки, розмір PR, час від коміту до продакшену (lead time for changes з DORA). Ростуть — модель не працює, як задумано.

Підсумок: модель розгалуження — інструмент для ритму постачання; довгі гілки відкладають інтеграцію, прапорці й короткі PR наближають її.

Приклад
Окно терминала
# Trunk-based: коротка гілка від свіжого main, у main — лише через PR
git switch -c fix/rounding origin/main
git commit -am "Fix rounding"
git push -u origin fix/rounding
# Релізна гілка відрізається від main і позначається тегом
git switch -c release/2.4 main
git tag -a v2.4.0 -m "Release 2.4.0"
# Виправлення спершу потрапило в main (коміт f2096a1), переносимо його в реліз
git switch release/2.4
git cherry-pick -x f2096a1 # -x додасть "(cherry picked from commit ...)"
git tag -a v2.4.1 -m "Hotfix 2.4.1"
git push origin release/2.4 v2.4.1
Граф після перенесення виправлення
$ git log --oneline --graph --all --decorate
* f2096a1 (main) Fix rounding
* b182e6f Feature B (behind flag)
| * bf02e76 (HEAD -> release/2.4, tag: v2.4.1) Fix rounding
|/
* 7f43956 (tag: v2.4.0) Feature A
Senior+

Як Git зберігає дані і як рятувати ситуації?

Section titled “Як Git зберігає дані і як рятувати ситуації?”
Відповідь

Що це

Коміт не зникає, коли ви відкотили гілку або видалили її. Git пам’ятає кожне переміщення HEAD і гілок у reflog.

Як користуватися

Ситуація Команда
Скасувати вже надісланий коміт git revert <hash>
Прибрати останній локальний коміт, зберігши правки git reset --soft HEAD~1
Повернути коміти після reset --hard git reflog, потім git reset --hard HEAD@{1}
Відкласти незавершену роботу git stash, потім git stash pop
Перенести один коміт в іншу гілку git cherry-pick <hash>

Типова помилка

git reset --hard за незакомічених правок. Reflog зберігає лише коміти, тому незбережені зміни зникнуть назавжди.

Як влаштовано

  • Гілка — файл у .git/refs/heads/ з хешем коміту. HEAD зазвичай посилається на гілку: ref: refs/heads/main.
  • Якщо HEAD вказує просто на коміт, це detached HEAD. Нові коміти в цьому стані загубляться, якщо не створити гілку.

reset проти revert

Команда Що змінює
reset --soft <коміт> лише гілку; зміни лишаються в індексі
reset --mixed <коміт> гілку й індекс; правки лишаються у файлах
reset --hard <коміт> гілку, індекс і робочу копію
revert <коміт> нічого не переписує, додає зворотний коміт

Інші інструменти

  • Випадково видалена гілка. git branch -D друкує хеш: (was f45de9d). Його ж видно в git reflog. Відновлення: git branch <ім'я> <hash>.
  • Stash. git stash -u відкладає й невідстежувані файли.
  • Bisect. git bisect start <поганий> <добрий>, потім good або bad на кожному кроці; git bisect run <команда> перевіряє автоматично.

Пастки

  • revert merge-коміту потребує -m 1. Якщо ту саму гілку потім влити знову, відкочені зміни не повернуться — спершу відкочують сам revert.
  • Скинутий stash у reflog не потрапляє. Його шукають через git fsck --unreachable.

Правило

Спільна історія — revert, своя локальна — reset.

Під капотом

  • Об’єкт адресується хешем свого вмісту (SHA-1; підтримується й SHA-256). Змінити коміт не можна — лише створити новий з іншим хешем.
  • Нові об’єкти лежать окремими стиснутими файлами, git gc пакує їх у pack-файли з дельта-стисненням.
  • Reflog локальний і спливає: за замовчуванням записи живуть 90 днів, недосяжні — 30. Після цього git gc видаляє коміти без посилань.

Секрет потрапив в історію

  1. Відкликати й змінити секрет. Це головний крок: копії історії вже є в усіх, хто зробив fetch.
  2. Переписати історію: git filter-repo --sensitive-data-removal з --invert-paths --path <файл> або --replace-text.
  3. Надіслати результат: git push --force --mirror origin.
  4. На GitHub посилання PR так не перезаписати — їх і кеші видаляє підтримка.
  5. Колеги переклоновують репозиторій. Повторенню запобігають .gitignore, сховище секретів і push protection.

Великі файли

Git LFS зберігає в репозиторії текстовий вказівник, а вміст — на окремому сервері. git lfs track пише правило в .gitattributes, git lfs migrate import переносить уже закомічені файли, переписуючи історію. GitHub блокує файли, більші за 100 MiB.

Компроміси

Переписування історії змінює всі хеші після першого зміненого коміту: відкриті PR і чужі клони ламаються, тому його роблять один раз і за домовленістю.

Підсумок: коміти незмінні й довго відновлювані локально, а секрет, що витік, рятує не чищення історії, а його заміна.

Приклад
Окно терминала
git reset --hard HEAD~3 # «загубили» три коміти
git reflog -2
# 7bf4219 HEAD@{0}: reset: moving to HEAD~3
# e87e8fa HEAD@{1}: commit: Step 8
git reset --hard HEAD@{1} # гілка знову на e87e8fa
git branch -D feature/x # Deleted branch feature/x (was f45de9d).
git branch feature/x f45de9d # гілку відновлено
# Шукаємо коміт, що зламав тест: v2.3.0 — добра версія, HEAD — погана
git bisect start HEAD v2.3.0
git bisect run dotnet test --filter "FullyQualifiedName~DiscountTests"
# f45de9dde16e396ecacc40236ccfa2d41ffdb7e6 is the first bad commit
git bisect reset # повернутися туди, де були до пошуку
Секрет в історії
Окно терминала
# 0. Секрет уже відкликано й замінено — без цього чищення марне
git clone --mirror https://github.com/acme/shop.git
cd shop.git
# У passwords.txt рядки на кшталт: P@ssw0rd==>***REMOVED***
git filter-repo --sensitive-data-removal --replace-text ../passwords.txt
git log -p --all | grep -c "P@ssw0rd" # 0 — в історії секрету більше немає
git push --force --mirror origin