Git
Як Git зберігає історію та синхронізує її із сервером, чим merge відрізняється від rebase, яку модель розгалуження обрати команді та як рятувати «втрачені» коміти. Git питають на будь-якій співбесіді: з відповідей видно, чи працював кандидат у команді з pull request, чи лише натискав кнопки в IDE.
Як влаштований Git і які команди потрібні щодня?
Section titled “Як влаштований Git і які команди потрібні щодня?”Що це
Коміт — знімок усіх файлів з автором, повідомленням і посиланням на попередній коміт. Зміна проходить три області:
- Робоча копія — файли на диску.
- Індекс — те, що увійде в наступний коміт.
- Репозиторій — тека
.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 shopgit 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 # які коміти прийшли на сервер у maingit push -u origin feature/discount # опублікувати гілку й запам'ятати upstream- Чим
git fetchвідрізняється відgit pull? —fetchлише завантажує й оновлюєorigin/*,pullще й вливає в поточну гілку. - Що таке індекс і навіщо він? — Проміжна область: дає зібрати в коміт лише частину змін, наприклад через
git add -p. - Що означає
[ahead 1, behind 2]уgit status -sb? — У вас один коміт, якого немає на сервері, і два серверні, яких немає у вас. - Як скасувати
git add? —git restore --staged <файл>: файл виходить з індексу, правки в робочій копії лишаються. - Чим
git switchвідрізняється відgit checkout? —switchіrestoreрозділили дві ролі перевантаженогоcheckout: перемикання гілок і відновлення файлів.
Чим merge відрізняється від rebase і коли що використовувати?
Section titled “Чим merge відрізняється від rebase і коли що використовувати?”Що це
Два способи перенести зміни однієї гілки в іншу.
- Merge з’єднує гілки. Якщо обидві пішли вперед, з’являється коміт злиття з двома батьками.
- Rebase застосовує ваші коміти заново поверх іншої гілки. Виходять нові коміти з новими хешами й пряма історія.
Як користуватися
- Оновити гілку:
git fetch, потімgit rebase origin/mainабоgit merge origin/main. - У разі конфлікту Git зупиняється й позначає спірні місця:
<<<<<<<,=======,>>>>>>>. - Виправте файл,
git add <файл>, потімgit rebase --continue(після merge —git commit). - Скасувати —
git rebase --abortабоgit merge --abort.
Типова помилка
Rebase гілки, яку вже забрали колеги, і git push --force: їхня історія розійдеться із сервером.
Як влаштовано
- Fast-forward. Якщо в цільовій гілці немає нових комітів, merge просто зсуває її вказівник уперед. Нового коміту немає.
- Тристороннє злиття. Інакше Git порівнює спільного предка й дві вершини та створює коміт злиття.
--no-ffстворює його навіть там, де можливий fast-forward. - 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/discountgit fetch origingit rebase origin/main # переносимо свої коміти поверх свіжого main# CONFLICT (content): Merge conflict in price.txtgit status --short # UU price.txt — файл з конфліктом# правимо price.txt і прибираємо маркери <<<<<<< ======= >>>>>>>git add price.txtgit rebase --continue # застосовуємо решту комітівgit push --force-with-lease # відмовить, якщо гілку на сервері хтось змінив# До: гілки розійшлися$ 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.
- Що таке fast-forward і як його заборонити? — Зсув вказівника без нового коміту;
git merge --no-ffзавжди створює коміт злиття. - Чим
--force-with-leaseкращий за--force? — Перезаписує гілку, лише якщо на сервері вона така сама, якою ви її бачили під час останньогоfetch. - Як відкотити влитий merge-коміт? —
git revert -m 1 <hash>:-m 1каже, якого батька вважати основною лінією. - Чому після squash merge гілку треба видалити? — Її коміти не потрапили в
main, і наступний PR з неї знову покаже старі зміни або конфлікти. - Як розділити один великий коміт на кілька? —
git rebase -i, позначити комітedit, потімgit reset HEAD~1і закомітити частинами.
Які бувають моделі розгалуження і як обрати?
Section titled “Які бувають моделі розгалуження і як обрати?”Що це
Модель розгалуження відповідає на запитання: від якої гілки створювати свою, куди її вливати і що викладається в прод.
Як користуватися: GitHub Flow
- Створіть гілку від свіжого
main:git switch -c feature/discount origin/main. - Робіть невеликі коміти й надсилайте гілку на сервер.
- Відкрийте pull request. CI перевіряє збірку й тести, колеги роблять рев’ю.
- Після схвалення 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 рев’юють поверхово: обмежена увага рецензента, а не кількість рядків.
Релізні гілки
- Гілку
release/2.4відрізають відmain, коли версія готова, і позначають тегом. - Нові функції в неї не потрапляють — лише виправлення.
- Виправлення роблять у
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 — лише через PRgit switch -c fix/rounding origin/maingit commit -am "Fix rounding"git push -u origin fix/rounding
# Релізна гілка відрізається від main і позначається тегомgit switch -c release/2.4 maingit tag -a v2.4.0 -m "Release 2.4.0"
# Виправлення спершу потрапило в main (коміт f2096a1), переносимо його в релізgit switch release/2.4git 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- Чому автор Git Flow сам не радить його для вебсервісів? — Модель створювали для версійованого ПЗ; за continuous delivery він рекомендує простіший GitHub Flow.
- Як випустити незавершену функцію в trunk-based? — Влити її вимкненою за feature flag і ввімкнути, коли готова.
- Куди спершу вносити виправлення, якщо є релізна гілка? — У
main, потім cherry-pick у реліз; інакше виправлення легко загубити. - Що зазвичай входить у захист
main? — PR зі схваленням, обов’язкові перевірки CI, заборона force push і видалення гілки. - Навіщо merge queue? — Перевіряє PR поверх уже прийнятих, щоб два окремо зелені PR разом не зламали
main.
Як 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 <команда>перевіряє автоматично.
Пастки
revertmerge-коміту потребує-m 1. Якщо ту саму гілку потім влити знову, відкочені зміни не повернуться — спершу відкочують сам revert.- Скинутий stash у reflog не потрапляє. Його шукають через
git fsck --unreachable.
Правило
Спільна історія — revert, своя локальна — reset.
Під капотом
- Об’єкт адресується хешем свого вмісту (SHA-1; підтримується й SHA-256). Змінити коміт не можна — лише створити новий з іншим хешем.
- Нові об’єкти лежать окремими стиснутими файлами,
git gcпакує їх у pack-файли з дельта-стисненням. - Reflog локальний і спливає: за замовчуванням записи живуть 90 днів, недосяжні — 30. Після цього
git gcвидаляє коміти без посилань.
Секрет потрапив в історію
- Відкликати й змінити секрет. Це головний крок: копії історії вже є в усіх, хто зробив
fetch. - Переписати історію:
git filter-repo --sensitive-data-removalз--invert-paths --path <файл>або--replace-text. - Надіслати результат:
git push --force --mirror origin. - На GitHub посилання PR так не перезаписати — їх і кеші видаляє підтримка.
- Колеги переклоновують репозиторій. Повторенню запобігають
.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 8git 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.0git bisect run dotnet test --filter "FullyQualifiedName~DiscountTests"# f45de9dde16e396ecacc40236ccfa2d41ffdb7e6 is the first bad commitgit bisect reset # повернутися туди, де були до пошуку# 0. Секрет уже відкликано й замінено — без цього чищення марнеgit clone --mirror https://github.com/acme/shop.gitcd shop.git# У passwords.txt рядки на кшталт: P@ssw0rd==>***REMOVED***git filter-repo --sensitive-data-removal --replace-text ../passwords.txtgit log -p --all | grep -c "P@ssw0rd" # 0 — в історії секрету більше немаєgit push --force --mirror origin- Чим відрізняються
reset --soft,--mixedі--hard? — Що скидається слідом за гілкою: нічого, індекс або індекс з робочою копією. - Як повернути гілку, видалену через
git branch -D? — Знайти хеш у виводі команди або вgit reflogі виконатиgit branch <ім'я> <hash>. - Скільки живуть записи reflog? — За замовчуванням 90 днів, для недосяжних комітів — 30; reflog є лише в локальному репозиторії.
- Чи достатньо видалити файл з паролем новим комітом? — Ні, він лишається в історії; секрет змінюють, історію чистять
git filter-repo. - Що робить
git bisect run, якщо команда повернула 125? — Пропускає коміт: його не можна перевірити, наприклад він не збирається.