Git

Модуль 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: Отредактировать файл

Откройте конфликтный файл в редакторе. Вам нужно:

  1. Удалить все маркеры (<<<<<<<, =======, >>>>>>>)
  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: Создать и разрешить конфликт

  1. Создайте новый репозиторий с файлом index.html
  2. Создайте ветку feature/header, измените заголовок <h1>
  3. Вернитесь в main, измените тот же <h1> по-другому
  4. Выполните git merge feature/header
  5. Разрешите конфликт, объединив оба варианта
  6. Завершите merge и проверьте историю: git log --oneline --graph

Задание 2: Сравнить стратегии

  1. Создайте репозиторий с 3 коммитами в main
  2. Создайте ветку feature с 2 коммитами (без изменений в main)
  3. Выполните git merge feature — какая стратегия использовалась?
  4. Откатите: git reset --hard HEAD~1
  5. Теперь git merge --no-ff feature — в чём разница в git log --graph?

Задание 3: Настроить rerere

  1. Включите rerere: git config rerere.enabled true
  2. Создайте конфликт (как в задании 1) и разрешите его
  3. Откатите merge: git reset --hard HEAD~1
  4. Повторите merge — Git должен разрешить конфликт автоматически!

Настройки

Цветовая схема

Тема