Ты создал ветку, накатал в ней фичу, всё работает. Теперь надо вернуть эту работу в 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(-)
Код: Выделить всё
$ git log --oneline --graph --all
* 9e4b8d1 (HEAD -> main, feature) step 3
* 4c0a7f2 step 2
* 7a1f2c0 base
Сценарий 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
Соберём расхождение и слияние:
Код: Выделить всё
$ 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(+)
Код: Выделить всё
$ git log --oneline --graph
* 2f8c1aa (HEAD -> main) Merge branch 'feature'
|\
| * b3d90e5 (feature) feat: new file
* | a17c4d2 main: hotfix
|/
* 7a1f2c0 base
Код: Выделить всё
$ git cat-file -p HEAD | head -3
tree 5a0c...
parent a17c4d2...
parent b3d90e5...
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
git merge --ff-only - противоположность: разрешает только перемотку. Если fast-forward невозможен (истории разошлись), команда не сделает merge-коммит, а упадёт с ошибкой:
Код: Выделить всё
$ git merge --ff-only feature
fatal: Not possible to fast-forward, aborting.
Стратегия 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 справляется аккуратнее.
- При конфликте рабочее дерево не засоряется кучей промежуточных файлов так, как это бывало раньше.
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
Типичные грабли
- "Я слил, а 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. О нём - в следующем уроке.