git merge vs rebase: покинул ли коммит вашу машину?
Одно дерево, две истории — стендовый прогон, где 5d48940 вернулся как 6b2be0a, предупреждение про общую ветку и выход через reflog, когда побеждает не тот вариант.
■ space · проза
Один вопрос всё решает: покинул ли коммит вашу машину? Непушнутая работа → rebase. Всё, что другие могли скачать → merge.
Merge записывает, что произошло — один коммит, два родителя, ни один SHA не меняется. Rebase переписывает: в стенде ниже коммит 5d48940 вернулся как 6b2be0a.
Rebase пошёл не туда? git reflog + git reset --hard HEAD@{1} вернёт ветку. Ничего не пропало.
ДВЕ ИСТОРИИ
Обе команды приносят один и тот же код; они спорят о том, что история должна рассказывать после. Я прогнал обе в одноразовом стенде на git 2.47.3, каждый раз одни и те же пять коммитов — прогон merge сохранил каждый исходный SHA и добавил один узел; прогон rebase сменил личность фичевых коммитов. То же дерево, другая история:
fig — «примы» — это те же изменения с новыми ID коммитов: c3 вошёл как 5d48940 и вышел как 6b2be0a
проверка: git log --oneline --graph → узел "|\\\\" означает merge, прямая линия означает, что победил rebase
КОГДА MERGE СТАНОВИТСЯ FAST-FORWARD
Если main не двигался с момента создания ветки, сливать нечего — git просто сдвигает метку вперёд. Это и есть fast-forward, и поэтому сторонники rebase получают свою чистую линию без всякого принуждения: сделайте rebase фичи на main, затем merge станет fast-forward, и граф останется одной прямой линией. Из стенда — пять коммитов, ноль узлов:
* 67c0442 c4 feature 2 # rebased, then merged — fast-forward
* 6b2be0a c3 feature 1 # new SHAs — 5d48940 before the rebase
* 1398836 c5 main moved on
* facd5a4 c2 main work
* f45b000 c1 base
проверка: git merge --ff-only feature → "Fast-forward" в выводе, merge-коммит не создан
Цена сделки: эти «примы»-SHA больше нигде не существуют, кроме вашей машины. Запушьте их — и клон коллеги всё ещё хранит оригиналы — две версии «того же самого» коммита.
КАКИЕ КОММИТЫ ПОКА ЕЩЁ ТОЛЬКО ВАШИ?
Ответ — одна команда, а не ощущение. git log origin/main..HEAD перечисляет коммиты, которые origin никогда не видел — именно их можно ребейзить. Если в списке есть коммит, который другой клон может уже иметь — пуш, открытый pull request, ветка, которую вы пушили и форс-пушили раньше, — он не только ваш, что бы ни обещал локальный граф. Непушнутая работа — ровно то, для чего сделан rebase: переиграйте её поверх обновлённой базы, и ваш pull request читается как одна чистая последовательность. Поэтому же git pull --rebase — разумное значение по умолчанию для приватных веток: он переигрывает ваши локальные коммиты поверх пришедшего, вместо того чтобы плести узел merge при каждой синхронизации. Проверяйте ветку, а не чутьё: сначала git fetch origin, потом git status -sb для пары впереди/позади.
ЧТО ЛОМАЕТСЯ НА ОБЩЕЙ ВЕТКЕ
Каждый клон со старыми коммитами теперь расходится с вашим в вопросе реальности. Rebase даёт вашим коммитам новые SHA; запушнутая ветка и переписанная ветка больше не имеют общего предка, поэтому следующий пуш потребует --force — и все, кто скачал старую версию, будут сливать свою копию коммитов, которые «больше не существуют». Их дубликаты всплывают в каждом будущем merge; распутывать это — минус вечер. Комментарии ревьюеров, привязанные к конкретным SHA, отрываются тем же движением — ребейзьте открытый pull request, и контекст ревью испарится вместе со старыми ID коммитов. Правило длиной в одно предложение, и официальная книга несёт его как предупреждение, а не как совет:
Не ребейзьте коммиты, которые существуют вне вашего репозитория и на которых люди могли построить свою работу.— Pro Git, 2-е издание, "Git Branching — Rebasing"
ОТМЕНИТЬ REBASE
Старые коммиты переживают rebase — reflog — их укрытие. Rebase двигает указатели веток; он не удаляет объекты. Восстановление из стенда после того, как reset --hard выбросил два коммита:
$ git reflog | head -4
1398836 HEAD@{0}: reset: moving to HEAD~2
67c0442 HEAD@{1}: merge feature: Fast-forward
1398836 HEAD@{2}: checkout: moving from feature to main
67c0442 HEAD@{3}: rebase (finish): returning to refs/heads/feature
$ git reset --hard HEAD@{1}
HEAD is now at 67c0442 c4 feature 2
Одна строка — ветка восстановлена, вместе с ней индексированная и неиндексированная работа. Записи reflog стареют примерно за 90 дней по умолчанию, так что у спасения есть часы.
проверка: git reflog | head -3 → кончик до rebase сидит в HEAD@{1}, в одном reset отсюда
ШЕСТЬ СИТУАЦИЙ
| Ситуация | Команда | Почему выигрывает |
|---|---|---|
| Ваша фичевая ветка, ни разу не запушена | git rebase main | Линейная история, одна чистая последовательность для ревью |
| Ветка, с которой другие скачивают | git merge | Без переписывания SHA — без коммитов-дубликатов ни в чьём клоне |
| Локальный main разошёлся, ничего не запушено | git pull --rebase | Синхронизирует без узла; ваши коммиты остаются сверху |
| Форк догоняет upstream | git merge upstream/main | PR остаётся при своих коммитах |
| Хотфикс, который должен попасть прямо сейчас | git merge --no-ff | Видимая метка места входа фикса, даже на fast-forward-способной ветке |
| Rebase пошёл не туда | git reset --hard HEAD@{1} | Reflog всё ещё хранит кончик до rebase (~90 дней) |
Куда вписывается --squash? Он забирает изменения ветки и ставит их в индекс одним некоммитнутым куском — без merge-коммита, без связи с историей ветки. Squash — третий ответ, для выбрасывания ветки после того, как её чистое изменение попало в основу; он ничего не переписывает, он просто отказывается записывать внутренние шаги ветки. И может ли команда выбрать что-то одно? Многие выбирают: rebase для локальной работы, merge в общее — самая частая политика, а «ребейзь всё» на весь репозиторий тоже работает — пока ни одна общая ветка никогда не переписывается, это единственное предложение, которое переживает каждый спор о workflow. Версия на десять секунд, в последний раз: покинул ли коммит вашу машину? Нет → rebase. Да → merge. Когда всё же побеждает не тот, у reflog есть выход.
конец двух историй · ничего не скачано · всё локально