Merge vs Rebase: что выбрать
Merge vs Rebase: что выбрать
Merge и rebase решают одну задачу - объединить изменения из двух веток. Но делают это принципиально по-разному: merge сохраняет реальную историю ветвления, а rebase переписывает историю так, будто ветвления не было. Выбор между ними - одна из самых горячих тем в Git, и в разных командах правила разные.
Чтобы принимать осознанное решение (а не просто делать как сказали), нужно понимать механику обоих подходов, их преимущества и ограничения.
Merge: честная история
Merge берёт два набора изменений и объединяет их в один новый коммит (merge-коммит), у которого два родителя.
# Стоим на main, хотим влить feature/auth
git switch main
git merge feature/auth
Fast-forward merge
Если main не продвинулся с момента создания ветки, Git просто переставляет указатель main вперёд. Merge-коммит не создаётся:
# Так выглядит в терминале
git switch main
git merge feature/auth
# Updating c3d4e5f..e6f7a8b
# Fast-forward
# internal/handler/auth.go | 45 ++++++++++++
# 1 file changed, 45 insertions(+)
3-way merge (true merge)
Если main и feature-ветка разошлись (в main появились новые коммиты), Git делает 3-way merge: находит общего предка, сравнивает обе ветки с ним и создаёт merge-коммит:
Если обе ветки правили одни и те же строки, 3-way merge не сможет решить сам и остановится на конфликте.
Три точки сравнения (3-way):
- Общий предок (C) - базовая версия
- main (F) - изменения в main
- feature/auth (E) - изменения в ветке
Флаг --no-ff
Иногда хочется всегда создавать merge-коммит, даже когда возможен fast-forward. Это полезно для наглядности - видно, что была feature-ветка:
git merge --no-ff feature/auth
Без --no-ff (fast-forward):
A ─── B ─── C ─── D ─── E (main)
# непонятно, что D-E были в отдельной ветке
С --no-ff:
A ─── B ─── C ──────── G (main)
\ /
D ─── E # видно, что это была feature-ветка
Rebase: линейная история
Rebase «переносит» коммиты ветки так, будто ты начал работу от последнего коммита main. Технически Git создаёт новые коммиты с тем же содержимым, но другим родителем и другим SHA-1.
# Стоим на feature/auth, ребейзим на main
git switch feature/auth
git rebase main
D' и E' - это новые коммиты. Они содержат те же изменения, что D и E, но имеют другого родителя (F вместо C) и другой SHA-1 хеш.
После rebase можно сделать fast-forward merge:
git switch main
git merge feature/auth
# Fast-forward
A ─── B ─── C ─── F ─── D' ─── E' (main, feature/auth)
Результат - линейная история без merge-коммитов.
Если всё же переписал лишнего - предыдущее состояние ветки обычно ещё лежит в reflog.
Безопасно ребейзить: свою локальную feature-ветку перед merge request. Опасно ребейзить: main, develop, или любую ветку, которую кто-то ещё использует.
Interactive rebase: наводим порядок
git rebase -i позволяет переписать серию коммитов перед тем, как отправить на review: объединить мелкие коммиты, переименовать, переставить местами, удалить.
# Интерактивный rebase последних 4 коммитов
git rebase -i HEAD~4
Git откроет редактор со списком коммитов:
pick a1b2c3d feat: add auth handler
pick d4e5f6a fix: typo in auth handler
pick g7h8i9j feat: add auth middleware
pick k0l1m2n fix: missing error check
# Rebase commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log message
# d, drop = remove commit
Squash: объединение коммитов
Типичный сценарий: ты сделал 5 коммитов в feature-ветке, два из них - исправления опечаток. Перед merge request объедини их:
pick a1b2c3d feat: add auth handler
fixup d4e5f6a fix: typo in auth handler
pick g7h8i9j feat: add auth middleware
fixup k0l1m2n fix: missing error check
Результат: 2 чистых коммита вместо 4. Merge request будет читабельнее: ревьюер видит логические шаги, а не «fix typo» пять раз.
# Быстрый squash: объединить всё в один коммит
git rebase -i HEAD~4
# Оставить первый pick, остальные заменить на fixup
Когда merge, когда rebase
Нет единственно правильного ответа - зависит от соглашений команды. Но есть общие рекомендации:
Используй merge:
- при слиянии feature-ветки в main/develop через Merge Request
- когда хочешь сохранить историю ветвления
- при работе с shared-ветками (develop, release)
Используй rebase:
- чтобы обновить свою feature-ветку последними изменениями из main
- перед созданием Merge Request - привести ветку в порядок
- чтобы убрать мусорные коммиты через interactive rebase
Типичный рабочий процесс в команде:
# 1. Создал feature-ветку от main
git switch -c feature/FL-123-auth main
# 2. Работаешь, коммитишь...
git commit -m "feat: add auth handler"
git commit -m "feat: add auth middleware"
git commit -m "fix: typo"
# 3. Перед merge request - обновляем от main и чистим историю
git fetch origin
git rebase origin/main # перенести коммиты поверх свежего main
git rebase -i HEAD~3 # объединить fix: typo с предыдущим коммитом
# 4. Push (force, потому что rebase переписал историю)
git push --force-with-lease
# 5. Создаём Merge Request в GitLab
# 6. После review - merge --no-ff в main
git pull --rebase
git pull по умолчанию делает fetch + merge. Это может создавать мусорные merge-коммиты вида «Merge branch 'main' of gitlab.com:team/project into main»:
# Обычный pull - может создать merge-коммит
git pull
# Pull с rebase - линейная история
git pull --rebase
# Настроить rebase по умолчанию для pull
git config --global pull.rebase true
Эту и другие полезные глобальные опции мы уже настраивали в уроке про конфигурацию Git.
Merge strategies
Git поддерживает несколько стратегий слияния. Обычно выбор автоматический, но иногда нужно указать явно:
# Стратегия ort (по умолчанию в Git 2.34+, раньше была recursive)
git merge feature/auth
# При конфликте - выбрать «нашу» версию для всех конфликтных файлов
git merge -X ours feature/auth
# Или «их» версию
git merge -X theirs feature/auth
# Стратегия ours: принять merge-коммит, но полностью игнорировать изменения из другой ветки
# (полезно для «закрытия» ветки без принятия её изменений)
git merge -s ours obsolete-branch
Мини-задание
- Создай ветку с 3 коммитами, вернись на main, сделай 1 коммит - теперь ветки разошлись
- Сделай merge с
--no-ffи посмотри граф черезgit log --oneline --graph - Откати merge (
git reset --hard HEAD~1), попробуй rebase той же ветки на main - Используй
git rebase -i HEAD~3чтобы объединить (squash) два коммита в один - Сравни итоговую историю merge vs rebase через
git log --graph --all --oneline