Конфликты в Git: почему возникают и как разрешать

Git Автор: Среда и версия: git 2.55.0, Linux; конфликты воспроизведены на одноразовых репозиториях 31 августа 2026
содержание

Конфликт — это признание Git в том, что он не может решить за тебя. Он умеет складывать непересекающиеся правки и честно останавливается там, где две ветки утверждают разное про одно и то же место.

Отсюда первое, что стоит понять и что снимает половину страха: правки одного файла ещё не означают конфликт: Git часто объединяет их автоматически. Типичный текстовый конфликт — несовместимые изменения пересекающихся участков. Бывают и другие: изменение удалённого другой веткой файла, несовместимые переименования или правки бинарных файлов.

Git / 01

Конфликт возникает в пересекающихся изменениях

Конфликт возникает в пересекающихся изменениях01 Разные строки A: строка 2 → 200 B: строка 4 → 2026 Git обычно объединяет изменения автоматически. 02 Одна строка A: цена → 200 B: цена → 150 Нужно выбрать или собрать итоговое значение вручную.01Разные строкиA: строка 2 → 200B: строка 4 → 2026Git обычно объединяет измененияавтоматически.02Одна строкаA: цена → 200B: цена → 150Нужно выбрать или собрать итоговоезначение вручную.
Один и тот же файл, одна и та же пара веток — разница только в номере строки. Git сливает по строкам, а не по файлам, поэтому «мы оба правили этот модуль» само по себе конфликт ещё не означает.

Проверяется за минуту. Файл из четырёх строк, две ветки правят разные:

git merge feature/price
Auto-merging page.txt
Merge made by the 'ort' strategy.
 page.txt | 2 +-
заголовок
цена: 200
валюта: руб
подвал 2026

Обе правки на месте, вопросов Git не задал. Теперь то же самое, но обе ветки меняют строку с ценой:

Auto-merging page.txt
CONFLICT (content): Merge conflict in page.txt
Automatic merge failed; fix conflicts and then commit the result.

Что теперь в файле

заголовок
<<<<<<< HEAD
цена: 150
=======
цена: 200
>>>>>>> feature/price
подвал
Git / 02

Маркеры разделяют две версии одного участка

Маркеры разделяют две версии одного участка01 / вход 02 / операция 03 / результат <<<<<<< HEAD начало текущей версии цена: 150 ======= граница вариантов цена: 200 >>>>>>> feature/price конец входящей версии собери итоговый текст01 / вход02 / операция03 / результат<<<<<<< HEADначало текущей версиицена: 150=======граница вариантовцена: 200>>>>>>> feature/priceконец входящей версиисобери итоговый текст
Всё это — обычный текст, который Git дописал в файл. Никакого особого режима нет: разрешить конфликт значит открыть файл и оставить в нём то, что должно там быть, вместе с удалением всех трёх строк-маркеров.

Состояние видно и в git status, и он же подсказывает оба выхода:

On branch main
You have unmerged paths.
  (fix conflicts and run "git commit")
  (use "git merge --abort" to abort the merge)

Unmerged paths:
  (use "git add <file>..." to mark resolution)
	both modified:   page.txt

В коротком виде это UU page.txt — две U от unmerged, в той же нотации двух столбцов, что и MM из разбора основных команд.

Два выхода

Отменить и подумать

git merge --abort

В этом примере до слияния рабочий каталог был чистым, и отмена возвращает его к исходному состоянию. Но git merge --abort не гарантирует восстановление правок, которые были не закоммичены до слияния, особенно если их меняли и после начала merge. Перед слиянием сохрани работу коммитом или через stash; нужные правки разрешения конфликта перед отменой скопируй отдельно.

Разрешить

Разрешение — это просто правка файла. Открыть, решить, какой вариант верен (или собрать третий), удалить все три строки маркеров и сказать Git, что готово:

git add page.txt
git status -s
M  page.txt

Буква U сменилась на M — файл больше не в конфликте. Остаётся закоммитить:

git commit --no-edit
git log --oneline --graph
*   e732486 Merge branch 'feature/price'
|\
| * 6123cb3 цена 200
* | 52bcefa цена 150
|/
* 1bcb57f исходная страница

Коммит слияния — единственный вид коммита с двумя родителями, и на графе это видно: две линии сходятся в одну.

Ловушка rebase: «мой» и «чужой» меняются местами

При merge всё интуитивно: стоишь на main, вливаешь feature/price, и под HEAD оказывается твой код.

<<<<<<< HEAD
цена: 150            ← main, ты здесь
=======
цена: 200            ← feature/price, входящая
>>>>>>> feature/price

При rebase те же самые две ветки дают перевёрнутую картину. Стоя на feature/price и выполняя git rebase main:

<<<<<<< HEAD
цена: 150            ← main! а ты стоишь на feature/price
=======
цена: 200            ← твой собственный коммит
>>>>>>> 6a4ce0c (цена 200)

Причина в том, что rebase не вливает чужое в твоё, а переносит твои коммиты по одному на чужую вершину. В момент конфликта HEAD указывает на уже построенную часть новой истории: исходную базу плюс успешно перенесённые коммиты, а «входящим» оказывается твой же коммит — он и подписан не именем ветки, а хешем и сообщением.

Практический вывод: --ours и --theirs при rebase означают противоположное тому, что подсказывает интуиция. Не полагайся на них по памяти, читай нижнюю строку блока: если там хеш с сообщением коммита, ты в rebase.

git status во время rebase тоже выглядит иначе и подсказывает свою команду продолжения:

interactive rebase in progress; onto dd18184
You are currently rebasing branch 'feature/price' on 'dd18184'.
  (fix conflicts and then run "git rebase --continue")

Отменяется он своей командой — git rebase --abort, а не merge --abort.

Как конфликтовать реже

  1. Забирай чужие изменения частоВетка, отставшая на два дня, конфликтует по паре строк; отставшая на две недели — по всему файлу. git pull в свою ветку дешевле, чем героическое слияние в конце.
  2. Держи ветки короткимиЗадача на день почти никогда не конфликтует. Ветка, живущая месяц, конфликтует всегда — это свойство календаря, а не Git.
  3. Не переформатируй файл заодноАвтоформатирование всего файла вместе с правкой двух строк превращает любую параллельную работу в конфликт по всему файлу. Форматирование — отдельным коммитом.
  4. Разрешай конфликт, а не выбирай сторонуКнопка «принять моё» в редакторе тихо выбрасывает чужую работу. Перед выбором посмотри git log --merge — он покажет коммиты обеих сторон, которые трогали этот файл.
  5. Включи diff3, если варианты непонятныgit config merge.conflictStyle diff3 добавляет в блок третью секцию — исходную версию до обеих правок. Часто именно она объясняет, что каждая сторона пыталась сделать.

Частые вопросы

Что значит both modified

Что файл изменён в обеих ветках и Git не смог соединить правки. В коротком git status -s это UU. Пока такие файлы есть, коммит слияния сделать нельзя.

Как отменить слияние и вернуть всё как было

Сначала прочитай git status: при merge нужна git merge --abort, при rebase — git rebase --abort. git pull может использовать оба режима в зависимости от настроек и флагов. Если до операции были несохранённые правки, полного восстановления через abort гарантировать нельзя.

Что выбрать: merge или rebase

merge сохраняет историю как было и добавляет коммит слияния; rebase переписывает свои коммиты поверх чужих и оставляет линейную историю. Практическое правило: rebase — только для своих неотправленных веток, merge — для всего, что уже видели другие.

Почему при rebase конфликт повторяется

Потому что rebase переносит коммиты по одному, и каждый может конфликтовать со своим предшественником. После разрешения — git rebase --continue, и следующий коммит начнёт переноситься. Если это надоело, помогает git rerere, запоминающий разрешения.

Можно ли просто удалить маркеры и закоммитить

Маркеры удалить нужно обязательно, но одного этого мало: оставив обе версии подряд, ты закоммитишь код, который вряд ли работает. Git проверять содержимое не будет — он лишь следит, чтобы файл был помечен через git add.

Где потренироваться

Конфликт воспроизводится за минуту в одноразовом репозитории: git init, коммит с файлом, две ветки, правящие одну строку, и git merge. Сломать нечего, а руки запомнят и маркеры, и --abort. Команды, из которых всё это состоит, разобраны в основных командах Git, а откат неудачных попыток — в разборе git reset.

Источники