Слияние и fast-forward
git merge вкладывает другую ветку в текущую. Если текущая ветка не двигалась, git делает fast-forward, сдвигая указатель без merge-коммита. Если обе разошлись, трёхстороннее слияние от общего предка (merge base) создаёт коммит с двумя родителями. --no-ff форсирует его.
Ты разделил работу на ветку; теперь её надо вернуть на 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-коммит.
git merge <ветка> вкладывает другую ветку в ту, на которой ты находишься. Ты переключаешься на назначение, затем называешь источник:
git switch main # ветка, которая должна принять работу
git merge feature # вложить 'feature' в 'main'Слияние всегда в текущую ветку. После успеха main содержит работу из feature, а feature не изменилась — слияние не двигает и не поглощает ветку-источник, оно лишь обновляет ветку, на которой стоит HEAD. Как именно main туда попадает — самое интересное, и это целиком зависит от того, двигался ли сам main с момента отделения ветки.
Fast-forward: если у текущей ветки нет собственных коммитов, git просто сдвигает её указатель вперёд. Допустим, main на коммите A, ты ответвил feature, добавил B, затем C — но main за это время новых коммитов не получил. main — это предок feature, так что объединять нечего; слиянию нужно лишь продвинуть main с A на C:
git merge feature
# Updating A..C
# Fast-forwardMerge-коммит не создаётся. История остаётся одной прямой линией, и после этого main и feature указывают на один и тот же коммит C. Это частый случай, когда main трогал только ты.
Трёхстороннее слияние: если обе ветки продвинулись, 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 и есть то, что делает автоматическое слияние возможным.
--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-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.