Merge и rebase

Материал из llmdocs — справочник разработчика
Портал: Git, CI/CD, DevOps
Merge и rebase
Командыgit merge, git rebase, git cherry-pick
Стратегия слияния по умолчаниюort (с Git 2.34, 2021), ранее recursive
Виды mergefast-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 в ветку релиза.

См. также

Источники

  1. Pro Git — 3.2 Basic Branching and Merging
  2. Pro Git — 3.6 Rebasing
  3. git-merge documentation
  4. git-rebase documentation
  5. Git 2.34 release notes (ort default)
  6. Pro Git — 7.6 Rewriting History