Олег Марков
Стратегии слияния в Git и других системах контроля версий
Введение
Стратегии слияния (merge strategy) определяют, как именно система контроля версий объединяет изменения из одной ветки в другую. От выбранной стратегии зависит история коммитов, удобство анализа изменений, скорость решения конфликтов и даже то, насколько просто будет откатить ошибку.
Смотрите, я покажу вам, как подойти к стратегиям слияния не только с точки зрения синтаксиса команд, но и с точки зрения процессов разработки. Мы разберем базовые и продвинутые стратегии Git, посмотрим, как они влияют на историю, и разберем практические сценарии: фича-ветки, релизы, срочные багфиксы.
Мы будем опираться в основном на Git, потому что он де-факто стандарт. Но многие идеи будут понятны и пользователям других систем контроля версий, например Mercurial.
Что такое стратегия слияния
Стратегия слияния отвечает на несколько практических вопросов:
- Нужно ли сохранять все промежуточные коммиты из feature-ветки или достаточно одного итогового?
- Должен ли результат слияния быть отдельным merge-коммитом или история может быть переписана так, будто ветвления не было?
- Как обрабатываются конфликты, если одни и те же строки менялись в разных ветках?
- Как будет выглядеть история в
git logиgit blame?
В Git это проявляется в трех уровнях:
Тип слияния по смыслу
- обычный merge
- rebase
- squash
- fast-forward
Техническая стратегия
-s(strategy)
Например:recursive,resolve,ours,octopus.Опции стратегии
-X(strategy options)
Например:-X ours,-X theirs,-X rename-threshold.
В реальной разработке вы чаще всего оперируете первым уровнем (merge, rebase, squash, fast-forward), а к -s и -X обращаетесь в более сложных ситуациях.
Основные типы слияния в Git
Обычное merge (non fast forward)
Это классический сценарий: вы берете feature-ветку и вливаете ее в main или develop через merge-коммит. Давайте разберемся на примере.
Представим историю:
- Ветка
main: A — B — C - Ветка
feature: ответвилась от B и имеет коммиты D — E
Теперь вы хотите влить feature в main.
Команда
# Переключаемся на основную ветку
git checkout main
# Сливаем feature в main
git merge feature
Git создаст новый merge-коммит M с двумя родителями: C (из main) и E (из feature).
Преимущества:
- Сохраняется полная история изменений
- Видно, какие коммиты относятся к какой задаче
- Merge-коммит фиксирует факт интеграции ветки
Недостатки:
- История становится древовидной, сложнее воспринимать линейно
- При большом количестве мелких фича-веток лог может "захламляться"
Часто в командах договариваются, что все слияния в основную ветку делаются через merge-коммиты, чтобы явно фиксировать каждое интеграционное событие.
Fast-forward merge
Fast-forward возможен, когда целевая ветка не ушла вперед относительно исходной. Смотрите, здесь логика простая: история main просто "догоняет" историю feature.
Исходная ситуация:
main: A — Bfeature: A — B — C — D
Если вы вольете feature в main, то Git просто передвинет указатель main на D. Merge-коммит не создается.
Команда
git checkout main
git merge feature # Если нет новых коммитов в main - произойдет fast-forward
История main станет: A — B — C — D
Плюсы:
- Линейная история, очень читаемая
- Нет лишних merge-коммитов
- Удобно для коротких и локальных веток
Минусы:
- Нету явного маркера "здесь была фича-ветка"
- Сложнее увидеть, какие коммиты относились к конкретной задаче, если их несколько
Если вы хотите запретить fast-forward и всегда делать merge-коммит, даже когда он не обязателен, используйте:
git checkout main
git merge --no-ff feature
Git создаст merge-коммит, и это позволит, например, легко откатить всю фичу одним revert merge-коммита.
Squash merge
Squash-слияние "сминает" все коммиты feature-ветки в один большой коммит в целевой ветке. История получается линейной, но без подробностей внутренних шагов разработки.
Представим:
main: A — Bfeature: B — C — D — E
Вы хотите, чтобы в main появился один аккуратный коммит с результатом всей работы.
Команда
git checkout main
# Делаем squash слияние
git merge --squash feature
# Git подготовит изменения в рабочей директории и индексе, но не создаст коммит
git commit -m "Добавлена новая функция расчета скидок"
Как видите, итоговым будет только один коммит в main, хотя внутри feature их было несколько.
Плюсы:
- Аккуратная и линейная история основной ветки
- Один коммит = одна значимая задача
- Удобно для командного code review: смотрите один цельный diff
Минусы:
- Теряется информация о внутренних шагах разработки
- Нельзя просто "переместить" фича-ветку в другие ветки вместе с ее внутренней историей
- Сложнее анализировать по коммитам, если внутри ветки были смысловые этапы
Squash часто используют вместе с GitHub-пулреквестами, где можно выбрать опцию Squash and merge.
Rebase как альтернатива merge
Rebase — это не совсем стратегия слияния, а операция переписывания истории. Но на практике его часто рассматривают как альтернативу merge.
Смотрите, как это работает.
Исходная ситуация:
main: A — B — Cfeature: ответвилась от B и имеет D — E
За время работы над feature в main появился коммит C. Теперь вы хотите "поставить" D и E поверх C, чтобы история была линейной.
Команда
# Переключаемся на feature
git checkout feature
# Переносим feature поверх актуального main
git rebase main
Git:
- найдет общий коммит (B)
- возьмет изменения из D и E
- "переиграет" их поверх C, создав новые коммиты D' и E'
Результат:
main: A — B — Cfeature: A — B — C — D' — E'
Теперь вы можете сделать fast-forward merge:
git checkout main
git merge feature # fast-forward
Плюсы:
- Линейная чистая история без merge-коммитов
- Удобно смотреть историю
git logи выполнятьgit bisect - Популярно в open-source, когда разработчик подстраивает ветку по актуальному
mainперед отправкой
Минусы:
- Переписывает историю, меняя идентификаторы коммитов
- Опасно делать rebase для уже "расшаренных" веток, которые кто-то успел забрать себе
Технические стратегии Git merge (-s)
Теперь давайте перейдем глубже: посмотрим, какие технические стратегии применяются Git при выполнении git merge. В большинстве случаев вам не нужно указывать их явно, но в сложных ситуациях это бывает полезно.
Стратегия recursive (по умолчанию)
recursive — основная стратегия, которая используется для слияния двух веток (двух родителей). Она умеет:
- автоматически находить общий предок
- анализировать изменения с обеих сторон
- обнаруживать переименования файлов
- пытаться минимизировать конфликты
Команда:
git merge -s recursive feature
На практике обычно так писать не нужно, потому что recursive и так выбирается автоматически, если сливаются две ветки.
Дополнительные опции для нее задаются с помощью -X, например:
git merge -s recursive -X ours feature
Обратите внимание, что -X ours здесь — это опция стратегии, а не отдельная стратегия ours. Разница между ними важна, ниже разберем.
Стратегия resolve
resolve старая и более простая стратегия. Она не поддерживает переименования файлов и хуже справляется со сложными конфликтами.
Команда:
git merge -s resolve feature
Используется редко, в основном по историческим причинам или для отладки. Если вы только настраиваете процессы — почти всегда остается recursive.
Стратегия ours (как стратегия -s)
Стратегия ours в виде -s ours означает: "сделать merge-коммит, но полностью игнорировать содержимое другой ветки".
Пример:
git merge -s ours old-experiment-branch
Что произойдет:
- Git создаст merge-коммит
- Все файлы в результате останутся такими, как в текущей ветке
- Ветка
old-experiment-branchбудет считаться слитой (с точки зрения истории), но ее содержимое не попадет в рабочую директорию
Когда это может пригодиться:
- Закрыть экспериментальную ветку, явно обозначив ее как "учтенную", но не внося ее изменения в код
- Разрезать историю на логические сегменты
Здесь я специально подчеркиваю: это не то же самое, что -X ours, хотя звучит похоже.
Стратегия octopus
octopus — стратегия для слияния трех и более веток одновременно. Как правило, применяется в автоматических сценариях, например при массовом обновлении веток.
Пример:
git merge feature1 feature2 feature3 -s octopus
Ограничения:
- Подходит только если нет сложных конфликтов
- В противном случае Git откажется от octopus-слияния
В повседневной разработке обычно хватает двухстороннего merge. Но иногда octopus используют при интеграции множества веток релиза.
Опции стратегий merge (-X)
Опции -X настраивают поведение конкретной стратегии (чаще всего recursive). Давайте посмотрим на самые востребованные.
Опция -X ours против стратегии -s ours
Очень частый источник путаницы.
-s ours— это стратегия слияния, которая полностью игнорирует изменения из второй ветки, но при этом создается merge-коммит-X ours— это опция дляrecursive, которая говорит: "если есть конфликт по строкам, выбирай версию из текущей ветки"
Давайте разберемся на примере.
Пример с -X ours
git merge -s recursive -X ours feature
Здесь:
- если файл менялся только в одной ветке — изменения будут учтены
- если один и тот же фрагмент изменяли в обеих ветках — Git выберет вариант из текущей ветки
Это удобно, когда вы заранее знаете, что текущая ветка более "важная" и ее изменения должны иметь приоритет.
Пример с -s ours
git merge -s ours feature
Здесь:
- все файлы останутся такими, как в текущей ветке
- содержимое
featureбудет полностью проигнорировано - ветка
featureбудет помечена как "слитая" в истории
Опция -X theirs
В отличие от -X ours, опция -X theirs дает приоритет изменениям из сливаемой ветки при разрешении конфликтов.
Пример:
git merge -s recursive -X theirs feature
Поведение:
- если конфликта нет — объединение выполняется обычно
- если конфликт — выбирается версия из
feature
Обратите внимание, что -X theirs влияет только на конфликтующие участки, а не на все изменения в целом.
Другие полезные опции
Некоторые дополнительные опции стратегии recursive могут помочь в специфических сценариях:
-X rename-threshold=<N>— управляет чувствительностью к переименованиям файлов-X patience— включает "терпеливый" алгоритм диффа, который иногда лучше справляется со сложными изменениями-X ignore-all-space— игнорирует изменения, связанные только с пробелами
Пример использования:
git merge -s recursive -X patience feature
Такой merge иногда дает более "чистый" diff, когда вы перераспределяете большие блоки кода.
Влияние стратегии слияния на историю и процессы
Теперь давайте посмотрим, как выбор стратегии влияет на рабочий процесс команды.
История коммитов и чтение git log
Чем больше вы используете merge-коммиты, тем больше история становится древовидной. Это:
- удобно, когда вы хотите видеть, какие фичи когда вливались
- неудобно, если вы любите линейную историю
Если вы предпочитаете линейную историю, чаще применяйте:
- rebase перед слиянием
- fast-forward
- squash merge
Если вы хотите видеть каждую интеграцию как отдельное событие, используйте:
git merge --no-ff feature- запрет fast-forward в настройках репозитория (например, в GitHub или GitLab)
Связь с ветвлением по Git Flow и Trunk Based Development
Разные процессы разработки предпочитают разные стратегии:
Git Flow
- фича-ветки вливают в
develop - релизные ветки и hotfix могут сливаться через merge-коммиты
- часто разрешают обычные merge с
--no-ff, чтобы все интеграции были видны
- фича-ветки вливают в
Trunk Based Development
- короткоживущие фича-ветки
- сильный акцент на линейной истории
- широко применяются rebase и fast-forward, squash через pull request
Выбор стратегии — часть договоренностей команды. Важно, чтобы все участники понимали, как в вашем проекте "правильно" сливать ветки.
Влияние на git blame и откаты
Стратегия слияния влияет на:
- то, какой коммит будет показан в
git blame - насколько легко откатывать отдельные изменения
Если вы используете squash:
blameпокажет один большой коммит, и отследить "кто поменял эту строку" будет проще, но… только до границ этого одного коммита- откатить всю фичу можно будет одним
git revert <hash>
Если вы используете обычный merge:
blameбудет ссылаться на конкретный коммит, в котором изменялась строка- иногда само изменение было в одном из внутренних фича-коммитов, а merge-коммит просто объединял их
Для откатов удобны merge-коммиты, созданные с --no-ff. Вы можете:
git revert -m 1 <hash-merge-коммита>
И этим откатить целую фичу одним действием.
Автоматизация выбора стратегии в Git
Чтобы вам не приходилось каждый раз вспоминать опции команд, можно настроить стратегии слияния по умолчанию.
Настройки merge по умолчанию
Вы можете задать стратегию слияния для конкретной ветки через конфигурацию.
Пример: запрет fast-forward для ветки main.
# Включаем для ветки main опцию no-ff
git config branch.main.mergeoptions "--no-ff"
Теперь, когда вы будете делать:
git checkout main
git merge feature
Git автоматически добавит --no-ff и создаст merge-коммит.
Проверить настройки можно так:
git config --get branch.main.mergeoptions
Хуки Git для контроля стратегии
Можно использовать хуки (hooks), чтобы проверять, как делается слияние.
Например, вы можете написать pre-commit или pre-receive хук, который:
- запрещает merge-коммиты без
--no-ffв основной ветке - не принимает коммиты, созданные после "грязного"
rebaseна общих ветках
Пример очень простого client-side hook (псевдокод, иллюстрация идеи):
# .git/hooks/pre-commit (примерный скрипт на bash)
# Здесь мы проверяем, не в ветке ли main мы и не делаем ли "грязный" merge
branch=$(git rev-parse --abbrev-ref HEAD)
if [ "$branch" = "main" ]; then
# Проверяем, нет ли не fast-forward merge
if git log --merges -1 --pretty=%P | grep -q " "; then
echo "Merge-коммиты в main должны быть только через pull request"
exit 1
fi
fi
# Разрешаем коммит
exit 0
Комментарии:
- Этот код иллюстративный. Его нужно доработать под ваши конкретные правила.
- Смотрите, идея в том, чтобы автоматикой защищать стратегию истории.
Настройки на уровне GitHub или GitLab
Большинство хостингов репозиториев позволяют выбрать, какие стратегии слияния разрешены через pull request.
Например, в GitHub вы можете:
- Разрешить или запретить:
- Merge commit
- Squash and merge
- Rebase and merge
Это напрямую определяет, как будет выглядеть история при принятии pull request.
Хорошая практика:
- В основных репозиториях (например, библиотека, которой пользуются внешние команды) чаще включают Squash and merge
- В внутренних моно-репозиториях часто разрешают обычный merge с
--no-ff, чтобы явно видеть пакеты изменений
Практические сценарии выбора стратегии
Теперь давайте пройдемся по типичным ситуациям и посмотрим, какие стратегии чаще всего подходят.
Короткая feature-ветка от одного разработчика
Сценарий:
- Вы создали ветку
feature/login-page - Сделали 3–5 коммитов
- Открыли pull request в
develop
Варианты:
Squash and merge
- В итоге в
developбудет один коммит - Удобно, если внутренняя история фичи вам не важна
- В итоге в
Rebase + fast-forward
- Перед merge делаете
git rebase develop - Ветка становится линейной
- Merge можно сделать fast-forward
- Перед merge делаете
Обычный merge
- История будет включать все внутренние коммиты
- Уместно, если каждый коммит несет осмысленные изменения
Чаще всего команды выбирают 1 или 2 вариант.
Долгоживущая feature-ветка
Сценарий:
- Несколько разработчиков
- Ветка живет неделями
- Регулярно нужно подтягивать изменения из
developилиmain
Здесь уже важна читаемость истории и простота интеграций.
Практика:
- регулярно делать
rebaseилиmergeизdevelopв feature-ветку - перед финальным слиянием в
develop:- либо делается финальный
rebaseи fast-forward - либо обычный merge с
--no-ff, чтобы явно зафиксировать "большую" фичу
- либо делается финальный
Hotfix для production
Сценарий:
- Проблема найдена в production
- Вы создаете ветку
hotfix/critical-bugотmain - После фикса нужно:
- вливать в
mainкак срочный патч - подтянуть этот фикс в
develop
- вливать в
Часто делают:
- Squash или обычный merge в
main - Затем merge
mainвdevelop(часто обычный merge)
Почему merge main в develop, а не cherry-pick:
- merge гарантирует, что все изменения production попадают в линию разработки
- cherry-pick может привести к расхождениям, если не контролировать их вручную
Массовое обновление множества веток
Представьте, что у вас десятки веток, и вы обновляете зависимости или инфраструктуру.
Возможные варианты:
- Делать отдельный merge для каждой ветки
- Попробовать использовать
octopusmerge, если нужно слить в одну ветку несколько других одновременно
Однако чаще всего практичнее:
- сделать изменение в общей базовой ветке
- затем разработчики просто поднимают свои feature-ветки через rebase или merge
Примеры пошаговой работы с разными стратегиями
Теперь вы увидите, как это выглядит в коде на практических примерах.
Пример 1. Обычный merge с конфликтом и его решением
Пусть у нас есть файл config.yml, и обе ветки меняют одну и ту же строку.
# Ветка main
git checkout main
echo "mode: production" > config.yml
git commit -am "Установлен режим production"
# Ветка feature
git checkout -b feature
echo "mode: debug" > config.yml
git commit -am "Установлен режим debug"
Теперь сливаем feature в main:
git checkout main
git merge feature
Git выдаст конфликт и пометит файл config.yml:
<<<<<<< HEAD
mode: production
=======
mode: debug
>>>>>>> feature
Решение:
# Допустим, хотим оставить режим production, но добавить комментарий
mode: production
# debug включайте только локально
Затем:
git add config.yml
git commit # Завершение merge-коммита
Комментарии:
- Здесь мы вручную решаем конфликт
- Merge-коммит будет иметь двух родителей
Пример 2. Использование -X theirs для приоритета фича-ветки
Представьте похожую ситуацию, но вы заранее знаете, что изменения в feature-ветке должны побеждать.
git checkout main
git merge -s recursive -X theirs feature
Git:
- автоматически выберет конфликтующие участки из
feature - попытается не требовать ручного вмешательства, если это возможно
Такую стратегию можно использовать для сценариев "обновляемся на базу новой версии, а локальные изменения считаем менее важными".
Пример 3. Rebase перед слиянием
Смотрите, здесь я размещаю цепочку команд, чтобы вам было проще понять полный цикл.
# Шаг 1. Создаем feature-ветку от main
git checkout main
git checkout -b feature/refactor-payment
# Делаем несколько коммитов в feature
# ...
git commit -am "Рефакторинг модуля платежей"
git commit -am "Добавлена логгирование ошибок платежей"
# За это время main обновился
git checkout main
git commit -am "Добавлена поддержка купонов" # пример
# Шаг 2. Обновляем feature по основному main
git checkout feature/refactor-payment
git rebase main # переносим фичу поверх свежего main
# Шаг 3. Вливаем через fast-forward
git checkout main
git merge feature/refactor-payment # fast-forward
Результат:
- История main будет линейной
- В ней будут подряд:
- "Добавлена поддержка купонов"
- "Рефакторинг модуля платежей"
- "Добавлена логгирование ошибок платежей"
Заключение
Стратегии слияния — это не просто набор флагов для команды git merge. Это часть архитектуры процессов разработки: как вы работаете с ветками, как выглядят ваши pull request, как проще всего анализировать историю и откатывать изменения.
Ключевые моменты:
- Обычный merge с
--no-ffудобен, когда вы хотите явно видеть моменты интеграции фич. - Fast-forward хорошо подходит для коротких веток и линейной истории.
- Squash делает историю основной ветки аккуратной, но прячет внутренние шаги разработки.
- Rebase помогает поддерживать линейную историю, но требует аккуратности с общими ветками.
- Стратегии
-sи опции-Xпозволяют точнее управлять поведением merge в сложных случаях и автоматизировать правила.
Главное — договориться в команде, какие стратегии вы используете в каких ситуациях, и зафиксировать это в документации проекта или в настройках хостинга репозитория. Тогда слияния перестанут быть "магией" и станут предсказуемым и понятным инструментом.
Частозадаваемые технические вопросы по merge strategy
Как запретить прямые push в основную ветку и заставить всех использовать pull request с нужной стратегией
Используйте защиту веток на стороне GitHub GitLab или другого сервиса.
В GitHub зайдите в Settings – Branches – Add rule – выберите основную ветку – включите Require a pull request before merging и далее выберите разрешенные способы слияния например только Squash and merge. Это гарантирует что в основную ветку нельзя будет запушить изменения напрямую и нельзя будет использовать неподходящие стратегии.
Как сделать так чтобы git pull всегда использовал rebase вместо merge
Настройте глобальную конфигурацию:
git config --global pull.rebase true
Теперь git pull по умолчанию будет делать fetch + rebase вместо merge. Для конкретного репозитория можно задать:
git config pull.rebase true
Если нужно вернуть поведение только для одной команды используйте git pull --no-rebase.
Как безопасно использовать rebase если ветку уже кто-то забрал себе
Если ветка уже пушилась в общий репозиторий лучше избегать переписывания истории. Если все же нужно сделать rebase:
- Согласуйте это с командой.
- Сделайте
git rebaseлокально. - Выполните
git push --force-with-lease.
Опция --force-with-lease безопаснее чем --force она проверяет что в удаленной ветке нет новых коммитов которых у вас еще нет. Если есть push будет отклонен и вы не затрете чужую работу.
Как узнать была ли использована стратегия squash при слиянии pull request
В чистом Git напрямую отличить squash от обычного коммита сложно потому что squash создает обычный коммит без информации о родителях. Но можно:
- посмотреть сообщение коммита – многие системы хостинга автоматически добавляют туда номера PR
- сравнить diff между целевой веткой до и после слияния – если там весь объем фича-ветки одним коммитом скорее всего был squash
- в GitHub и GitLab информация о типе merge хранится в интерфейсе PR а не в Git историях.
Как откатить squash merge если в нем много изменений
- Найдите хэш squash-коммита в
git log. - Выполните:
git revert <hash-squash-коммита>
Git создаст новый коммит который отменит все изменения того squash. Если при ревертировании возникнут конфликты решите их вручную добавьте файлы в индекс и завершите процесс командой git commit.
Постройте личный план изучения Git до уровня Middle — бесплатно!
Git — часть карты развития Frontend
100+ шагов развития
30 бесплатных лекций
300 бонусных рублей на счет
Бесплатные лекции
Все гайды по Git
Лучшие курсы по теме

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