linuxlab.io
Учебники▾
  • Линукс и сети
    Файловая система, процессы, TCP/IP, BGP и OSPF
    →
  • Terraform и IaC
    HCL, state, plan/apply на sandbox LocalStack
    →
  • Git и GitHub
    Объектная модель, plumbing, ветвление, GitHub Actions
    →
Все учебники →
ЦеныО платформеВойтиСоздать аккаунт
/
Intro
Lessons
Footer
linuxlab-УчебникиЦеныО платформеКонфиденциальность и куки
Copyright © 2026 LinuxLab. Все права защищены.
linuxlab.io
Учебники▾
  • Линукс и сети
    Файловая система, процессы, TCP/IP, BGP и OSPF
    →
  • Terraform и IaC
    HCL, state, plan/apply на sandbox LocalStack
    →
  • Git и GitHub
    Объектная модель, plumbing, ветвление, GitHub Actions
    →
Все учебники →
ЦеныО платформеВойтиСоздать аккаунт
/
  • Введение
  • Главы
  • How it works
  • Уроки
  • База знаний
  • Собеседование
Часть III — Ежедневный Git

$ глава 8 · 55 минут

Merge vs rebase: главная развилка

Самая частая холивар-тема в Git. «Только merge», «только rebase», «squash всё». На самом деле это два разных инструмента, и зрелые команды используют оба - в разных ситуациях.

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

Дополнительная важная команда - cherry-pick. Технически она не merge и не rebase, но логически живёт в той же экосистеме «как перенести изменения с одной ветки на другую».

8.1 Three-way merge: что внутри

Сценарий: на main есть свои коммиты после ветвления feat. На feat - тоже свои. Fast-forward невозможен (см. главу 6) - нужно настоящее слияние.

main:  A → B → E              ← на main был коммит E после развилки
          \
feat:      C → D              ← на feat был C, D после развилки

Алгоритм three-way merge:

  1. Найти общего предка двух веток (merge base). Здесь - коммит B.
  2. Сравнить B с E: какие правки сделали на main.
  3. Сравнить B с D: какие правки сделали на feat.
  4. Объединить оба набора правок. Если правки на разных строках/файлах - без конфликта. Если на одних - конфликт, нужно разрешить руками.
  5. Создать merge-коммит M с двумя родителями: E и D.
main:  A → B → E → M          ← merge-коммит, два родителя: E, D
          \      /
feat:      C → D

Merge-коммит - единственный тип коммита в Git, у которого больше одного родителя. У обычного two-way merge их два, у octopus merge (слияние трёх и более веток за раз) - три и больше. По нему всегда видно, что здесь несколько историй слились.

bash
git switch main
git merge feat
# Merge made by the 'ort' strategy.

Ort - это стратегия слияния по умолчанию с Git 2.34. Раньше была recursive. Разница в скорости и обработке сложных случаев; на 99% слияний результат идентичный.

См. merge.

8.1.1 Копнуть глубже: алгоритм слияния по строкам

Three-way merge на уровне отдельного файла сравнивает три версии: base (общий предок), ours (текущая ветка), theirs (сливаемая).

Для каждого блока строк решается:

  • base = ours = theirs → нет изменений, оставить как есть.
  • base = ours, theirs ≠ base → правка только на theirs, взять theirs.
  • base = theirs, ours ≠ base → правка только на ours, взять ours.
  • ours = theirs, оба ≠ base → обе ветки внесли одну и ту же правку, конфликта нет, взять любую.
  • ours ≠ theirs, оба ≠ base → конфликт, нужно решать руками.

В пятом случае Git ставит маркеры:

<<<<<<< HEAD
print("из main")
=======
print("из feature")
>>>>>>> feature

Это сырой материал - выбирай нужный вариант, удаляй маркеры, git add файл, git commit. Полезные опции:

  • git checkout --ours file.txt - принять полностью версию текущей ветки.
    • git checkout --theirs file.txt - принять версию сливаемой.
    • git merge --abort - отменить начатый merge, вернуться в исходное состояние.

8.2 Rebase: переписать историю

Rebase решает ту же задачу - «совместить две ветки» - но по-другому. Вместо создания merge-коммита, он переписывает коммиты одной ветки, делая вид, что они с самого начала ехали поверх другой.

до:    main:  A → B → E
                 \
       feat:      C → D
git switch feat
git rebase main
после: main:  A → B → E
                        \
       feat:              C' → D'    ← новые SHA, та же логика

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

  1. Git нашёл общего предка (B), как и для merge.
  2. Запомнил коммиты ветки feat после развилки: C, D.
  3. Откатил feat на main (точку E).
  4. Применил каждый запомненный коммит по очереди. Получились новые коммиты C' и D' - с другими SHA (потому что у них другой родитель), но с той же логикой правок.
  5. Передвинул указатель feat на верхний из новых коммитов.

История ветки feat теперь линейна - выглядит так, будто её разработчик ждал, пока main обновится до E, и только потом начал свою работу.

Если в процессе rebase возник конфликт на каком-то коммите - Git останавливается, дать разрешить, потом git rebase --continue. Можно --abort и вернуться в состояние до rebase'а.

См. rebase.

8.3 Правило: не rebase'ить публичную ветку

После rebase у коммитов новые SHA. Для Git это другие объекты. Для тебя - те же изменения, переписанные поверх нового базиса.

Это безопасно, пока ветку никто не видел. После push коммиты становятся «публичными» - кто-то их склонил, у кого-то они базис для своей работы. Если ты теперь сделаешь rebase и force-push, у этого «кого-то» возникнет каша:

У него локально:  A → B → C → D
Ты переписал:     A → B → E → C' → D'
Что делать с C и D на его машине? Никто не знает.

Поэтому правило: rebase'ить только ту ветку, на которую, кроме тебя, никто не смотрит. Обычно это твоя локальная feature-ветка, ещё не запушенная, или запушенная только для ревью.

На main, master, develop, любую публичную ветку - rebase запрещён. Хочешь обновить эти ветки - merge (fast-forward или three-way, если расходится).

Параллельно: на GitHub/GitLab у этих веток обычно включено branch protection, которое блокирует force-push. Это техническая страховка от случайного rebase.

8.4 Какую брать

Прагматичный набор правил:

Перед PR - rebase'ь свою feature-ветку на свежий main. Это даёт линейную, легко ревьюимую историю. И снимает с ревьюера головную боль «здесь конфликт с main, разруливай».

При merge PR - стратегия зависит от культуры команды:

  • Merge commit - стандартный three-way merge. История показывает «вот тут была отдельная фича-ветка». Подходит большим командам, где видеть факт ветвления полезно.
    • Squash - все коммиты PR сливаются в один на main. История чистая, на каждую PR - один коммит. Подходит, если хочется видеть на main только «единицы поставки», без процессуальной мелочи.
    • Rebase merge - rebase'ить коммиты PR на main без merge-коммита. Каждый коммит ветки оседает на main отдельно. Подходит, если коммиты внутри PR атомарные и интересны сами по себе.

Внутри своей работы - rebase разрешён и поощряется. Чем чище local history, тем приятнее работать.

Никогда не rebase'ь публичные ветки. Если случайно рестейтнул main, и git pull не помогает разрулить - звони коллегам, не пушь force.

Самый частый рабочий поток:

bash
# Начало работы
git switch main
git pull --ff-only
git switch -c feat/something
# ... коммиты ...
# Перед push'ем - обновить main и rebase'ить себя
git switch main
git pull --ff-only
git switch feat/something
git rebase main
# Если был конфликт - разрулить, git rebase --continue
# Force-push (после rebase нужен force)
git push --force-with-lease
# Создать PR, дождаться merge - обычно через squash или
# rebase-merge на GitHub

8.5 Interactive rebase

git rebase -i <base> открывает редактор со списком коммитов от <base> до HEAD. Можно их перенести, объединить, переименовать, удалить.

bash
git rebase -i main

Откроется примерно такое:

pick a1b2c3d add login form skeleton
pick d4e5f6a fix typo
pick 7a8b9c0 wire up backend
pick b1c2d3e address review comments
pick f4e5d6c another fix
# p, pick   = использовать коммит как есть
# r, reword = использовать коммит, поменять сообщение
# e, edit   = использовать коммит, остановиться для правки
# s, squash = слить с предыдущим коммитом (объединить сообщения)
# f, fixup  = слить с предыдущим, отбросить сообщение
# d, drop   = удалить коммит

Самое частое:

  • Squash «процессуального шума» перед PR. Коммиты «fix typo», «address review», «another fix» сливаются в фикс по теме.
  • Reword. Поправить сообщение коммита, который уже не последний (последний правится через --amend).
  • Drop. Удалить случайный коммит (например, debug-print).
  • Перестановка. Если порядок коммитов нелогичен - переставить строки в редакторе. Git применит в новом порядке.

Пример: было 5 «грязных» коммитов, хочется получить 2 чистых:

pick a1b2c3d feat: add login form
squash d4e5f6a fix typo
squash 7a8b9c0 wire up backend
pick b1c2d3e fix: handle null in user
fixup f4e5d6c another fix

Результат: один коммит «feat: add login form» (с объединёнными правками всех трёх первых) и один «fix: handle null in user» (с подмешанным fixup, без отдельного сообщения).

После i-rebase у тебя новая локальная история. Если ветка запушена - нужен push --force-with-lease.

См. interactive-rebase.

8.6 cherry-pick: перенести один коммит

Иногда нужно взять один коммит из другой ветки. Не merge'ить, не rebase'ить, не тащить всю историю - только этот один.

bash
git switch main
git cherry-pick <sha-коммита-из-feat>

Git возьмёт правку этого коммита и применит на текущей ветке, создав новый коммит с тем же содержимым, но другим SHA (как при rebase). Сообщение коммита скопируется как есть.

Типичные сценарии:

  • Hotfix в нескольких ветках. Починил баг на main - cherry-pick в release/v1.4 и release/v1.5.
  • «Достать» полезный коммит из заброшенной ветки.
  • Перенести коммит, ошибочно сделанный не в ту ветку. Из примера в главе 6.

Полезные опции:

bash
git cherry-pick -n <sha>     # применить правку, но не коммитить
                              # (--no-commit) - потом можно собрать
                              # с другими в один коммит
git cherry-pick <sha1>..<sha3>  # диапазон коммитов
git cherry-pick --abort       # отменить начатый cherry-pick
                              # (если был конфликт)

Конфликт при cherry-pick разрешается так же, как при merge: разруливаешь, git add, git cherry-pick --continue.

Опасности:

  • Дубль коммита. После cherry-pick один и тот же набор правок лежит в двух коммитах с разными SHA. Если потом merge'ить ветки между собой - Git это обычно понимает и не дублирует, но иногда вылетает конфликт «обе стороны внесли одинаковую правку».
  • Зависимости. Cherry-pick переносит один коммит без его родителей. Если коммит опирается на функцию, добавленную в предыдущем коммите ветки-источника, - на ветке-цели её может не быть, и cherry-pick сломает сборку.

См. cherry-pick.

8.7 Сравнительная таблица

Для быстрого выбора:

СценарийИспользовать
Обновить main свежими коммитами от другого разработчикаgit pull (fetch + merge fast-forward)
Слить feature в mainmerge commit или squash (зависит от культуры команды)
Обновить свою feature свежим main перед PRgit rebase main
Переименовать или сжать коммиты в своей веткеgit rebase -i
Поправить только что сделанный коммитgit commit --amend
Перенести один коммит на другую веткуgit cherry-pick
Перенести цепочку коммитов на другую веткуgit rebase --onto или серия cherry-pick
Откатить коммит в публичной историиgit revert (см. главу 9)

Эту таблицу полезно держать как шпаргалку первые пару недель, пока выбор не станет автоматическим.

8.7.1 Подводный камень: --onto для сложных пересадок

Базовый git rebase main берёт коммиты от merge-base до HEAD. Иногда нужно пересадить только часть коммитов на другую ветку. Для этого --onto:

git rebase --onto <new-base> <old-base> [<branch>]

Пример. У тебя:

main:      A → B
                 \
feat-base:        C
                    \
feat-detail:         D → E

Хочется забрать D и E себе на main, оставив C ветке feat-base:

bash
git switch feat-detail
git rebase --onto main feat-base

Git возьмёт коммиты от feat-base до HEAD (это D и E) и применит их на main. Получится:

main:    A → B → D' → E'        ← HEAD теперь здесь

--onto редко нужен, но в сложных историях с цепочкой зависимых веток - единственный чистый способ. Если в команде используют stacked PRs (несколько связанных PR друг над другом)

  • --onto приходится использовать часто.

Уроки в sandbox

lab-8.1. Один и тот же конфликт через merge и через rebase

Цель - увидеть разницу между merge и rebase глазами. Один и тот же сценарий проигрывается двумя путями, видно, что итог одинаковый, но история разная. Также - потренироваться в разрешении конфликтов.

  1. Создай репозиторий и базовый коммит: mkdir merge-rebase-lab && cd merge-rebase-lab && git init && echo "line 1" > note.txt && echo "line 2" >> note.txt && echo "line 3" >> note.txt && git add note.txt && git commit -m "base".

  2. Создай две ветки. main: поменяй line 2 на line 2 main, коммить. feat: создай заранее: git switch -c feat, поменяй line 2 на line 2 feat, коммить. Сейчас обе ветки разошлись на одном и том же месте - гарантированный конфликт при слиянии.

  3. Прогон 1 - merge. Вернись на main, помержи feat: git switch main && git merge feat. Будет конфликт. Открой файл, увидь <<<<<<< маркеры. Выбери, как должен выглядеть итог (например, line 2 main + feat), сохрани. git add note.txt && git commit (Git предложит готовое сообщение). Запусти git log --oneline --graph --all - увидишь развилку и merge-коммит.

  4. Сбрось всё для повтора: запомни SHA коммита «base» (git log --oneline), сделай git reset --hard <sha> и удали ветку feat: git branch -D feat. Воссоздай feat от base: git switch -c feat <sha-base>, тот же коммит с line 2 feat.

  5. Воссоздай и состояние main: git switch main && git reset --hard <sha-base>, новый коммит с line 2 main.

  6. Прогон 2 - rebase. Вместо merge, теперь rebase feat'a на main: git switch feat && git rebase main. Будет тот же конфликт. Разреши так же, как в первом прогоне. git add note.txt && git rebase --continue. Запусти git log --oneline --graph --all - увидишь линейную историю без merge-коммита, feat'овый коммит «приклеен» поверх main.

  7. Посмотри git reflog - увидишь оба прогона как историю позиций HEAD. Этот лог спасает, если что-то пошло не так.

  8. Итог для головы: финальное содержимое note.txt одинаковое, история разная. Merge оставил факт развилки. Rebase спрятал его, дав линейную цепочку.

sandbox с автопроверкой - открыть в песочнице

Резюме

  • Three-way merge ищет общего предка двух веток, объединяет правки относительно него, создаёт merge-коммит с двумя родителями.
  • Rebase переписывает коммиты одной ветки, делая вид, что они сделаны поверх другого базиса. SHA коммитов меняются, история становится линейной.
  • Главное правило: не rebase'ить публичные ветки. Force-push после rebase ломает работу всех, кто склонил эти коммиты.
  • Перед PR - rebase своей feature-ветки на свежий main. При merge PR - стратегия зависит от культуры команды (merge commit / squash / rebase merge).
  • Interactive rebase (`git rebase -i`) позволяет менять, объединять, переименовывать, удалять коммиты пачкой. Используется для очистки истории перед PR.
  • Cherry-pick переносит один коммит на другую ветку, создавая дубликат с тем же содержимым, но новым SHA. Полезен для hotfix'ов в нескольких релиз-ветках.
  • При конфликте в любой команде (merge / rebase / cherry-pick) разрешение одинаковое: править маркеры в файле, `git add`, `git <command> --continue`. Или `--abort` для отмены.

Контрольные вопросы

  1. Я сделал rebase своей feature-ветки на main, попытался push - отказ. Почему?

    Показать ответ

    Потому что после rebase у твоих коммитов новые SHA. Для Git это другие объекты. Удалённая ветка содержит старые SHA, и обычный push отказывается перезаписать историю. Решение - git push --force-with-lease (не --force!). Lease-вариант проверит, что remote не успел уйти вперёд (кто-то другой не запушил туда свои коммиты), иначе откажет. Это и есть штатный сценарий для feature-ветки: rebase → force-with-lease. Условие - на ветку никто, кроме тебя, не смотрит.

  2. В чём смысловая разница между merge-коммитом, squash и rebase-merge при закрытии PR на GitHub?

    Показать ответ

    Merge-коммит создаёт классический three-way merge: все коммиты PR прилетают на main как есть, плюс ещё один merge-коммит сверху с двумя родителями. История нелинейная, зато видно «здесь была ветка». Squash сжимает все коммиты PR в один на main: чистая линейная история, но детали PR (если они были атомарными) теряются. Rebase-merge применяет каждый коммит PR поверх main без merge-коммита: история линейная, коммиты сохраняются. Выбирай по тому, что важнее: видеть факт ветвления (merge), видеть «единицы поставки» (squash), или сохранять атомарные коммиты внутри PR (rebase-merge).

  3. Я cherry-pick'нул коммит из feat в main. Теперь при merge feat → main возникает конфликт. Почему, если правка та же?

    Показать ответ

    Потому что после cherry-pick на main лежит коммит с новым SHA - он содержит те же правки, что и оригинал на feat, но для Git это другой объект. Когда ты пытаешься помержить feat, Git видит «обе ветки сделали правку в одних и тех же строках», и не всегда может автоматически догадаться, что это одна и та же правка. Чаще всего ort-стратегия с этим справляется, и конфликта нет. Если возникает - разруливай как обычный конфликт, выбирая любую из идентичных версий. Чтобы избегать ситуации: после cherry-pick в обе стороны делать осознанный rebase либо удалять одну из веток после cherry-pick.

  4. В чём разница между `git rebase main` и `git rebase --onto main`?

    Показать ответ

    Да, git rebase --abort полностью отменит rebase и вернёт ветку в состояние до начала операции. Никаких следов не останется, кроме записи в reflog (которой обычно никто не пользуется, кроме как для восстановления). Это полная страховка - если запутался, всегда можно --abort и попробовать заново, или вместо rebase сделать merge. Никогда не редактируй файлы .git/rebase-merge/ или .git/rebase-apply/ руками - это внутренние файлы rebase'а, их трогать только через --continue и --abort.

← Предыдущая07-commits-proСледующая →09-undo
Footer
linuxlab-
Copyright © 2026 LinuxLab. Все права защищены.
Учебники
Цены
О платформе
Конфиденциальность и куки