Две ветви истории Git соединяются с помощью merge и выстраиваются через rebase
SaaS и инструменты

Git merge и rebase: как объединять ветки и не терять историю

Подробно сравниваем merge, rebase, fast-forward и squash: граф коммитов, конфликты, безопасная отмена, общие ветки и выбор стратегии для команды.

Содержание

git merge и git rebase решают одну общую задачу: переносят изменения из одной линии разработки в другую. Разница в истории. Merge сохраняет существующие ветви и связывает их, а rebase создаёт новые версии коммитов поверх другой базы, делая линию визуально прямой.

Выбирать команду по красоте графа опасно. Сначала нужно понять, опубликованы ли коммиты, работает ли с ними кто-то ещё и нужна ли отдельная точка объединения. В собственной ветке rebase часто упрощает подготовку изменений. Для общей ветки merge сохраняет уже известную участникам историю.

Ветка — это ссылка, а не отдельная папка

Commit Git хранит снимок проекта и ссылки на родительские коммиты. Идентификатор commit вычисляется из его содержимого и метаданных. Ветка — подвижное имя, указывающее на один commit.

Представим историю:

A---B---C  main
     \
      D---E  feature

Ветка feature началась от B. После этого в main появился C, а в feature — D и E. Файлы двух веток могли изменяться независимо, но Git объединяет не каталоги, а результаты относительно общего предка B.

Перед любым объединением сохраните текущую работу commit или stash и убедитесь, что понимаете, на какой ветке находитесь. Базовые команды создания и отправки веток разобраны в статье о Git для начинающих.

Когда merge выполняется без отдельного commit

Если целевая ветка не менялась после ответвления, Git просто передвигает её указатель вперёд:

A---B          main
     \
      D---E    feature

Если main не изменялся после ответвления feature, merge достаточно передвинуть ссылку main на E. Получится такой граф:

A---B---D---E  main, feature

Это fast-forward, то есть быстрое продвижение. Новый commit объединения не нужен, потому что E уже содержит B в своей цепочке родителей.

Fast-forward не означает, что Git пропустил проверку содержимого. Он означает только форму графа. Если команда хочет всегда видеть границу feature, можно запретить fast-forward для конкретного merge:

git switch main
git merge --no-ff feature

Команды выполняются в локальном репозитории с чистым рабочим деревом. git switch main выбирает целевую ветку, а --no-ff создаёт merge commit даже при возможности простого продвижения. Перед выполнением обновите сведения об удалённой ветке и следуйте правилам проекта; создание лишних merge commit может противоречить принятой стратегии.

Что создаёт обычный merge

В исходном примере main и feature разошлись. Merge создаёт commit M с двумя родителями:

A---B---C-------M  main
     \         /
      D---E----/   feature

Существующие C, D и E не меняются. M фиксирует результат совместного применения изменений. По истории видно, какие commits разрабатывались параллельно и где ветка вошла в main.

Для объединения feature в main:

git status
git switch main
git merge --no-ff feature

git status должен показать чистое рабочее дерево. Merge изменит файлы и создаст commit, если нет конфликтов и не требуется редактирование сообщения. После команды запустите тесты проекта и просмотрите git diff HEAD^1..HEAD: диапазон показывает, что добавил merge относительно первого родителя.

Если результат ещё не отправлен и оказался неверным, безопаснее создать исправляющий commit или вернуть merge командой git revert -m 1 ИДЕНТИФИКАТОР. Перемещение ветки назад допустимо только для локальной неопубликованной истории и требует отдельного осознанного решения.

Почему возникает конфликт

Git автоматически объединяет изменения, когда может однозначно определить результат. Конфликт возникает не потому, что две ветки существуют, а потому, что совместный результат неоднозначен: обе изменили одну область, одна удалила файл, который другая редактировала, или произошло несовместимое переименование.

В конфликтном файле Git оставляет маркеры:

  <<<<<<< HEAD
вариант целевой ветки
  =======
вариант присоединяемой ветки
  >>>>>>> feature

Это не два ответа, между которыми всегда нужно выбрать один. Часто правильный результат сочетает обе идеи или переписывает участок. Сначала выясните назначение изменений по commits и тестам, затем удалите все маркеры и сформируйте рабочий код.

Список конфликтов показывает:

git status
git diff --name-only --diff-filter=U

Вторая команда выводит только файлы с неразрешёнными конфликтами. После ручного исправления проверьте содержимое, выполните git add ПУТЬ для каждого готового файла и завершите merge командой git commit. Если объединение начато ошибочно, не разрешайте конфликты наугад:

git merge --abort

Команда пытается восстановить состояние до merge. Незакоммиченные изменения, существовавшие до начала, могут усложнить восстановление — поэтому чистое дерево было обязательным условием.

Что делает rebase с тем же графом

Rebase берёт commits, уникальные для текущей ветки, и воспроизводит их поверх выбранной базы. Если находиться в feature и выполнить rebase на main, D и E станут новыми D’ и E’:

A---B---C            main
         \
          D'---E'    feature

Старые D и E не переносятся буквально. У новых commits другие родители, время создания и идентификаторы, даже если итоговый diff похож. Поэтому rebase называют переписыванием истории.

Для собственной локальной feature:

git status
git switch feature
git rebase main

После чистого git status выбирается feature, затем её commits воспроизводятся поверх main. Успех — команда завершилась без ошибок, тесты проходят, а git log --graph --oneline --decorate показывает feature поверх main.

Теперь fast-forward merge feature в main не требует отдельного merge commit. Цена линейной истории — потеря исходной формы параллельной разработки и новые идентификаторы commits.

Конфликты при rebase повторяются по commit

Merge собирает совместный результат в одной операции. Rebase применяет commits по очереди, поэтому конфликт может возникать несколько раз в похожем месте.

После исправления текущего конфликта:

git status
git add ПУТЬ_К_ИСПРАВЛЕННОМУ_ФАЙЛУ
git rebase --continue

Замените плейсхолдер реальным путём. git add отмечает разрешённый результат, --continue переходит к следующему commit. Не используйте git commit вместо continue без причины: rebase сам создаёт переписанный commit.

Если выбран неверный подход:

git rebase --abort

Ветка вернётся к позиции до начала операции. git rebase --skip пропускает текущий commit целиком и может незаметно удалить нужное изменение; применять его можно только после проверки, что изменение уже присутствует или действительно не нужно.

Почему нельзя без согласования rebase общей ветки

Допустим, вы отправили D и E, а коллега создал поверх E commit F. После вашего rebase удалённая история содержит новые D’ и E’, а локальная копия коллеги — старые D, E и F. Обычный push отвергается, потому что ветви разошлись.

Принудительная отправка способна убрать F из видимой ветки. Даже более осторожный --force-with-lease защищает лишь от обновления удалённой ссылки, которого ваша копия не видела; он не превращает переписывание общей истории в безопасную норму.

Практичное правило:

  • rebase своей ветки до review допустим, если команда это ожидает;
  • после начала совместной работы предупреждайте участников;
  • защищённую main не переписывайте;
  • опубликованную ошибку исправляйте новым commit или revert;
  • автоматизация должна ссылаться на неизменный commit SHA, а не только имя ветки.

Если платформа выполняет squash merge или rebase merge при принятии Merge Request, это отдельная серверная операция. Локальную ветку не обязательно заранее превращать в точно такой же граф.

Interactive rebase готовит понятную серию commits

Интерактивный rebase позволяет изменить порядок, объединить и переименовать собственные commits. Например, подготовить последние четыре:

git status
git rebase -i HEAD~4

Команда откроет редактор со списком. pick сохраняет commit, reword меняет сообщение, squash объединяет с предыдущим с редактированием сообщения, fixup объединяет и обычно отбрасывает отдельное сообщение.

Операция переписывает эти commits. До запуска убедитесь, что они принадлежат только вашей ветке и есть понятная точка возврата. Полезно временно создать резервную ссылку:

git branch backup/feature-before-rebase

Она не копирует файлы, а сохраняет указатель на текущий commit. После проверки обновлённой истории ветку backup можно удалить обычной командой удаления ветки. Не отправляйте временную ссылку без необходимости.

Reflog помогает найти прежнюю позицию

Reflog хранит локальный журнал перемещений ссылок. Он часто позволяет найти commit до ошибочного rebase или reset:

git reflog --date=local

Команда только читает локальный журнал. Найдите запись перед операцией и сначала осмотрите её через git show ИДЕНТИФИКАТОР. Затем можно создать от неё новую ветку:

git branch recovery/feature ИДЕНТИФИКАТОР

Это безопаснее немедленного reset: текущая ветка остаётся на месте, а найденный commit получает отдельное имя. Reflog локален и очищается со временем, поэтому не является резервной копией удалённого репозитория.

Merge, rebase и squash отвечают на разные вопросы

Merge commit сохраняет границу и факт совместного объединения. Он полезен, когда feature представляет значимую единицу работы или нужно видеть исходные commits.

Rebase переносит серию собственных commits на новую базу. Он удобен для подготовки ветки, последовательного review и уменьшения вспомогательных merge из main.

Squash merge создаёт в целевой ветке один commit с итогом feature. История main становится компактной, но отдельные шаги ветки не попадают в неё как родители. Исходная ветка и обсуждение могут сохраниться на платформе, но обычный Git-граф main их не содержит.

Fast-forward просто двигает ссылку. Он возможен, когда целевая ветка уже является предком источника.

Нет универсально лучшего режима. Команде нужна одна документированная стратегия, чтобы авторы понимали, будет ли важна структура их commits после принятия изменения.

Практичная стратегия для небольшой команды

Один из понятных вариантов:

  1. main защищена и принимает изменения только через review;
  2. разработчик делает небольшие commits в feature;
  3. перед завершением review обновляет feature через rebase на свежую main;
  4. CI проверяет новый commit SHA;
  5. платформа выполняет squash merge для небольшой задачи или merge commit для большой самостоятельной ветки;
  6. после публикации исходная ветка удаляется;
  7. исправление production выполняется revert или новым commit, без переписывания main.

Если требования аудита предполагают сохранение каждого проверенного commit, squash может не подойти. Если команда ценит поиск причины через git bisect, каждый сохранённый commit должен собираться и проходить тесты.

Перед объединением используйте Git-проверки проекта. Для разницы между самим Git и платформами review прочитайте разбор GitHub.

Источники

Рекламное местоВаша компания здесьРазместить рекламу

Самопроверка

Проверьте, что материал усвоен

Ответьте на все вопросы. Результат сохранится только в этом браузере и будет учтён в статистике прочитанных материалов.

01Почему rebase общей опубликованной ветки опасен?
02Какую команду использовать для отмены незавершённого merge с конфликтами?

Разбираем коротко

Частые вопросы

Что безопаснее для общей ветки: merge или rebase?

Merge сохраняет существующие коммиты и обычно безопаснее для уже опубликованной общей ветки. Rebase переписывает коммиты, поэтому его применяют к собственной ветке до совместной работы или после согласования с командой.

Удаляет ли git merge исходную ветку?

Нет. Merge добавляет связь историй, но ссылка на исходную ветку остаётся. Её удаляют отдельно после проверки объединённого результата.

Можно ли отменить конфликтный rebase?

Да. Пока rebase не завершён, команда git rebase --abort возвращает ветку в состояние до его начала. После завершения исходную позицию обычно находят через reflog.