open atlas
↑ К треку
Git: от нуля до сеньора GIT · 03 · 03

Слияние и fast-forward

git merge вкладывает другую ветку в текущую. Если текущая ветка не двигалась, git делает fast-forward, сдвигая указатель без merge-коммита. Если обе разошлись, трёхстороннее слияние от общего предка (merge base) создаёт коммит с двумя родителями. --no-ff форсирует его.

GIT Middle ◷ 17 min
Уровень
ОсновыJuniorMiddleSenior

Ты разделил работу на ветку; теперь её надо вернуть на main. git merge — это глагол, который вкладывает одну ветку в другую, но делает он это двумя очень разными способами в зависимости от формы истории, и они дают разные графы коммитов. Один оставляет идеально прямую линию; другой добавляет коммит с двумя родителями. Выбор между ними (и знание, что именно ты получил) — это разница между историей, которую можно читать, и той, которую нельзя.

К концу этого урока ты сможешь предсказать, делает ли слияние fast-forward или создаёт merge-коммит, объяснить трёхстороннее слияние и его merge base и управлять результатом через --no-ff и --ff-only.

Цель

После этого урока ты сможешь запускать git merge, предсказывать, сделает ли он fast-forward или создаст merge-коммит, объяснять, как git использует merge base (общего предка) для объединения разошедшихся веток, и применять --no-ff и --ff-only, чтобы форсировать или запретить merge-коммит.

1

git merge <ветка> вкладывает другую ветку в ту, на которой ты находишься. Ты переключаешься на назначение, затем называешь источник:

git switch main          # ветка, которая должна принять работу
git merge feature        # вложить 'feature' в 'main'

Слияние всегда в текущую ветку. После успеха main содержит работу из feature, а feature не изменилась — слияние не двигает и не поглощает ветку-источник, оно лишь обновляет ветку, на которой стоит HEAD. Как именно main туда попадает — самое интересное, и это целиком зависит от того, двигался ли сам main с момента отделения ветки.

2

Fast-forward: если у текущей ветки нет собственных коммитов, git просто сдвигает её указатель вперёд. Допустим, main на коммите A, ты ответвил feature, добавил B, затем C — но main за это время новых коммитов не получил. main — это предок feature, так что объединять нечего; слиянию нужно лишь продвинуть main с A на C:

git merge feature
# Updating A..C
# Fast-forward

Merge-коммит не создаётся. История остаётся одной прямой линией, и после этого main и feature указывают на один и тот же коммит C. Это частый случай, когда main трогал только ты.

3

Трёхстороннее слияние: если обе ветки продвинулись, git строит новый merge-коммит с двумя родителями. Теперь допустим, что после ответвления feature получил B и C, и main получил свой коммит D. Ветки разошлись — ни одна не является предком другой — так что fast-forward невозможен. Git выполняет трёхстороннее слияние: он находит merge base (последний коммит, который оба разделяют, здесь A), затем объединяет три снимка — базу A, твою сторону D и их сторону C — в новый коммит:

git merge feature
# Merge made by the 'recursive' strategy.

У этого нового merge-коммита два родителя (D и C), связывающие две линии истории обратно вместе. Это единственный вид коммита с более чем одним родителем, и именно так граф веток разветвляется и снова сходится.

Почему это работает

Почему три снимка, а не два? Сравнение только двух вершин не может сказать про изменённую строку, какая сторона её изменила — может, обе, может, одна изменила, а другая оставила оригинал. Merge base — это точка отсчёта: для каждого файла git смотрит база-против-твоей и база-против-их версии. Если регион изменила только одна сторона, берётся это изменение. Если обе стороны изменили один и тот же регион по-разному, git не может решить и сообщает о конфликте (следующий урок). Без общего предка каждая различающаяся строка выглядела бы как конфликт — merge base и есть то, что делает автоматическое слияние возможным.

4

--no-ff форсирует merge-коммит; --ff-only его запрещает. Когда fast-forward возможен, ты всё равно можешь выбрать записать merge-коммит или настоять на чистом fast-forward:

git merge --no-ff feature    # всегда создать merge-коммит, даже если FF был возможен
git merge --ff-only feature  # fast-forward если возможно, иначе прервать (без merge-коммита)

--no-ff — частая командная политика: он сохраняет явную отметку, что «эти коммиты пришли вместе как одна фича», оставляя форму ветки видимой в истории. --ff-only — настройка безопасности для подтягивания общих веток: если твоя локальная ветка разошлась, он отказывает, а не молча создаёт неожиданный merge-коммит. Многие команды ставят git config merge.ff only или используют это на git pull.

Разбор примера

Одно и то же слияние, две истории.

История на данный момент: main и feature оба происходят от коммита A. feature добавил B и C.

Случай 1 — main не тронут. main всё ещё на A.

git switch main
git merge feature      # Fast-forward
git log --oneline --graph
# * C
# * B
# * A

Прямая линия; main и feature теперь оба указывают на C. Merge-коммита нет, потому что A был предком C — примирять было нечего.

Случай 2 — main тоже сдвинулся. Пока у feature были B и C, коллега положил D на main.

git switch main        # main на D
git merge feature
git log --oneline --graph
# *   M  (merge-коммит: родители D и C)
# |\
# | * C
# | * B
# * | D
# |/
# * A

Теперь есть merge-коммит M с двумя родителями. Та же команда, но поскольку обе ветки ушли за merge base A, git пришлось примирить D и C и записать результат. Граф разветвляется на A и снова сходится на M — эта форма ромба и есть визуальная подпись трёхстороннего слияния.

Частая ошибка

Частый сюрприз: «я слил, но merge-коммита нет — оно сработало?» Да; ты получил fast-forward, который является успешным слиянием, просто не потребовавшим нового коммита. Обратная ловушка — считать, что каждый git pull делает fast-forward: если удалённая и твоя локальная ветка обе продвинулись, pull делает трёхстороннее слияние и создаёт merge-коммит, которого ты, возможно, не хотел. Команды, предпочитающие линейную историю, используют --ff-only (или rebase) на pull, чтобы избежать этих неожиданных merge-коммитов.

Проверь себя
Викторина

Когда git создаёт merge-коммит (с двумя родителями) вместо fast-forward?

Итог

git merge <ветка> вкладывает другую ветку в текущую. Если текущая ветка является предком источника — она не получила собственных коммитов — git делает fast-forward, сдвигая указатель без нового коммита и сохраняя линейную историю. Если обе ветки разошлись, git делает трёхстороннее слияние: находит merge base (общего предка), примиряет изменения каждой стороны относительно него и записывает merge-коммит с двумя родителями, разветвляя-и-сводя граф. --no-ff форсирует merge-коммит, даже когда fast-forward был возможен; --ff-only отказывает во всём, кроме fast-forward. Когда обе стороны меняют одни и те же строки, git не может примирить автоматически — и это тот конфликт слияния, который ты научишься разрешать дальше.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 4 завершено

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources2
expand
  1. 01
  2. 02

Trademarks belong to their respective owners. Editorial reference only.