Коміт у Git - це знімок дерева файлів плюс посилання на батьківський коміт, і SHA обчислюється з усього цього разом. З цього випливає решта: змінити базу коміту, не отримавши при цьому інший коміт, неможливо. git merge цього й не намагається робити - він створює один новий коміт із двома батьками, а обидві історії лишаються на місці зі своїми SHA. git rebase натомість бере зміни ваших комітів як патчі й прикладає їх по черзі поверх нової бази. Робота виглядає тією самою, але об'єкти вже інші: старі коміти нікуди не діваються, просто на них більше ніщо не вказує, і вони лежать у сховищі до наступного git gc.
Звідси робоче правило: rebase - у гілці, яку крутите тільки ви, merge - у спільній. Локальний git rebase origin/main щодня тримає вашу гілку близько до main і робить конфлікти дрібними: ви розв'язуєте розбіжність за один день роботи, а не за тиждень. Той самий rebase у main, куди вже пушила решта команди, переписує спільну історію: у колег лишаються коміти зі старими SHA, і після вашого force push їхній git pull змержить дві версії того самого й продублює коміти. У main зміни потрапляють через pull request - рев'ю, зелений CI, потім merge. У trunk-based механіка та сама, інша тривалість життя гілки: години або день замість тижнів, з незакінченою функціональністю за feature flag. Довгоживучих develop і release/* там просто немає.
Конфлікт з'являється там, де обидві сторони змінили той самий діапазон рядків і Git відмовився вибирати замість вас. Маркери читаються так: між <<<<<<< і ======= стоїть «ours», між ======= і >>>>>>> - «theirs». Пастка для junior саме тут. Під час rebase «ours» - це та гілка, на яку ви перебазовуєтесь (зазвичай origin/main), а ваш власний код виявляється в «theirs», бо Git по черзі прикладає ваші коміти поверх уже наявного стану. При merge розподіл протилежний. git config merge.conflictStyle zdiff3 додає в блок спільного предка, і тоді видно, що конкретно змінила кожна сторона, а не лише два фінальні варіанти. Прибрати маркери недостатньо: після кожного розв'язаного файла код треба перечитати цілком, бо два коректні шматки легко дають зламану логіку без жодної синтаксичної помилки. Далі git add файла і git rebase --continue; звичайний git commit у цьому стані ламає послідовність. Якщо стало ясно, що rebase був помилкою, git rebase --abort повертає гілку в точний вихідний стан.
Тепер про «зникли правки». git reflog - локальний журнал усіх переміщень HEAD: кожен commit, checkout, rebase, reset залишає там запис із SHA, який зберігається за замовчуванням 90 днів для досяжних комітів і 30 для недосяжних (gc.reflogExpire, gc.reflogExpireUnreachable). Після зіпсованого rebase знаходите у виводі рядок на кшталт HEAD@{5}: commit: Add renderer і робите git reset --hard HEAD@{5}; якщо потрібен один коміт - git cherry-pick <sha>. Складніший випадок: форс-пуш зробив колега і ваш git pull впав. Спершу git branch backup/my-work, щоб прив'язати свої коміти до імені, потім git fetch origin без merge, подивитись git log --oneline origin/feature-x, і вже свідомо вибирати між git rebase origin/feature-x та git reset --hard origin/feature-x з наступним cherry-pick своїх комітів із backup-гілки. git pull навпомання в такій ситуації дає дублікати комітів.
Схема має межу: reflog локальний, тож на нього не можна покластися, якщо хтось затер коміти, яких у вас ніколи не було в клоні. Тому спільні гілки пушать із --force-with-lease: він відхиляє push, якщо віддалений вказівник змінився після вашого останнього fetch, тобто ловить саме той випадок, коли --force тихо зносить чужу роботу. На рівні репозиторію те саме закривають захистом гілки на GitHub чи GitLab: заборона force push у main, обов'язковий PR і зелений CI перед мержем. Залишається компроміс, який кожна команда вирішує сама: squash-merge дає чисту історію main і придатний git bisect, але робить гілку непридатною для продовження роботи, бо після нього коміт у main має інший SHA, і наступний rebase покаже ті самі зміни як конфлікт.
# 1. Коротка гілка від свіжого main - основа trunk-based
git switch main && git pull --rebase
git switch -c feature/invoice-pdf
# 2. Щодня підтягуємо main під себе. rebase, бо гілка ще тільки моя.
git fetch origin
git rebase origin/main
# Конфлікт: "ours" = origin/main, "theirs" = мій коміт (Git кладе мої поверх)
git status # список файлів у стані both modified
git config merge.conflictStyle zdiff3 # показувати ще й спільного предка
# ...правимо файл, прибираємо маркери, перечитуємо логіку...
git add app/Invoice/PdfRenderer.php
git rebase --continue # НЕ git commit
# git rebase --abort # якщо все пішло не туди: повернутись як було
# 3. Пуш переписаної гілки - тільки з перевіркою чужих комітів
git push --force-with-lease origin feature/invoice-pdf
# 4. У main - через pull request і merge, без rebase спільної історії
# 5. "Коміти зникли" після rebase чи reset - вони в reflog
git reflog # HEAD@{0}: rebase (finish), HEAD@{5}: commit: Add renderer
git reset --hard HEAD@{5} # повернути гілку на стан до rebase
git cherry-pick 9a3f1c2 # або витягти один втрачений коміт за SHA
# 6. Страховка перед будь-якою ризикованою операцією
git branch backup/before-rebase # ім'я тримає коміти живими
Скажіть коротко: «rebase - локально, merge - у main». Далі додайте, що жодні коміти в Git не зникають самі: поки не пройшов `git gc`, усе лежить у reflog, і ви знаєте, як їх звідти дістати. Інтерв'юер почує людину, яка вже виплутувалась із зіпсованої історії, а не переказ туторіалу.