Олег Марков
Слияние веток в Git - полное руководство по git merge
Введение
Слияние веток в Git с помощью команды git merge — одна из ключевых операций, без которой невозможно эффективно вести разработку в команде. Как только в проекте появляются отдельные ветки для фич, багфиксов или релизов, возникает задача собрать изменения вместе и не потерять ни одну строку кода.
Здесь вы разберете, как работает git merge изнутри, чем слияние отличается от других способов интеграции изменений (например, rebase), какие бывают типы слияний, как выглядят конфликты и как их безопасно решать. Я покажу вам примеры реальных команд и ситуаций, с которыми вы столкнетесь в ежедневной работе.
Главная цель статьи — дать вам не только набор команд, но и понимание, что делает Git при слиянии, какие есть стратегии и как вы можете контролировать историю коммитов.
Базовые понятия для понимания git merge
Прежде чем писать команды, важно коротко проговорить несколько концепций, на которых основано слияние.
Что такое ветка в Git с точки зрения слияния
Ветка в Git — это просто «указатель» на определенный коммит. Когда вы создаете ветку:
git branch feature/login
Git создает новый указатель, который указывает на текущий коммит. Когда вы двигаетесь вперед по истории (делаете новые коммиты), указатель ветки смещается на новый коммит.
Для слияния это важно: когда вы делаете git merge, Git сравнивает не «ветки» как сущности, а наборы коммитов, на которые указывают эти ветки, и их общую историю.
Общий предок веток и трехстороннее слияние
Когда вы сливаете две ветки, Git ищет их общего предка (common ancestor). Это тот коммит, от которого когда-то «разошлись» ветки. Дальше Git выполняет трехстороннее слияние:
- база — общий предок;
- ваша текущая ветка (куда вы сливаете, часто называют target или destination);
- ветка, которую вы вливаете (source).
Смотрите, как это выглядит на схеме:
- Коммит C0 — общий предок.
- Коммиты C1, C2 — продолжение ветки main.
- Коммиты F1, F2 — продолжение ветки feature.
Когда вы выполняете:
git checkout main
git merge feature
Git сравнивает изменения:
- от C0 до C2 (ваша ветка main),
- от C0 до F2 (ветка feature), и создаёт новый коммит слияния M, который объединяет оба набора изменений.
Основы работы с git merge
Простое слияние без конфликтов
Для начала давайте разберемся с ситуацией, когда конфликты не возникают.
Предположим, у вас есть две ветки:
- main — основная ветка;
- feature/header — ветка с новой фичей.
Вы хотите влить изменения из feature/header в main.
# Переключаемся в ветку, куда будем вливать изменения
git checkout main
# Вливаем изменения из ветки feature/header
git merge feature/header
Если Git смог автоматически объединить все изменения, вы увидите примерно такое сообщение:
Updating a1b2c3d..e4f5g6h
Fast-forward
src/header.html | 10 ++++++++++
1 file changed, 10 insertions(+)
Обратите внимание на слова Fast-forward — это один из типов слияния, о котором поговорим отдельно.
Что происходит при простом merge
- Git находит общий предок веток.
- Сравнивает изменения.
- Если нет конфликтующих изменений (одни и те же строки не были изменены по-разному), Git автоматически создает новый коммит слияния или просто перемещает указатель (fast-forward).
Fast-forward merge — «перемотка» ветки вперед
Fast-forward (FF) — это случай, когда ветка-приемник не имеет своих уникальных коммитов после того места, где ветки разошлись. Проще: main «отстает» от feature и может просто «догнать» ее.
Давайте разберемся на простом примере.
# Создаем репозиторий
git init
# Создаем файл и первый коммит
echo "v1" > version.txt
git add version.txt
git commit -m "Initial version"
# Создаем новую ветку feature
git branch feature
# Переключаемся в ветку feature и делаем изменения
git checkout feature
echo "v2" > version.txt
git commit -am "Update to v2"
Теперь история такая:
- main указывает на коммит "Initial version";
- feature указывает на коммит "Update to v2".
Если вы выполните:
git checkout main
git merge feature
Git просто передвинет указатель main на тот же коммит, на который указывает feature. Новый коммит не создается — это и есть fast-forward merge.
Пример вывода
Updating 1a2b3c4..5d6e7f8
Fast-forward
version.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
Merge с коммитом слияния (non fast-forward)
Когда у целевой ветки есть свои изменения, которые не присутствуют в вливаемой ветке, Git не может просто переместить указатель. В этом случае создается merge-коммит — специальный коммит с двумя родителями.
Давайте наглядно:
# Исходно вы на main
git checkout main
# Делаем коммит на main
echo "main v2" > main.txt
git add main.txt
git commit -m "Main changes"
# Создаем ветку feature от main
git branch feature
# Переключаемся в feature
git checkout feature
# Делаем изменения в feature
echo "feature v1" > feature.txt
git add feature.txt
git commit -m "Feature changes"
Теперь:
- main имеет свои изменения (main.txt),
- feature имеет свои (feature.txt).
Если вы выполните:
git checkout main
git merge feature
Git создаст новый коммит слияния. Вывод может быть такого вида:
Merge made by the 'ort' strategy.
feature.txt | 1 +
1 file changed, 1 insertion(+)
create mode 100644 feature.txt
История теперь содержит merge-коммит, у которого два родителя:
- один — последний коммит main,
- второй — последний коммит feature.
Типы и режимы слияния
Теперь давайте разберем разные варианты поведения git merge, которые вы можете настраивать.
Слияние с запретом fast-forward — флаг --no-ff
Иногда вы хотите всегда иметь явный merge-коммит, даже если с точки зрения Git возможно fast-forward слияние. Это важно, когда вы хотите сохранить «ветвистость» истории и явно видеть, где заканчивалась работа над фичей.
Для этого используется опция:
git checkout main
git merge --no-ff feature/login
Что делает этот флаг:
- даже если main можно просто перемотать вперед,
- Git все равно создаст отдельный merge-коммит.
Плюс такого подхода — в истории хорошо видно, какие коммиты относятся к конкретной задаче или ветке. Минус — история становится более «шумной» из-за большого количества merge-коммитов.
Слияние с запретом автоматического коммита — флаг --no-commit
Если вы хотите сначала посмотреть, какие именно изменения подготовит Git, и только потом решить, делать ли коммит слияния, используйте:
git checkout main
git merge --no-commit feature/search
Git:
- выполнит слияние,
- запишет изменения в рабочую директорию и индекс (staging area),
- но не создаст коммит.
Дальше вы можете:
просмотреть изменения:
git status git diff --cached- подправить файлы, если нужно;
сделать свой коммит слияния:
git commit -m "Merge feature/search into main with manual adjustments"
Такой подход полезен, если вы ожидаете, что после слияния придется что-то немного доправить вручную.
Отмена слияния до коммита — флаг --abort
Представьте, что вы начали слияние, появились конфликты, а вы поняли, что сейчас разруливать их неудобно, или вы вообще выбрали не ту ветку.
Вы можете откатиться к состоянию до начала merge:
git merge feature/report
# Конфликты, вы передумали
git merge --abort
Git вернет репозиторий к состоянию, в котором он был до запуска merge. Это удобно, когда вы хотите полностью «забыть», что вообще начинали слияние.
Конфликты при слиянии: почему возникают и как решать
Теперь давайте разберем наиболее волнительную часть — конфликты.
Когда возникают конфликты
Конфликт возникает, когда:
- в обеих ветках были изменены одни и те же строки одного и того же файла, и
- Git не может автоматически решить, какая версия «правильная».
Например:
- Ветка main изменила строку
title = "App"наtitle = "Main App"; - Ветка feature изменила ту же строку на
title = "Feature App".
Git не может угадать, что вы имели в виду, и просит вас принять решение вручную.
Как выглядит конфликт в файле
Смотрите, я покажу вам, как Git помечает конфликтующие участки. Допустим, файл config.txt после merge содержит:
app_name = My App
<<<<<<< HEAD
title = Main App
=======
title = Feature App
>>>>>>> feature/header
version = 1.0.0
Комментарии, которые добавил Git:
<<<<<<< HEAD— начало вашего варианта (ветка, куда вы сливаете);=======— разделитель между вариантами;>>>>>>> feature/header— конец варианта из вливаемой ветки feature/header.
Ваша задача — вручную отредактировать этот фрагмент, удалить служебные строки и оставить итоговый вариант, который вам нужен. Например:
app_name = My App
title = Main App with feature header
version = 1.0.0
Здесь вы явно решили, как объединить оба изменения.
Типовой процесс решения конфликтов
Давайте разберем пошагово, что вам нужно делать, когда конфликты возникли.
Предположим, вы выполняете:
git checkout main
git merge feature/api
И видите что-то вроде:
Auto-merging src/api.js
CONFLICT (content): Merge conflict in src/api.js
Automatic merge failed; fix conflicts and then commit the result.
Теперь по шагам:
Посмотреть статус:
git statusВы увидите файлы в состоянии both modified (оба изменили).
Открыть конфликтные файлы в редакторе.
Там вы увидите блоки с
<<<<<<<,=======,>>>>>>>.Отредактировать файлы:
- убрать маркеры конфликтов,
- вручную привести код к нужному виду.
Пометить конфликты как решенные, добавив файл в индекс:
git add src/api.jsКогда все конфликтные файлы будут добавлены, сделать коммит слияния:
git commitGit сам предложит шаблон сообщения вида:
Merge branch 'feature/api'Вы можете его оставить или изменить.
Использование инструментов для решения конфликтов
Решать конфликты можно и вручную, но в реальных проектах изменения бывают сложными. Для этого у Git есть поддержка merge-tool — внешних инструментов.
Пример запуска встроенного механизма:
git mergetool
Git откроет указанный в настройках инструмент сравнения (например, kdiff3, meld, VS Code merge view и т.п.). Там вы увидите:
- базовую версию;
- вашу версию;
- версию из другой ветки;
- и финальный результат, который нужно сохранить.
Пример минимальной настройки (в .gitconfig):
[merge]
tool = code
[mergetool "code"]
cmd = code --wait --merge $REMOTE $LOCAL $BASE $MERGED
Комментарии:
- Здесь мы настраиваем VS Code как merge-tool.
- После этого
git mergetoolзапустит VS Code в режиме слияния.
Стратегии слияния в Git
Git поддерживает несколько стратегий слияния. В большинстве случаев вам хватает стратегии по умолчанию, но бывают ситуации, когда другие режимы полезны.
Стратегия по умолчанию: ort (раньше recursive)
В современных версиях Git (2.34+) по умолчанию используется стратегия ort. Раньше основная стратегия называлась recursive. Для понимания статьи важно лишь знать:
- это интеллектуальная стратегия трехстороннего слияния;
- она умеет неплохо разруливать разнесенные изменения;
- она работает по умолчанию, если вы не задаете явно другую стратегию.
Вы можете явно указать:
git merge -s ort feature/payment
но обычно это не требуется.
Стратегия ours — игнорировать изменения из вливаемой ветки
Стратегия ours может показаться странной на первый взгляд, но она полезна в определенных сценариях.
Смотрите, что она делает:
- создает merge-коммит,
- но при этом в содержимом файлов сохраняет только версию текущей ветки (HEAD),
- изменения из вливаемой ветки фактически игнорируются.
Пример:
git checkout main
git merge -s ours legacy-branch -m "Merge legacy-branch but keep main changes"
Когда это бывает нужно:
- вы хотите «закрыть» старую ветку, чтобы Git считал, что она влита,
- но на самом деле ее код вам уже не нужен (например, старая архитектура, которую вы переписали, и хотите сохранить только новую).
Стратегия theirs — почему это не опция merge
Новички часто ищут стратегию theirs, чтобы «принять все изменения из другой ветки». В git merge такой стратегии нет. Вместо этого:
- theirs работает в контексте git checkout или git restore при решении конфликтов, когда вы можете явно принять версию файла из конкретной стороны.
Пример, как принять их версию при конфликте:
# Принять версию файла из другой ветки (theirs) для конкретного файла
git checkout --theirs path/to/file # в новых версиях лучше использовать git restore
# Или с новым синтаксисом
git restore --source=MERGE_HEAD -- path/to/file
# Потом не забываем добавить файл в индекс
git add path/to/file
Здесь вы фактически говорите: «В конфликте по этому файлу выбери полностью версию из вливаемой ветки».
Merge нескольких веток за один раз
Вы можете сливать не только одну ветку за раз, но и несколько:
git checkout main
git merge feature/auth feature/profile bugfix/header
Git последовательно вольет указанные ветки в текущую:
- сначала feature/auth,
- затем feature/profile,
- затем bugfix/header.
Однако такой подход может усложнить разбор конфликтов. Чаще в командах предпочитают сливать ветки по одной, чтобы легче было понять происхождение конфликтов.
Разница между git merge и git rebase
Очень частый вопрос: когда использовать merge, а когда rebase. Давайте коротко, но по сути.
Как работает git merge
- сохраняет историю ветвления;
- создает merge-коммиты;
- не переписывает существующие коммиты;
- удобно, когда важна полная история изменений, включая то, как ветки реально развивались.
История выглядит «ветвистой», но правдивой.
Как работает git rebase
- переписывает историю:
- берет ваши коммиты и как бы «накладывает» их поверх другой ветки;
- не создает merge-коммитов (в простых сценариях);
- делает историю линейной, но меняет хэши коммитов.
Для командной разработки есть одно важное правило:
- не делайте rebase публичных веток, которые уже кто-то забрал себе (pull), иначе коллегам придется разруливать рассинхронизацию истории.
Когда выбирать merge
Используйте git merge, если:
- вы работаете в команде и хотите сохранять честную историю ветвления;
- вы завершили фичу и хотите влить ее в main, сохранив отдельный merge-коммит (часто используют в Git Flow и похожих процессах);
- вам важно избежать переписывания истории в общих ветках.
Практические сценарии слияния веток
Теперь давайте посмотрим на несколько реальных кейсов и команд, которые вы будете применять очень часто.
Слияние фич-ветки в main
Один из самых типичных сценариев.
Вы создаете ветку от main:
git checkout main git pull origin main # подтягиваем свежие изменения с сервера git checkout -b feature/searchРаботаете в ветке, делаете коммиты:
# Изменили файлы git add . git commit -m "Implement basic search"Завершив работу, переходите на main и сливаете:
git checkout main git pull origin main # сначала обновить main git merge --no-ff feature/searchРазрешаете конфликты, если есть, и пушите:
git push origin mainПосле успешного слияния фич-ветку можно удалить:
git branch -d feature/search # локально git push origin --delete feature/search # на сервере
Слияние обновлений main в вашу фич-ветку
Это обратный сценарий. Вы работаете над фичей, в main за это время появляются изменения. Чтобы уменьшить риск конфликтов при финальном слиянии, полезно периодически подтягивать main в свою ветку.
Последовательность:
# Находитесь в своей фич-ветке
git checkout feature/report
# Подтягиваем свежий main с сервера
git checkout main
git pull origin main
# Возвращаемся в фичу и вливаем main
git checkout feature/report
git merge main
Теперь фич-ветка содержит актуальные изменения из main, и позже, когда вы будете сливать feature/report обратно в main, конфликтов может быть меньше.
Отмена merge-коммита
Иногда вы влили ветку, но потом поняли, что это было преждевременно (например, появились критические баги). Тогда нужно откатить merge-коммит.
Вариант через git revert:
Находите хэш merge-коммита:
git log --onelineПредположим, хэш — abc1234.
Откатываете именно merge-коммит, указав родителя:
git revert -m 1 abc1234Здесь:
-m 1означает, что в качестве «основной» линии истории используется первый родитель коммита (обычно это ветка main перед слиянием).
Это создаст новый коммит, который отменит изменения, внесенные merge-коммитом, но сохранит историю (и сам merge-коммит тоже останется в истории).
Безопасные практики при слиянии веток
Работайте в чистем рабочем каталоге
Перед запуском git merge убедитесь, что:
git status
показывает:
- нет незакоммиченных изменений (working tree clean),
- нет файлов в состоянии «staged but not committed», если вы не планируете делать коммит прямо сейчас.
Если у вас есть незавершенные изменения, у вас два варианта:
- закоммитить их;
временно спрятать с помощью stash:
git stash # потом после merge git stash pop
Сначала обновляйте целевую ветку с сервера
Очень распространенный паттерн:
git checkout main
git pull origin main # сначала обновляем main
git merge featureX
git push origin main
Так вы уменьшаете вероятность, что в удаленной ветке main уже есть какие-то изменения, о которых вы не знаете, и усложняете себе жизнь при пуше.
Пишите понятные сообщения к merge-коммитам
По умолчанию git merge создает такое сообщение:
Merge branch 'feature/payment'
Многие команды оставляют его как есть, но иногда полезно добавить уточнение, особенно если merge решает что-то нетривиальное:
git commit
И в редакторе изменить сообщение, например, на:
Merge branch 'feature/payment' into main
Resolve API version conflicts and align error handling
Так будущим читателям истории будет легче понять, почему merge был важен.
Восстановление после неудачного merge
Ошибки при слиянии — нормальная часть работы. Важно понимать, как их безопасно откатить.
Если вы еще не сделали коммит
Если вы запустили merge, получили конфликты или видите результат, который вам не нравится, и при этом коммит слияния еще не сделан, у вас есть два пути.
Полностью отменить слияние
git merge --abort
Git вернет состояние ветки к тому коммиту, где вы были до merge.
Отменить изменения в отдельных файлах
Если вы хотите только один (или несколько) файлов вернуть к состоянию HEAD:
git restore path/to/file
или в старом синтаксисе:
git checkout -- path/to/file
Если merge-коммит уже сделан и запушен
Если merge уже попал в общую историю, рекомендуется не переписывать историю (не делать reset --hard, rebase и т.п.). Вместо этого используйте revert:
git revert -m 1 <hash_merge_commit>
Это создаст новый коммит, который отменит изменения merge-коммита, но оставит сам merge в истории.
Распространенные ошибки и как их избегать
Случайный merge не в ту ветку
Иногда вы забываете переключиться на нужную ветку и делаете merge не туда. Типичный сценарий:
# Вы думали, что сейчас на main, а на самом деле еще в feature/old
git merge feature/new
Если вы это заметили до пуша на сервер:
просто откатите ветку с помощью reset:
git reset --hard HEAD~1Этот вариант подходит, если:
- вы уверены, что merge-коммит был последним действием,
- никто еще не успел забрать эту ветку.
Если уже успели запушить — лучше использовать revert, как описано выше, чтобы не ломать историю.
Попытка пуша после merge, когда удаленная ветка ушла вперед
Еще одна типичная ситуация:
- вы сделали git merge локально,
- пытаетесь сделать git push,
- а удаленная ветка уже содержит новые коммиты.
Git скажет вам что-то вроде:
To origin/main
! [rejected] main -> main (non-fast-forward)
error: failed to push some refs to '...'
Решение:
Сначала подтянуть изменения с сервера:
git pull --rebase origin mainили (при строгом использовании merge):
git pull origin main- Разрешить конфликты, если будут.
Потом снова сделать push:
git push origin main
Краткое резюме по git merge
- git merge — основной механизм объединения изменений из одной ветки в другую.
- При merge Git использует трехстороннее слияние, сравнивая обе ветки и их общего предка.
- Существует два ключевых типа результата:
- fast-forward — когда целевая ветка просто «перематывается» вперед без нового коммита;
- non fast-forward — создается merge-коммит с двумя родителями.
- Флаги:
- --no-ff — принудительно создать merge-коммит, даже если возможен fast-forward;
- --no-commit — подготовить изменения слияния без немедленного создания коммита;
- --abort — откатить начатое, но не завершенное слияние.
- Конфликты при merge — нормальное явление, если одна и та же строка была изменена в разных ветках. Решаются вручную или с помощью mergetool.
- Стратегии merge (ort, ours и др.) позволяют гибко управлять тем, какие именно изменения попадут в итог.
- Для командной работы merge предпочтителен там, где важна честная история и прозрачность того, как развивался код.
Частозадаваемые технические вопросы по теме и ответы
Как сделать так, чтобы при merge автоматически выбиралась версия из другой ветки (theirs) для всех файлов
Вы можете после начала merge принять «их» версию для всех конфликтных файлов таким способом:
# Начали слияние и получили конфликты
git merge feature/api
# Принять их версии файлов для всех конфликтов
git diff --name-only --diff-filter=U | xargs git checkout --theirs
# Добавить все решенные файлы в индекс
git add .
# Завершить merge-коммит
git commit
Комментарии:
- git diff --name-only --diff-filter=U выводит список конфликтных файлов;
- git checkout --theirs подставляет версию из вливаемой ветки.
Как объединить несколько последовательных merge-коммитов в один
Если в истории накопилось несколько соседних merge-коммитов, их напрямую «объединить» нельзя, но вы можете переписать историю с помощью интерактивного rebase (если ветка еще не ушла в общий репозиторий):
git rebase -i <hash_коммита_до_серии_merge>
Важно:
- интерактивный rebase плохо работает с merge-коммитами;
- для уже опубликованных веток лучше не применять этот подход, чтобы не ломать историю коллегам.
Почему git merge иногда автоматически переименовывает файлы
Если в одной ветке файл был переименован, а в другой — изменен, стратегия merge может автоматически сопоставить эти изменения и решить, что это один и тот же файл. Git делает это по содержимому и эвристикам переименования. Вы можете управлять порогом определения переименований флагом:
git merge -X find-renames=75% other-branch
Здесь 75% — порог схожести содержимого.
Как отключить автоматический запуск mergetool при каждом merge
Если у вас в конфиге включен автоматический запуск mergetool и это мешает, проверьте настройки:
git config --global --unset merge.tool
git config --global --unset mergetool.prompt
После этого mergetool будет запускаться только вручную по команде:
git mergetool
Как ограничить глубину истории при merge, чтобы он был быстрее на огромном репозитории
Для очень больших репозиториев можно ограничить глубину поиска общего предка:
git merge --depth=50 other-branch
Однако:
- такой режим используется редко,
- может привести к менее корректному анализу истории,
- лучше оптимизировать репозиторий другими способами (shallow clone, sparse checkout) и применять merge в обычном режиме.
Постройте личный план изучения Git до уровня Middle — бесплатно!
Git — часть карты развития Frontend
100+ шагов развития
30 бесплатных лекций
300 бонусных рублей на счет
Бесплатные лекции
Все гайды по Git
Лучшие курсы по теме

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