Модуль 4: Слияние и разрешение конфликтов
Слияние (merge) — один из ключевых моментов работы с Git. Это точка, где разрозненные линии разработки сходятся в единый результат. В этом модуле мы подробно разберём, как Git выполняет слияние, почему возникают конфликты, и — самое главное — как их уверенно и безопасно разрешать.
Если вы когда-нибудь боялись нажать git merge, потому что «а вдруг всё сломается» — этот модуль для вас. После него конфликты перестанут пугать.
4.1 Fast‑Forward vs 3‑way merge
Когда вы выполняете git merge, Git выбирает одну из двух стратегий в зависимости от состояния истории.
Fast‑Forward (перемотка вперёд)
Если целевая ветка (например, main) не получила новых коммитов с момента ответвления, Git просто «перемотает» указатель вперёд. Новый коммит слияния не создаётся.
Когда происходит Fast‑Forward
Представьте: вы создали ветку feature от main, сделали 3 коммита, а в main за это время никто ничего не добавлял. Git просто переставит указатель main на последний коммит feature.
До merge:
main: A --- B
\
feature: C --- D --- E
После git merge feature (FF):
main: A --- B --- C --- D --- E
# Переключаемся на main
git switch main
# Сливаем feature (Git выполнит FF автоматически)
git merge feature/login
# Результат: Fast-forward
# main теперь указывает на тот же коммит, что и feature/login
3‑way merge (трёхсторонее слияние)
Если обе ветки получили новые коммиты после точки расхождения, Git выполняет трёхстороннее слияние: сравнивает общего предка, текущую ветку и вливаемую ветку, а затем создаёт специальный merge‑коммит с двумя родителями.
Когда происходит 3‑way merge
До merge:
main: A --- B --- F --- G
\
feature: C --- D --- E
После git merge feature (3-way):
main: A --- B --- F --- G --- M (merge-коммит)
\ /
feature: C --- D --- E
Merge-коммит M имеет двух родителей: G и E. Он фиксирует факт слияния в истории.
# Принудительно создать merge-коммит, даже если возможен FF
git merge --no-ff feature/login
# Разрешить только FF (если невозможно — отмена)
git merge --ff-only feature/login
Когда что использовать?
Сравнение стратегий
- FF — чистая линейная история, но скрывает факт существования ветки. Подходит для маленьких правок и личных веток.
- --no-ff — всегда создаёт merge-коммит. Видно, какие коммиты пришли из ветки. Стандарт для командной работы.
- --ff-only — безопасный режим: если FF невозможен, merge отменяется. Полезно в CI/CD скриптах.
Совет для команды: Договоритесь о стратегии заранее. Многие команды используют --no-ff по умолчанию, чтобы в истории всегда было видно, откуда пришли изменения. Это упрощает поиск регрессий: git log --merges покажет все точки слияния.
4.2 Почему возникают конфликты
Конфликт — это не ошибка. Это сигнал Git: «Я вижу, что одни и те же строки изменены в обеих ветках, и не могу решить за вас, какой вариант правильный».
Когда Git справляется сам
Если изменения в двух ветках затрагивают разные файлы или разные участки одного файла, Git объединит их автоматически. Вы даже не заметите, что произошло слияние.
# Ветка A изменила строку 10 в файле app.py
# Ветка B изменила строку 50 в файле app.py
# Git объединит оба изменения без конфликта
Когда возникает конфликт
Конфликт возникает, когда обе ветки изменили одни и те же строки в одном файле по-разному. Git не знает, какую версию выбрать — вашу или коллеги — и просит разобраться вручную.
Типичные причины конфликтов
- Одна строка, два варианта: вы написали
color: blue, коллега —color: red - Удаление vs изменение: вы удалили функцию, коллега её отредактировал
- Переименование файла: вы переименовали файл, коллега отредактировал его под старым именем
- Рефакторинг: вы перенесли код в другое место, коллега изменил его на старом месте
Пошаговый пример: создаём конфликт
Давайте намеренно создадим конфликт, чтобы понять механику.
# Шаг 1: Создаём репозиторий и начальный файл
mkdir conflict-demo && cd conflict-demo
git init
echo "Добро пожаловать в наш проект" > README.md
echo "Версия: 1.0" >> README.md
git add README.md
git commit -m "Начальный коммит"
# Шаг 2: Создаём ветку и меняем вторую строку
git switch -c feature/update-version
# Меняем «Версия: 1.0» на «Версия: 2.0»
sed -i 's/Версия: 1.0/Версия: 2.0/' README.md
git add README.md
git commit -m "Обновляем версию до 2.0"
# Шаг 3: Возвращаемся в main и тоже меняем вторую строку
git switch main
# Меняем «Версия: 1.0» на «Версия: 1.5-beta»
sed -i 's/Версия: 1.0/Версия: 1.5-beta/' README.md
git add README.md
git commit -m "Ставим бета-версию 1.5"
# Шаг 4: Пробуем слить — получаем конфликт!
git merge feature/update-version
# CONFLICT (content): Merge conflict in README.md
# Automatic merge failed; fix conflicts and then commit the result.
Не паникуйте! Конфликт — это нормальная ситуация. Git не потерял ваши данные. Обе версии сохранены в файле, и вы можете в любой момент отменить слияние командой git merge --abort.
4.3 Маркеры конфликтов: что вы видите в файле
Когда Git обнаруживает конфликт, он записывает в файл обе версии, разделённые специальными маркерами. Давайте разберём их подробно.
Добро пожаловать в наш проект
<<<<<<< HEAD
Версия: 1.5-beta
=======
Версия: 2.0
>>>>>>> feature/update-version
Расшифровка маркеров
<<<<<<< HEAD— начало вашей версии (текущая ветка, куда вы сливаете)- Текст между
<<<<<<<и=======— ваши изменения =======— разделитель между двумя версиями- Текст между
=======и>>>>>>>— изменения из вливаемой ветки >>>>>>> feature/update-version— конец, имя вливаемой ветки
Конфликт в нескольких местах
В одном файле может быть несколько конфликтных участков. Каждый будет обёрнут своими маркерами. Нужно разрешить все конфликты в файле.
# Как найти все конфликтные маркеры в проекте:
grep -rn "<<<<<<<" .
# Посмотреть список конфликтных файлов:
git diff --name-only --diff-filter=U
Подсказка: если вы видите маркеры <<<<<<< в коде после коммита — значит, вы забыли разрешить конфликт и закоммитили файл с маркерами. Это частая ошибка новичков! Проверяйте файлы перед коммитом.
4.4 Разрешение конфликтов: пошаговый walkthrough
Разрешение конфликта — это процесс из четырёх шагов. Разберём каждый подробно.
Шаг 1: Понять, что произошло
Прежде чем редактировать файл, выясните контекст: кто и зачем сделал каждое изменение.
# Посмотреть статус — какие файлы в конфликте
git status
# On branch main
# You have unmerged paths.
# (fix conflicts and run "git commit")
#
# Unmerged paths:
# both modified: README.md
# Посмотреть, что изменилось в каждой ветке
git log --oneline --left-right main...feature/update-version
# > abc1234 Обновляем версию до 2.0
# < def5678 Ставим бета-версию 1.5
# Посмотреть diff с трёхсторонним контекстом
git diff
Шаг 2: Отредактировать файл
Откройте конфликтный файл в редакторе. Вам нужно:
- Удалить все маркеры (
<<<<<<<,=======,>>>>>>>) - Оставить правильную версию (или написать новую, объединяющую обе)
Три варианта решения
- Вариант A — оставить свою версию:
Добро пожаловать в наш проект Версия: 1.5-beta
- Вариант B — принять версию коллеги:
Добро пожаловать в наш проект Версия: 2.0
- Вариант C — объединить обе версии (часто лучший выбор):
Добро пожаловать в наш проект Версия: 2.0-beta
Шаг 3: Добавить в staging
# После редактирования — помечаем файл как разрешённый
git add README.md
# Проверяем статус — файл больше не в "Unmerged"
git status
# All conflicts fixed but you are still merging.
Шаг 4: Завершить слияние
# Завершаем merge (Git предложит стандартное сообщение)
git commit
# Merge branch 'feature/update-version'
# Или с кастомным сообщением:
git commit -m "Merge feature/update-version: обновлена версия до 2.0-beta"
Если что-то пошло не так — отмена
# Отменить merge в любой момент (до git commit)
git merge --abort
# Всё вернётся к состоянию до merge
Совет: Минимизируйте конфликты — синхронизируйтесь с основной веткой чаще. Чем дольше ветка живёт отдельно, тем больше конфликтов. Регулярный git merge main или git rebase main в feature-ветке — хорошая практика.
4.5 Инструменты для разрешения конфликтов
Разрешать конфликты в текстовом редакторе — рабочий, но не самый удобный способ. Существуют специальные инструменты, которые показывают три версии рядом: общего предка, вашу и вливаемую.
VS Code (встроенный merge-редактор)
VS Code автоматически подсвечивает конфликтные участки и предлагает кнопки:
- Accept Current Change — оставить вашу версию
- Accept Incoming Change — принять вливаемую
- Accept Both Changes — вставить обе версии
- Compare Changes — открыть 3-way diff
Начиная с версии 1.69, VS Code предлагает полноценный 3-way merge editor: три панели (ваша версия, общий предок, вливаемая) и итоговый результат внизу.
Внешние merge-утилиты
# Настроить внешний инструмент
git config --global merge.tool meld # или kdiff3, p4merge, vimdiff
# Запустить инструмент для разрешения конфликта
git mergetool
# После разрешения Git спросит: была ли merge успешна?
# Ответьте y, файл будет помечен как разрешённый
Популярные merge-утилиты
- meld — графический, кросс-платформенный, бесплатный. Отлично показывает 3-way diff
- kdiff3 — трёхсторонее сравнение с авторазрешением простых конфликтов
- P4Merge (Perforce) — мощный бесплатный инструмент с хорошей визуализацией
- vimdiff — для тех, кто живёт в терминале
- IntelliJ / WebStorm — один из лучших встроенных merge-редакторов на рынке
Быстрое разрешение: принять одну сторону целиком
# Принять нашу версию файла целиком (ours)
git checkout --ours path/to/file.txt
git add path/to/file.txt
# Принять версию вливаемой ветки целиком (theirs)
git checkout --theirs path/to/file.txt
git add path/to/file.txt
Осторожно с --ours / --theirs! Эти команды принимают файл целиком, а не отдельные конфликтные участки. Если в файле были и ваши изменения, и чужие в разных местах, вы потеряете одни из них.
4.6 Стратегии и опции merge
Команда git merge принимает множество флагов, которые управляют поведением слияния.
Флаги Fast-Forward
# По умолчанию: FF если возможно, иначе 3-way
git merge feature
# Всегда создавать merge-коммит (рекомендуется для командной работы)
git merge --no-ff feature
# Только FF — если невозможно, отменить merge
git merge --ff-only feature
Squash merge
Все изменения из ветки собираются в один коммит, без создания merge-коммита. История ветки теряется — остаётся один сжатый коммит.
# Собрать все изменения из feature в один коммит
git merge --squash feature
# Изменения в staging, но не закоммичены — нужно коммитить вручную
git commit -m "feat: добавлена авторизация (squash из feature/auth)"
Стратегии слияния
Основные стратегии merge
- ort (по умолчанию с Git 2.34+) — быстрый, улучшенная детекция переименований
- recursive — предыдущий алгоритм по умолчанию, всё ещё работает
- octopus — для одновременного слияния нескольких веток (используется автоматически при
git merge A B C) - ours — принимает текущую ветку целиком, игнорируя вливаемую (создаёт merge-коммит, но без изменений)
# Явно указать стратегию
git merge -s ort feature
git merge -s recursive feature
# Опции стратегии (через -X)
git merge -X theirs feature # при конфликтах автоматически принимать theirs
git merge -X ours feature # при конфликтах автоматически принимать ours
git merge -X patience feature # использовать «терпеливый» алгоритм diff
4.7 Алгоритмы ort и recursive
ort (Ostensibly Recursive's Twin) — это современная замена recursive, ставшая стратегией по умолчанию в Git 2.34+. Он быстрее и лучше обрабатывает переименования файлов.
Что улучшено в ort
- Скорость: ort значительно быстрее при большом количестве файлов
- Переименования: лучше обнаруживает переименованные файлы и корректнее объединяет изменения
- Внутренняя архитектура: не модифицирует рабочую директорию до тех пор, пока слияние не вычислено — меньше шансов на «грязное» состояние при ошибке
# Проверить версию Git (ort доступен с 2.34+)
git --version
# Явно использовать ort
git merge -s ort feature/auth
# Или recursive (для старых версий Git)
git merge -s recursive feature/auth
На практике: если ваш Git версии 2.34+, ort используется автоматически. Разница заметна в крупных репозиториях с тысячами файлов. Для маленьких проектов результат идентичен.
4.8 Переименования, пробелы и .gitattributes
Часто конфликты возникают из-за «шума» — изменений пробелов, окончаний строк или переименований файлов. Git предоставляет инструменты для борьбы с этим.
Детекция переименований
Git не отслеживает переименования явно. Вместо этого он эвристически сравнивает удалённый файл с добавленным: если содержимое похоже на N% — считается переименованием.
# Установить порог похожести (по умолчанию 50%)
git merge -X rename-threshold=30 feature # более агрессивная детекция
# Отключить детекцию переименований
git merge -X no-renames feature
Игнорирование пробелов при merge
# Игнорировать изменения в количестве пробелов
git merge -X ignore-space-change feature
# Игнорировать все пробелы
git merge -X ignore-all-space feature
.gitattributes: контроль окончаний строк
# Файл .gitattributes в корне проекта:
*.md text eol=lf # Markdown — всегда LF
*.sh text eol=lf # Bash скрипты — LF
*.bat text eol=crlf # Windows батники — CRLF
*.png binary # Изображения — бинарные, не мёржить
Совет: Создайте .gitattributes в начале проекта. Это предотвратит массу ложных конфликтов из-за разных окончаний строк у Windows и Linux разработчиков.
4.9 rerere: запоминание решений конфликтов
rerere расшифровывается как Reuse Recorded Resolution. Эта функция запоминает, как вы разрешили конфликт, и автоматически применяет то же решение, если конфликт повторится.
Когда это полезно
- Вы делаете
rebaseи каждый раз сталкиваетесь с одним и тем же конфликтом - Вы регулярно мёржите одну и ту же ветку (например,
developвfeature) - Вы тестируете merge, отменяете его, а потом делаете заново
Как включить
# Включить rerere глобально
git config --global rerere.enabled true
# Проверить, что включено
git config rerere.enabled
# true
Как это работает
# 1. Делаете merge, получаете конфликт
git merge feature
# CONFLICT ...
# 2. Разрешаете конфликт вручную
# (редактируете файл, убираете маркеры)
# 3. Git записывает ваше решение
git add file.txt
git commit
# Recorded resolution for 'file.txt'.
# 4. В следующий раз при таком же конфликте:
git merge feature
# Resolved 'file.txt' using previous resolution.
# Git автоматически применил ваше прошлое решение!
Где хранятся решения: в папке .git/rr-cache/. Это локальные данные, они не попадают в удалённый репозиторий.
4.10 Squash merge и влияние на историю
--squash — один из самых спорных инструментов. Он собирает все коммиты ветки в один, но не создаёт merge-коммит. Это значит, что Git не знает, что слияние произошло.
Как работает squash merge
# Ветка feature/auth имеет 15 коммитов
git merge --squash feature/auth
# Все изменения в staging, но НЕ закоммичены
git commit -m "feat: авторизация пользователей"
# Результат: один чистый коммит в main
# НО: Git не знает, что feature/auth была слита
Плюсы и минусы
Когда squash — хорошо
- Ветка содержит «мусорные» коммиты (
fix typo,wip,test) - Вы хотите чистую линейную историю в main
- Feature маленькая и логически единая
Когда squash — плохо
- Ветка большая — теряется контекст отдельных изменений
git logне покажет, что ветка была слита → повторный merge вызовет конфликтыgit bisectне сможет найти коммит с багом внутри «сжатого» коммита- После squash ветку нужно удалять — повторно слить её невозможно
Рекомендация: для коротких feature-веток (1-5 коммитов) squash удобен. Для длинных веток лучше --no-ff merge или предварительная «уборка» через rebase -i.
Практическое задание
Задание 1: Создать и разрешить конфликт
- Создайте новый репозиторий с файлом
index.html - Создайте ветку
feature/header, измените заголовок<h1> - Вернитесь в
main, измените тот же<h1>по-другому - Выполните
git merge feature/header - Разрешите конфликт, объединив оба варианта
- Завершите merge и проверьте историю:
git log --oneline --graph
Задание 2: Сравнить стратегии
- Создайте репозиторий с 3 коммитами в
main - Создайте ветку
featureс 2 коммитами (без изменений в main) - Выполните
git merge feature— какая стратегия использовалась? - Откатите:
git reset --hard HEAD~1 - Теперь
git merge --no-ff feature— в чём разница вgit log --graph?
Задание 3: Настроить rerere
- Включите rerere:
git config rerere.enabled true - Создайте конфликт (как в задании 1) и разрешите его
- Откатите merge:
git reset --hard HEAD~1 - Повторите merge — Git должен разрешить конфликт автоматически!