
Привет, Хабр!
Конфликты в Git заставляют немного паниковать, пока не научишься понимать, что именно происходит. Почему возник конфликт? Что я сделал не так? Не потеряю ли я свои изменения? И как теперь продолжить работу?
В этой статье я специально создам конфликт, разберу его изнутри и посмотрю, что происходит с историей Git до, во время и после слияния. Чтобы при следующей встрече с CONFLICT не нажимать кнопки наугад, с единственным желанием поскорее выбраться из этой ямы, а спокойно разобраться в ситуации и принять правильное решение.
Что такое Git-конфликт?
Во-первых, Git позволяет нам, разработчикам, независимо работать над одним проектом в отдельных ветках. Пока изменения затрагивают разные файлы или разные независимые участки одного файла, Git обычно способен объединить их автоматически. Но что произойдет, если они затронут одинаковый участок файла? Давайте разберемся.
Подготовим репозиторий
Для примера, создадим репозиторий с одним файлом:
mkdir git-conflict-repocd git-conflict-repogit init
Создадим простой текстовый файл:
echo 'Привет, Хабр!' > text.txt
Далее добавим файл в индекс и сделаем наш первый коммит:
git add text.txtgit commit -m "Initial commit"
Проверим нашу историю:
git log --oneline
Получим что-то вроде:
f2650a5 (HEAD -> main) Initial commit
Итого: сейчас в репозитории один коммит, это наша общая стартовая точка.
Создадим новую ветку feat/branch-01:
git checkout -b feat/branch-01
Изменим нашу строку в файле:
echo 'Привет, Хабр! Это изменение из первой ветки.' > text.txt
Сохраним изменение в коммит и посмотрим историю:
git add .git commit -m 'Изменили текст в файле на ветке 01'git log --oneline
Создадим еще одну ветку feat/branch-02:
Сначала вернемся в основную ветку main, от нее создадим вторую ветку, тем самым у нас получится одна стартовая точка.
git checkout maingit checkout -b feat/branch-02
Изменим ту же самую строку, но немного иначе:
echo 'Привет, Хабр! Это изменение из второй ветки.' > text.txt
Сохраним изменения и посмотрим историю:
git add .git commit -m 'Изменили текст в файле на второй ветке 02'git log --oneline
Теперь у нас есть две ветки, в которых мы изменили одну и ту же строку в одинаковом файле. Именно такая ситуация приводит к конфликту при последующем слиянии.

Вольём первую ветку в main, имитируя работу первого разработчика, который закончил со своей задачей и решил объединить историю:
git checkout maingit merge feat/branch-01# Сразу проверим файлcat text.txt
Теперь main содержит изменение из feat/branch-01. Попробуем влить ветку второго разработчика.
git merge feat/branch-02
И вот здесь Git нас остановит:
Auto-merging text.txtCONFLICT (content): Merge conflict in text.txtAutomatic merge failed; fix conflicts and then commit the result.
Это произошло ровно потому, что в main сейчас уже лежит версия из первой ветки, в feat/branch-02 лежит другая версия той же строки. Git видит, что обе ветки изменили одну и ту же строку относительно общего предка. Git не может предположить, какой вариант нужно оставить, поэтому необходимо, чтобы разработчик сам принял решение.
Чаще всего в реальной работе это всплывает при выполнении команды git pull, потому что мы подтягиваем чужие изменения в ветку, где уже есть наши локальные. Под капотом, чаще всего git pull делает ровно две вещи:
git fetch # При помощи этой команды скачиваем с удаленного сервера все новые коммиты. # А также обновляем информацию о ветках.git merge # Объединяем скачанные изменения с нашей текущей локальной веткой.
Продолжим, посмотрим состояние репозитория:
git status
Git нам скажет, что файл находится в состоянии конфликта:
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: text.txt
Посмотрим, что хранится в самом файле:
cat text.txt
Внутри окажутся специальные маркеры конфликта:
<<<<<<< HEADПривет, Хабр! Это изменение из первой ветки.======= Привет, Хабр! Это изменение из второй ветки.>>>>>>> feat/branch-02
Коротко разберем, что здесь написано.
<<<<<<< HEAD - (Ниже находится версия из текущей ветки main)======= - (Строка разделяющая две версии)>>>>>>> feat/branch-02 - (Выше этой строки версия, которую мы пытаемся влить)
Итак, Git начал слияние, дошел до спорного места и остановился. Еще нет нового merge-коммита. Посмотрим текущую историю в виде графа:
git log --oneline --graph --all
Получим такую картину:
* 9039daf (feat/branch-02) Изменили текст в файле на второй ветке 02| * 7c6f936 (HEAD -> main, feat/branch-01) Изменили текст в файле на ветке 01|/ * f2650a5 Initial commit
Это означает что история разошлась и репозиторий сейчас находится в промежуточном состоянии:
-
merge уже начался,
-
конфликт найден,
-
Git в ожидании, пока мы исправим файл и завершим слияние итоговым merge-коммитом.
Самое главное, какие у нас есть варианты решения конфликта?
Есть 4 основных варианта:
-
Оставить текущую версию
-
Оставить входящую версию
-
Самостоятельно сделать третью версию
-
Отменить merge
Скорее всего, обе правки важны, поэтому зачастую верное решение это не выбирать чью-то из сторон, а объединить их.
Заменим содержимое файла на итоговый вариант:
echo 'Привет, Хабр! Это изменение из первой и второй ветки.' > text.txt
Таким образом мы заменили строку на итоговый вариант, а также удалили конфликтные маркеры. Если оставить эти маркеры и выполнить git add, Git будет считать файл исправленным, но в коде останется мусор.
Завершаем merge
Добавим файл в индекс и проверим состояние:
git add text.txtgit status
Получим следующее сообщение от Git:
All conflicts fixed but you are still merging.
Это значит то, что конфликт мы разрешили, но сам merge нет. Завершим его коммитом:
git commit -m "Влили ветку feat/branch-02 в main"
После этого слияние завершено и Git нам покажет:
nothing to commit, working tree clean
Как изменилась история после merge? Посмотрим на графе при помощи git log --oneline --graph --all:
* b471e0c (HEAD -> main) Влили ветку feat/branch-02 в main|\ | * 9039daf (feat/branch-02) Изменили текст в файле на второй ветке 02* | 7c6f936 (feat/branch-01) Изменили текст в файле на ветке 01|/ * f2650a5 Initial commit

Появился новый merge-коммит, для которого первый родитель — текущее состояние из main, второй родитель — коммит из feat/branch-02.
Именно этот merge-коммит фиксирует результат слияния. В нашем случае он хранит уже не первую и не вторую версию строки, а итоговый вариант, который мы выбрали сами.
Можно ли уменьшить вероятность конфликтов или вовсе от них избавиться?
Полностью избежать конфликтов нельзя. Если несколько человек работают над одним проектом, рано или поздно Git встретит изменения, которые не сможет объединить автоматически. Но вероятность конфликтов можно заметно снизить:
-
Одна из частых причин больших конфликтов — ветка слишком долго живет отдельно от основной разработки. Поэтому следует регулярно подтягивать изменения из базовой ветки.
-
Если два человека одновременно работают и меняют один и тот же модуль, один и тот же компонент или одну и ту же область бизнес-логики, вероятность конфликта резко растет. Поэтому иногда простой вопрос вашему коллеге, работает ли он сейчас с таким же компонентом, сэкономит вам время на разборы будущих конфликтов.
-
Делать менее громоздкие изменения. Таким образом каждая ветка живет меньше времени и меньше расходится с базовой.
Конфликт — это не поломка, это лишь место, где Git честно передал свое решение в руки человека.
Спасибо за внимание, читатель! В этой статье мы разобрали базовый сценарий: две ветки, один файл, один конфликт и обычный merge. Но в Git есть и другие ситуации, которые тоже часто вызывают вопросы:
-
git revert— как отменять изменения новым коммитом, не переписывая историю; -
git stash— как временно спрятать незавершенную работу; -
git cherry-pick— как перенести отдельный коммит из одной ветки в другую; -
git rebase— как переигрывать коммиты поверх другой ветки и почему при этом тоже могут возникать конфликты.
ссылка на оригинал статьи https://habr.com/ru/articles/1053136/