Как на самом деле работают merge и rebase
Merge находит ближайшего общего предка (merge base), делает трёхстороннее слияние base/ours/theirs и пишет один коммит с двумя родителями — настоящая история. Rebase переигрывает твои коммиты на новой базе как новые SHA — линейная, но переписанная история.
Две ветки разошлись. Их можно примирить через git merge или git rebase, и священные войны о том, что выбрать, по большей части идут от людей, которые ни разу не посмотрели, что каждая из команд делает с графом объектов. Этот урок — про механизм, а не про флаги команд. Как только ты увидишь, что merge пишет коммит с двумя родителями, а rebase переигрывает твои коммиты как новые объекты, любое запутанное поведение — повторяющиеся конфликты, «почему сменились мои SHA», золотое правило — становится очевидным.
К концу ты сможешь объяснить merge base, почему трёхстороннее слияние его требует, и что именно rebase переписывает и почему это даёт линейную историю.
После этого урока ты сможешь объяснить merge base (ближайшего общего предка), описать, как трёхстороннее слияние объединяет base/ours/theirs в один коммит с двумя родителями, противопоставить это тому, как rebase переигрывает коммиты как новые SHA на новой базе, и сформулировать компромисс и золотое правило rebase.
Обе команды начинают с поиска merge base — ближайшего общего предка. Когда feature и main разошлись, они всё ещё разделяют историю до какого-то коммита. Merge base — это самый недавний коммит, достижимый из обеих вершин:
git merge-base main feature # -> SHA, где две ветки в последний раз совпадалиЭто точка отсчёта, относительно которой меряется всё остальное: она говорит git, какие изменения уникально твои, а какие уникально их с момента расхождения. Ошибись с базой — и не отличишь реальную правку от «это всегда тут было».
Merge делает трёхстороннее слияние: base против ours против theirs. Двусторонний дифф двух вершин не скажет, кто изменил строку, — только что они различаются. Поэтому git сравнивает три снимка: merge base, ours (текущая ветка) и theirs (вливаемая ветка). Для каждого участка файла:
- изменён только на одной стороне → берём эту сторону автоматически;
- изменён на обеих сторонах одинаково → берём;
- изменён на обеих сторонах по-разному → конфликт, ты его разрешаешь.
База — это то, что делает «изменена только одна сторона» разрешимым; без неё каждое различие выглядело бы конфликтом.
Merge пишет ОДИН коммит с ДВУМЯ родителями, сохраняя форму ветвления. Результат успешного слияния — новый merge-коммит, чей первый родитель — старая вершина main, а второй — вершина feature:
git switch main
git merge feature # -> создаёт merge-коммит, родители: [main, feature]История теперь явно ветвится и сходится обратно. Ничего не переписано: обе исходные ветки существуют дословно, и граф фиксирует, что слияние произошло, когда и что сошлось вместе. Это настоящая история — включая грязную реальность того, что две линии работы шли параллельно.
Rebase переигрывает твои коммиты один за другим на новой базе как совершенно новые коммиты. Вместо соединения веток rebase перемещает твою. git switch feature; git rebase main концептуально делает:
- Вычисляет патч каждого коммита
featureс момента merge base. - Сбрасывает
featureна вершинуmain. - Заново применяет эти патчи по порядку, создавая новый коммит на каждый — новый родитель, новая метка времени, значит, новый SHA.
Старые коммиты брошены (достижимы только через reflog). Результат — прямая линия: история main, затем твои коммиты сверху, как будто ты всё время начинал с текущего main.
git switch feature
git rebase main # переигрывает коммиты feature на вершину main▸Граничные случаи
Почему конфликты rebase могут повторяться на каждый коммит. Merge разрешает объединённое различие один раз, в одном merge-коммите. Rebase заново применяет твои коммиты по отдельности, поэтому если коммит 2 и коммит 4 оба трогают один конфликтный участок, ты можешь разрешить конфликт на коммите 2 и снова наткнуться на родственный на коммите 4 — по разу на каждый переигранный коммит. Это поэкоммитное переигрывание ровно и есть причина, по которой долгие rebase ощущаются повторяющимися, и это та проблема, ради которой существует инструмент следующего урока (rerere) — записывать и переигрывать твои разрешения.
Компромисс: настоящая история против чистой линейной — и золотое правило. Merge хранит точную, ветвящуюся запись ценой более загруженного графа. Rebase даёт опрятную, удобную для bisect прямую линию ценой переписывания коммитов в новые SHA. Это переписывание безопасно на коммитах, которые есть только у тебя, — и опасно на разделяемых:
Золотое правило rebase: никогда не делай rebase коммитов, существующих за пределами твоего локального репозитория (на которых другие могли основать работу).
Переписывание публичных коммитов даёт всем остальным расходящуюся историю и вынуждает к запутанным force-push и повторным слияниям. Делай rebase своей приватной фиче-ветки до того, как поделишься; merge — когда она уже публична.
Одно расхождение, оба исхода.
Начинаем от базового коммита B. main добавляет коммит M; твой feature добавляет F1, затем F2. Ветки разошлись, и git merge-base main feature возвращает B.
Путь merge:
git switch main && git merge feature
# новый коммит X с родителями [M, F2]; граф: B -> M -> X и B -> F1 -> F2 -> XF1, F2 и M не тронуты; X фиксирует, что они сошлись. git log --graph показывает видимый ромб.
Путь rebase:
git switch feature && git rebase main
# F1 -> F1' (родитель теперь M, новый SHA)
# F2 -> F2' (родитель теперь F1', новый SHA)
# граф: B -> M -> F1' -> F2' (одна прямая линия)Содержимое итогового дерева одинаково в обоих случаях — те же файлы. Но merge хранит четыре коммита плюс merge-коммит в развилке, тогда как rebase оставляет чистую линию из трёх коммитов, где F1 и F2 заменены копиями с новыми SHA F1'/F2'. Если бы feature уже была запушена, эти переписанные SHA столкнулись бы с копиями, которые держат другие, — нарушено золотое правило.
▸Частая ошибка
«Rebase избегает конфликтов» — это миф. Rebase и merge поднимают одни и те же конфликтующие правки — ни тот, ни другой не может волшебно объединить два несовместимых изменения одной строки. Rebase часто ощущается как больше работы с конфликтами, потому что переигрывает коммит за коммитом и может заново показать родственный конфликт несколько раз, тогда как merge разрешает всё расхождение за один проход. Выбирай rebase ради чистой истории, а не чтобы увернуться от конфликтов.
В чём ключевое структурное различие между коммитами, которые создают merge и rebase?
Merge и rebase оба начинают с merge base — ближайшего общего предка двух вершин веток. Merge запускает трёхстороннее слияние base/ours/theirs и пишет один коммит с двумя родителями, сохраняя ветвящуюся историю ровно такой, какой она была. Rebase вместо этого переигрывает твои коммиты на новой базе, создавая совершенно новые коммиты с новыми SHA и отбрасывая оригиналы, давая линейную историю ценой её переписывания. Компромисс — настоящая история против чистой, управляемый золотым правилом: никогда не делай rebase коммитов, которые могут уже быть у других. Дальше ты встретишь хуки — скрипты, которые git запускает вокруг этих операций, чтобы навязать правила твоей команды.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.