Learning Platform
Глоссарий Troubleshooting
Урок 09.01 · 20 мин
Средний
Gitrebasemergehistory

Rebase vs merge: разные философии

Каждое утро ты приходишь на работу, на main у коллег появились новые коммиты, а у тебя — своя feature ветка с тремя коммитами в работе. Чтобы синхронизироваться с main, есть два пути: merge и rebase. Они дают разный результат, разную историю, и каждый имеет своё место.

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


Стартовая ситуация

Представь: ты создал ветку feature/x от main неделю назад. С тех пор:

  • В main коллеги добавили 3 коммита (M1, M2, M3).
  • В feature/x ты сделал 3 своих (F1, F2, F3).
Стартовая ситуация: две ветки разошлись
merge base
main
feature/x

Тебе нужно “вобрать” изменения из main. Есть два пути.


Путь 1: git merge main (merge approach)

$ git switch feature/x
$ git merge main

Git создаёт merge commit (M*), у которого два родителя: твой F3 и M3 из main.

После merge: граф ветвистый, но история сохранена
После merge main

Что произошло:

  • Твои коммиты F1, F2, F3 не изменились — те же SHA, те же сообщения.
  • M1, M2, M3 не изменились.
  • Создан новый коммит M* с двумя родителями: F3 и M3.
  • M* содержит “снимок” объединённого состояния.

Плюсы:

  • История точно отражает реальность: “вот когда я мерджнул main в feature”.
  • Не теряются original SHA — ссылки на коммиты остаются валидными.
  • Безопасно для shared branches: никто не получает rewritten history.

Минусы:

  • История становится “ёлкообразной” — много веток и слияний.
  • Сложно читать git log --graph для большого проекта.
  • В feature-ветке появляется лишний merge commit “Merge branch ‘main’ into feature”.

Путь 2: git rebase main (rebase approach)

$ git switch feature/x
$ git rebase main

Git переписывает твои коммиты F1, F2, F3 — переставляет их поверх M3.

После rebase: линейная история, но твои коммиты теперь новые
После rebase main

Что произошло:

  • Twoи коммиты F1, F2, F3 переписаны как F1’, F2’, F3’ с новыми SHA. Old F1/F2/F3 потеряны (но ещё доступны через reflog).
  • Новые F1’, F2’, F3’ имеют те же изменения (по тексту), но новые parent: F1’ -> M3 (а не C0).
  • M1, M2, M3 не изменились.
  • Merge commit не создаётся.

Плюсы:

  • История линейнаяgit log читается как roadmap, без ёлок.
  • git bisect работает идеально (последовательность бинарного поиска).
  • Visual git tools показывают чистый граф.

Минусы:

  • Твои коммиты получили новые SHA. Если кто-то уже видел старые — у него теперь “старая” версия твоей ветки.
  • При конфликте — нужно разрешать его для каждого коммита в процессе rebase (не один раз, как при merge).
  • Опасно на shared branches: переписывая history, ты заставляешь всех делать сложные операции восстановления.

Что значит “новые SHA”

SHA коммита — это hash от содержимого + метаданных + parent. Если меняется любое из этого — SHA меняется. При rebase меняется parent (была C0, стала M3), значит и SHA меняется.

# До rebase
$ git log --oneline feature/x
abc1234 F3
def5678 F2
9876543 F1
c0c0c0c init

# После rebase
$ git log --oneline feature/x
fff8888 F3 новый SHA
eee7777 F2 новый SHA
ddd6666 F1 новый SHA
m3m3m3m M3
m2m2m2m M2
m1m1m1m M1
c0c0c0c init

Старые коммиты abc1234, def5678, 9876543 всё ещё в .git/objects/ (до запуска git gc), доступны через git reflog. Но указатель feature/x теперь смотрит на новый top.


Сравнение бок-о-бок

Аспектgit mergegit rebase
ИсторияСохраняется ветвистойПереписывается линейной
SHA коммитов веткиНе меняютсяМеняются (новые объекты)
Merge commitsСоздаютсяНе создаются
Безопасность на shared branchesБезопасноОпасно — нужен force push
Конфликты разрешаютсяОдин раз для всего mergeДля каждого коммита по отдельности
git bisect качествоХуже (merge commits сбивают)Лучше (линейная история)
git blame качествоТе же оригинальные коммитыМожно потерять контекст
Сложность для джунаЛегче понятьСложнее (новые SHA, конфликты на каждом шаге)

Аналогия: запись или редактирование

Хорошая аналогия — дневник vs. книга:

  • Merge = дневник. Записываешь по дням, как было. Не подчищаешь “ну я тогда передумал”. История правдива, но местами хаотична.

  • Rebase = книга. Переставляешь главы, переписываешь, шлифуешь. Финальный текст читается легко, но не отражает, в каком порядке ты на самом деле писал.

Оба варианта валидны. Команда выбирает policy: “у нас merge culture” или “у нас rebase culture”.


Pull через merge vs pull через rebase

Команда git pull тоже делает выбор:

# По дефолту pull = fetch + merge
$ git pull

# Pull = fetch + rebase (если настроено или явно)
$ git pull --rebase

# Глобально включить rebase-first pull
git config --global pull.rebase true

Если ты часто делаешь git pull на feature-ветке (что чтобы вобрать main), --rebase обычно лучше: не создаёт merge commits, история чище.

TIP

Хороший дефолт для джуна: pull.rebase = true плюс pull.ff = only. Тогда git pull либо делает fast-forward (всё ок), либо требует явный rebase. Это убирает класс ошибок “случайно создал ветвистую историю”.


Squash merge — третий вариант

В дополнение к merge и rebase, GitHub/GitLab часто предлагают третий option при merge PR: squash merge.

# Локальный эквивалент
git merge --squash feature/x
git commit -m "feat: add x feature"

Что происходит:

  • Все коммиты из feature/x объединяются в один коммит.
  • Этот один коммит ложится на main без merge-commit (как fast-forward).
  • Original ветка feature/x остаётся с её коммитами (если она не удалена).
Squash merge: 3 коммита feature становятся одним в main
до squash
после squash в main

Плюсы squash:

  • Одна “единица” в main history — легко откатить (git revert одного коммита).
  • Не загромождает main промежуточными “wip” и “fix typo” коммитами.

Минусы squash:

  • Теряется детальная история работы.
  • Авторство в git blame указывает на squash-коммит, а не на original commits.

Многие команды используют squash как дефолт для PR merge, оставляя rebase для personal sync с main.


Когда использовать что: cheat sheet

СитуацияВыбор
Синхронизировать свою личную feature ветку с main (до PR)rebase
Долгоживущий шеренгу ветку, на которой работает несколько людейmerge
Сливать готовую feature в main через PRsquash (для атомарности) или merge (для детальной истории) — зависит от team policy
Сливать release ветку в main после релизаmerge (с merge commit) — отражает событие “release X.Y”
Откатить только что сделанный mergegit reset --hard ORIG_HEAD или git revert -m 1 <merge-sha>
Подтянуть свежий main в свою ветку, чтобы тесты CI работалиrebase или merge — по vкусу команды

Demo: одна и та же ситуация двумя путями

mkdir rebase-vs-merge && cd rebase-vs-merge
git init -b main

# Базовый коммит
echo "v0" > file.txt
git add . && git commit -m "C0 init"

# Симулируем 3 коммита в main
for i in 1 2 3; do
    echo "main change $i" >> file.txt
    git commit -am "M$i: main change $i"
done

# Возвращаемся в C0 и делаем ветку feature
git switch -c feature HEAD~3

# 3 коммита в feature
for i in 1 2 3; do
    echo "feature change $i" > feature$i.txt
    git add .
    git commit -m "F$i: feature change $i"
done

# Сравним подходы — клонируем репо в отдельные папки
cd ..

cp -r rebase-vs-merge merge-demo
cp -r rebase-vs-merge rebase-demo

# Демо merge
cd merge-demo
git switch feature
git merge main
git log --oneline --graph --all
# Увидишь ромб

# Демо rebase
cd ../rebase-demo
git switch feature
git rebase main
git log --oneline --graph --all
# Увидишь линию

Открой результат каждой команды визуально (gitk --all или GitLens в VS Code), и разница в графе будет очевидна.


Feature-branch в dbt: rebase vs merge — что принято в команде
Проверка знанийKnowledge check
Команда настроила `pull.rebase = true` глобально. Junior, не зная этого, сделал на своей `feature` ветке несколько локальных коммитов и `git pull`. Pull прошёл без проблем. Чем результат отличается от того, что junior получил бы при `pull.rebase = false`, и почему важно знать настройку команды?
ОтветAnswer
С `pull.rebase = true`: команда выполнила `git fetch + git rebase origin/feature`. Это переписало локальные коммиты junior'а — их SHA изменились, и они теперь идут после коммитов, которые были на сервере. История остаётся **линейной**, без merge commits. Junior может это и не заметить — внешне всё работает. С `pull.rebase = false` (дефолт без настройки): команда выполнила `git fetch + git merge origin/feature` — Git создал бы merge commit 'Merge branch ... into feature'. История стала бы ветвистой. Если бы junior потом делал `git push`, всё бы прошло, но в feature-ветку влетел бы лишний merge commit. Почему важно знать настройку команды: (1) при rebase **меняются SHA локальных коммитов** — если junior уже их кому-то показал (например, для review), эти SHA теперь invalid. (2) Команды rebase-culture не любят merge commits в feature-ветках, у них есть branch protection / CI правила, которые могут отвергать PR с extra merge commits. (3) При rebase **конфликты разрешаются по одному для каждого коммита** — это совсем другой опыт. Junior должен спросить: 'какой workflow в команде, rebase или merge?', и настроить локально `pull.rebase` соответственно. Дополнительно, для безопасности новичков, полезен `pull.ff = only` — Git разрешит pull только если возможен fast-forward, иначе явно потребует решения rebase vs merge.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 5. Главное отличие между `git merge` и `git rebase` для синхронизации feature ветки с main?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5