Программирование и IT

Как разрешить конфликт слияния

Конфликт — не поломка, а вопрос. Git сливает изменения автоматически, пока они в разных местах файла; когда две ветки правят одни и те же строки, выбрать правильный вариант может только человек. Задача — прочитать оба варианта, собрать из них рабочий код и сказать git, что вопрос закрыт.

Обновлено
В этой статье

Откуда берётся конфликт#

Слияние сравнивает три состояния: общего предка двух веток и обе их вершины.
Если строку изменила только одна сторона, git берёт её изменение молча. Если
обе стороны изменили одну строку по-разному — оснований предпочесть одно
другому у программы нет, и она останавливается.

Конфликт бывает и без совпадения строк: одна ветка правит файл, другая его
удаляет или переименовывает. Такой случай git описывает отдельно, словами
deleted by them или added by us в выводе состояния.

Важно: конфликт не откатывает работу. Слияние просто остаётся незавершённым,
пока ты его не доделаешь или не отменишь.

Как выглядит остановка#

$ git merge feature-vat
Auto-merging billing/rates.py
CONFLICT (content): Merge conflict in billing/rates.py
Automatic merge failed; fix conflicts and then commit the result.

Первая строка говорит, что часть правок слилась сама. Вторая называет файл с
вопросом. Полная картина — в состоянии:

$ 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:   billing/rates.py

Список только конфликтных файлов, без лишнего, выводится так:

$ git diff --name-only --diff-filter=U
billing/rates.py

Что написано в файле#

Открыв файл, ты увидишь обе версии, разделённые метками:

<<<<<<< HEAD
VAT_RATE = 0.20
def with_vat(sum_):
    return round(sum_ * (1 + VAT_RATE), 2)
=======
VAT_RATE = Decimal("0.22")
def with_vat(sum_):
    return sum_ * (1 + VAT_RATE)
>>>>>>> feature-vat

Между <<<<<<< и ======= — то, что было у тебя (HEAD, текущая ветка).
Между ======= и >>>>>>> — то, что приходит из сливаемой ветки; её имя
написано в последней метке.

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

VAT_RATE = Decimal("0.22")
def with_vat(sum_):
    return round(sum_ * (1 + VAT_RATE), 2)

Полезная настройка — показывать ещё и состояние предка:

git config --global merge.conflictStyle zdiff3

Тогда в метках появляется третий кусок, ||||||| — исходный текст до обеих
правок. С ним гораздо понятнее, кто что менял: видно, что одна сторона
поменяла ставку, а другая — округление, и значит нужны оба изменения.

Порядок разрешения#

$ git diff --name-only --diff-filter=U
billing/rates.py
$ nano billing/rates.py          # правим, метки удаляем
$ git add billing/rates.py
$ git status -s
M  billing/rates.py
$ git commit
[main 8b2f0d5] Merge branch 'feature-vat'

Команда git add здесь работает не как «добавить файл», а как «этот вопрос
закрыт». Пока хоть один файл не добавлен, git не даст завершить слияние.
Сообщение коммита он предложит сам — обычно его оставляют как есть.

Когда вариант нужен целиком, метки править руками не обязательно:

git checkout --ours  billing/rates.py    # оставить версию текущей ветки
git checkout --theirs billing/rates.py   # оставить версию сливаемой ветки
git add billing/rates.py

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

Как выйти, если запутались#

Пока слияние не завершено, всё откатывается одной командой:

$ git merge --abort

Ветка и рабочая копия возвращаются в состояние до слияния. Для переноса
коммитов то же делает git rebase --abort, для обратного коммита —
git revert --abort.

Если слияние уже закоммичено и результат плохой, остаётся откат вершины ветки
назад на один коммит — при условии, что коммит слияния ещё не отправлен:
режимы и опасность --hard разобраны
в статье о git reset. Отправленный коммит слияния
отменяют командой git revert -m 1 <хеш>, где единица указывает, какую из
двух линий считать основной.

Частые ошибки#

Метки остались в коде. Классика: файл добавлен и закоммичен вместе со
строками <<<<<<<. Ловится просмотром перед коммитом (git diff --cached)
или поиском по проекту: grep -rn '<<<<<<<' ..

Выбрали свою версию, не читая чужую. Быстро и почти всегда неверно: чужие
правки исчезают без следа, а тесты падают через неделю. Если непонятно, зачем
коллега менял строку, историю этого места покажет git log --merge -p billing/rates.py — в выводе останутся только коммиты, трогавшие конфликтное
место.

Конфликты одни и те же при каждом переносе. Включи запоминание
разрешений — git будет подставлять их сам:

git config --global rerere.enabled true

Слияние затеяно на грязной рабочей копии. Git откажется начинать, если
незакоммиченные правки задевают сливаемые файлы. Сначала коммит или полка
(git stash push), потом слияние: как пользоваться полкой, описано
в статье о git stash.

Огромный конфликт после долгой жизни ветки. Лечится не приёмами, а
привычкой: забирать основную ветку к себе почаще. Ветка, отставшая на два
дня, сливается почти всегда без вопросов; отставшая на два месяца — почти
всегда с ними.

План по этапам

  1. Увидеть список конфликтовgit status или git diff --name-only --diff-filter=U — только нерешённые файлы.
  2. Прочитать оба вариантаМетки <<<<<<<, =======, >>>>>>>; при zdiff3 виден ещё и общий предок.
  3. Собрать правильный кодОставить нужное, удалить все метки; при необходимости — checkout --ours или --theirs.
  4. Отметить как решённоеgit add по каждому файлу; проверить, что не осталось меток (grep по проекту).
  5. Завершить и проверитьgit commit, затем сборка и тесты: слияние без конфликта не значит рабочий код.

Начать изучать эту тему у себя

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

Начать план

Проверь себя

1.В конфликтном файле часть между <<<<<<< HEAD и ======= — это чей код?

2.Конфликты разрешены в редакторе. Какая команда говорит git, что файл готов?

3.Слияние начато, конфликтов много, нужно вернуть всё как было. Что выполнить?

4.Какая настройка заставляет git показывать в метках ещё и версию общего предка?

Источники

  • Pro Git, русское изданиеГлава «Ветвление в Git» — базовое слияние и конфликты, глава «Инструменты Git» — сложные случаи
    бесплатно
  • Справка git mergeРаздел HOW CONFLICTS ARE PRESENTED — что означают метки
    бесплатно
  • Справка git rerereЗапоминание разрешённых конфликтов для повторных слияний
    бесплатно

Было полезно?

Ещё темы

Программирование и IT git merge Слияние переносит работу из одной ветки в другую. Команда git merge берёт указанную ветку и соединяет её историю с текущей — иногда просто сдвигая указатель, иногда создавая отдельный коммит, а иногда останавливаясь и прося помощи. Программирование и IT git rebase Команда переносит коммиты твоей ветки так, будто ты начал работу не от старого состояния, а от текущего. История становится линейной, но коммиты при этом создаются заново — с новыми хешами. Отсюда главное ограничение: переносить можно только то, чем не пользуется никто другой. Программирование и IT Как создать репозиторий с нуля Репозиторий — это папка проекта плюс скрытая папка `.git`, в которой хранится вся история. Создаётся одной командой, но между ней и первым осмысленным коммитом есть несколько шагов, которые потом трудно переделать: имя ветки, список игнорируемых файлов, привязка к серверу. Программирование и IT git push Пока ты не выполнил git push, твои коммиты существуют только на твоём диске. Команда отправляет их в удалённый репозиторий и сдвигает там указатель ветки — поэтому она же чаще всего и отказывает, если кто-то успел отправить своё раньше. Программирование и IT git commit Коммит — это сохранённое состояние проекта с автором, временем и сообщением. Команда git commit берёт то, что ты заранее отобрал командой git add, и записывает это в историю одной неделимой порцией. Программирование и IT git clone Команда git clone забирает репозиторий с сервера целиком — со всей историей, ветками и тегами — и кладёт копию в новую папку на твоём компьютере. С неё начинается работа с любым уже существующим проектом, своим или чужим.

Ещё сценарии