Предлагаю читателям "Хабрахабра" перевод публикации "Painless Merge Conflict Resolution in Git"
из блога blog.wuwon.id.au.


В моей повседневной работе, часто приходится иметь дело со множеством git ветвей (branch). Это могут быть ветви промежуточных релизов, ветви с устаревшим API находящиеся на поддержке для некоторых клиентов, или ветви с экспериментальными свойствами. Лёгкость создания ветвей в модели Git так и соблазняет разработчиков создавать все больше и больше ветвей, и как правило бремя от большого количества ветвей становится очень ощутимым, когда приходится все эти ветви поддерживать и периодически делать слияния (merge) с другими ветвями.



Слияния очень важны для поддержания кода в актуальном состоянии, и как правило ошибка сделанная при слиянии может привести к большей головной боли, нежели ошибка сделанная при простом коммите. К сожалению ошибки слияния далеко не редкость, потому что во-первых слияния имеют несколько родительских ветвей. Даже при анализе истории слияния ветвей, бывает очень трудно понять, какие же изменения были сделаны для разрешения конфликта. Во-вторых, отмена неудачного слияния может превратиться в большую головную боль. В-третьих, большая часть конфликтов слияния происходит при работе с чужим кодом, потому что само понятие ветвей подразумевает множество пользователей, т.е. далеко не всегда слияние производит тот же человек который работал с той или иной веткой. В сухом остатке, сделать ошибку при слиянии очень легко, её трудно исправить и трудно найти. Таким образом время потраченное на изучение и понимание процесса слияния ветвей, окупится с лихвой.


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


В данной статье я надеюсь продемонстрировать что процесс разрешения конфликтов может быть пошагово точным, при котором отпадает необходимость что-либо там угадывать.


Голубые Розы (Roses are Blue)


Давайте предположим что вашей команде поручили писать поэмы в отведённом для этих целей репозитории. (Какой кошмар!) А вам доверили самое главное — делать слияния последних фиксов из ветки master в ветку beta. Итак, вы переключаетесь в ветку beta и выполняете следующую команду:


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

Ого, это конфликт. Вы решаете просмотреть файл на который ссылается git:


$ cat roses.txt
<<<<<<< HEAD
roses are #ff0000
violets are #0000ff
all my base
are belong to you
=======
Roses are red,
Violets are blue,
All of my base
Are belong to you.
>>>>>>> master

(Listing 1)

Замечательно! Весь файл, как показывает Listing 1, находится в конфликтном состоянии. Какой же вариант файла является более корректным? Оба варианта выглядят корректно. Верхний вариант написан в хакер-стиле с элементами цветовой кодировки в стиле HTML и с использованием только строчных букв. Нижний вариант выглядит более натурально, с использованием пунктуации и заглавных букв.


Если бы это был ваш проект, вы бы могли просто выбрать один вариант и покончить с этим слиянием. Но проблема в том, что это не ваша поэма, вы никогда не читали эту поэму раньше, не были ответственны за написание или редактирование, и вы отлично понимаете что в случае не верного решения чья-то тяжёлая работа может кануть в небытие. Однако вас всё же назначили ответственным по слиянию этих веток. Что же вам делать?


Назад к Базе (Back to Base)


Хитрость заключается в том, что Listing 1 не даёт вам полную информацию, необходимую для совершения корректного слияния. На самом деле, в процессе слияния участвуют четыре важных части информации (состояния), три из которых просто необходимы для успешного разрешения конфликта. В случае Listing 1, Git предоставил вам только два состояния.


Следующая диаграмма иллюстрирует эти четыре состояния:


four states


Состояния (B) и © относятся к текущим положениям (head) веток master и beta соответственно, эти два состояния как раз таки и отражены в Listing 1. Состояние (D) это результат слияния, то что вы хотите получить/сгенерировать в конечном итоге (в большинстве случаев Git автоматически генерирует состояние (D)). Состояние (А) на самом верху, представляет собой базу (основу) слияния веток master и beta. База слияния (A) это последний общий предок веток master и beta, и пока предположим что это база слияния уникальна. Как мы увидим позже состояние (A) играет ключевую роль в разрешении конфликтов. На диаграмме я также отразил дельты 1 и 2, которые представляют изменения между состояниями (A)-(B), и (A)-© соответственно. Зная состояния (A), (B) и © дельты 1 и 2 могут быть легко получены (вычислены). Обратите внимание, что дельты 1 и 2 могут состоять из более чем одного коммита. Но для наших целей будем считать что все дельты монолитны.


Чтобы понять, как получить состояние (D), вы должны понимать что же операция слияния пытается сделать. Состояние (D) должно представлять собой сочетание изменений, внесённых в ветку master и beta соответственно. Т.е. другими словами сочетание дельт 1 и 2. Идея проста на поверхности и большую часть времени не требует вмешательства со стороны человека, за исключением особых случаев когда дельты затрагивают наслаиваемые (пересекающиеся) части файла. В такой ситуации вам требуется помочь машине сгенерировать результат (D), путём сравнения дельт 1 и 2.


Определение Отличий (Identifying the Differences)


Для того чтобы найти изменения внесённые в каждую ветку, необходимо знать как выглядит база слияния, состояние (A). Самый простой механизм получения информации о базе слияния, это установка опции merge.conflictstyle в значение diff3


$ git config merge.conflictstyle diff3

После включения этой опции, попробуйте заново сделать слияние (git reset --hard; git merge master) и проинспектируйте конфликтующий файл ещё раз:


$ cat roses.txt
<<<<<<< HEAD
roses are #ff0000
violets are #0000ff
all my base
are belong to you
|||||||
roses are red
violets are blue
all my base
are belong to you
=======
Roses are red,
Violets are blue,
All of my base
Are belong to you.
>>>>>>> master

(Listing 2)

Теперь мы видим третий фрагмент посередине, который и является базой слияния или состояние (A). Изменения видны как на ладони: в ветке beta (HEAD) человеческие названия цветов были заменены на HTML коды, а в ветку master добавили капитализацию и пунктуацию. Основываясь на этих знаниях, мы теперь знаем что результат должен включать в себя капитализацию, пунктуацию и HTML коды цветов.


В принципе на этом можно было бы и закончить, потому что результат достигнут. Но есть решение и получше.


Графическое Слияние (GUI Merging)


Хотя и простое текстовое представление конфликта слияния делает свою работу в простых случаях, на практике конфликты могут быть более радикальными и сложными. В таких случаях могут помочь графические инструменты. Мой выбор пал на простой инструмент написанный на Python под названием meld, но может подойти любой другой графический инструмент, способный представить слияние в трёх-колоночном виде.


Для использования графического инструмента (он должен быть установлен), после того как git пожаловался что есть конфликт, введите следующую команду:


$ git mergetool

Последует вопрос какой программой для слияния вы хотели бы воспользоваться, просто введите meld и нажмите Enter. Вот как окно программы может выглядеть (подразумевается опция merge.conflictstyle не была включена):



Несмотря на то что информация представлена бок о бок, она не отображает нужные фрагменты которые были в Listing 2. Мы не видим здесь фрагмента базы слияния (состояния (A)), что мы видим это файл roses.txt.LOCAL.2760.txt в левой колонке и файл roses.txt.REMOTE.2760.txt в правой колонке и файл посередине это неудачное слияние. Т.е. по сути нам представили состояния (B), © и несостоявшееся состояние (D), но состояние (A) отсутствует...


Правда отсутствует? Давайте проверим, в старом добром терминале:


$ ls -1
roses.txt
roses.txt.BACKUP.2760.txt
roses.txt.BASE.2760.txt
roses.txt.LOCAL.2760.txt
roses.txt.REMOTE.2760.txt

Видим интересующий нас файл: roses.txt.BASE.2760.txt. Это и есть файл базы слияния. Теперь нам осталось всего лишь найти изменения внесённые в ветки master и beta, по отношению к базе. Мы можем сделать это двумя отдельными вызовами meld:


$ meld roses.txt.LOCAL.2760.txt roses.txt.BASE.2760 &
$ meld roses.txt.BASE.2760 roses.txt.REMOTE.2760.txt &

(Кто-то может подметить что было бы более разумно, поменять порядок аргументов в первом вызове, для того чтобы файл базы находился в левой колонке в обоих случаях, но именно такой порядок сохраняет подобие трёх-колоночного вида, при котором база остаётся по середине.) Результат выполнения — два окна как показано ниже:



При чтении первого окна справа налево и второго окна слева направо, становится ясно как день, какие изменения произошли в каждой ветке. Так как meld любезно подсветил все изменения, теперь практически не возможно пропустить даже мелко заметные правки (Кто-нибудь заметил добавление предлога "of" при просмотре текстового представления разрешения конфликта Listing 1 или даже Listing 2?)


Вооружившись этими знаниями, мы теперь можем вернуться к трёх-колоночному представлению и сделать изменения. Моя стратегия ручного слияния это взять весь текст из ветки с более весомыми изменениями (в данном случае master/REMOTE т.е. beta), и поверх него производить пошаговые правки, т.е. вносить изменения сделанные в другой ветке (master). Вот что получилось:



А теперь всё вместе (All Together Now)


Надеюсь, вы найдёте этот трёх-окошечный метод разрешения конфликтов, таким же полезным каким нахожу его я. Но согласитесь что запускать новые вызовы meld вручную каждый раз при разрешении конфликтов, не очень то и удобно. Выход, это настроить git таким образом чтобы все три окна открывались автоматически при вызове команды git mergetool. Для этого можно создать выполняемый скрипт, который должен находится в переменной окружения PATH (например $HOME/bin/gitmerge), со следующим содержимым:


#!/bin/sh
meld $2 $1 &
sleep 0.5
meld $1 $3 &
sleep 0.5
meld $2 $4 $3

И добавьте следующее в ваш ~/.gitconfig файл:


[merge]
    tool = mymeld
[mergetool "mymeld"]
    cmd = $HOME/bin/gitmerge $BASE $LOCAL $REMOTE $MERGED

Теперь, когда вы в следующий раз будете запускать команду git mergetool для разрешения конфликта, откроются все три окна:


Окно дифа между BASE и LOCAL
Окно дифа между BASE и REMOTE
Окно трёх-колоночного вида

После того как вы привыкните к такому разрешению конфликтов с использованием трёх вышеупомянутых окон, вы скорее всего обнаружите, что процесс стал более методичным и механическим. В большинстве случаев, вам даже не придётся читать и понимать куски кода из каждой ветки, для того чтобы понять какой же вариант применить для слияния. Вам больше не понадобится догадываться, потому что вы будете гораздо более уверенным в корректности вашего комита. Из-за этой уверенности, появится чувство что разрешение конфликтов превратилось в увлекательное занятие.


Бонус от переводчика


Для тех кто пользуется tmux и n?vim, предлагаю следующий скрипт gitmerge:


#!/bin/sh
sn=gitmerge

tmux new-session -d -s "$sn" -n "diff3" "nvim -d $2 $4 $3"
tmux split-window -t "$sn:1" -v "nvim -d $2 $1"
tmux split-window -t "$sn:1" -h "nvim -d $1 $3"

Примечание: если вы не используете эту опцию в своем ~/.tmux.conf, то вам надо поменять в двух последних строках "$sn:1" на "$sn:0"


Соответственно добавьте следующее в ваш ~/.gitconfig


[mergetool "gitmerge"]
    cmd = $HOME/bin/gitmerge \"$BASE\" \"$LOCAL\" \"$REMOTE\" \"$MERGED\"
[merge]
    tool = gitmerge

Воркфлоу разрешения конфликта будет выглядеть так:


git merge master workflow


Пока игнорируем вопрос (Was the merge successful [y/n]?) и переключаемся в сессию под названием gitmerge (сочетание TMUXPREFIX + s):


sessiow switch


Видим наше трёх-оконное представление на одном экране. Цифрами обозначены сплиты (panes) tmux'a, буквами соответствующие состояния. Делаем правки для разрешения конфликта, т.е. редактируем состояние (D) и сохраняем. После этого возвращаемся обратно в исходную сессию tmux'a и подтверждаем что слияние произошло успешно.


git merge master


git rebase master


Лично я предпочитаю и считаю более правильным делать сначала rebase master в ветке beta, и только после этого переключаться в master и делать git merge beta. В принципе воркфлоу не сильно отличается, за исключением трёх-оконного вида.


git merge master workflow


Переключаемся в сессию gitmerge


sessiow switch


Обратите внимание, что состояния (B) и © поменялись местами:


git merge master


Рекомендую всем поиграться с примером репозитария хотя бы один раз, сделать разрешение конфликта по вышеописанной схеме. Лично я больше не гадаю а что же выбрать "Accept theirs" или "Accept yours".

Поделиться с друзьями
-->

Комментарии (22)


  1. VBauer
    05.03.2017 14:06
    +2

    Заранее извиняюсь за вольный перевод, но я старался передать смысл, а не быть гугл переводчиком )


  1. aragaer
    05.03.2017 14:36
    +4

    Забавно, но у меня трехсторонний merge получается просто сразу из коробки через vimdiff (той же самой командой git mergetool). Правда там четыре окна — LOCAL, BASE, REMOTE сверху и результат внизу.


    1. VBauer
      05.03.2017 14:42

      Да вы правы, vimdiff, из коробки показывает BASE. Но не видно диффа, как при merge.tool = gitmerge. Вся прелесть заключается в том что сразу видны диффы между (A)-(B) и (A)-©.


      1. aragaer
        05.03.2017 14:58

        Диффы видны и подсвечены, но, к сожалению, одной большой кучей — если в А и B строки одинаковы, а в C отличается, то вся строка будет подсвечена. Лечится тем, что подсветку можно выборочно включать/отключать командой :diffoff/:diffthis в нужном буфере — :diffoff в буфере А оставит подсвеченными только отличия между B и C. Тут уже вопрос привычки — для заядлых пользователей вима это норма, а для остальных людей не очень. Себя я отношу к первым.


  1. foal
    05.03.2017 14:43
    +5

    Я бы рекомендовал сразу взять готовую программу, которая поддерживает 3-way merge. Сам я пользовался kdif3, теперь использую Beyond Compare Pro. И тот и другой продукт умеют 3-way merge, разница в лицензиях и плюшках. Оба интегрируются как с мерком, так и с git'ом.


  1. gasizdat
    05.03.2017 14:44

    Случай с розами слишком частный. Чем поможет 3-х колоночный анализ, если, например, в ветке B в строке N сделали изменения, а в ветке C строку N вообще удалили? Что оставить в D?


    1. VBauer
      05.03.2017 14:51
      -1

      Ну он поможет это просто заметить. Если тот кто делает merge не обладает достаточной компетенцией, чтоб решить что пойдет в D, он может просто спросить у тех кто эти изменения делал.


      1. gasizdat
        06.03.2017 21:42

        В данной статье я надеюсь продемонстрировать что процесс разрешения конфликтов может быть пошагово точным, при котором отпадает необходимость что-либо там угадывать.

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


    1. aragaer
      05.03.2017 14:53
      +1

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


  1. Antelle
    05.03.2017 14:59
    +4

    У JetBrains крутой mergetool, который тоже можно использовать с git, он есть даже в бесплатном Community.
    https://www.jetbrains.com/help/idea/2016.3/running-intellij-idea-as-a-diff-or-merge-command-line-tool.html
    https://coderwall.com/p/gc_hqw/use-intellij-or-webstorm-as-your-git-diff-tool-even-on-windows


  1. skyeff
    05.03.2017 18:27
    -3

    Но проблема в том, что это не ваша поэма, вы никогда не читали
    эту поэму раньше, не были ответственны за написание или редактирование, и вы
    отлично понимаете что в случае не верного решения чья-то тяжёлая работа может
    кануть в небытие. Однако вас всё же назначили ответственным по слиянию этих
    веток. Что же вам делать?


    Срочно увольняться! Мерж должен делать тот кто модифицировал код. Если же политики доступа запрещают девелоперам делать мерж, есть ребейз и черри пик.


    1. netch80
      06.03.2017 09:45
      +1

      Мерж должен делать тот кто модифицировал код.

      Попробуйте посоветовать это Торвальдсу. :)
      На самом деле, в простом случае я согласен — очень удобно переложить все проблемы мержа на разработчиков конкретного изменения, заставляя их обеспечивать бесконфликтно прилагаемую ветку. Но это будет работать ой не всегда и не везде. И да, у конкретного автора может не быть права коммитить в общую ветку — поэтому я и говорю про то, что он должен обеспечить бесконфликтный мерж.


      Если же политики доступа запрещают девелоперам делать мерж, есть ребейз и черри пик.

      Дело ведь не в этом. Ребейз часто удобнее мержа, да, но часто и хуже. Особенно если что-то уже давно разработано и принято.


      Оффтопик: "черри пик" звучит как карта. Черри треф, черри чирв :)


  1. NorthDakota
    05.03.2017 18:34
    -6

    Имхо если возник конфликт, значит что то не так с менеджментом проекта,
    Либо неправильно разбиты таски, неправильно составлен спринт, или же кто то полез в ту часть проекта за которую не отвечает.


  1. Biga
    05.03.2017 19:51

    Использую diffuse с четырьмя колонками: remote, base, local, current(result).
    diffuse на порядок отзывчевее, чем meld, но чуток беднее по функционалу.


  1. masterspline2
    06.03.2017 08:55

    Прочитал первую половину (там где про meld), но так и не понял, как делаются сами разрешения конфликтов. Увидел рассказ, про то, как найти отличия между базовой и двумя производными ветками, но как в результате разрешить конфликт, чтобы объединить изменения из обоих веток (большие буквы из одной ветки, hex цвета из другой) так и не понял? Предполагаю, что это делается ручной правкой текста (т.е. нельзя мышой тыкнуть в обе ветки, чтобы автоматом эти изменения объединились в результирующей версии). При этом удобство использования графического инструмента как раз в том, что он подсвечивает мелкие правки, которые глазами можно пропустить (маленькая/большая буква, запятая, два пробела стали одним...), но если он не умеет объединять такие изменения, то его польза сильно уменьшается.


    1. VBauer
      06.03.2017 09:39

      Да вы правильно предположили, разрешение описанного конфликта делается ручной правкой текста.


      Моя стратегия ручного слияния это взять весь текст из ветки с более весомыми изменениями
      (в данном случае master/REMOTE т.е. beta), и поверх него производить пошаговые правки,
      т.е. вносить изменения сделанные в другой ветке (master).

      Да графический инструмент (meld) только подсветил изменения сделанные в каждой ветке, и сразу стало видно что в ветке beta цвета поменяли на hex коды, а в ветке master добавили капитализацию, пунктуацию и появился предлог "of" которого изначально без подсветки вообще было не заметить.


      Но мне не очень нравится жанглировать окнами графического инструмента, поэтому я и предложил свой скрипт gitmerge, где всё сразу на одном экране, и если что в tmux'e каждый сплит можно зумить на весь экран.


  1. technic93
    06.03.2017 08:57

    А кто знает как быть с таким случаем:
    Есть апстрим в котором активно пилят некий проект. Наша команда сделала форк и затачивает его под свои определенные задачи. Кто-то работает над одним классом кто-то над другим. Получается каждый разбирается и работает только с одной или двумя частями большого проекта.
    Но в один ужасный момент надо смержить фиксы и новые фичи из апстрима. Было бы хорошо чтобы каждый мержил соответсвенно свою часть, потому что нету человека который знает все нюансы и может разрешить конфликты во всех частях проекта.
    Подскажите что делать в этом случае?


    1. aragaer
      06.03.2017 09:15
      +1

      Мержить чаще. В том числе и разбивать мерж на несколько — сначала подтянуть «от начала и до этого места», потом от «от этого места и до конца». Еще разумнее мержить по фичам — сначала вмержим к себе эту фичу, потом следующую, потом еще одну.

      Чуть более сложный вариант это создать отдельную мерж-ветку — то есть в форке некая ветка, которая начинается от того места, где мастер апстрима. И на ней последовательно делается следующее:

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


    1. KIVagant
      06.03.2017 11:28

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


    1. maxzuber
      07.03.2017 13:32

      Немного внешней координации, и можно действовать по такой схеме:

      1) разработчик делает в своей ветке rebase master, разрешает конфликты.
      2) ветка вливается в master.
      3) переходят к следующему разработчику или ветке.


    1. sspat
      07.03.2017 13:33

      Разбить работу над форком на отдельные ветки на которых работают компетентные в своей части разработчики и когда придет время забрать изменения со стороны, делать мердж с апстримом по очереди в каждой из них, последний мерджер и получит финальный вариант. Минус только в том, что если разработчики где-то все-таки пересекаются по коду, им придется заново разрешать конфликты которые они уже разрешали сливая свои ветки в мастер вашей версии ранее.


  1. comerc
    14.03.2017 00:01

    Должен же быть плагин для Atom, но пока не нашёл.