Merge vs Rebase: что выбрать

Merge vs Rebase: что выбрать

Merge и rebase решают одну задачу - объединить изменения из двух веток. Но делают это принципиально по-разному: merge сохраняет реальную историю ветвления, а rebase переписывает историю так, будто ветвления не было. Выбор между ними - одна из самых горячих тем в Git, и в разных командах правила разные.

Чтобы принимать осознанное решение (а не просто делать как сказали), нужно понимать механику обоих подходов, их преимущества и ограничения.

Один сценарий: merge сохраняет историю расхождения через merge-коммит, rebase перекладывает feature-коммиты поверх main линейно

Merge: честная история

Merge берёт два набора изменений и объединяет их в один новый коммит (merge-коммит), у которого два родителя.

# Стоим на main, хотим влить feature/auth
git switch main
git merge feature/auth

Fast-forward merge

Если main не продвинулся с момента создания ветки, Git просто переставляет указатель main вперёд. Merge-коммит не создаётся:

Fast-forward merge: до - main на C, feature/auth ушёл вперёд с D-E; после - указатель main переставлен на E, без 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: общий предок C, main продолжается коммитом F, feature/auth идёт через D-E; merge-коммит G имеет двух родителей - F и E

Если обе ветки правили одни и те же строки, 3-way merge не сможет решить сам и остановится на конфликте.

Три точки сравнения (3-way):

  1. Общий предок (C) - базовая версия
  2. main (F) - изменения в main
  3. 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-ветка
При merge через веб-интерфейс (Merge Request / Pull Request) большинство хостингов по умолчанию создают merge-коммит, что эквивалентно `--no-ff`. Это стандартная практика в командах.

Rebase: линейная история

Rebase «переносит» коммиты ветки так, будто ты начал работу от последнего коммита main. Технически Git создаёт новые коммиты с тем же содержимым, но другим родителем и другим SHA-1.

# Стоим на feature/auth, ребейзим на main
git switch feature/auth
git rebase main

Rebase: до - feature/auth от общего предка C с D-E, main ушёл в F; после - D' и E' переписаны поверх F, история стала линейной

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-коммитов.

Никогда не ребейзь коммиты, которые уже запушены и которыми пользуются другие. Rebase переписывает историю - у коммитов меняются SHA-1 хеши. Если коллега основывал работу на старых коммитах, после rebase у него сломается история.

Если всё же переписал лишнего - предыдущее состояние ветки обычно ещё лежит в 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
Если нужно только поправить сообщение коммита (не последнего), замени `pick` на `reword`. Git остановится и даст отредактировать сообщение.

Когда 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 push --force` перезаписывает remote-ветку безусловно. `--force-with-lease` проверяет, что remote-ветка не изменилась с момента твоего последнего fetch. Это защищает от случайного затирания чужих коммитов.

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
`-X ours` (strategy option) применяется только к конфликтным участкам - неконфликтные изменения сливаются нормально. `-s ours` (strategy) полностью игнорирует все изменения из другой ветки. Путать их - опасно.

Мини-задание

  • Создай ветку с 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

Зарегистрируйтесь бесплатно, чтобы пройти квиз, решить задание с автопроверкой и вести прогресс.