Стратегии слияния в Git и других системах контроля версий

17 декабря 2025
Автор

Олег Марков

Введение

Стратегии слияния (merge strategy) определяют, как именно система контроля версий объединяет изменения из одной ветки в другую. От выбранной стратегии зависит история коммитов, удобство анализа изменений, скорость решения конфликтов и даже то, насколько просто будет откатить ошибку.

Смотрите, я покажу вам, как подойти к стратегиям слияния не только с точки зрения синтаксиса команд, но и с точки зрения процессов разработки. Мы разберем базовые и продвинутые стратегии Git, посмотрим, как они влияют на историю, и разберем практические сценарии: фича-ветки, релизы, срочные багфиксы.

Мы будем опираться в основном на Git, потому что он де-факто стандарт. Но многие идеи будут понятны и пользователям других систем контроля версий, например Mercurial.


Что такое стратегия слияния

Стратегия слияния отвечает на несколько практических вопросов:

  • Нужно ли сохранять все промежуточные коммиты из feature-ветки или достаточно одного итогового?
  • Должен ли результат слияния быть отдельным merge-коммитом или история может быть переписана так, будто ветвления не было?
  • Как обрабатываются конфликты, если одни и те же строки менялись в разных ветках?
  • Как будет выглядеть история в git log и git blame?

В Git это проявляется в трех уровнях:

  1. Тип слияния по смыслу

    • обычный merge
    • rebase
    • squash
    • fast-forward
  2. Техническая стратегия -s (strategy)
    Например: recursive, resolve, ours, octopus.

  3. Опции стратегии -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 — B
  • feature: 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 — B
  • feature: 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 — C
  • feature: ответвилась от 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 — C
  • feature: 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

Варианты:

  1. Squash and merge

    • В итоге в develop будет один коммит
    • Удобно, если внутренняя история фичи вам не важна
  2. Rebase + fast-forward

    • Перед merge делаете git rebase develop
    • Ветка становится линейной
    • Merge можно сделать fast-forward
  3. Обычный 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

Часто делают:

  1. Squash или обычный merge в main
  2. Затем merge main в develop (часто обычный merge)

Почему merge main в develop, а не cherry-pick:

  • merge гарантирует, что все изменения production попадают в линию разработки
  • cherry-pick может привести к расхождениям, если не контролировать их вручную

Массовое обновление множества веток

Представьте, что у вас десятки веток, и вы обновляете зависимости или инфраструктуру.

Возможные варианты:

  • Делать отдельный merge для каждой ветки
  • Попробовать использовать octopus merge, если нужно слить в одну ветку несколько других одновременно

Однако чаще всего практичнее:

  • сделать изменение в общей базовой ветке
  • затем разработчики просто поднимают свои 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:

  1. Согласуйте это с командой.
  2. Сделайте git rebase локально.
  3. Выполните 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 если в нем много изменений

  1. Найдите хэш squash-коммита в git log.
  2. Выполните:
git revert <hash-squash-коммита>

Git создаст новый коммит который отменит все изменения того squash. Если при ревертировании возникнут конфликты решите их вручную добавьте файлы в индекс и завершите процесс командой git commit.

Интерактивное перебазирование Git rebase -i - подробное практическое руководствоСтрелочка вправо

Постройте личный план изучения Git до уровня Middle — бесплатно!

Git — часть карты развития Frontend

  • step100+ шагов развития
  • lessons30 бесплатных лекций
  • lessons300 бонусных рублей на счет

Бесплатные лекции

Все гайды по Git

Открыть базу знаний

Лучшие курсы по теме

изображение курса

Основы Git

Антон Ларичев
AI-тренажеры
Гарантия
Бонусы
иконка звёздочки рейтинга4.9
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

TypeScript с нуля

Антон Ларичев
AI-тренажеры
Практика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Next.js - с нуля

Антон Ларичев
AI-тренажеры
Практика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее

Отправить комментарий