Перейти к содержанию
Learning Platform
Глоссарий Troubleshooting
Урок 08.01 · 18 мин
Начальный
Gitmergeconflictsdiff3

Когда возникают конфликты

Конфликт слияния — самая страшилка для джуна. На самом деле конфликты — это не ошибки и не баги Git. Это штатная ситуация: Git честно говорит “я не знаю, какую версию оставить, реши ты, человек”. В этом уроке мы разбираемся, когда конфликты случаются, когда — нет, и почему алгоритм three-way merge иногда сдаётся.


Когда конфликт ТОЧНО случится

Конфликт возникает, когда две ветки изменили одни и те же строки одного файла по-разному, и Git не может однозначно решить, какую версию оставить.

Классический конфликт: один и тот же файл, одна и та же строка
merge base (C0)
main (C1)
feature (C2)
git merge feature -> CONFLICT

Пример:

# Стартовая ситуация
$ 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-bsrc/loader.py. Merge пройдёт без вопросов, Git просто возьмёт обе версии.

2. Изменения в одном файле, но в разных местах

Если изменения не пересекаются по строкам, и между ними есть достаточно “контекста” неизменных строк, Git их сольёт автоматически.

Изменения в разных местах файла — merge OK
main: добавил функцию в начале
feature: добавил функцию в конце
Auto-merge succeeded
$ 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: смотрит на три снимка файла.

Три точки three-way merge
OURS (HEAD)
THEIRS
BASE
Per-line comparison

Для каждой строки/блока 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 featureOURS=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 делает свою работу честно: говорит “я не знаю, что хотел программист”. Все альтернативы хуже:

  • Молча выбрать одну из версий -> потерять работу одной из сторон.
  • Случайно объединить -> создать нерабочий код.
  • Спросить пользователя — это и есть конфликт.
TIP

Конфликты — нормальная часть рабочего процесса в команде. Если ты их боишься — это сигнал отработать workflow в безопасной песочнице (см. практику ниже).


Как минимизировать конфликты

Конфликты невозможно убрать полностью, но их можно резко уменьшить:

  1. Частые маленькие коммиты + частая синхронизация с main. Если ты три недели работаешь в изоляции, при merge получишь катастрофу. Если каждый день делаешь git fetch && git rebase origin/main — конфликты будут крошечные.

  2. Договорённости в команде про форматирование. Если каждый файл переформатируется на каждое сохранение разным линтером — постоянные конфликты на whitespace. Решение: один общий config (.editorconfig, pre-commit hooks, форматирование в CI).

  3. Маленькие PR. Огромный PR на 50 файлов -> больше шансов конфликта при долгом review. Маленькие PR -> быстрый merge -> меньше окно для конфликта.

  4. Чёткое разделение ответственности. Если два человека одновременно правят один файл — это сигнал, что либо плохо разделили задачи, либо файл слишком большой и нужен рефакторинг.

  5. 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-проекте
Проверка знанийKnowledge check
У вас в команде каждый день случается 5-10 merge-конфликтов на одном и том же файле `src/config/settings.py`. Каждый PR ломает merge для других. Какие три действия можно предпринять, не меняя структуру кода?
ОтветAnswer
Конфликты на одном файле — почти всегда симптом организационной проблемы. Три действия: (1) **Договоритесь о коротких циклах**: каждый PR делает 1-2 изменения в settings.py максимум, мерджится в течение дня. Долгоживущие ветки на settings.py — главный источник конфликтов. (2) **Делегируйте порядок**: один человек/команда — owner файла, остальные обращаются через PR к нему, не править параллельно. (3) **Добавьте `merge.conflictStyle = zdiff3`** в `.gitconfig` — показывает общего предка, что радикально упрощает понимание конфликта (когда видишь, ЧТО было изначально, обычно понимаешь, какую сторону взять). Структурное решение, если ничего не помогает: разделить settings.py на доменные файлы (`db_settings.py`, `kafka_settings.py`, `airflow_settings.py`) — каждый сабсет правит свой файл, конфликты резко падают. Бонусный приём — `merge=union` атрибут в `.gitattributes` для специфических файлов типа списков constants, где можно автоматически объединять обе стороны (но это работает только для строк типа CHANGELOG.md, не для кода).

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 4