Слияние веток: fast-forward против трёхстороннего merge

Рейтинг: 61% · 6 голосов
Самый подробный курс по Git - от первого коммита до объектной базы и packfiles. Терминал и первый репозиторий, ветки и слияние, конфликты, удалённая работа и форджи (GitHub, GitLab), rebase и переписывание истории, спасение кода через reflog и fsck, stash и worktree, bisect и blame, подмодули и монорепо, хуки и подпись коммитов, внутреннее устройство и масштаб. Для новичков и профи. Актуально на 2026 (Git 2.54, дорожная карта 3.0).
Ответить
Аватара пользователя
Pavel_Git
Сообщения: 52
Зарегистрирован: 11 май 2026, 05:31

Слияние веток: fast-forward против трёхстороннего merge

Сообщение Pavel_Git »

Оглавление курса (52)
  1. Что такое контроль версий и почему победил Git
  2. Установка Git на Windows, macOS и Linux и первая настройка
  3. Терминал и как спрашивать у Git: выживание в командной строке
  4. Ментальная модель Git: три состояния, коммит-снимок и граф
  5. Первый репозиторий: init, add, commit, status
  6. Просмотр изменений: status, diff и индексация по частям
  7. История проекта: git log, диапазоны и поиск
  8. .gitignore: что не должно попасть в репозиторий
  9. .gitattributes: окончания строк, бинарники и атрибуты
  10. Ветки как указатели: branch, switch и detached HEAD
  11. Слияние веток: fast-forward против трёхстороннего merge (вы здесь)
  12. Конфликты слияния: анатомия и уверенное разрешение
  13. Практикум: твой первый день с Git от нуля до push
  14. Удалённые репозитории: remote, fetch, базовый pull и refspec
  15. git push: безопасная отправка и защита от перезаписи
  16. Клонирование: shallow, partial clone, bundle и backfill
  17. GitHub, GitLab и форки: pull request, rulesets и защита веток
  18. Рабочие процессы команды: GitFlow, GitHub Flow, trunk-based
  19. Merge queue и merge trains: безопасное вливание в trunk
  20. Культура code review и Conventional Commits
  21. git reset: три дерева и отмена индексации и коммитов
  22. git revert: безопасная отмена в общей истории
  23. Правка последнего коммита: git commit --amend
  24. git rebase: пересадка истории и золотое правило
  25. Интерактивный rebase: переписать серию коммитов
  26. git cherry-pick: перенос отдельных коммитов
  27. Stacked diffs: цепочка зависимых PR
  28. Массовое переписывание истории: git filter-repo и git replace
  29. git reflog: журнал, который помнит всё
  30. Восстановление утерянных коммитов, веток и файлов
  31. Откат опасных операций: reset --hard, сломанный rebase, плохой merge
  32. git stash: тайник для незавершённой работы
  33. git worktree: несколько рабочих деревьев одного репозитория
  34. git bisect: бинарный поиск регрессии
  35. git blame, mailmap и археология кода
  36. Теги, релизы, archive и bundle
  37. Подмодули: внешние репозитории внутри проекта
  38. Subtree, монорепозитории и sparse-checkout
  39. Git LFS и большие файлы
  40. Вспомогательные команды: clean, mv, rm, restore
  41. Псевдонимы и кастомизация .gitconfig
  42. Хуки Git: автоматизация на клиенте и сервере
  43. Подпись коммитов и тегов: SSH, GPG и Sigstore
  44. Внутреннее устройство I: объектная база Git
  45. Внутреннее устройство II: ссылки, HEAD, notes и reftable
  46. Внутреннее устройство III: индекс, packfiles, gc и обслуживание
  47. От SHA-1 к SHA-256: переход на новый хеш
  48. Огромные репозитории: partial clone, FSMonitor, Scalar
  49. Git в CI/CD: shallow, кеш, merge_group и безопасность
  50. Куда движется Git: дорожная карта 3.0 и экосистема
  51. Профессиональный setup: собираем рабочее окружение Git
  52. 30+ типичных ошибок Git и как из них выбираться
Боль, которую решает merge

Ты создал ветку, накатал в ней фичу, всё работает. Теперь надо вернуть эту работу в main так, чтобы не сломать чужие коммиты и не потерять свои. Это и есть слияние веток git - команда git merge. Звучит просто: "возьми эту ветку и приклей к моей". Но за этой простотой прячется развилка, на которой спотыкаются почти все: иногда merge просто двигает указатель и история остаётся прямой как палка, а иногда рождается отдельный merge commit с двумя родителями, и в логе появляется promежуток с раздвоением. Эти два сценария называются fast forward git и трёхстороннее (3-way) слияние, и понимать разницу между ними обязательно - от неё зависит, как выглядит история проекта, можно ли потом откатиться и кто вообще поймёт, что тут происходило.

Если ты помнишь из прошлых уроков, что коммит - это снимок дерева плюс ссылка на родителя, то merge ляжет легко. Ветка - это просто подвижный ярлык, указывающий на конкретный коммит (файл в .git/refs/heads). HEAD говорит, на какой ветке ты стоишь сейчас. Объединить ветки git - значит так подвинуть один из этих ярлыков (и, возможно, создать новый коммит), чтобы он начал содержать работу из обеих линий.

Изображение

Сценарий 1: fast-forward - перенос указателя

Fast forward git случается, когда между твоей текущей веткой и той, что ты вливаешь, нет расхождения. То есть текущая ветка - прямой предок вливаемой. Ничего не менялось в main с тех пор, как ты отпочковал feature. Тогда Git не выдумывает новый коммит: он просто перетаскивает ярлык main вперёд по цепочке, на тот же коммит, куда смотрит feature. Отсюда и название - "перемотка вперёд".

Смоделируем. Создадим репозиторий, базовый коммит, потом ветку с парой коммитов:

Код: Выделить всё

$ git init -b main demo && cd demo
$ echo "v1" > app.txt && git add app.txt && git commit -m "base"
$ git switch -c feature
$ echo "v2" > app.txt && git commit -am "step 2"
$ echo "v3" > app.txt && git commit -am "step 3"
$ git switch main
$ git merge feature
Updating 7a1f2c0..9e4b8d1
Fast-forward
 app.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
Ключевая строка - Fast-forward. Никакого нового коммита не появилось, число коммитов не выросло. Git напечатал "Updating 7a1f2c0..9e4b8d1" - это значит "я двигаю main с 7a1f2c0 на 9e4b8d1". Посмотрим граф:

Код: Выделить всё

$ git log --oneline --graph --all
* 9e4b8d1 (HEAD -> main, feature) step 3
* 4c0a7f2 step 2
* 7a1f2c0 base
Обе ветки - main и feature - смотрят на один и тот же коммит. История абсолютно линейна, ни одного раздвоения. С точки зрения main так, будто ты сам, не отвлекаясь, написал step 2 и step 3 подряд. Это чисто и приятно для коротких личных веток, но у этого есть оборотная сторона: по истории невозможно сказать, что step 2 и step 3 вообще были отдельной фичей в отдельной ветке. Ветка feature растворилась без следа.

Сценарий 2: трёхсторонний merge и merge base

Теперь интересное. Что если, пока ты пилил feature, в main кто-то тоже коммитил? Тогда история разошлась: у main есть свои коммиты, которых нет в feature, и наоборот. Просто перемотать ярлык вперёд нельзя - feature не является прямым потомком main. Git вынужден сделать настоящее слияние и родить merge commit.

Почему оно "трёхстороннее"? Потому что Git берёт три снимка, а не два: вершину твоей ветки (main), вершину вливаемой ветки (feature) и их общего предка - merge base, тот последний коммит, в котором обе линии ещё были одним целым. Git находит merge base сам, проходя историю назад от обеих вершин до точки схождения:

Код: Выделить всё

$ git merge-base main feature
7a1f2c0
Дальше Git сравнивает: что изменилось от base к main и что изменилось от base к feature. Если правки в разных местах - он спокойно складывает их вместе автоматически. Если обе стороны трогали одни и те же строки по-разному - вот тут и рождается конфликт (про него отдельный урок). Сравнение именно с базой, а не лоб в лоб двух вершин, и есть смысл слова "трёхстороннее": две стороны плюс точка отсчёта. Без базы Git не отличил бы "я добавил строку" от "ты удалил строку" - он бы видел только, что строки разные.

Соберём расхождение и слияние:

Код: Выделить всё

$ echo "hotfix" > readme.txt && git add readme.txt && git commit -m "main: hotfix"
$ git switch feature
$ echo "feature work" > feat.txt && git add feat.txt && git commit -m "feat: new file"
$ git switch main
$ git merge feature
Merge made by the 'ort' strategy.
 feat.txt | 1 +
 1 file changed, 1 insertion(+)
Строка Merge made by the 'ort' strategy - это уже не перемотка. Git создал новый коммит. Посмотрим граф до и после в одном логе:

Код: Выделить всё

$ git log --oneline --graph
*   2f8c1aa (HEAD -> main) Merge branch 'feature'
|\
| * b3d90e5 (feature) feat: new file
* | a17c4d2 main: hotfix
|/
* 7a1f2c0 base
Вот оно, раздвоение и схождение. Merge-коммит 2f8c1aa - единственный в репозитории, у кого два родителя: a17c4d2 (бывшая вершина main, первый родитель) и b3d90e5 (вершина feature, второй родитель). Порядок родителей не случаен: первый - это та ветка, на которой ты стоял при merge. Это важно для команд вроде git log --first-parent, которые умеют идти только по основной линии, игнорируя нутро влитых веток.

Код: Выделить всё

$ git cat-file -p HEAD | head -3
tree 5a0c...
parent a17c4d2...
parent b3d90e5...
Два parent в объекте коммита - это и есть техническая суть merge commit. Всё остальное (граф со звёздочками, "Merge branch") - просто красивое отображение этого факта.

git merge no-ff и --ff-only: управляем стратегией ярлыка

По умолчанию Git перематывает, когда может, и делает merge-коммит, только когда вынужден. Но иногда тебе нужно поведение жёстче. Тут три режима.

git merge --no-ff - запрещает перемотку. Даже если fast-forward возможен, Git всё равно создаст merge commit. Зачем? Чтобы в истории остался маркер фичи. Команды, где правило "каждая фича вливается через merge-коммит", получают по логу честную картину: вот тут началась ветка задачи JIRA-123, вот тут она влилась, вот всё, что в неё входило. Сравни:

Код: Выделить всё

$ git switch main
$ git merge --no-ff feature -m "Merge feature: profile page"
$ git log --oneline --graph
*   d9e1f30 (HEAD -> main) Merge feature: profile page
|\
| * 9e4b8d1 step 3
| * 4c0a7f2 step 2
|/
* 7a1f2c0 base
Даже на линейной истории появился merge-коммит-обёртка. Теперь group of commits step 2/step 3 визуально сгруппирована под одной задачей, и откатить всю фичу можно одним revert merge-коммита. Именно --no-ff обычно прячется за кнопкой "Create a merge commit" в GitHub/GitLab.

git merge --ff-only - противоположность: разрешает только перемотку. Если fast-forward невозможен (истории разошлись), команда не сделает merge-коммит, а упадёт с ошибкой:

Код: Выделить всё

$ git merge --ff-only feature
fatal: Not possible to fast-forward, aborting.
Это страховка. Ставишь в pull.ff=only или дёргаешь руками, когда хочешь быть уверен, что твоя ветка - чистый потомок и никаких неожиданных merge-коммитов не родится. Очень полезно в скриптах CI и при обновлении локальной main от origin: либо ровно перемоталось, либо стоп и разбирайся.

Стратегия ort - дефолт с Git 2.34

Ты заметил в выводе слова "the 'ort' strategy". Стратегия - это алгоритм, которым Git реально считает слияние деревьев. С Git 2.34 (ноябрь 2021) дефолтной стратегией стала ort (название шутливо расшифровывают как "Ostensibly Recursive's Twin" - якобы близнец recursive). Она заменила старую recursive, которая раньше много лет была дефолтом, а теперь снята с дефолта и осталась только для совместимости.

Что дала ort на практике, без выдуманных "новинок 2026":
  • Слияние считается целиком в памяти, по структурам данных, а не через временные записи в индекс и рабочее дерево. Отсюда заметное ускорение, особенно на больших репозиториях и на слияниях, где много файлов.
  • Заметно лучше и точнее обрабатываются переименования файлов (rename detection). Старая recursive на массовых переименованиях работала медленно и иногда путалась; ort справляется аккуратнее.
  • При конфликте рабочее дерево не засоряется кучей промежуточных файлов так, как это бывало раньше.
Менять стратегию руками почти никогда не нужно - ort правильный выбор по умолчанию. Явно её задаёт флаг -s ort, а octopus (слияние больше двух веток разом) и ours (взять только свою сторону) - это отдельные стратегии для редких случаев. На 2026 год по умолчанию у тебя ort, и это хорошо; никаких действий не требуется.

git merge --abort: безопасный откат

Слияние может остановиться на полпути - из-за конфликта. Ты оказываешься в "состоянии слияния": индекс полон, рабочее дерево с маркерами конфликта, merge-коммит ещё не создан. Если передумал или понял, что момент неудачный, не надо паниковать и руками чистить файлы. Есть штатный выход:

Код: Выделить всё

$ git merge feature
Auto-merging app.txt
CONFLICT (content): Merge conflict in app.txt
Automatic merge failed; fix conflicts and then commit the result.

$ git merge --abort
git merge --abort возвращает ветку ровно в то состояние, в котором она была до запуска merge: HEAD на месте, индекс и рабочее дерево чистые. Под капотом он опирается на MERGE_HEAD и ORIG_HEAD - служебные ссылки, которые Git кладёт в .git при старте слияния. Пока merge не завершён, ORIG_HEAD хранит коммит, где ты стоял до начала, и abort просто откатывает к нему. Это твоя кнопка "отмена" - запомни её раньше, чем встретишь первый конфликт.

Типичные грабли
  • "Я слил, а merge-коммита нет." Это был fast-forward. Не баг. Если хочешь маркер фичи - сливай через --no-ff.
  • "Случайный merge-коммит при git pull." Дефолтный pull = fetch + merge, и если локальная ветка ушла вперёд, он лепит лишний merge. Лечится настройкой pull.ff=only или pull.rebase - но это тема следующего урока.
  • Конфликт != ошибка Git. Git честно говорит, что две стороны несовместимы автоматически, и ждёт твоего решения. Паниковать и удалять репозиторий не нужно - либо чини, либо --abort.
  • Путаница в порядке родителей. При revert merge-коммита надо указывать -m 1 или -m 2 (какую сторону считать "главной"). Первый родитель - ветка, на которой ты стоял при merge.
  • Слияние не туда. Перед git merge всегда проверь git status и git branch - убедись, что стоишь на той ветке, КУДА вливаешь, а не наоборот. merge тащит в текущую ветку.
Мини-лаба: пощупай оба сценария руками

Пройди по шагам в чистой папке, не копируй слепо - смотри на вывод.
  • 1. git init -b main lab && cd lab; сделай первый коммит (echo a > f.txt; git add .; git commit -m base).
  • 2. git switch -c feat; добавь коммит (echo b >> f.txt; git commit -am b). Вернись: git switch main.
  • 3. git merge feat - увидишь Fast-forward. Проверь git log --oneline --graph - линия прямая.
  • 4. Откати: git reset --hard base-хеш (возьми его из git log). Теперь устроим расхождение.
  • 5. На main: echo m > main.txt; git add .; git commit -m "main side". На feat: git switch feat; echo x > feat.txt; git add .; git commit -m "feat side".
  • 6. git switch main; git merge-base main feat - запиши хеш базы. git merge feat - теперь это "Merge made by the 'ort' strategy".
  • 7. git log --oneline --graph - найди раздвоение и merge-коммит. git cat-file -p HEAD - убедись, что два parent.
  • 8. Бонус: повтори шаг 3 с флагом --no-ff и сравни граф. Затем спровоцируй конфликт (правь одну строку с двух сторон) и потренируй git merge --abort.
Контрольные вопросы
  • 1. В каком случае git merge сделает fast-forward, а в каком создаст merge commit? Что должно быть верно про ветки?
  • 2. Зачем нужен merge base и почему слияние называют трёхсторонним, а не двухсторонним?
  • 3. Чем отличаются --no-ff и --ff-only и в какой ситуации каждый уместен?
  • 4. Какая стратегия слияния дефолтная с Git 2.34 и чем она лучше предшественницы на переименованиях?
Итог

git merge живёт в двух режимах. Fast-forward - когда история не разошлась: Git просто двигает ярлык ветки вперёд, и лог остаётся линейным. Трёхсторонний merge - когда линии разошлись: Git находит общего предка (merge base), сравнивает обе стороны с ним и рождает merge-коммит с двумя родителями. Флагом --no-ff ты заставляешь сохранять merge-коммит как маркер фичи, --ff-only страхует от незапланированных слияний, --abort откатывает незавершённое слияние, а считает всё это стратегия ort, дефолтная с Git 2.34. Merge сохраняет настоящую историю как она была. Но когда тебе нужна не правда о ветвлении, а гладкая линейная лента - в игру вступает rebase. О нём - в следующем уроке.
👍 ❤️4 🔥1 😄 🤔
Аватара пользователя
linux_kun
Сообщения: 1
Зарегистрирован: 12 май 2026, 16:32

Re: Слияние веток: fast-forward против трёхстороннего merge

Сообщение linux_kun »

Наконец дошло, почему иногда после merge нет отдельного коммита а иногда есть. Раньше думал что это рандом, оказывается fast-forward. Спасибо за графы, по ним сразу видно.
👍 ❤️1 🔥 😄 🤔
Аватара пользователя
pguser
Сообщения: 1
Зарегистрирован: 16 май 2026, 18:17

Re: Слияние веток: fast-forward против трёхстороннего merge

Сообщение pguser »

А можно так: всегда сливать с --no-ff чтобы в истории были видны все фичи? Или это считается перебором и лог раздувается лишними merge-коммитами на каждый чих?
👍1 ❤️ 🔥 😄 🤔
Ответить
← Предыдущая глава
Ветки как указатели: branch, switch и detached HEAD
Следующая глава →
Конфликты слияния: анатомия и уверенное разрешение

Все главы курса «Git профессионально: от первого коммита до внутреннего устройства»

Поделиться темой: ✈ Telegram VK
Похожие запросы: Разрешение конфликтов слияния в Gitgit merge или rebase: что выбратьgit worktree: несколько рабочих деревьевgit merge: слияние ветокgit branch: работа с ветками

Вернуться в «Git профессионально: от первого коммита до внутреннего устройства»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 2 гостя