Гілки та злиття
Зміст
Гілка (branch) звучить страшно — наче «окрема копія проєкту» чи «паралельний всесвіт коду». Насправді все прозаїчніше: гілка — це рухомий вказівник на один коміт, файл розміром у кілька десятків байтів. Зрозумієш це одне речення — і решта теми (HEAD, злиття, detached-стан) складеться сама, бо все крутиться навколо того, куди який вказівник дивиться.
Для AQA гілки — щоденний інструмент, а не абстракція. Свій код автотестів ти пишеш у гілці, віддаєш його в pull request, а в CI пайплайн запускається саме на конкретній гілці й конкретному коміті. Питання «чому тест зелений локально, а в пайплайні червоний» дуже часто починається з «а на якій гілці й на якому коміті він насправді запустився». І на співбесіді в junior-AQA різницю між fast-forward і звичайним злиттям питають часто: це швидкий маркер, що людина розуміє модель Git, а не завчила три команди.
Гілка — це вказівник на коміт
У попередніх главах (модель Git та базовий цикл) ми бачили, що кожен коміт — це знімок стану проєкту, який знає хеш свого батька. Ланцюжок цих посилань «дитина → батько» утворює всю історію: щоб її пройти, Git просто йде від коміту до коміту назад.
Гілка на цьому тлі — річ навмисно примітивна. Це іменований вказівник на один коміт (той, що вважається «кінцем» гілки, її tip). Фізично він лежить у файлі .git/refs/heads/<назва> (або спакований у packed-refs) і містить лише хеш одного коміту. Створити гілку — це записати ці кілька десятків байтів, тому в Git гілкування миттєве й дешеве, на відміну від старих систем, що копіювали файли.
Найважливіший наслідок: коли ти робиш коміт, вказівник поточної гілки автоматично переїжджає вперед на новий коміт. Історію тримають самі коміти через посилання на батьків; гілка лише позначає, де ця лінія зараз закінчується.
Суцільні стрілки тут — «коміт вказує на свого батька», пунктирні — «вказівник вказує на коміт». main і feature розійшлися на коміті C2: main пішла в C3, feature — в C4. Жодного дублювання файлів — лише два вказівники на два коміти спільного дерева.
HEAD і detached HEAD
Є ще один особливий вказівник — HEAD. Він відповідає на питання «де я зараз». Здебільшого HEAD вказує не прямо на коміт, а на гілку: у файлі .git/HEAD лежить рядок ref: refs/heads/main. Тобто ланцюжок такий: HEAD показує на гілку, гілка показує на коміт. Саме тому, коли ти комітиш, Git знає, який вказівник рухати вперед — той, на який зараз дивиться HEAD.
Це нормальний, «прикріплений» стан: ти стоїш на гілці, і нові коміти лягають на неї.
Detached HEAD («відʼєднаний HEAD») — це коли HEAD вказує не на гілку, а прямо на конкретний коміт. Ти потрапляєш туди, коли перемикаєшся на коміт за його хешем, на тег або на віддалений стан безпосередньо:
git checkout 3a7f1c9 # переходимо прямо на коміт
git checkout v1.4.0 # тег — теж коміт, а не гілка
У detached-стані все працює: можна дивитися код, білдити, ганяти тести й навіть комітити. Пастка в тому, що нові коміти не належать жодній гілці. Щойно ти перемкнешся деінде, на них не залишиться жодного вказівника — вони стають недосяжними й з часом їх прибирає збирач сміття (garbage collector). Врятувати їх можна лише через reflog (див. скасування змін). Якщо роботу в detached-стані треба зберегти — прикріпи до неї гілку:
git switch -c fix/hotfix-from-tag # створюємо гілку прямо тут, коміти врятовано
Для AQA detached HEAD — не екзотика, а буденність CI. Типовий чекаут у пайплайні (наприклад, actions/checkout у GitHub Actions на подію push) лишає репозиторій саме у detached-стані на конкретному SHA — тому в логах CI ти бачиш не назву гілки, а хеш. Це нормально: агент не «сидить на гілці», він відтворює точний коміт, який тригернув запуск.
switch проти checkout
Історично одна команда git checkout робила надто багато: перемикала гілки, створювала їх, відновлювала окремі файли й відʼєднувала HEAD. Це збивало новачків — той самий git checkout міг як перемкнути гілку, так і мовчки затерти твої зміни у файлі.
Тому у Git 2.23 (2019) обовʼязки розвели на дві команди з говорючими назвами:
git switch— тільки перемикання й створення гілок;git restore— тільки відновлення файлів (детальніше — у главі про скасування змін).
git checkout нікуди не зник і досі працює, але для ясності новий код варто читати й писати через switch. Основні операції:
git switch main # перейти на наявну гілку main
git switch -c feature/search # створити гілку feature/search і одразу перейти на неї
git switch - # повернутися на попередню гілку
git switch --detach 3a7f1c9 # свідомо піти в detached-стан на коміт
Ключова перевага switch — він безпечніший: не дасть випадково відʼєднати HEAD без явного --detach і чіткіше повідомляє про неоднозначні ситуації. Стара пара тих самих дій виглядала як git checkout main і git checkout -b feature/search.
Злиття: fast-forward проти three-way merge
Рано чи пізно роботу з гілки треба влити (merge) назад — зазвичай у main. Тут Git обирає один із двох принципово різних шляхів, і саме цю різницю питають на співбесідах.
Fast-forward («перемотка»). Уяви, що поки ти працював у feature, гілка main не рухалася. Тоді коміт main — прямий предок кінця feature, історія лінійна, і зливати нічого: Git просто пересуває вказівник main уперед на кінець feature. Жодного нового коміту не створюється.
git switch main
git merge feature # main не рухалась → fast-forward: вказівник просто переїхав уперед
Three-way merge (трибічне злиття). А тепер уяви, що поки ти працював, колеги влили свої коміти в main. Тепер обидві гілки мають власні нові коміти після точки розходження — історія розійшлася, і простою перемоткою вказівника не обійтися. Git бере три коміти: кінець main, кінець feature і їхнього спільного предка (merge base), зводить зміни докупи й створює новий merge-коміт із двома батьками.
Merge-коміт M має двох батьків (A3 з боку main і B2 з боку feature) — саме за наявністю двох батьків його впізнають у графі. Якщо обидві гілки правили ті самі рядки, Git не вгадає, чию версію взяти, і зупиниться на merge-конфлікті — це окрема велика тема наступної глави.
Різниця між шляхами — не косметична, вона визначає форму історії:
| Аспект | Fast-forward | Three-way merge |
|---|---|---|
| Коли можливий | Цільова гілка не рухалась | Обидві гілки розійшлися |
| Новий коміт | Ні, лише зсув вказівника | Так, merge-коміт із двома батьками |
| Форма історії | Лінійна | З «розгалуженням» і злиттям |
| Видно, що був окремий feature | Ні | Так (топологія збережена) |
| Можливий конфлікт | Ні | Так |
Керувати цим вибором можна прапорцями:
git merge --no-ff feature— примусово створити merge-коміт, навіть коли можлива перемотка. Так роблять, щоб у графі лишався слід, що коміти належали окремій фічі.git merge --ff-only feature— злити лише якщо можлива перемотка, інакше впасти з помилкою. Зручно в скриптах і CI, коли лінійну історію тримають навмисно.
Який стиль обрати на рівні команди (merge-коміт, squash чи rebase) — це вже питання стратегії гілкування й pull request. Альтернатива злиттю, яка переписує історію лінійно замість merge-коміту, — це rebase.
Іменування гілок
Назва гілки — це не формальність, а комунікація: за нею колеги (і пайплайн) розуміють, що всередині. Усталена практика — префікс за типом роботи плюс ідентифікатор задачі й короткий опис:
feature/JIRA-123-add-search-filter
bugfix/PROJ-88-login-500
hotfix/payment-timeout
release/2.4.0
Кілька практичних правил:
- Слеш (
/) створює умовне «групування» — багато інструментів показуютьfeature/*як теку. Через це не можна одночасно мати гілкуfeatureі гілкуfeature/login: ім`я не буває водночас файлом і текою. - Без пробілів, у нижньому регістрі, слова через дефіс. Ідентифікатор задачі в назві звʼязує гілку з таск-трекером і майбутнім pull request.
- Головна гілка проєкту історично звалася
master; нині за замовчуванням її частіше називаютьmain(GitHub перейшов наmainдля нових репозиторіїв у 2020 році). Обидві назви — просто звичайні гілки без магії, «головна» лише за домовленістю.
Для AQA іменування має ще й технічний бік: правила CI часто матчаться саме за назвою гілки. Пайплайн може запускати повний прогін тільки на release/*, піднімати окреме превю-середовище на кожну feature/*` або ганяти лише смоук на решті. Неохайна назва — і твоя гілка не потрапить під потрібне правило, а тести мовчки не запустяться там, де мали б.
Видалення гілок: локально й на remote
Гілка — дешевий вказівник, тож після вливання її прибирають, щоб не засмічувати список. Важливо памʼятати: локальна гілка й однойменна гілка на сервері (remote) — дві незалежні речі, видалення однієї не чіпає іншу.
Локально:
git branch -d feature/search # видалити, ТІЛЬКИ якщо гілку вже влито (безпечно)
git branch -D feature/search # примусово, навіть якщо не влито (можна втратити коміти)
Мала -d — запобіжник: Git не дасть видалити гілку з невлитими комітами. Велика -D цей запобіжник знімає; якщо коміти нікуди більше не вказують, вони стануть недосяжними (рятує знову ж таки reflog). Поточну гілку — ту, на якій ти стоїш, — видалити не можна: спершу перейди на іншу.
На remote:
git push origin --delete feature/search # видалити гілку на сервері
Окрема пастка: після того як гілку видалили на сервері, у тебе локально ще довго висять remote-tracking-посилання на кшталт origin/feature/search — Git не прибирає їх сам. Почистити:
git fetch --prune # прибрати посилання на гілки, яких уже немає на сервері
Щоб це відбувалося автоматично при кожному fetch, вмикають git config --global fetch.prune true. Багато команд ще й налаштовують GitHub/GitLab так, щоб гілка видалялася автоматично після мержу pull request — тоді ручне прибирання майже не потрібне.
Типовий флоу фічі
Складемо все докупи в послідовність, яку AQA проходить щодня, коли додає чи править автотести (чи будь-хто — фічу):
- Оновитися від головної.
git switch mainіgit pull— щоб стартувати з найсвіжішого стану, а не від тижневої давнини. - Створити гілку.
git switch -c feature/JIRA-123-add-search— окремий простір для своєї роботи. - Працювати маленькими комітами. Правки,
git add,git commit— кілька осмислених комітів краще за один величезний. - Опублікувати гілку.
git push -u origin feature/JIRA-123-add-search— прапорець-uпривʼязує локальну гілку до віддаленої, далі досить гологоgit push. Саме цей push зазвичай і тригерить CI. - Відкрити pull request і пройти рев`ю (окрема глава); на зауваження — нові коміти в ту саму гілку.
- Влити обраним командою способом, коли пайплайн зелений і рев`ю пройдено.
- Прибрати за собою.
git switch main,git pull, видалити локальну й віддалену гілку.
Маленька гілка з коротким життям — не естетика, а профілактика: чим довше вона живе окремо від main, тим більше та встигає піти вперед і тим болючіші майбутні конфлікти при вливанні.
Типові помилки
- Виглядає як «гілка — це копія файлів», а насправді — один вказівник на коміт. Тому створення гілки миттєве й нічого не «копіює»; сплутати ці моделі — значить боятися гілкування там, де воно безкоштовне.
- Виглядає як «мої коміти зникли», а насправді — вони в detached HEAD. Накомітив, стоячи на голому SHA, перемкнувся — і не бачиш роботи. Коміти живі, поки їх тримає reflog; витягуються через
git switch -cна потрібний хеш. - Виглядає як «Git видалив мою гілку в CI», а насправді — це штатний detached-стан. У логах пайплайна замість назви гілки — хеш коміту; це нормальний чекаут на конкретний SHA, а не поломка.
- Виглядає як «fast-forward зламав історію», а насправді — він її просто спрямив. Після перемотки в графі не лишається сліду, що був окремий feature. Якщо команді цей слід потрібен — це аргумент за
--no-ff, а не баг. - Виглядає як «видалив гілку, а вона ще в списку», а насправді — висить застаріле
origin/*-посилання. Лікуєтьсяgit fetch --prune, а не повторним видаленням. - Виглядає як «тест червоний через баг», а насправді — гілка сильно відстала від
main. Автотест ганяється на старому стані, де фічі ще немає або поведінка інша. Спершу підтягниmain, потім став діагноз застосунку — це продовження принципу «фейл автотесту ≠ баг продукту».
Підсумок
- Гілка — це рухомий іменований вказівник на один коміт; новий коміт лягає туди, куди дивиться
HEAD, і вказівник поточної гілки автоматично їде вперед. HEADзазвичай показує на гілку; коли він показує прямо на коміт (detached HEAD), нові коміти не належать жодній гілці й легко губляться.- Fast-forward лише пересуває вказівник по лінійній історії; three-way merge створює merge-коміт із двома батьками, коли гілки розійшлися, і тільки він може дати конфлікт.
git switch/git restore— сучасна заміна перевантаженомуgit checkout; локальна й віддалена гілки незалежні, а-dзахищає від видалення невлитого.- Осмислена назва гілки — це і комунікація, і матчинг правил CI; коротке життя гілки економить майбутні конфлікти.
Що питають на співбесіді
- «Що таке гілка в Git?» Слабка відповідь — «окрема версія проєкту». Сильна — «рухомий вказівник на коміт, фактично файл із хешем»; інтервʼюер перевіряє, чи розумієш модель, а не завчив команди.
- «Чим fast-forward відрізняється від three-way merge?» Очікують: перемотка вказівника при лінійній історії проти merge-коміту з двома батьками при розходженні гілок; бонус — коли Git обирає який і навіщо
--no-ff. - «Що таке detached HEAD і як туди потрапити?» Дивляться, чи звʼяжеш це з
checkoutна коміт/тег, з ризиком втратити коміти й із порятунком через нову гілку — і чи впізнаєш цей стан у логах CI. git switchvsgit checkout? Перевіряють обізнаність зі сучасним Git (2.23+) і розуміння, чому старийcheckoutбув неоднозначним.- «Як видалити гілку локально й на сервері й у чому різниця
-dі-D?» Тут цінують згадку про незалежність local/remote і про застаріліorigin/*-посилання та--prune. - «Опишіть свій типовий git-флоу для нової задачі». Поведінкове питання: слухають дисципліну (гілка від свіжої
main, маленькі коміти, PR, прибирання) — сигнал, що з людиною буде комфортно в команді.
Джерела
- Pro Git, розділ 3 «Git Branching» — git-scm.com/book/en/v2/Git-Branching-Branches-in-a-Nutshell (канонічне, безкоштовне джерело моделі гілок і HEAD).
- Документація
git switch— git-scm.com/docs/git-switch. - Документація
git merge— fast-forward,--no-ff,--ff-only— git-scm.com/docs/git-merge. - Документація
git branch— створення й видалення (-d/-D) — git-scm.com/docs/git-branch.
Що таке гілка в Git?
Гілка — це іменований вказівник на один конкретний коміт, а не окрема копія проєкту. Фізично вона лежить у файлі .git/refs/heads/<назва> і містить усього лиш хеш того коміту, який зараз вважається кінцем гілки (tip). Саме тому створення гілки коштує кілька десятків байтів і відбувається миттєво — нічого не копіюється. Історію тримають самі коміти через посилання на батьків, а гілка тільки позначає, де ця лінія наразі закінчується. Слабка відповідь на співбесіді звучить як «окрема версія проєкту»; сильна — «рухомий вказівник на коміт», бо саме вона показує, що людина розуміє модель, а не завчила команди.
Що таке HEAD і що означає, що він «прикріплений» до гілки?
HEAD відповідає на питання «де я зараз перебуваю». Зазвичай він вказує не прямо на коміт, а на гілку: у файлі .git/HEAD лежить рядок на кшталт ref: refs/heads/main. Виходить ланцюжок HEAD → гілка → коміт, і саме завдяки йому Git знає, який вказівник рухати вперед під час коміту — той, на який дивиться HEAD. Це і є нормальний, прикріплений стан: ти стоїш на гілці, і кожен новий коміт лягає на неї, автоматично зсуваючи її tip. Такий рівень непрямування (HEAD показує на гілку, а не на коміт) — ключ до розуміння, чому коміт «сам» пересуває гілку.
Що таке detached HEAD і як туди потрапити?
Detached HEAD — це стан, коли HEAD вказує напряму на коміт, а не на гілку. Ти потрапляєш туди, коли перемикаєшся на конкретний коміт за хешем, на тег або на віддалений стан безпосередньо — наприклад, git checkout 3a7f1c9 чи git checkout v1.4.0. У цьому стані все робоче: код читається, білдиться, тести ганяються, коміти навіть створюються. Небезпека в тому, що нові коміти не належать жодній гілці — на них немає іменованого вказівника. Тому detached HEAD — не поломка, а свідомий режим «подивитися точний зріз історії», який просто вимагає обережності, якщо ти там комітиш.
Накомітив у detached HEAD, перемкнувся на гілку — і роботи не видно. Куди вона поділась?
Нікуди — коміти живі, просто на них не лишилось видимого вказівника. У detached-стані нові коміти не прив'язані до гілки, тож щойно HEAD переїхав деінде, дістатися до них по звичних гілках уже не можна: вони стали недосяжними (unreachable). З часом такі коміти прибирає збирач сміття (garbage collector), але доти їх тримає reflog — журнал переміщень HEAD. Порятунок: знайти потрібний хеш у git reflog і прикріпити до нього гілку через git switch -c fix/rescue <хеш>. Мораль проста: перш ніж лишати detached-стан, у якому щось накомічено, створи на цьому місці гілку — і коміти врятовано.
Чому в логах CI замість назви гілки часто видно голий хеш коміту?
Бо типовий чекаут у пайплайні лишає репозиторій саме в detached HEAD на конкретному SHA. Наприклад, actions/checkout у GitHub Actions на подію push не «сідає на гілку», а відтворює точний коміт, який тригернув запуск. Це навмисно: агенту важлива не рухома мітка, а незмінний зріз коду, на якому має відпрацювати збірка. Тому побачити в логах хеш замість feature/... — нормально, а не ознака того, що «Git загубив гілку». Для AQA це щоденна буденність, і плутати штатний detached-стан із поломкою — типова помилка новачка.
Чим git switch відрізняється від git checkout і навіщо їх розділили?
Історично git checkout робив надто багато: перемикав гілки, створював їх, відновлював окремі файли й відʼєднував HEAD — той самий виклик міг як перемкнути гілку, так і мовчки затерти правки у файлі. У Git 2.23 (2019) ці обовʼязки розвели на дві команди з говорючими назвами: git switch відповідає тільки за перемикання й створення гілок, а git restore — тільки за відновлення файлів. git checkout нікуди не зник і працює далі, але новий код для ясності варто читати й писати через switch. Головна перевага switch — безпека: він не дасть випадково відʼєднати HEAD без явного --detach і чіткіше повідомляє про неоднозначні ситуації. Знання цієї пари — маркер, що кандидат знайомий зі сучасним Git, а не завис на командах десятирічної давнини.
Чим fast-forward відрізняється від three-way merge?
Це два принципово різні шляхи влити гілку, і Git обирає між ними за формою історії. Fast-forward («перемотка») можливий, коли цільова гілка не рухалась з моменту розходження: тоді її коміт — прямий предок кінця твоєї гілки, історія лінійна, зливати нема чого, і Git просто пересуває вказівник цільової гілки вперед без жодного нового коміту. Three-way merge потрібен, коли обидві гілки встигли обрости власними комітами після точки розходження — історія розійшлася, і перемоткою не обійтися. Тоді Git бере три коміти (кінець кожної гілки та їхнього спільного предка), зводить зміни докупи й створює новий merge-коміт із двома батьками. Практичний наслідок: fast-forward конфлікту дати не може й сліду про окрему фічу в графі не лишає, а three-way merge зберігає топологію розгалуження й саме він здатен упертися в merge-конфлікт.
Що таке merge base (спільний предок) і навіщо він при злитті?
Merge base — це найближчий спільний предок двох гілок, тобто коміт, від якого вони розійшлися. При three-way merge Git спирається саме на нього як на «точку відліку»: він порівнює, що змінилось у кожній гілці відносно цього спільного предка, і зводить обидва набори змін в один результат. Звідси й назва «трибічне» злиття — беруться три коміти: два кінці гілок і їхній merge base. Якщо обидві гілки правили різні місця — Git зведе все автоматично; якщо ж вони змінили ті самі рядки по-різному, Git не вгадає, чию версію лишити, і зупиниться на конфлікті. Без merge base не було б орієнтира, відносно чого вважати зміни, — тому це серцевина механізму злиття.
За якою ознакою в графі історії впізнати merge-коміт?
За кількістю батьків: merge-коміт — єдиний, у кого їх двоє (чи більше), тоді як звичайний коміт має рівно одного батька. Один батько веде в бік однієї гілки (наприклад, кінець main на момент злиття), другий — у бік іншої (кінець гілки, яку вливали). Саме ця «вилка з двох батьків» і робить видимою топологію: у графі лишається слід, що колись існувала окрема лінія робіт, яку потім звели назад. Тому fast-forward, який merge-коміту не створює, цей слід стирає — історія стає лінійною, ніби окремої гілки й не було. Якщо команді слід про фічу потрібен, це аргумент за примусовий merge-коміт.
Навіщо існують прапорці --no-ff і --ff-only?
Вони дають керувати вибором між перемоткою й merge-комітом замість того, щоб віддавати рішення Git за замовчуванням. git merge --no-ff примушує створити merge-коміт навіть тоді, коли можлива перемотка, — так у графі лишається явний слід, що коміти належали окремій фічі, і фічу видно як згорнуту гілку. git merge --ff-only, навпаки, дозволяє злиття лише якщо можлива чиста перемотка, а інакше падає з помилкою — це зручно в скриптах і CI, де лінійну історію тримають навмисно й не хочуть жодних merge-комітів. Тобто --no-ff про збереження топології, --ff-only про її відсутність, а вибір між ними — питання командної домовленості про форму історії, а не «правильно/неправильно».
Чому не можна одночасно мати гілку feature і гілку feature/login?
Бо слеш у назві гілки створює умовне групування, і feature не може бути водночас і «файлом», і «текою». Багато інструментів показують feature/* як окрему теку з гілками всередині — це зручно для навігації, але накладає обмеження: якщо існує feature/login, то feature вже сприймається як контейнер, а не як самостійна гілка, і навпаки. Це не примха Git, а наслідок того, що refs зберігаються шляхами у файловій системі (.git/refs/heads/...), де тека й файл з однаковим імʼям співіснувати не можуть. Практичний висновок: обирай схему іменування наперед і не змішуй короткі імена з префіксними на тому ж рівні. Решта правил — нижній регістр, слова через дефіс, без пробілів, ідентифікатор задачі в назві — про читабельність і звʼязок із таск-трекером.
Як назва гілки впливає на те, що запуститься в CI?
Часто напряму: правила пайплайна нерідко матчаться саме за назвою гілки через шаблони. Пайплайн може ганяти повний прогін тільки на release/*, піднімати окреме прев'ю-середовище на кожну feature/*, а на решті гілок запускати лише швидкий смоук. Це означає, що неохайна чи «не в тому форматі» назва здатна тихо вивести гілку з-під потрібного правила — і тести, які мали відпрацювати, просто не запустяться, без жодної явної помилки. Для AQA це має цілком технічний бік: іменування — не косметика, а те, від чого залежить, який набір перевірок узагалі торкнеться твого коду. Тому варто знати конвенцію свого проєкту й звірятися з тим, як саме сконфігуровані тригери.
Чим git branch -d відрізняється від git branch -D?
Мала -d — це запобіжник: Git видалить гілку лише якщо її вже влито, і відмовиться, якщо на ній лишились коміти, яких немає ніде більше. Велика -D цей запобіжник знімає й видаляє гілку примусово, незалежно від того, влита вона чи ні. Небезпека -D у тому, що якщо видалені комітами вказівника нікуди більше не вказують, вони стають недосяжними — і теоретично можуть бути прибрані збирачем сміття (рятує, знову ж таки, reflog). Тому -d — щоденний безпечний варіант після мержу, а -D — свідоме рішення викинути невлиту роботу. Окрема деталь: поточну гілку, на якій ти стоїш, видалити не можна — спершу перемкнись на іншу.
Локальну гілку видалив, а на сервері вона лишилась (і навпаки). Чому, і як прибрати origin/*?
Тому що локальна гілка й однойменна гілка на remote — дві незалежні речі: видалення однієї не чіпає іншу. Локально гілку прибирають через git branch -d, а на сервері — окремою командою git push origin --delete feature/search. Є ще й третя сутність: remote-tracking-посилання на кшталт origin/feature/search — це локальний «слід» серверної гілки, і Git не прибирає його автоматично навіть після того, як гілку видалили на сервері. Тому після видалення на remote застаріле origin/feature/search ще довго висить у списку й збиває з пантелику. Лікується це git fetch --prune, який викидає посилання на гілки, яких на сервері вже немає; щоб це відбувалось саме, вмикають git config --global fetch.prune true.
Опишіть свій типовий git-флоу, коли берете нову задачу.
Спершу оновлююсь від головної: git switch main і git pull, щоб стартувати зі свіжого стану, а не з тижневої давнини. Далі створюю окрему гілку під задачу з осмисленою назвою — git switch -c feature/JIRA-123-add-search. Працюю маленькими осмисленими комітами замість одного величезного, потім публікую гілку через git push -u origin ..., і саме цей push зазвичай тригерить CI. Відкриваю pull request, проходжу рев'ю (на зауваження — нові коміти в ту саму гілку), і коли пайплайн зелений, вливаю обраним командою способом. Наприкінці прибираю за собою: повертаюсь на main, підтягую його й видаляю локальну та віддалену гілку. Це поведінкове питання: інтервʼюер слухає дисципліну — коротке життя гілки, свіжий старт, охайні коміти, прибирання, — бо саме це сигналить, що з людиною буде комфортно працювати в команді.
Автотест червоний у пайплайні, але зелений локально. Чому тут варто перевірити стан гілки?
Бо частий, але неочевидний винуватець — гілка, що сильно відстала від main. Якщо твоя гілка давно не підтягувала головну, автотест ганяється на застарілому зрізі коду, де потрібної фічі ще немає або поведінка інша, — і падає не через баг застосунку, а через розбіжність станів. Тому перший крок діагностики — не заводити дефект, а підтягнути свіжий main у гілку й перезапустити: часто після цього тест зеленіє. Це пряме продовження принципу «фейл автотесту ≠ баг продукту»: спершу переконайся, що тест узагалі бігав на актуальному коді. Побіжно варто глянути, на якому саме коміті стартував прогін у CI (памʼятаючи про штатний detached-стан), бо питання «а на якій гілці й на якому SHA це запустилось» часто і є ключем до розгадки.
Три кейси, у яких плутанина з гілками коштує години: як за виводом git log відрізнити перемотку від справжнього злиття, як витягнути «зниклі» коміти з detached HEAD через reflog, і як за назвою гілки та її свіжістю зрозуміти, чому в пайплайні червоно. Скрізь — що дивитися й чому.
Кейс 1. Перемотка чи merge-коміт: читаємо граф історії
Уяви дві ситуації з тією самою командою git merge feature, але різним результатом. Спершу — цільова гілка не рухалась, поки ти працював. Тоді Git перемотує вказівник, і граф лишається лінійним:
$ git switch main
$ git merge feature
Updating a1b2c3d..f9e8d7c
Fast-forward
tests/search.spec.ts | 24 ++++++++++++++++++++++++
1 file changed, 24 insertions(+)
$ git log --oneline --graph
* f9e8d7c (HEAD -> main, feature) add search filter tests
* a1b2c3d update fixtures
Зверни увагу: main і feature тепер показують на той самий коміт f9e8d7c, нового коміту не зʼявилось, а слова «Fast-forward» у виводі — прямий маркер перемотки. Тепер друга ситуація: поки ти працював, колеги влили свої коміти в main. Гілки розійшлися, тож перемотка неможлива, і Git створює merge-коміт:
$ git merge feature
Merge made by the 'ort' strategy.
tests/search.spec.ts | 24 ++++++++++++++++++++++++
1 file changed, 24 insertions(+)
$ git log --oneline --graph
* 3c4d5e6 (HEAD -> main) Merge branch 'feature'
|\
| * f9e8d7c (feature) add search filter tests
* | b7a6c5d fix flaky login wait
|/
* a1b2c3d update fixtures
Що дивитися й чому:
- Слово у виводі мержу — найшвидший діагноз. «Fast-forward» означає, що нового коміту немає й вказівник просто переїхав; «Merge made by...» означає merge-коміт із двома батьками. Це видно ще до
git log. - Розгалуження
|\в графі — це і є збережена топологія. Merge-коміт3c4d5e6має двох батьків:b7a6c5dз бокуmainіf9e8d7cз бокуfeature. За наявністю двох батьків merge-коміт і впізнають. - Після перемотки сліду про фічу не лишається. Якщо команді важливо бачити, що коміти належали окремій задачі, перемотку відключають примусово:
git merge --no-ff featureстворить merge-коміт навіть там, де можлива перемотка. - Дзеркальна вимога — лінійна історія. Коли merge-коміти в проєкті небажані, у CI ставлять
git merge --ff-only: воно зіллє лише перемоткою, а на розбіжності гілок впаде з помилкою, змусивши спершу оновитись відmain.
Кейс 2. «Мої коміти зникли»: рятуємо роботу з detached HEAD
Класична паніка: перевіряв, як виглядав тест на старому коміті, накомітив там правку, перемкнувся назад — і роботи не видно. Спершу відтворимо, як у неї потрапляють:
$ git checkout 3a7f1c9 # пішли на конкретний коміт → detached HEAD
Note: switching to '3a7f1c9'.
You are in 'detached HEAD' state...
$ # ... правимо, git add, git commit → зʼявляється коміт 7d8e9f0
$ git switch main # повертаємось — і 7d8e9f0 більше нікуди не вказує
Warning: you are leaving 1 commit behind, not connected to any of your branches:
7d8e9f0 quick fix for legacy selector
Коміт 7d8e9f0 живий, але недосяжний: жодна гілка на нього не дивиться. Дістаємо його з reflog — журналу переміщень HEAD:
$ git reflog
f9e8d7c (HEAD -> main) HEAD@{0}: checkout: moving from 3a7f1c9 to main
7d8e9f0 HEAD@{1}: commit: quick fix for legacy selector
3a7f1c9 HEAD@{2}: checkout: moving from main to 3a7f1c9
$ git switch -c fix/legacy-selector 7d8e9f0 # прикріпили гілку → коміт врятовано
Switched to a new branch 'fix/legacy-selector'
Що дивитися й чому:
- Warning при виході з detached-стану вже містить хеш. Git прямо друкує, який коміт ти лишаєш позаду, — його достатньо скопіювати в
git switch -c <хеш>. Не встиг записати — знайдеш уgit reflog. - reflog — це страхувальна сітка, не історія. Він локальний, живе обмежений час (за замовчуванням недосяжні записи чистяться приблизно за 30 днів) і на сервер не їде. Тобто вікно для порятунку не безкінечне, але для «щойно перемкнувся» його з головою.
- Профілактика дешевша за порятунок. Якщо збираєшся комітити, стоячи на голому SHA, одразу створи гілку:
git switch -c <назва> <хеш>замістьgit checkout <хеш>. Тоді коміти від початку належать гілці й губитися нема чому.
Кейс 3. Червоно в CI: винна гілка, а не застосунок
Тест зелений локально, а в пайплайні падає. Перш ніж заводити дефект на продукт, AQA перевіряє два «гілкові» підозри — свіжість гілки й те, чи вона взагалі потрапила під потрібне правило CI. Ось таблиця рішень для типових симптомів:
| Симптом | Найімовірніша причина | Що зробити спершу |
|---|---|---|
| Локально зелено, у CI червоно, гілка давно не оновлювалась | Гілка відстала від main, тест біжить на старому коді | git switch main && git pull, потім злити main у гілку й перезапустити |
| У логах CI хеш замість назви гілки | Штатний detached-чекаут на SHA | Нічого — це норма; звірити, що SHA справді твій коміт |
| Пайплайн узагалі не запустив потрібний джоб | Назва гілки не матчить правило (feature/*, release/*) | Перевірити конвенцію та тригери, за потреби перейменувати гілку |
| Тест бачить фічу, якої «не має бути» | Злиття підтягнуло чужі коміти в main перед прогоном | Глянути merge-коміт і на якому SHA стартував запуск |
Розберемо перший рядок — він найчастіший. Швидка перевірка, наскільки гілка відстала:
$ git fetch origin
$ git log --oneline origin/main ^HEAD # що є в main, чого немає у твоїй гілці
b7a6c5d fix flaky login wait
c3d2e1f bump playwright to 1.50
d4e5f6a update staging base URL ← ось воно: тест біжить на старій адресі стенду
Що дивитися й чому:
- «Тест червоний → баг у застосунку» — не перший висновок, а останній. Це продовження принципу «фейл автотесту ≠ баг продукту»: спершу переконайся, що тест узагалі бігав на актуальному коді, а не на тижневій давнині.
- Відставання гілки лікується оновленням, а не правкою тесту. Якщо
origin/mainмістить коміти, яких немає в гілці (як зміну базового URL стенду вище), підтягни їх і перезапусти — часто після цього стає зелено без жодних змін у самому тесті. - Назва гілки — теж частина «чому не запустилось». Коли потрібний джоб взагалі не стартував, дивись не в код тесту, а в тригери пайплайна: неохайне ім'я тихо виводить гілку з-під правила
feature/*, і перевірки просто не торкаються твого коду.
Гілка та HEAD
- Можу пояснити, що гілка — це рухомий вказівник на один коміт (файл із хешем у
.git/refs/heads), а не копія проєкту; тому створення гілки миттєве й нічого не копіює. - Знаю, що при коміті вказівник поточної гілки автоматично їде вперед, а історію тримають самі коміти через посилання на батьків.
- Можу пояснити роль HEAD і ланцюжок HEAD → гілка → коміт у нормальному, прикріпленому стані.
Detached HEAD
- Знаю, що detached HEAD — це коли HEAD дивиться прямо на коміт (за хешем, тегом чи віддаленим станом), а не на гілку.
- Розумію головну пастку: коміти в detached-стані не належать жодній гілці й стають недосяжними після перемикання.
- Можу пояснити, що недосяжні коміти тримає reflog, а рятує їх
git switch -cна потрібний хеш. - Розумію, чому хеш замість назви гілки в логах CI — штатний detached-чекаут на конкретний SHA, а не поломка.
switch, checkout, restore
- Знаю, що до Git 2.23 один
checkoutперемикав гілки, створював їх, відновлював файли й відʼєднував HEAD — і чому це збивало з пантелику. - Можу пояснити розподіл:
switch— тільки гілки,restore— тільки файли, аswitchще й безпечніший (не відʼєднає HEAD без--detach).
Fast-forward і three-way merge
- Можу пояснити fast-forward: цільова гілка не рухалась, історія лінійна, вказівник просто переїжджає вперед без нового коміту.
- Можу пояснити three-way merge: гілки розійшлися, Git бере кінці обох і спільного предка (merge base) і створює merge-коміт із двома батьками.
- Знаю, що merge-коміт упізнають за двома батьками й що тільки three-way merge може дати конфлікт (fast-forward — ніколи).
- Знаю, навіщо
--no-ff(примусовий merge-коміт заради сліду) і--ff-only(злиття лише перемоткою, інакше помилка — для лінійної історії в CI).
Іменування й видалення гілок
- Знаю усталені префікси (
feature/,bugfix/,hotfix/,release/) і можу пояснити, чому не можна одночасно мати гілкуfeatureіfeature/login(слеш групує гілки — ім'я не буває водночас текою і файлом). - Розумію, що назва гілки може матчити правила CI (
feature/*,release/*), тож неохайна назва тихо виводить гілку з-під потрібного тригера. - Знаю різницю
-d(видалить лише влиту, безпечно) і-D(примусово, можна втратити коміти), і що поточну гілку видалити не можна. - Розумію, що локальна й віддалена гілки незалежні: на сервері —
git push origin --delete, а застаріліorigin/*-посилання чиститьgit fetch --prune.
Флоу фічі очима AQA
- Можу описати типовий цикл: свіжий
main→ нова гілка → маленькі коміти →push -u(тригерить CI) → PR і рев'ю → мерж → прибирання. - Розумію, чому коротке життя гілки — профілактика: чим довше вона живе окремо, тим болючіші майбутні конфлікти.
- Пам'ятаю, що червоний тест у CI при зеленому локально часто означає гілку, що відстала від
main, — спершу підтягни головну, потім став діагноз застосунку.
Що таке гілка в Git з погляду внутрішнього устрою?
Питання
Гілка (branch) у Git — це що насправді?