Rebase, cherry-pick і переписування історії
Зміст
Твоя гілка з автотестами висить у ревʼю третій день, а main за цей час пішов далеко вперед. Ти хочеш підтягнути свіжий код, зробити історію охайною перед мержем — і тут постає вибір: merge чи rebase? А колега мимохідь кидає: «тільки не роби force-push, бо позбиваєш мені коміти». Ці слова — не забобон, а наслідок того, як Git влаштовує історію. Хто не розуміє механіки переписування, той або боїться rebase як вогню, або одного разу зносить чужу роботу однією командою.
Ця глава — про інструменти, що переписують історію: rebase, інтерактивний rebase, cherry-pick. Усі вони не «редагують» старі коміти, а створюють нові з новими хешами, а старі лишають сиротами. Зрозумієш це — і перестанеш плутати «чисту» історію з «правдивою», а --force з --force-with-lease. На співбесіді middle-рівня це один із найчастіших блоків: різницю merge vs rebase і золоте правило питають майже завжди.
Merge vs rebase: дві філософії історії
Уяви: гілка main має коміти A → B → C, а ти від коміта B відгалузив feature і зробив там D → E. Тепер треба обʼєднати свою роботу з main. Є два способи, і вони дають різну форму графа історії.
Merge (злиття) створює новий коміт злиття (merge commit) з двома батьками — він зшиває дві гілки, зберігаючи їхню реальну топологію. У графі лишається «розвилка й сходження»: видно, що робота велася паралельно й коли її звели докупи. Механіку three-way merge і fast-forward детально розібрано в главі «Гілки та злиття».
Rebase (перебазування) діє інакше: він бере твої коміти D і E, «відклеює» їх від B і відтворює заново поверх C. Виходить рівний ланцюжок A → B → C → D' → E' без жодного коміта злиття. Історія лінійна, ніби ти писав свою фічу вже після того, як зʼявився C.
Ключове: merge нічого не переписує — старі коміти лишаються собою, додається лише M. Rebase же знищує D і E у тому сенсі, що на їхньому місці в історії гілки тепер стоять інші коміти — D' і E'. Вони містять ті самі зміни, але це вже не ті самі обʼєкти. Чому — розберемо далі, бо це джерело половини непорозумінь навколо rebase.
Rebase зсередини: чому змінюються хеші
Кожен коміт у Git має ідентифікатор — хеш (за замовчуванням SHA-1). Це не випадковий номер: хеш обчислюється з вмісту коміта. А до вмісту входять не лише знімок файлів (дерево, tree), а й: хеш батьківського коміта, автор і коммітер, повідомлення, часові мітки. Зміни будь-якого з цих полів — і хеш стає іншим.
Тепер очевидно, чому rebase породжує нові коміти. Коли D переїжджає з батька B на батька C, у нього змінюється батьківський хеш. Отже, обовʼязково змінюється і його власний хеш: батько D змінюється з B на C, і D перетворюється на D'. А оскільки батько E — це D, у E теж змінюється батьківський хеш, тож E стає E'. Ефект каскадний: rebase переписує весь ланцюжок від точки розгалуження й далі.
Старі D і E нікуди не діваються фізично одразу — вони стають недосяжними (unreachable): на них більше не вказує жодна гілка. Git тримає їх, поки збирач сміття (garbage collection) не почистить недосяжні обʼєкти, а дістатися до них можна через reflog — журнал, де Git пише, куди вказував HEAD у кожен момент. Тому невдалий rebase майже завжди оборотний: reflog показує хеш стану «до», і туди можна повернутися через reset. Це страхувальна сітка, яку детально розібрано в главі «Скасування змін: reset, revert, stash».
# Підтягнути свою гілку поверх свіжого main
git switch feature
git rebase main
# Щось пішло не так — подивитися журнал і повернутися
git reflog
git reset --hard HEAD@{2}
Практичний висновок для AQA: якщо після rebase ти шукаєш «той самий» коміт за старим хешем у CI-логах чи в TMS — не знайдеш. Хеш — це ідентичність коміта, а rebase її змінює. Посилатися на коміти, що ще можуть бути перебазовані, за хешем — ненадійно.
Інтерактивний rebase: squash, reword, drop
Звичайний rebase просто переносить коміти. Інтерактивний rebase (git rebase -i) дає над ними повний контроль: перед відтворенням Git відкриває редактор зі списком комітів і командами, які можна на них навісити.
git rebase -i HEAD~4 # взяти під редагування останні 4 коміти
У редакторі зʼявляється так званий todo-список — коміти в порядку від старіших до новіших, кожен зі словом-командою:
pick a1b2c3d Додати перевірку логіну
squash e4f5g6h fix друкарки в тесті
reword i7j8k9l WIP
drop m0n1o2p тимчасовий console.log
Найуживаніші команди:
| Команда | Що робить |
|---|---|
pick | лишити коміт як є (дефолт) |
reword (r) | лишити зміни, але переписати повідомлення |
squash (s) | вліпити коміт у попередній, обʼєднавши обидва повідомлення |
fixup (f) | те саме, що squash, але повідомлення цього коміта викинути |
edit (e) | зупинитися на цьому коміті, щоб доредагувати зміни |
drop (d) | викинути коміт цілком |
Порядок рядків = порядок комітів, тож їх можна ще й переставляти. squash і fixup — головні робочі конячки перед відкриттям PR: п'ять комітів на кшталт «wip», «fix», «ще fix» згортаються в один осмислений «Додати E2E-перевірку відновлення пароля». reword виправляє соромʼязливе повідомлення, drop прибирає випадковий console.log, що заліз в окремий коміт.
Тут важлива та сама механіка: інтерактивний rebase переписує всі зачеплені коміти, навіть ті, які ти позначив pick. Досить змінити один коміт у середині — і всі, що йдуть після нього, отримають нові хеші, бо в них зміниться ланцюжок батьків. Це прямий місток до наступного розділу.
Золоте правило: не переписуй спільну історію
Ось головне правило, заради якого існує ця глава: не переписуй історію, яка вже вийшла за межі твого локального репозиторію й на яку могли спертися інші. Формулювання з книги Pro Git пряме: не роби rebase комітів, які ти вже запушив у публічну гілку, де на них базується чужа робота.
Чому це болить, а не просто «не рекомендовано». Коли ти перебазовуєш уже опубліковані коміти, ти створюєш їхні копії з новими хешами, а старі оголошуєш неіснуючими. Але в колеги, що вже стягнув гілку, старі коміти лишилися. Тепер його історія й твоя розійшлися: Git бачить дві різні лінії з тими самими змінами. Коли колега спробує синхронізуватися, він отримає купу дублікатів і конфліктів на порожньому місці, бо система намагається звести дві версії однієї й тієї самої роботи.
Практична межа проста. Гілка, яку бачиш лише ти (не пушив, або пушив у свій особистий feature-бранч, з яким більше ніхто не працює), — переписуй скільки завгодно, це нормальна гігієна. Спільна гілка на кшталт main чи develop, або гілка, куди вже коммітить колега, — недоторканна: там правки вносять тільки новими комітами. Для скасування вже опублікованого коміта є revert, що додає коміт-антидот, не чіпаючи історію (див. главу «Скасування змін»).
--force vs --force-with-lease
Коли ти перебазував свою feature-гілку, яку вже пушив, виникає технічна проблема: локальна гілка й віддалена розійшлися. Твій новий ланцюжок D'→E' не є прямим продовженням старого D→E на сервері. Звичайний git push Git відхилить із помилкою non-fast-forward — він захищає віддалену гілку від затирання. Щоб пропхати переписану історію, потрібен force push (примусовий пуш).
І тут два інструменти, різниця між якими — улюблене питання на співбесідах.
git push --force каже серверу: «постав гілку на мій коміт, що б там зараз не було». Він затирає віддалену гілку беззастережно. Проблема: якщо між твоїм останнім fetch і цим пушем колега встиг запушити свій коміт, --force знесе його безслідно. Класична катастрофа спільної гілки.
git push --force-with-lease додає запобіжник: він оновить віддалену гілку, тільки якщо вона й досі там, де ти востаннє її бачив (на твоєму remote-tracking посиланні). Якщо хтось запушив після твого останнього fetch, сервер уже не на очікуваному коміті — і push відхиляється. Ти дізнаєшся про чужу роботу замість того, щоб її стерти. «Lease» — це «оренда»: ти ніби взяв гілку в оренду в тому стані й дієш, лише поки цей стан не змінився.
# Небезпечно: затре все, що на сервері, навіть чужі свіжі коміти
git push --force
# Безпечніше: відхилить push, якщо хтось пушив після твого fetch
git push --force-with-lease
Правило-звичка: якщо взагалі доводиться force-пушити — завжди --force-with-lease. Одна засторога: --force-with-lease порівнює із твоїм remote-tracking посиланням, тож автоматичний або фоновий git fetch може «освіжити» його й приспати захист. Тому перед примусовим пушем не роби наосліп зайвих fetch і дивися, що саме переписуєш.
cherry-pick: один коміт без цілої гілки
cherry-pick вирішує іншу задачу: узяти один конкретний коміт і застосувати його зміни на поточну гілку, не тягнучи за собою всю гілку, звідки він родом.
git switch release/2.4
git cherry-pick a1b2c3d # застосувати саме цей фікс на реліз-гілку
Механіка та сама, що й у rebase: cherry-pick не «переносить» коміт, а створює новий коміт із новим хешем, який містить той самий діф (набір змін) і те саме повідомлення, але з іншим батьком. Тобто після cherry-pick одна й та сама зміна фізично існує у двох комітах з різними хешами.
Найтиповіший сценарій — бекпорт (backport) виправлення: критичний баг полагоджено в main, але цей фікс потрібен ще й у старій реліз-гілці, куди весь main мержити не можна. Береш один коміт із фіксом і cherry-pick-аєш його в реліз. Для AQA це часта операція: гарячий фікс автотесту чи конфігурації треба доставити у гілку, з якої вже збирають реліз.
Прапорець -x дописує в повідомлення рядок (cherry picked from commit ...) — сліди, звідки коміт прийшов. Це рятує від плутанини, коли той самий фікс живе у двох гілках. Але памʼятай про зворотний бік: cherry-pick множить коміти з однаковими змінами, і зловживання ним засмічує історію дублікатами, які потім спантеличують при мержі й читанні git log.
«Чиста» vs «правдива» історія
За всіма цими інструментами стоїть один світоглядний холівар. Що має відображати історія — як воно було насправді чи як його зручно читати?
Правдива історія фіксує реальний хід подій: усі «wip», «fix typo», коміти злиття, метання туди-сюди. Ніщо не переписано, граф показує фактичну паралельну роботу. Аргумент: історія — це журнал, а журнали не підробляють.
Чиста (курована) історія — це відредагований результат: лінійний ланцюжок атомарних комітів, де кожен — логічно завершена робоча зміна з осмисленим повідомленням. Досягається rebase-ом і squash-ем перед мержем. Аргумент: історію читають набагато частіше, ніж пишуть, і охайний git log — це документація.
Однозначно правильної відповіді немає — це командна домовленість. Squash-merge на злитті PR дає гранично чисту історію ціною втрати проміжних кроків; merge-коміти зберігають контекст ціною шуму. Стратегії злиття PR (merge commit vs squash vs rebase) розібрано в главі «Pull Request і стратегії гілкування».
Чому це не абстракція для AQA. Чиста лінійна історія, де кожен коміт складається й проходить тести, робить git bisect — бінарний пошук коміта, що зламав тест, — по-справжньому дієвим: пошук приземляється на реальну зміну-винуватця. Історія з поламаними проміжними комітами й густими merge-комітами перетворює bisect на муку. Так само blame читається чисто, коли коміти атомарні. Ці інструменти пошуку винуватця — тема глави «Git для AQA: bisect, blame, хуки й теги».
Типові помилки
-
Виглядає як «rebase зламав мою гілку», а насправді коміти цілі — просто змінилися їхні хеші, а старий стан лежить у
reflog. Перш ніж панікувати після невдалого rebase, подивисяgit reflogі зробиreset --hardна потрібний запис. -
Виглядає як «
--force— це просто “запушити напевно”», а насправді--forceбеззастережно затирає віддалену гілку разом із чужими комітами, які зʼявилися після твого fetch. За замовчуванням бери--force-with-lease. -
Виглядає як «я почистив історію rebase-ом на спільній гілці», а насправді ти створив розбіжність: у колег лишилися старі коміти, і синхронізація вибухне дублікатами й конфліктами. Rebase — лише для історії, що не вийшла за межі твого репозиторію.
-
Виглядає як «cherry-pick переніс фікс в іншу гілку», а насправді він створив другий коміт із тим самим дифом і новим хешем. Це не той самий коміт; при подальших мержах дублікат може дати конфлікт або порожнє злиття.
-
Виглядає як «squash усе згорнув, історія чиста», а насправді якщо це вже опублікована спільна гілка, ти щойно переписав чужу базу. Squash перед PR — так; squash уже змерджених у
mainкомітів — ні.
Підсумок
- Merge зберігає топологію й додає коміт злиття; rebase робить історію лінійною, відтворюючи коміти поверх нової бази.
- Rebase, інтерактивний rebase і cherry-pick не редагують старі коміти — вони створюють нові з новими хешами, бо хеш залежить від батька, а він змінюється. Старий стан рятує
reflog. - Золоте правило: переписувати можна лише історію, що не вийшла за межі твого локального репозиторію (чи твоєї особистої гілки). Спільні гілки правлять тільки новими комітами (
revert). - Якщо потрібен force push — завжди
--force-with-lease, а не--force: перший відхилить пуш, якщо хтось встиг запушити, другий мовчки зітре чуже. - «Чиста» vs «правдива» історія — командна домовленість, а не абсолют; але чиста лінійна історія робить
bisectіblameреально корисними для пошуку багів.
Що питають на співбесіді
- «У чому різниця між merge і rebase?» Інтервʼюер хоче почути про форму графа (merge-коміт і збережена топологія vs лінійна історія) і про те, що rebase переписує коміти з новими хешами. Слабка відповідь зупиняється на «rebase робить красивіше»; сильна пояснює механізм і наслідки.
- «Що таке золоте правило rebase / чому не можна rebase-ити спільну гілку?» Перевіряють, чи розумієш ти, що переписування опублікованої історії породжує розбіжність для інших. Очікують згадку про
revertяк безпечну альтернативу для публічної історії. - «Чим
--force-with-leaseкращий за--force?» Тут дивляться, чи розумієш ти конкретний механізм захисту:--force-with-leaseвідхиляє пуш, якщо віддалена гілка змістилася відносно твого останнього fetch, тобто не дає стерти чужу свіжу роботу. - «Навіщо cherry-pick і чим він відрізняється від merge?» Хочуть почути про бекпорт одного коміта в реліз-гілку й про те, що cherry-pick створює новий коміт-копію (дублікат діфа з іншим хешем), а не переносить оригінал.
- «Як виправити останні кілька комітів перед PR?» Очікують
git rebase -iзі squash/reword/drop і застереження, що це безпечно, лише поки гілка не спільна.
Джерела
- Pro Git — Rebasing — merge vs rebase, «The Perils of Rebasing» і золоте правило.
- Pro Git — Rewriting History — інтерактивний rebase (squash/reword/drop), перевпорядкування й розбиття комітів,
filter-branch. - git-rebase Documentation — офіційна дока з усіма командами todo-списку.
- git-push Documentation — семантика
--forceі--force-with-lease. - git-cherry-pick Documentation — прапорці, зокрема
-x.
Git не є окремою темою силабусу ISTQB CTFL 4.0 — управління версіями там згадується лише в межах управління конфігурацією (configuration management), тож канонічне джерело з цієї теми — офіційна документація Git і книга Pro Git.
Чим відрізняються merge і rebase?
Обидва обʼєднують роботу двох гілок, але дають різну форму графа. merge додає окремий коміт злиття з двома батьками й лишає видимою реальну топологію — розвилку й точку сходження. rebase натомість відклеює твої коміти від старої бази й відтворює їх поверх нової, тож виходить рівний лінійний ланцюжок без коміта злиття. Головне: merge нічого не переписує — старі коміти лишаються собою, просто зʼявляється ще один; rebase створює для твоєї роботи нові коміти з новими хешами, а старі стають недосяжними. На співбесіді слабка відповідь зупиняється на «rebase робить красивіше», сильна пояснює, що це різні компроміси: merge — правдива топологія ціною шуму, rebase — чиста лінія ціною переписаних комітів.
Чому після rebase у комітів змінюються хеші?
Хеш коміта обчислюється з його вмісту, а хеш батьківського коміта — частина цього вмісту. Коли rebase переносить коміт на нову базу, у нього змінюється батько, отже, неминуче змінюється й власний хеш. Далі спрацьовує каскад: наступний коміт указував на старий батьківський хеш, тепер той інший — тож і сам він отримує новий хеш, і так до кінця ланцюжка. Тому rebase переписує всю послідовність від точки розгалуження й далі, навіть якщо самі зміни у файлах ідентичні. Коміти містять ті самі діфи, але це вже інші обʼєкти з іншими ідентифікаторами.
Що саме входить у обчислення хеша коміта?
Не лише зміни у файлах, як багато хто думає. У хеш (за замовчуванням SHA-1) закладено: знімок дерева файлів (tree), хеш батьківського коміта, автора й коммітера, повідомлення та часові мітки. Досить змінити будь-яке з цих полів — і хеш стає іншим обʼєктом. Саме тому rebase (новий батько), commit --amend (нове дерево чи повідомлення) і reword (нове повідомлення) усі породжують нові коміти, а не редагують старі. Зрозумівши це, перестаєш вірити, ніби Git «правит» коміт на місці — він завжди створює новий.
Що таке інтерактивний rebase і навіщо він?
git rebase -i дає повний контроль над діапазоном комітів перед їх відтворенням. Git відкриває редактор зі списком (todo-list) від старіших до новіших, і на кожен коміт можна навісити команду: лишити (pick), переписати повідомлення (reword), злити з попереднім (squash/fixup), зупинитися для правки (edit) чи викинути (drop). Порядок рядків задає порядок комітів, тож їх ще й переставляють. Найчастіший сценарій — причесати гілку перед відкриттям PR: пʼять комітів на кшталт «wip», «fix», «ще fix» згорнути в один осмислений. Механіка та сама: усі зачеплені коміти отримують нові хеші, навіть позначені pick, бо в них зміниться ланцюжок батьків.
У чому різниця між squash і fixup в інтерактивному rebase?
Обидві команди зливають коміт у попередній, різниця лише в долі повідомлення. squash зберігає обидва повідомлення й відкриває редактор, щоб ти звів їх у одне осмислене. fixup мовчки викидає повідомлення цього коміта й лишає тільки текст попереднього. На практиці fixup беруть для суто службових комітів, де повідомлення все одно сміттєве («fix друкарки», «ще раз те саме»), а squash — коли обидва тексти несуть контекст, який хочеться поєднати. Результат ідентичний за вмістом, відрізняється лише підсумкове повідомлення.
Що таке золоте правило rebase і чому воно існує?
Правило: не переписуй історію, яка вже вийшла за межі твого локального репозиторію й на яку могли спертися інші. Формулювання з Pro Git пряме — не роби rebase комітів, які вже запушено в публічну гілку. Причина не в етикеті, а в механіці: переписуючи опубліковані коміти, ти створюєш їхні копії з новими хешами, а старі оголошуєш неіснуючими. Але в колеги, який уже стягнув гілку, старі коміти лишилися, тож дві історії розходяться. Практична межа проста: локальну чи власну особисту гілку переписуй скільки завгодно, а спільну (main, develop чи ту, куди коммітить колега) прав тільки новими комітами — для скасування є revert.
Що конкретно ламається, коли хтось rebase-ить спільну гілку?
Уяви: колега вже стягнув гілку зі старими комітами D і E. Ти перебазовуєш її, і на сервері замість них тепер стоять D' і E' — ті самі зміни, але з іншими хешами. Тепер історія колеги й твоя розійшлися: Git бачить дві різні лінії, що містять фактично одну й ту саму роботу. Коли колега спробує синхронізуватися, він отримає купу дублікатів і конфліктів буквально на порожньому місці, бо система намагається звести дві версії однакових змін. Замість чистої історії команда дістає розгардіяш, який доводиться руками розплутувати. Тому опубліковане скасовують revert-ом, а не переписуванням.
У чому різниця між --force і --force-with-lease?
Після rebase уже запушеної гілки локальна й віддалена версії розходяться, і звичайний git push Git відхиляє помилкою non-fast-forward — потрібен примусовий пуш. git push --force каже серверу поставити гілку на твій коміт беззастережно: якщо між твоїм останнім fetch і цим пушем колега встиг щось запушити, --force знесе його безслідно. git push --force-with-lease додає запобіжник — він оновить гілку, тільки якщо вона й досі там, де ти її востаннє бачив (за твоїм remote-tracking посиланням); якщо хтось пушив після твого fetch, сервер уже не на очікуваному коміті, і push відхиляється. Тобто lease дає тобі дізнатися про чужу роботу, а не стерти її. Звичка проста: якщо взагалі доводиться force-пушити — завжди --force-with-lease.
Коли --force-with-lease може підвести й пропустити чужу роботу?
Запобіжник порівнює стан не з живим сервером, а з твоїм локальним remote-tracking посиланням — тим, що останній fetch записав. Якщо якийсь автоматичний чи фоновий git fetch (IDE, хук, cron) «освіжив» це посилання вже після того, як колега запушив, lease вирішить, що гілка «там, де ти її бачив», і пропустить пуш — захист приспано. Тому перед примусовим пушем не варто наосліп ганяти зайві fetch і корисно окремо подивитися, що саме ти збираєшся переписати. Lease не магія — він захищає лише від змін, про які твоє посилання ще не знає.
Що таке cherry-pick і коли він доречний?
cherry-pick бере один конкретний коміт і застосовує його зміни на поточну гілку, не тягнучи за собою всю гілку, звідки той родом. Найтиповіший сценарій — бекпорт (backport) виправлення: критичний баг полагоджено в main, а фікс потрібен ще й у старій реліз-гілці, куди весь main мержити не можна. Механіка та сама, що й у rebase: cherry-pick не переносить коміт, а створює новий коміт із новим хешем, що містить той самий діф і повідомлення, але з іншим батьком. Тож після нього одна й та сама зміна фізично живе у двох комітах з різними хешами. Прапорець -x дописує рядок (cherry picked from commit ...), щоб не загубити, звідки прийшла зміна.
Чим cherry-pick відрізняється від merge?
Merge зводить цілу гілку разом з її топологією й посилається на оригінальні коміти — нічого не дублюється. Cherry-pick витягує рівно один коміт і створює його копію: новий коміт із тим самим дифом, але новим хешем і новим батьком. Наслідок — дублювання: та сама зміна тепер існує у двох місцях як два різні обʼєкти. Зловживання cherry-pick засмічує історію дублікатами, які потім спантеличують при читанні git log і можуть дати конфлікт чи порожнє злиття, коли ці гілки нарешті мержать. Тому cherry-pick — інструмент точкового бекпорту, а не заміна нормальному мержу.
Як відкотити невдалий rebase?
Старі коміти після rebase не зникають фізично одразу — вони стають недосяжними (unreachable), тобто на них більше не вказує жодна гілка, але вони лежать у репозиторії, поки їх не почистить збирач сміття (garbage collection). Дістатися до них можна через reflog — журнал, де Git фіксує, куди вказував HEAD у кожен момент. Там видно хеш стану «до rebase», і туди повертаються командою git reset --hard HEAD@{n} на потрібний запис. Саме тому невдалий rebase майже завжди оборотний, і перше, що варто зробити після паніки, — подивитися git reflog, а не переробляти все з нуля.
Feature-гілку треба оновити від свіжого main. Merge чи rebase?
Залежить від того, чи гілка спільна, і від командної домовленості. rebase на main дасть лінійну історію, ніби ти писав фічу вже після останніх змін основної гілки, — чисто, але твої коміти буде переписано, і якщо гілку вже пушено, знадобиться --force-with-lease. merge main у feature збереже топологію й не переписує нічого, але додасть коміти злиття й шум. Практичне правило: якщо над гілкою працюєш лише ти — rebase нормальний і навіть бажаний як гігієна; якщо з гілкою працює ще хтось — обирай merge, щоб не переписувати спільні коміти й не наступити на золоте правило.
«Чиста» vs «правдива» історія — у чому суперечка?
Це світоглядний холівар про те, що має відображати git log. Правдива історія фіксує реальний хід подій: усі «wip», «fix typo», коміти злиття, метання туди-сюди — нічого не переписано, граф показує фактичну паралельну роботу; аргумент — журнал не підробляють. Чиста (курована) історія — відредагований результат: лінійний ланцюжок атомарних комітів, де кожен логічно завершений і має осмислене повідомлення, досягається rebase-ом і squash-ем перед мержем; аргумент — історію читають частіше, ніж пишуть, тож охайний лог це документація. Однозначно правильної відповіді немає — це командна домовленість, а не абсолют.
Як чиста лінійна історія допомагає bisect і blame?
Коли кожен коміт складається й проходить тести, git bisect — бінарний пошук коміта, що зламав тест, — стає по-справжньому дієвим: пошук приземляється на реальну зміну-винуватця, а не на проміжний зламаний стан. Історія з поламаними напівкомітами й густими merge-комітами перетворює bisect на муку, бо ти раз у раз потрапляєш на коміт, який і сам не збирається. Так само blame читається чисто, коли коміти атомарні: кожен рядок веде до логічно завершеної зміни, а не до випадкового «wip». Це і є практична вигода курованої історії для AQA — швидший пошук причини червоного тесту.
Чому не варто посилатися на коміт за хешем, поки гілка може бути перебазована?
Хеш — це ідентичність коміта, а rebase її змінює. Якщо ти зафіксував хеш коміта з відкритого PR у баг-репорті, CI-логах чи TMS, а автор потім зробить rebase чи squash, за цим хешем ти вже нічого не знайдеш — обʼєкт став недосяжним, а зміни живуть під новим ідентифікатором. Тому посилатися на коміти, що ще можуть переписатися, за хешем ненадійно. Стабільніше вказувати гілку чи тег або дочекатися, поки історія «устаканиться» після мержу, коли хеші вже не змінюватимуться.
Три ситуації з робочого життя AQA, де переписування історії вирішує, буде злиття охайним чи перетвориться на полювання за примарами: таблиця вибору між merge, rebase і cherry-pick; причісування feature-гілки з автотестами перед PR через інтерактивний rebase із безпечним force-push; і бекпорт гарячого фіксу в реліз-гілку. Скрізь — що робити й чому.
Кейс 1. Таблиця рішень: merge, rebase чи cherry-pick
Половина помилок з історією — це неправильно обраний інструмент. Орієнтир простий: спочатку спитай, чи гілка спільна, а потім — що саме тобі треба перенести.
| Ситуація | Рішення | Чому |
|---|---|---|
Оновити свою особисту feature-гілку від свіжого main | rebase main | лінійна історія, коміти лише мої — переписувати безпечно |
| Оновити гілку, над якою працює ще й колега | merge main у гілку | rebase переписав би спільні коміти й розсинхронив колегу |
| Причесати 5 «wip»-комітів перед відкриттям PR | rebase -i (squash/reword/drop) | гілка ще не спільна, чиститься вільно |
Скасувати вже змерджений у main коміт | revert | main спільний — тільки новим комітом, не переписуванням |
Доставити один фікс зі main у стару реліз-гілку | cherry-pick | весь main мержити не можна, потрібен рівно один коміт |
Влити завершену фічу в main зі збереженням контексту | merge (merge commit) | видно, що робота велася паралельно, коли її звели |
Ключова розвилка — колонка «Чому»: усе, що стосується спільної гілки (main, develop або гілки з колегою), правиться лише новими комітами (merge, revert), а переписування (rebase, rebase -i) лишається для того, що бачиш лише ти. cherry-pick стоїть окремо: він не про «обʼєднати гілки», а про «взяти рівно одну зміну».
Кейс 2. Причесати гілку з автотестами перед PR
Типова картина: гілка feature/restore-password-e2e набрала хаотичних комітів, поки ти ганяв тест до зеленого. Перед ревʼю історію треба звести до одного осмисленого коміта. Гілка ще нікуди не мержилась і працюєш над нею лише ти — переписувати можна.
Стан до чистки:
a1b2c3d Додати E2E-перевірку відновлення пароля
e4f5g6h fix друкарки в локаторі
i7j8k9l WIP
m0n1o2p тимчасовий console.log
Запускаємо інтерактивний rebase на останні чотири коміти й редагуємо todo-список:
git switch feature/restore-password-e2e
git rebase -i HEAD~4
pick a1b2c3d Додати E2E-перевірку відновлення пароля
fixup e4f5g6h fix друкарки в локаторі
squash i7j8k9l WIP
drop m0n1o2p тимчасовий console.log
Що тут відбувається й чому:
fixupна «fix друкарки» зливає цей коміт у перший і викидає його сміттєве повідомлення — контексту в ньому нема.squashна «WIP» теж зливає в перший, але дає редактор, щоб звести повідомлення в одне осмислене.dropна «console.log» повністю прибирає випадковий коміт із відлагоджувальним сміттям.- Усі чотири коміти отримають нові хеші, бо змінюється ланцюжок батьків, — старий стан лишається в
reflog.
Оскільки гілку вже пушено в PR, локальна й віддалена розійшлися, і потрібен примусовий пуш — але безпечний:
# --force тут небезпечний: зніс би коміт, якби ревʼюер щось дописав у гілку,
# тому беремо lease — він відхилить пуш, якщо гілка змістилася
git push --force-with-lease
Якщо --force-with-lease відхилив пуш — це не помилка, а сигнал: у гілці зʼявився чужий коміт після твого останнього fetch. Спершу подивися, що там (git fetch + git log), і лише тоді вирішуй, як інтегрувати.
Якщо rebase пішов не так (не ті коміти, зіпсований конфлікт) — відкат по reflog:
git reflog # знаходимо хеш стану "до rebase"
git reset --hard HEAD@{5} # повертаємось рівно туди
Кейс 3. Бекпорт гарячого фіксу в реліз-гілку через cherry-pick
Реліз збирають із гілки release/2.4, а критичний фікс флакі-тесту щойно змерджено в main. Мержити весь main у реліз не можна — туди заїде купа непротестованого коду. Треба доставити рівно один коміт.
# 1) знаходимо коміт із фіксом у main
git switch main
git log --oneline -5
# a1b2c3d Виправити гонку в очікуванні тосту (flaky fix)
# 2) застосовуємо саме його на реліз-гілку зі слідом походження
git switch release/2.4
git cherry-pick -x a1b2c3d
У реліз-гілці зʼявиться новий коміт із тим самим дифом, але іншим хешем і батьком, а -x допише в повідомлення рядок (cherry picked from commit a1b2c3d).
Що дивитися й чому:
- Це не той самий коміт. Після cherry-pick фікс фізично існує у двох комітах з різними хешами — один у
main, копія вrelease/2.4. Шукати «той коміт» за хешем main у реліз-гілці марно. -xрятує від плутанини. Коли той самий фікс живе у двох гілках, рядок «(cherry picked from...)» показує, звідки він прийшов, — це прямий доказ при розборі, чому зміна є в обох місцях.- Не зловживай. Якщо бекпортити десятки комітів по одному, історія реліз-гілки заросте дублікатами, і коли гілки колись зійдуться в мержі, ці дублі дадуть конфлікти або порожні злиття. Багато комітів — привід подумати про окрему злиту гілку, а не про серію cherry-pick.
- Перевір збірку в релізі окремо. Коміт, що зелений у
main, міг спиратися на сусідні зміни, яких уrelease/2.4немає, — cherry-pick принесе лише сам діф, тож після нього обовʼязково прожени тести саме на реліз-гілці.
Merge vs rebase
- Розумію, що merge зберігає топологію й додає коміт злиття з двома батьками, а rebase відтворює коміти поверх нової бази й дає лінійну історію без коміта злиття.
- Можу пояснити, що merge нічого не переписує (старі коміти лишаються собою), а rebase створює для твоєї роботи нові коміти з новими хешами.
Механіка хешів і reflog
- Можу перелічити, що входить у хеш коміта: дерево файлів, хеш батька, автор і коммітер, повідомлення, часові мітки — і чому зміна будь-якого поля дає новий обʼєкт.
- Розумію каскад: зміна батька в одного коміта переписує всі коміти після нього, бо в кожного зміниться ланцюжок батьків.
- Знаю, що після rebase старі коміти стають недосяжними (unreachable), а не зникають одразу, і можу відкотити невдалий rebase через
git reflog+git reset --hard HEAD@{n}на стан «до».
Інтерактивний rebase
- Знаю призначення команд todo-списку:
pick,reword,squash,fixup,edit,drop. - Можу пояснити різницю
squashvsfixup: обидва зливають у попередній коміт, але fixup викидає повідомлення, а squash його зберігає й дає звести. - Розумію, що інтерактивний rebase переписує всі зачеплені коміти, навіть позначені
pick, і застосовую його переважно для причісування гілки перед PR.
Золоте правило і спільні гілки
- Можу сформулювати золоте правило: переписувати можна лише історію, що не вийшла за межі мого локального репо (чи моєї особистої гілки).
- Розумію, чому rebase спільної гілки породжує розбіжність: у колег лишаються старі коміти, синхронізація дає дублікати й конфлікти на порожньому місці.
- Знаю, що опублікований коміт скасовують
revert-ом (коміт-антидот), а не переписуванням історії.
Force push
- Розумію, чому після rebase запушеної гілки звичайний
pushвідхиляється помилкою non-fast-forward і потрібен force push. - Можу пояснити різницю
--force(затирає віддалену гілку беззастережно, зносить чужі свіжі коміти) vs--force-with-lease(оновить, лише якщо гілка там, де я її бачив), і за звичкою беру lease. - Знаю пастку
--force-with-lease: фоновийgit fetchможе «освіжити» remote-tracking посилання й приспати захист.
cherry-pick
- Можу пояснити, що cherry-pick бере один коміт і застосовує його на поточну гілку, не тягнучи всю гілку-джерело.
- Розумію, що cherry-pick створює копію коміта (новий хеш, той самий діф), тож одна зміна опиняється у двох комітах.
- Знаю головний сценарій — бекпорт фіксу в реліз-гілку — роль прапорця
-xі ризик зловживання (дублікати засмічуютьgit logі дають конфлікти при мержі).
Чиста vs правдива історія
- Можу протиставити правдиву історію (реальний хід подій, увесь шум) і чисту (лінійні атомарні коміти) як командну домовленість, а не абсолют.
- Розумію, чому чиста лінійна історія робить
git bisectіgit blameреально корисними для пошуку винуватця. - Знаю, що посилатися на коміт за хешем, поки гілку можуть перебазувати, ненадійно — хеш це ідентичність, і rebase її змінює.
Яку форму історії дає rebase порівняно з merge?
Питання
Merge vs rebase — у чому головна різниця форми історії?