Merge и rebase
| Команды | git merge, git rebase, git cherry-pick |
|---|---|
| Стратегия слияния по умолчанию | ort (с Git 2.34, 2021), ранее recursive |
| Виды merge | fast-forward, трёхсторонний (merge commit), squash |
| Интерактивный режим | git rebase -i |
| Повторное использование решений конфликтов | git rerere |
| Главное правило | не переписывать опубликованную общую историю |
Merge (слияние) и rebase (перебазирование) — два способа перенести изменения из одной ветки Git в другую. Оба дают одинаковый итоговый код, но по-разному записывают историю. git merge соединяет две линии разработки, сохраняя их как есть, и при необходимости создаёт коммит слияния с двумя родителями.[1] git rebase берёт коммиты ветки и заново применяет их поверх другой, как будто работа начиналась с её последнего состояния, — история получается линейной.[2] Выбор между ними — один из главных вопросов командных договорённостей о работе с ветками (см. Ветвление в Git).
Merge
Предположим, ветка feature ответвилась от main, и с тех пор в обеих появились новые коммиты.
A---B---C feature
/
D---E---F---G main
git switch main && git merge feature находит общего предка (merge base, здесь E), сравнивает с ним обе вершины и создаёт коммит M, объединяющий изменения:
A---B---C
/ \
D---E---F---G---M main
Это трёхстороннее слияние (three-way merge). Если же в main новых коммитов не было, Git просто передвигает указатель ветки вперёд — это fast-forward, без коммита слияния. Флаг --no-ff заставляет создать коммит слияния и в этом случае, чтобы в истории было видно, что изменения пришли пачкой из отдельной ветки; --ff-only наоборот — разрешает только перемотку.[3]
С версии Git 2.34 (2021) по умолчанию используется стратегия слияния ort — переписанная и заметно более быстрая замена прежней recursive.[5]
Squash-слияние (git merge --squash) собирает все изменения ветки в один обычный коммит без ссылки на исходные — так часто вливают pull request, чтобы в основной ветке был один коммит на задачу.
Rebase
git switch feature && git rebase main вычисляет изменения коммитов A, B, C и по очереди применяет их поверх G:
D---E---F---G main
\
A'--B'--C' feature
A', B', C' — новые коммиты с тем же содержанием изменений, но другими родителями и хешами. После этого main можно перемотать на C' через fast-forward, и история останется прямой линией.
Интерактивный rebase
git rebase -i HEAD~5 открывает список последних коммитов, в котором для каждого можно выбрать действие:[4][6]
| Команда | Действие |
|---|---|
pick |
оставить как есть |
reword |
изменить сообщение |
edit |
остановиться, чтобы поправить коммит |
squash / fixup |
слить с предыдущим (с сохранением сообщения или без) |
drop |
удалить коммит |
Строки можно переставлять. Так перед публикацией приводят в порядок историю: убирают коммиты «опечатка», «ещё фикс», объединяют связанные изменения. Связка git commit --fixup <hash> и git rebase -i --autosquash делает это почти автоматически.
Главное правило rebase
Не перебазируйте коммиты, которые уже опубликованы и на которых кто-то основывает свою работу.[2] Rebase создаёт новые коммиты вместо старых; у коллег останутся старые версии, и при следующей синхронизации история раздвоится. Переписывать свою личную ветку можно, но отправлять её придётся с принудительной перезаписью — безопаснее вариантом git push --force-with-lease, который откажется затирать чужие коммиты, появившиеся на сервере.
Конфликты
Если обе ветки изменили одни и те же строки, Git останавливается и размечает файл маркерами <<<<<<<, =======, >>>>>>>. Дальше нужно отредактировать файл, выполнить git add и продолжить: git merge --continue или git rebase --continue. Отменить всё — git merge --abort / git rebase --abort.
При merge конфликты решаются один раз для всего слияния, при rebase — отдельно для каждого переносимого коммита, что бывает утомительно. Помогает механизм rerere (reuse recorded resolution, git config rerere.enabled true): Git запоминает, как был решён конфликт, и повторяет решение автоматически.
Что выбрать
| Merge | Rebase | |
|---|---|---|
| История | истинная, с ветвлениями | линейная, «как будто последовательно» |
| Хеши коммитов | не меняются | меняются |
| Безопасность для общих веток | безопасен | только для личных веток |
Чтение git log |
сложнее при множестве веток | проще |
Поиск ошибки git bisect |
работает | работает удобнее на линейной истории |
Распространённая практика: обновлять свою рабочую ветку из main через rebase (git pull --rebase), а в основную ветку вливать через merge или squash-merge при закрытии pull request. Командам стоит договориться о едином стиле и закрепить его настройками репозитория на GitHub или в GitLab.
Cherry-pick (git cherry-pick <hash>) — родственная операция: переносит отдельный коммит в текущую ветку, например исправление ошибки из main в ветку релиза.