Когда возникают конфликты
Конфликт слияния — самая страшилка для джуна. На самом деле конфликты — это не ошибки и не баги Git. Это штатная ситуация: Git честно говорит “я не знаю, какую версию оставить, реши ты, человек”. В этом уроке мы разбираемся, когда конфликты случаются, когда — нет, и почему алгоритм three-way merge иногда сдаётся.
Когда конфликт ТОЧНО случится
Конфликт возникает, когда две ветки изменили одни и те же строки одного файла по-разному, и Git не может однозначно решить, какую версию оставить.
Пример:
# Стартовая ситуация
$ cat src/etl.py
def fetch():
return "v1"
$ git switch main
$ sed -i '' 's/v1/v2/' src/etl.py
$ git add . && git commit -m "main: v2"
$ git switch feature
$ sed -i '' 's/v1/v3/' src/etl.py
$ git add . && git commit -m "feature: v3"
$ git switch main
$ git merge feature
Auto-merging src/etl.py
CONFLICT (content): Merge conflict in src/etl.py
Automatic merge failed; fix conflicts and then commit the result.
Git показывает файл с маркерами конфликта (об этом в следующем уроке) и предлагает тебе разобраться.
Когда конфликта НЕ будет
Не всякое изменение в обеих ветках вызывает конфликт. Часто Git мерджит автоматически.
1. Изменения в разные файлы
Очевидный случай. Ветка feature-a поменяла src/etl.py, ветка feature-b — src/loader.py. Merge пройдёт без вопросов, Git просто возьмёт обе версии.
2. Изменения в одном файле, но в разных местах
Если изменения не пересекаются по строкам, и между ними есть достаточно “контекста” неизменных строк, Git их сольёт автоматически.
$ git merge feature
Auto-merging src/etl.py
Merge made by the 'ort' strategy.
3. Одинаковые изменения с обеих сторон
Редкий случай: обе ветки внесли одно и то же изменение в одни и те же строки. Git это распознаёт и не конфликтует.
# main: добавил logger.info("started")
# feature: добавил такой же logger.info("started") в той же строке
$ git merge feature
Auto-merging src/etl.py
Merge made by the 'ort' strategy.
Это convergent change — обе стороны независимо пришли к одному и тому же. Бывает, когда два человека параллельно фиксят один и тот же баг.
Three-way merge: как Git вообще мерджит
Чтобы понять, почему конфликт случается, нужно знать алгоритм мерджа. Git использует three-way merge: смотрит на три снимка файла.
Для каждой строки/блока Git смотрит:
- Если строка одинакова в BASE и OURS, но изменена в THEIRS -> берёт THEIRS.
- Если строка одинакова в BASE и THEIRS, но изменена в OURS -> берёт OURS.
- Если строка одинакова в OURS и THEIRS (но изменена относительно BASE) -> берёт это значение (convergent change).
- Если строка изменена и в OURS, и в THEIRS, по-разному -> конфликт.
Это объясняет, почему “просто добавить строку” обычно не вызывает конфликт: одна сторона не трогала это место, другая — добавила. Видя BASE, Git понимает, кто что сделал, и берёт изменение.
Найти merge base руками
$ git merge-base main feature
a1b2c3d4e5f6...
$ git log --oneline a1b2c3d4..main
f4e5d6c main: v2
$ git log --oneline a1b2c3d4..feature
9876543 feature: v3
Это полезно при диагностике: “от какой точки разошлись ветки?”.
Другие виды конфликтов
Конфликт — это не только “строки в файле”. Бывают более редкие случаи:
Rename/edit conflict
Одна сторона переименовала файл, другая — изменила его содержимое.
# main: переименовал src/etl.py -> src/pipeline.py
# feature: изменил содержимое src/etl.py
$ git merge feature
CONFLICT (modify/delete): src/etl.py deleted in HEAD and modified in feature.
Delete/edit conflict
Одна сторона удалила файл, другая — изменила.
$ git merge feature
CONFLICT (modify/delete): src/old.py deleted in HEAD and modified in feature.
Решается через git rm <file> (подтверждаешь удаление) или git checkout <file> (восстанавливаешь файл).
Binary conflict
Для текстовых файлов Git показывает diff построчно. Для бинарных (картинки, parquet, sqlite) — нет.
$ git merge feature
CONFLICT (content): Merge conflict in data/sample.parquet
Решение: выбрать одну из версий целиком через git checkout --ours data/sample.parquet или --theirs.
Submodule conflict
Если в репо есть submodules и обе ветки изменили коммит submodule на разные значения — конфликт. Решается через git submodule update и явное переключение submodule на нужный коммит.
Конфликты возникают не только при merge
В Git есть несколько операций, которые могут привести к конфликтам — везде по тому же алгоритму three-way merge:
| Операция | Что мерджит |
|---|---|
git merge feature | OURS=HEAD, THEIRS=feature |
git rebase main | Для каждого коммита перебазируемой ветки: OURS=новая база, THEIRS=коммит из ветки |
git cherry-pick <sha> | OURS=HEAD, THEIRS=изменения коммита SHA |
git pull | По сути fetch + merge (или + rebase) |
git revert <sha> | Применяет обратные изменения |
git stash pop | Мерджит stashed changes в working tree |
Где бы конфликт ни возник, разрешается одинаково — об этом следующие уроки.
Почему конфликты — не баг
Конфликт = Git делает свою работу честно: говорит “я не знаю, что хотел программист”. Все альтернативы хуже:
- Молча выбрать одну из версий -> потерять работу одной из сторон.
- Случайно объединить -> создать нерабочий код.
- Спросить пользователя — это и есть конфликт.
Конфликты — нормальная часть рабочего процесса в команде. Если ты их боишься — это сигнал отработать workflow в безопасной песочнице (см. практику ниже).
Как минимизировать конфликты
Конфликты невозможно убрать полностью, но их можно резко уменьшить:
-
Частые маленькие коммиты + частая синхронизация с
main. Если ты три недели работаешь в изоляции, при merge получишь катастрофу. Если каждый день делаешьgit fetch && git rebase origin/main— конфликты будут крошечные. -
Договорённости в команде про форматирование. Если каждый файл переформатируется на каждое сохранение разным линтером — постоянные конфликты на whitespace. Решение: один общий config (
.editorconfig,pre-commithooks, форматирование в CI). -
Маленькие PR. Огромный PR на 50 файлов -> больше шансов конфликта при долгом review. Маленькие PR -> быстрый merge -> меньше окно для конфликта.
-
Чёткое разделение ответственности. Если два человека одновременно правят один файл — это сигнал, что либо плохо разделили задачи, либо файл слишком большой и нужен рефакторинг.
-
merge.conflictStyle = zdiff3— улучшает читаемость конфликта (об этом в следующем уроке).
Попробуй сам: создай конфликт намеренно
# 1. Создай тестовый репо
mkdir conflict-demo && cd conflict-demo
git init
cat > file.txt <<EOF
line 1
line 2 — important value v1
line 3
EOF
git add . && git commit -m "init"
# 2. Создай две ветки от одной точки
git switch -c branch-a
sed -i.bak 's/v1/v2-from-a/' file.txt && rm file.txt.bak
git commit -am "branch-a: change v1 to v2"
git switch main
git switch -c branch-b
sed -i.bak 's/v1/v3-from-b/' file.txt && rm file.txt.bak
git commit -am "branch-b: change v1 to v3"
# 3. Попробуй смерджить
git switch main
git merge branch-a # этот пройдёт fast-forward
git merge branch-b # этот будет CONFLICT!
# 4. Посмотри что Git сделал с файлом
cat file.txt
# Увидишь маркеры <<< === >>>
# 5. Откати, чтобы не разбираться сейчас
git merge --abort
git status
Это безопасный sandbox для экспериментов. В следующем уроке мы разберём, что делать с маркерами и как их правильно разрешать.
.env и pydantic-settings: конфигурация в Python-проекте