<? phpukraine СПІВБЕСІДИ
⌕Пошук по платформі
DEVOPS · JUNIOR ЧАСТО ПИТАЮТЬ

Git у команді: гілки, pull request, rebase проти merge, як не втратити правки?

Працюють у короткій feature-гілці від main, підтягують чужі зміни через rebase локально, у main вливають через pull request і merge, а втрачені після rebase чи force push коміти дістають із git reflog.

Ви зробили git pull, і ваші два коміти зникли з історії. Куди вони поділись?
Колега зробив force push у гілку, де ви теж працювали. Що робите далі?
Чим rebase відрізняється від merge і що з них ви робите у своїй feature-гілці?
У нас 300 рядків конфліктів після тижня роботи в гілці. Що зробили не так ще до конфлікту?
Git rebase merge pull request reflog конфлікти

Коміт у 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 переписує коміти (нові SHA, нові дати застосування), тому його місце - у власній локальній гілці, а не в спільній.
Розуміння, що `git pull` без налаштувань робить merge і створює зайвий «Merge branch 'main' into main»; тому ставлять `pull.rebase true` або тягнуть `git pull --rebase`.
Що конфлікт - це не помилка Git, а місце, де він відмовився вибирати; маркери `<<<<<<< HEAD` / `=======` / `>>>>>>>` треба вміти прочитати, зокрема знати, що під час rebase «ours» - це вже цільова гілка, а не ваша.
Що `git reflog` тримає локальну історію переміщень HEAD (за замовчуванням 90 днів) і будь-який «втрачений» коміт дістається через `git reset --hard HEAD@{N}` або `git cherry-pick <sha>`.
Що у спільну гілку пушать `--force-with-lease`, а не `--force`, і що коротка гілка з одним PR дешевша за довгу з десятком мержів.
Казати «rebase кращий за merge» без уточнення, де саме: rebase у main, куди вже пушили інші, переписує чужу історію.
Після невдалого rebase робити `git checkout .` або видаляти теку проєкту й клонувати заново, замість `git rebase --abort`.
Вважати, що конфлікт зник, якщо прибрати маркери `<<<<<<<` і `>>>>>>>`: файл треба ще й перечитати, бо логіка могла зламатися без синтаксичної помилки.
Робити `git push --force` у гілку з PR і затирати коміти колеги, який туди щось довантажив.
Тримати feature-гілку тиждень і потім розв'язувати один величезний конфлікт замість щоденного `git pull --rebase origin main`.
Комітити з `git commit -am` після конфлікту й ламати rebase: у ньому продовжують через `git rebase --continue`, а не звичайним комітом.
ПОРАДА

Скажіть коротко: «rebase - локально, merge - у main». Далі додайте, що жодні коміти в Git не зникають самі: поки не пройшов `git gc`, усе лежить у reflog, і ви знаєте, як їх звідти дістати. Інтерв'юер почує людину, яка вже виплутувалась із зіпсованої історії, а не переказ туторіалу.

оновлено 30 вересня 2026 · ліцензія CC-BY-SA-4.0 Знайшли неточність? Напишіть →
ПЕРЕВІРТЕ СЕБЕ

rebase створює нові SHA, тому безпечний лише в гілці, яку крутить одна людина; спільну історію main змінювати не можна, туди вливають merge-комітом через PR.