Олег Марков
Слияние с fast-forward в Git - fast-forward merge без лишних коммитов
Введение
Fast-forward merge (слияние с перемоткой) — это особый режим слияния веток в Git, при котором не создается новый merge-коммит, а указатель ветки просто "перемещается" вперед на уже существующий коммит.
Проще говоря, если ветка main отстает от ветки feature, и между ними нет "развилки" с независимыми коммитами, Git может просто передвинуть main на тот же коммит, на котором находится feature. Так вы получаете линейную историю без дополнительных merge-коммитов.
В этой статье вы увидите:
- в чем суть fast-forward merge и когда он возможен;
- как понять по истории репозитория, что использовалось fast-forward слияние;
- как управлять тем, будет ли Git делать fast-forward автоматически;
- как связаны fast-forward merge и rebase;
- какие есть плюсы и минусы fast-forward-подхода для командной разработки;
- что делать, если вы "случайно" сделали fast-forward, а нужен был merge-коммит.
Давайте постепенно разберем эти вопросы на наглядных примерах.
Что такое fast-forward merge в Git
Краткое определение
Fast-forward merge — это тип слияния, при котором:
- не создается новый коммит слияния;
- целевой указатель ветки (например,
main) просто сдвигается вперед на коммит ветки-источника (feature); - история остается строго линейной, без "узлов" merge-коммитов.
Когда fast-forward возможен
Смотрите, я покажу вам простую ситуацию.
Исходно у вас есть только ветка main:
- A — коммит в
main
Вы создаете ветку feature от main:
- A — общий коммит для
mainиfeature
Делаете несколько коммитов в feature:
- A — общий предок
- B — первый коммит в
feature - C — второй коммит в
feature
При этом в main никто ничего не менял: там по-прежнему только A.
Теперь вы хотите "слить" ветку feature обратно в main. В этот момент:
- ветка
mainуказывает на A; - ветка
featureуказывает на C; - история
main— это строгий префикс историиfeature.
В такой ситуации Git может выполнить fast-forward:
- он просто передвинет указатель
mainс A на C; - коммиты B и C "становятся" коммитами ветки
mainбез создания merge-коммита.
Когда fast-forward невозможен
Теперь немного осложним пример.
Исходно:
- A — общий коммит для
mainиfeature
Дальше:
- в
featureпоявляются B и C; - в
mainпоявляются D и E.
Граф истории уже разветвился:
- A — общий предок
- B — ветка
feature- C
- D — ветка
main- E
- B — ветка
Теперь ветка main уже не префикс ветки feature — они живут параллельно после точки A.
В этом случае Git не может просто "перемотать" main на C, потому что тогда потерялись бы коммиты D и E. Вместо этого он создаст merge-коммит:
- M — новый коммит, который объединяет изменения и из
feature, и изmain.
Здесь fast-forward уже невозможен, нужен обычный merge.
Базовые команды fast-forward merge
Классический сценарий fast-forward
Давайте разберемся на конкретных командах.
- Создаем новую ветку от
main:
git checkout main # Переходим на основную ветку
git checkout -b feature # Создаем новую ветку feature от main
- Делаем несколько коммитов в
feature:
# Вносим какие-то изменения в файлы
git add . # Добавляем изменения в индекс
git commit -m "Добавить новую фичу" # Создаем коммит
# Повторяем нужное количество раз
Никто не трогает
mainв это время.Сливаем
featureвmain:
git checkout main # Возвращаемся на main
git merge feature # Пытаемся слить feature в main
Если main не изменялся с момента создания feature, Git выполнит fast-forward автоматом. Новый merge-коммит не появится, а main просто "прыгает" на последний коммит ветки feature.
Как увидеть, что произошло fast-forward слияние
После выполнения слияния можно посмотреть историю:
git log --oneline --decorate --graph --all
Если произошло fast-forward:
- вы увидите линейную историю без узлов;
- не будет "Merge branch 'feature'" коммитов;
- указатели
mainиfeatureбудут стоять на одном и том же коммите.
Пример вывода логов в случае fast-forward (упрощенно):
* abc1234 (HEAD -> main, feature) Добавить новую фичу
* 789abcd Предыдущий коммит в main
* 456def0 Еще более старый коммит
Здесь видно, что main и feature указывают на один и тот же коммит abc1234. Это как раз результат fast-forward merge.
Как Git решает — делать fast-forward или нет
По умолчанию команда:
git merge feature
ведет себя так:
- если fast-forward возможен, Git его сделает;
- если нет, создаст merge-коммит.
Это поведение можно переопределять флагами:
--ff— явно попросить fast-forward, если возможно (по сути поведение по умолчанию);--no-ff— запретить fast-forward, всегда создавать merge-коммит;--ff-only— разрешить только fast-forward, а иначе завершить с ошибкой.
Сейчас мы подробно разберем каждый вариант.
Управление fast-forward: флаги и настройки
Принудительный fast-forward: git merge --ff-only
Флаг --ff-only особенно полезен, когда вы хотите гарантировать линейную историю и не допустить автоматического создания merge-коммита.
Команда:
git checkout main # Переходим на main
git merge --ff-only feature # Разрешаем только fast-forward
- Если история
main— префикс историиfeature, Git сделает fast-forward. - Если за время работы в
featureкто-то успел закоммитить что-то вmain, fast-forward станет невозможен, и команда завершится ошибкой.
Сообщение обычно выглядит примерно так:
fatal: Not possible to fast-forward, aborting.
Так вы явно видите, что история разошлась, и можете принять решение:
- сделать обычный
merge; - сделать
rebaseперед merge; - или разобраться вручную.
Отключение fast-forward: git merge --no-ff
Иногда наоборот важно всегда создавать merge-коммит, даже если fast-forward возможен. Например, чтобы:
- каждый feature-branch сливался в
mainотдельным merge-коммитом; - визуально в истории было видно, какие коммиты относятся к какой задаче;
- иметь удобные "точки" для отката цельных фич.
В этом случае используют флаг --no-ff:
git checkout main # Переходим на main
git merge --no-ff feature # Всегда создаем merge-коммит
Даже если main не изменялся с момента ответвления feature, Git создаст новый merge-коммит. В истории вы увидите:
* 1111111 (HEAD -> main) Merge branch 'feature'
|\
| * abc1234 (feature) Добавить новую фичу
|/
* 789abcd Предыдущий коммит в main
Коммит 1111111 — это merge-коммит, созданный специально, хотя с точки зрения diff он не добавляет новых изменений по сравнению с последним коммитом feature.
Управление поведением по умолчанию через конфиг
Чтобы не писать флаг --no-ff или --ff-only каждый раз, можно настроить поведение через конфигурацию Git.
Например, в репозитории или глобально:
# В конкретном репозитории
git config merge.ff false # Эквивалентно --no-ff по умолчанию
# Глобально для всех репозиториев текущего пользователя
git config --global merge.ff false
Возможные значения:
true— поведение по умолчанию (fast-forward, если возможно, иначе merge-коммит);false— всегда использовать no-ff (всегда создавать merge-коммит);only— эквивалент--ff-only.
Пример:
git config --global merge.ff only
# Теперь git merge по умолчанию будет вести себя как git merge --ff-only
Эта настройка помогает стандартизировать стиль истории в команде.
Визуальное представление fast-forward и обычного merge
Граф fast-forward слияния
Давайте посмотрим, что происходит в терминах графа коммитов.
Исходный граф:
A --- B --- C (feature)
\
D --- E (main)
Так выглядят расходящиеся ветки. Здесь:
- общий предок — A;
- в
feature— коммиты B, C; - в
main— коммиты D, E.
Это не случай fast-forward, здесь нужен merge-коммит.
А теперь рассмотрим fast-forward вариант:
A --- B --- C (feature)
\
D --- E (main)
Вот здесь main не отстает, он идет по отдельной линии.
Для fast-forward нам нужна другая картина. Пример корректного случая для fast-forward:
A --- B --- C (feature, main)
То есть:
- сначала была ветка
mainс A; - от нее создали
feature; - все коммиты делали только в
feature; - а
main"стоял на месте" на коммите A.
Граф до слияния:
A --- B --- C (feature)
^
|
main
После fast-forward:
A --- B --- C (main, feature)
То есть указатель main просто "прыгает" с A на C.
Как отличить fast-forward в истории
Хороший способ — использовать флаги --graph и --oneline:
git log --oneline --graph --decorate
- Линейная история без "ветвлений" и merge-коммитов — признак активного использования fast-forward.
- Наличие коммитов вида "Merge branch '...' " — признак обычных merges.
Если в вашей команде принята политика --no-ff, ветвления в истории вы будете видеть даже в тех случаях, когда fast-forward технически был возможен.
Связь fast-forward merge и rebase
Почему rebase часто используют перед fast-forward
На практике часто встречается связка:
git pull --rebaseилиgit rebase mainв вашей ветке;- затем fast-forward merge этой ветки в
main.
Смотрите, как это работает на примере.
Представьте:
main— A — D — E;feature— ответвилась от A и имеет B — C.
Граф:
A --- D --- E (main)
\
B --- C (feature)
Fast-forward здесь невозможен.
Вы хотите, чтобы история была линейной. Тогда можно сделать так:
- Перейти в
feature:
git checkout feature
- Перебазировать ее на текущий
main:
git rebase main
Git "перепишет" историю feature так, как будто вы начинали работу не от A, а от E:
A --- D --- E --- B' --- C' (feature)
^
main
Теперь main — строгий префикс feature. Можно делать fast-forward:
git checkout main
git merge feature # Здесь произойдет fast-forward
После этого:
A --- D --- E --- B' --- C' (main, feature)
Вы получили линейную историю без merge-коммита.
Автоматический rebase при pull
Очень часто разработчики настраивают Git так, чтобы git pull работал в режиме rebase, а не обычного merge. Это помогает избегать лишних merge-коммитов типа "Merge branch 'origin/main'".
Пример настройки:
git config --global pull.rebase true
Теперь при git pull будет происходить:
- сначала загрузка новых коммитов с сервера;
- затем
rebaseтекущей ветки на обновленную удаленную ветку.
В итоге, когда вы будете сливать свою ветку в main на сервере (например, через pull request), сервер с большой вероятностью сможет выполнить fast-forward merge.
Плюсы и минусы fast-forward merge
Преимущества fast-forward
Линейная история
История без merge-коммитов проще читается новичками. Коммиты идут один за другим, как "лента времени".
Меньше "шумных" merge-коммитов
Если вы часто обновляете свою ветку из
mainчерез обычный merge, появляются лишние merge-коммиты, которые не несут новых изменений. Fast-forward помогает уменьшить их количество.Удобный git bisect
Когда история линейная, поиск проблемного коммита через
git bisectкажется проще, граф не разветвляется.Прозрачные pull request в некоторых случаях
Если ветка маленькая и короткоживущая, fast-forward делает ее интеграцию в
mainпрактически "невидимой": коммиты просто вливаются в основной поток.
Недостатки fast-forward
Сложнее видеть границы фич
Когда вы всегда делаете fast-forward при слиянии веток, у вас нет merge-коммитов, по которым удобно понять, какие коммиты вошли в конкретную задачу.
Сложнее откатывать целые фичи
Если вы слили ветку с помощью обычного merge-коммита, вы можете
revertсделать по одному merge-коммиту и откатить всю фичу разом. При fast-forward иногда приходится откатывать серию отдельных коммитов.История "редко отражает" реальное ветвление
Линейная история после rebase + fast-forward может выглядеть так, будто все коммиты делались последовательно в одной ветке, хотя в реальности работали несколько людей параллельно. Это не всегда плохо, но иногда мешает анализу разработки.
Командные практики
На практике команды выбирают один из подходов:
Строгий fast-forward с rebase
- Все фичи проходят через rebase.
- В
mainтолько линейная история без merge-коммитов. - Часто применяется в проектах, где важна "чистая" история.
Всегда no-ff для фич
- При слиянии feature-веток в
mainиспользуют--no-ff. - Каждая фича отмечена merge-коммитом.
- История более ветвящаяся, но явно показывает группы коммитов.
- При слиянии feature-веток в
Смешанный режим
- Для краткоживущих мелких веток — fast-forward.
- Для крупных фич, релизных веток —
--no-ff.
Выбор зависит от размера команды, требований к аудиту истории и привычек.
Практические сценарии использования fast-forward
Сценарий 1. Личная ветка фичи без параллельных изменений
Это самый "чистый" случай.
- Вы создаете ветку
featureотmain. - Никто не меняет
main, пока вы работаете. - По окончании работы:
git checkout main
git merge feature # происходит fast-forward
Результат — линейная история, все коммиты вашей фичи теперь в main.
Сценарий 2. Небольшие локальные изменения в общем репозитории
Представьте, что вы делаете небольшой фикс в ветке hotfix:
- Создаете ветку:
git checkout main
git pull # обновляемся с сервера
git checkout -b hotfix
- Делаем пару коммитов в
hotfixи пушим:
git add .
git commit -m "Исправить баг валидации"
git push -u origin hotfix
Открываете pull request.
На сервере (например, GitHub) можно выбрать стратегию слияния:
- "Create a merge commit" — это как
--no-ff; - "Squash and merge" — сжатие всех коммитов в один;
- "Rebase and merge" — линейная история через rebase;
- "Fast-forward only" — если платформа такое поддерживает.
- "Create a merge commit" — это как
Если main не изменялся, сервер выполнит fast-forward или эквивалент (например, через rebase + fast-forward).
Сценарий 3. Отказ от fast-forward для "логических" группировок
Бывает, что ветку фичи хочется "зафиксировать" в истории как единое событие.
Представьте, что у вас фича из 10 коммитов, над которой работали 2 недели. Вы хотите:
- видеть точный момент, когда фича попала в
main; - уметь откатить ее одним
git revert.
В этом случае вы делаете:
git checkout main
git merge --no-ff feature
Даже если fast-forward был возможен, вы сохраняете отдельный merge-коммит, который логически группирует изменения.
Как отменить нежелательный fast-forward
Иногда случается так:
- вы ожидали merge-коммит;
- но забыли указать
--no-ff; - Git сделал fast-forward;
- а теперь нужно "откатить" и сделать merge-коммит или вернуть состояние.
Вариант 1. Использовать git reset (если еще не пушили)
Пусть вы:
- Были на ветке
main; - Выполнили
git merge feature(получился fast-forward); - Поняли, что хотели
--no-ff.
Если вы еще не отправляли изменения на сервер (git push не делали), можно:
git checkout main
git reset --hard HEAD@{1} # Возвращаемся к состоянию до merge
Комментарий:
- Git хранит "reflog" — журнал перемещений HEAD;
HEAD@{1}— предыдущее положение HEAD до последнего изменения.
После этого можно выполнить merge заново:
git merge --no-ff feature # Теперь будет создан merge-коммит
Вариант 2. Если уже запушили
Если изменения уже попали в общий репозиторий, история стала "общей", и переписывать ее через reset --hard не рекомендуется.
В таком случае чаще всего:
- оставляют fast-forward как есть;
- при необходимости создают дополнительный "маркирующий" коммит с пояснением;
- либо договариваются в команде, что дальше внимательно следят за
--no-ffи настройкамиmerge.ff.
Типичные ошибки и заблуждения вокруг fast-forward
Заблуждение 1. Fast-forward — это "более безопасно"
Fast-forward сам по себе не делает историю более "правильной" или более "безопасной". Он лишь меняет то, как оформлено слияние:
- с новой точкой (merge-коммитом),
- или без нее (просто перемещение указателя).
Конфликты, несогласованные изменения и баги никак не зависят от выбранного режима слияния. Все равно важно:
- обновляться из
mainрегулярно; - внимательно разрешать конфликты;
- писать понятные сообщения коммитов.
Заблуждение 2. Fast-forward нельзя использовать в больших командах
На самом деле fast-forward отлично работает и в больших командах, просто обычно его сочетают с:
- строгими правилами работы с ветками;
- регулярным rebase;
- ревью через pull request.
Часто используют комбинированный вариант:
- локальная работа — через rebase и fast-forward;
- крупные релизы — через no-ff merge для явного маркирования.
Заблуждение 3. Fast-forward и rebase — одно и то же
Это связанные, но разные вещи:
- rebase переписывает историю ветки так, чтобы она базировалась на другом коммите;
- fast-forward merge просто двигает указатель ветки вперед, если длительность истории это позволяет.
Но вместе они часто используются, чтобы:
- выровнять ветку с
main(через rebase); - после этого сделать чистый fast-forward merge.
Заключение
Fast-forward merge в Git — это механизм, который позволяет:
- выполнять слияние веток без создания merge-коммита;
- поддерживать линейную и визуально простую историю;
- гибко управлять поведением слияний через флаги (
--ff-only,--no-ff) и настройки (merge.ff).
Вы увидели, что fast-forward:
- возможен только тогда, когда целевая ветка является префиксом ветки-источника;
- тесно связан с практиками rebase, особенно в командной работе;
- имеет как плюсы (читаемость, простота), так и минусы (сложнее группировать и откатывать целые фичи).
Ключевая идея — не в том, чтобы "всегда делать fast-forward" или "всегда его запрещать", а в том, чтобы осознанно выбирать стратегию для вашего репозитория и команды:
- где нужна прозрачная линейная история — использовать rebase + fast-forward;
- где важны логические "узлы" фич — использовать
--no-ff.
Теперь, когда вы понимаете, как работает fast-forward merge, вам будет проще настроить удобный рабочий процесс и объяснить коллегам, почему история репозитория выглядит именно так.
Частозадаваемые технические вопросы и ответы
Вопрос 1. Как запретить fast-forward только для ветки main но разрешить в других ветках
Можно использовать защиту ветки на стороне сервера или хуки. Простейший вариант — pre-receive hook на сервере Git, который отклоняет push, если обновление refs/heads/main идет fast-forward-ом. Логика хука примерно такая
- Проверить старый и новый SHA для
main - Если обновление является fast-forward (старый коммит — предок нового) и push содержит merge без merge-коммита — отклонить
На GitHub и GitLab проще использовать настройки protected branch и выбор стратегии merge например по умолчанию запретить fast-forward и включить только merge-коммиты или squash.
Вопрос 2. Как на GitHub включить стратегию fast-forward only для pull request
На GitHub зайдите в Settings репозитория затем Branches и в разделе Merge button снимите галочки с Create a merge commit и Squash merging оставив только Rebase and merge или Fast-forward only если опция доступна. Тогда при слиянии PR интерфейс не позволит создать merge-коммит и будет использовать только линейное слияние.
Вопрос 3. Как узнать было ли конкретное слияние fast-forward если оно уже давно сделано
Если нет merge-коммита который явно указывает на слияние то это и есть признак fast-forward. Для проверки
- Найдите коммиты по сообщениям или диапазону
- Убедитесь что коммит где должна была "влиться" ветка не является merge-коммитом (у него один родитель)
- Посмотрите граф
git log --graph --oneline --decorate --all— если ветка просто "слилась" в линию без узла значит использовали fast-forward.
Вопрос 4. Можно ли сделать fast-forward merge локально если на сервере уже есть merge-коммиты
Да но только в своей локальной истории. Сначала подтяните изменения с сервера git fetch origin. Затем создайте локальную ветку от нужного коммита и сделайте rebase так чтобы ваша фича шла после серверных merge-коммитов. После этого можно выполнить локальный fast-forward merge своей ветки в локальный main. Но при push Git сравнит истории и если серверная история несовместима (другие merge-коммиты) push будет отклонен или потребует принудительного --force-with-lease что нужно делать очень осторожно.
Вопрос 5. Как настроить чтобы git pull в моей фиче не создавал merge-коммиты а позволял потом сделать fast-forward в main
Настройте rebase при pull
- Глобально
git config --global pull.rebase true - Или только для конкретной ветки
git config branch.feature.rebase true
Теперь приgit pullваша ветка будет автоматически перебазироваться на свежий main. Когда вы закончите работу и будете сливать фичу в main сервер или локальный репозиторий с высокой вероятностью сможет сделать fast-forward merge.
Постройте личный план изучения Git до уровня Middle — бесплатно!
Git — часть карты развития Frontend
100+ шагов развития
30 бесплатных лекций
300 бонусных рублей на счет
Бесплатные лекции
Все гайды по Git
Лучшие курсы по теме

Основы Git
Антон Ларичев
TypeScript с нуля
Антон Ларичев