Конфликт слияния
Допустим, файл index.html отредактировал кто-то еще в том же месте. Тогда при попытке выполнить команду merge мы получим сообщение:
$ git merge hotfix Auto-merging index.html CONFLICT (content): Merge conflict in index.html Automatic merge failed; fix conflicts and then commit the result.
Что же делать? Если открыть index.html, мы увидим, что Git его пометил примерно так:
<<<<<<< HEAD:index.html <div id="footer">contact : [email protected]</div> ======= <div id="footer"> please contact us at [email protected] </div> >>>>>>> hotfix:index.html
Надо удалить разметку в файле и оставить только один вариант спорной строки. После этого нужно добавить конфликтный файл в индекс:
git add index.html
И сделать снимок:
git commit
На этом все, конфликт будет разрешен.
Если же разрешать конфликты в данный момент некогда, можно отменить текущее слияние merge (чтобы выполнить его в следующий раз):
git merge abort
Графическое Слияние (GUI Merging)
Хотя и простое текстовое представление конфликта слияния делает свою работу в простых случаях, на практике конфликты могут быть более радикальными и сложными. В таких случаях могут помочь графические инструменты. Мой выбор пал на простой инструмент написанный на Python под названием meld, но может подойти любой другой графический инструмент, способный представить слияние в трёх-колоночном виде.
Для использования графического инструмента (он должен быть установлен), после того как git пожаловался что есть конфликт, введите следующую команду:
Последует вопрос какой программой для слияния вы хотели бы воспользоваться, просто введите meld и нажмите Enter. Вот как окно программы может выглядеть (подразумевается опция merge.conflictstyle не была включена):
Несмотря на то что информация представлена бок о бок, она не отображает нужные фрагменты которые были в Listing 2. Мы не видим здесь фрагмента базы слияния (состояния (A)), что мы видим это файл roses.txt.LOCAL.2760.txt в левой колонке и файл roses.txt.REMOTE.2760.txt в правой колонке и файл посередине это неудачное слияние. Т.е. по сути нам представили состояния (B), (C) и несостоявшееся состояние (D), но состояние (A) отсутствует…
Правда отсутствует? Давайте проверим, в старом добром терминале:
Видим интересующий нас файл: roses.txt.BASE.2760.txt. Это и есть файл базы слияния. Теперь нам осталось всего лишь найти изменения внесённые в ветки master и beta, по отношению к базе. Мы можем сделать это двумя отдельными вызовами meld:
(Кто-то может подметить что было бы более разумно, поменять порядок аргументов в первом вызове, для того чтобы файл базы находился в левой колонке в обоих случаях, но именно такой порядок сохраняет подобие трёх-колоночного вида, при котором база остаётся по середине.) Результат выполнения — два окна как показано ниже:
При чтении первого окна справа налево и второго окна слева направо, становится ясно как день, какие изменения произошли в каждой ветке. Так как meld любезно подсветил все изменения, теперь практически не возможно пропустить даже мелко заметные правки (Кто-нибудь заметил добавление предлога “of” при просмотре текстового представления разрешения конфликта Listing 1 или даже Listing 2?)
Вооружившись этими знаниями, мы теперь можем вернуться к трёх-колоночному представлению и сделать изменения. Моя стратегия ручного слияния это взять весь текст из ветки с более весомыми изменениями (в данном случае master/REMOTE т.е. beta), и поверх него производить пошаговые правки, т.е. вносить изменения сделанные в другой ветке (master). Вот что получилось:
Отмена слияний
Теперь когда вы знаете, как создать коммит слияния, вы можете сделать его по ошибке. Одна из замечательных вещей в работе с Git – это то, что ошибки совершать не страшно, так как есть возможность исправить их (и в большинстве случаев сделать это просто).
Коммит слияния – не исключение. Допустим, вы начали работать в ветке , случайно слили ее в , и теперь ваша история коммитов выглядит следующим образом:
Рисунок 137 – Случайный коммит слияния
Есть два подхода к решению этой проблемы, в зависимости от того, какой результат вы хотите получить.
Исправление ссылок
Если нежелаемый коммит слияния существует только в вашем локальном репозитории, то простейшее и лучшее решение состоит в перемещении веток так, чтобы они указывали туда, куда вам нужно. В большинстве случаев, если вы после случайного выполните команду , то указатели веток восстановятся так, что будут выглядеть следующим образом:
Рисунок 138 – История после git reset —hard HEAD~
Мы рассматривали команду ранее в «Раскрытие тайн », поэтому вам должно быть не сложно разобраться с тем, что здесь происходит. Небольшое напоминание: обычно выполняет три шага:
- перемещает ветку, на которую указывает . В данном случае мы хотим переместить туда, где она была до коммита слияния ();
- приводит индекс к такому же виду, что и ;
- приводит рабочий каталог к такому же виду, что и индекс.
Недостаток этого подхода состоит в изменении истории, что может привести к проблемам в случае совместно используемого репозитория. Посмотрите «», чтобы узнать, что именно может произойти; проще говоря, если у других людей уже есть какие-то из изменяемых вами коммитов, вы должны отказаться от использования . Этот подход также не будет работать, если после слияния уже был сделан хотя бы один коммит; перемещение ссылки фактически приведёт к потере этих изменений.
Отмена коммита
Если перемещение указателей ветки вам не подходит, Git предоставляет возможность сделать новый коммит, который откатывает все изменения, сделанные в другом. Git называет эту операцию «восстановлением» (revert); в данном примере вы можете вызвать её следующим образом:
Опция указывает какой родитель является «основной веткой» и должен быть сохранен. Когда вы выполняете слияние в (), новый коммит будет иметь двух родителей: первый из них (), а второй – вершина ветки, которую сливают с текущей (). В данном случае, мы хотим отменить все изменения, внесённые слиянием родителя #2 (), и сохранить при этом всё содержимое из родителя #1 ().
История с коммитом восстановления (отменой коммита слияния) выглядит следующим образом:
Рисунок 139 – История после git revert -m 1
Новый коммит имеет точно такое же содержимое как , таким образом, начиная с него всё выглядит так, как будто слияние никогда не выполнялось, за тем лишь исключением, что «теперь уже не слитые» коммиты всё также присутствуют в истории . Git придет в замешательство, если вы вновь попытаетесь слить в ветку :
В ветке нет ничего, что ещё недоступно из ветки . Плохо, что в случае добавления новых наработок в , при повторении слияния Git добавит только те изменения, которые были сделаны после отмены слияния:
Рисунок 140 – История с плохим слиянием
Лучшим решением данной проблемы является откат коммита отмены слияния, так как теперь вы хотите внести изменения, которые были отменены, а затем создание нового коммита слияния:
Рисунок 141 – История после повторения отменённого слияния
В этом примере, и отменены. В коммите фактически сливаются изменения из и , а в – изменения из , таким образом, ветка полностью слита.
Безболезненное разрешение Merge конфликтов в Git +41
- 05.03.17 10:48
•
VBauer
•
#323234
•
Хабрахабр
•
Tutorial
•
•
15000
Git
Предлагаю читателям «Хабрахабра» перевод публикации «Painless Merge Conflict Resolution in Git» из блога blog.wuwon.id.au.
В моей повседневной работе, часто приходится иметь дело со множеством git ветвей (branch). Это могут быть ветви промежуточных релизов, ветви с устаревшим API находящиеся на поддержке для некоторых клиентов, или ветви с экспериментальными свойствами. Лёгкость создания ветвей в модели Git так и соблазняет разработчиков создавать все больше и больше ветвей, и как правило бремя от большого количества ветвей становится очень ощутимым, когда приходится все эти ветви поддерживать и периодически делать слияния (merge) с другими ветвями.
Слияния очень важны для поддержания кода в актуальном состоянии, и как правило ошибка сделанная при слиянии может привести к большей головной боли, нежели ошибка сделанная при простом коммите. К сожалению ошибки слияния далеко не редкость, потому что во-первых слияния имеют несколько родительских ветвей. Даже при анализе истории слияния ветвей, бывает очень трудно понять, какие же изменения были сделаны для разрешения конфликта. Во-вторых, отмена неудачного слияния может превратиться в большую головную боль. В-третьих, большая часть конфликтов слияния происходит при работе с чужим кодом, потому что само понятие ветвей подразумевает множество пользователей, т.е. далеко не всегда слияние производит тот же человек который работал с той или иной веткой. В сухом остатке, сделать ошибку при слиянии очень легко, её трудно исправить и трудно найти. Таким образом время потраченное на изучение и понимание процесса слияния ветвей, окупится с лихвой.
Удивительно, но я обнаружил, что многие доступные инструменты и интерфейсы предназначенные для выполнения слияний, не достаточно хорошо оснащены для эффективного выполнения этого процесса. Часто программист просто надеется что команда git merge сделает за него всю работу. Но когда все-таки происходит конфликт, то обычно стратегия слияния заключается в беглом просмотре кода вокруг строки конфликта, и интуитивном угадывании что именно данный кусок кода предпочтительней другого.
В данной статье я надеюсь продемонстрировать что процесс разрешения конфликтов может быть пошагово точным, при котором отпадает необходимость что-либо там угадывать.
Перебазирование
Имея в виду процесс слияния, рассмотрим тот же пример, но уже с точки зрения перебазирования. Так же как и в предыдущем случае, перед интеграцией изменений Satoshi должен убедиться, что его локальная и удаленная ветки синхронизированы. Но затем вместо обычного слияния, сохраняющего историю в ее поэтапном виде, он может интегрировать все изменения с помощью операции перебазирования, таким образом перезаписывая историю.
Выполняя перебазирование относительно , Git вернется назад и повторно выполнит коммиты и один за другим прямо поверх , создавая впечатление, что изначально была ответвлением конечных изменений Ada.
Процесс перебазирования:
После повторной интеграции всех изменений Satoshi может продолжить работу над своей тематической веткой.
Ниже представлен конечный результат перебазирования
Обратите внимание на повторное выполнение коммитов и поверх , повлекшее за собой перезаписывание истории разработки и полное удаление старых коммитов!. Как видно, хеши C5 и C6 изменились
Обусловлено это тем, что фактически они являются вновь созданными коммитами (хотя их содержание все еще может быть идентичным)
Как видно, хеши C5 и C6 изменились. Обусловлено это тем, что фактически они являются вновь созданными коммитами (хотя их содержание все еще может быть идентичным)
Понимая главное отличие между слиянием и перебазированием, проанализируем конечные результаты этих операций.
А теперь всё вместе (All Together Now)
Надеюсь, вы найдёте этот трёх-окошечный метод разрешения конфликтов, таким же полезным каким нахожу его я. Но согласитесь что запускать новые вызовы meld вручную каждый раз при разрешении конфликтов, не очень то и удобно. Выход, это настроить git таким образом чтобы все три окна открывались автоматически при вызове команды git mergetool. Для этого можно создать выполняемый скрипт, который должен находится в переменной окружения PATH (например $HOME/bin/gitmerge), со следующим содержимым:
И добавьте следующее в ваш ~/.gitconfig файл:
Теперь, когда вы в следующий раз будете запускать команду git mergetool для разрешения конфликта, откроются все три окна:
После того как вы привыкните к такому разрешению конфликтов с использованием трёх вышеупомянутых окон, вы скорее всего обнаружите, что процесс стал более методичным и механическим. В большинстве случаев, вам даже не придётся читать и понимать куски кода из каждой ветки, для того чтобы понять какой же вариант применить для слияния. Вам больше не понадобится догадываться, потому что вы будете гораздо более уверенным в корректности вашего комита. Из-за этой уверенности, появится чувство что разрешение конфликтов превратилось в увлекательное занятие.
Бонус от переводчика
Для тех кто пользуется tmux и n?vim, предлагаю следующий скрипт gitmerge:
Примечание: если вы не используете в своем ~/.tmux.conf, то вам надо поменять в двух последних строках на
Соответственно добавьте следующее в ваш
Воркфлоу разрешения конфликта будет выглядеть так:
Пока игнорируем вопрос (Was the merge successful [y/n]?) и переключаемся в сессию под названием gitmerge (сочетание TMUXPREFIX + s):
Видим наше трёх-оконное представление на одном экране. Цифрами обозначены сплиты (panes) tmux’a, буквами соответствующие состояния. Делаем правки для разрешения конфликта, т.е. редактируем состояние (D) и сохраняем. После этого возвращаемся обратно в исходную сессию tmux’a и подтверждаем что слияние произошло успешно.
git rebase master
Лично я предпочитаю и считаю более правильным делать сначала rebase master в ветке beta, и только после этого переключаться в master и делать git merge beta. В принципе воркфлоу не сильно отличается, за исключением трёх-оконного вида.
Переключаемся в сессию gitmerge
Обратите внимание, что состояния (B) и поменялись местами:
Рекомендую всем поиграться с примером репозитария хотя бы один раз, сделать разрешение конфликта по вышеописанной схеме. Лично я больше не гадаю а что же выбрать «Accept theirs» или «Accept yours».
Rebase
Мы только что увидели, как можно применить изменения из одной ветки в другую, выполнив . Другой способ добавить изменения из одной ветки в другую — выполнить .
Git rebase копирует коммиты из текущей ветки и помещает эти скопированные коммиты поверх указанной ветки.
Отлично, теперь у нас есть все изменения, которые были сделаны в ветке и в ветке.
Большая разница по сравнению с мержем заключается в том, что Git не будет пытаться выяснить, какие файлы сохранить и не сохранить. В ветке, которую мы обновляем, всегда есть последние изменения, которые мы хотим сохранить! Таким образом, вы не столкнетесь ни с какими мерж конфликтами и у вас будет хорошая линейная история.
Этот пример показывает rebase в ветке. Однако в больших проектах вы обычно не захотите этого делать. Git rebase изменяет историю проекта, поскольку для скопированных коммитов создаются новые хэши.
Rebase отлично подходит, когда вы работаете над feature branch, а master ветка была обновлена. Вы можете получить все обновления в своей ветке, которые предотвратят будущие merge конфликты
Interactive Rebase
Перед rebas’ом коммитов мы можем их изменить! Мы можем сделать это с помощью Interactive Rebase. Interactive Rebase также может быть полезен для ветки, над которой вы сейчас работаете, и хотите изменить некоторые коммиты.
Есть 6 действий, которые мы можем выполнить над коммитами, которые мы rebas’им:
- : Изменить коммит меседж
- : Изменить коммит
- : Объеденить коммит в предыдущий коммит
- : Объединить коммит с предыдущим коммитом, не сохраняя commit’s log message
- : Запустить команду для каждого коммита, который мы хотим rebase
- : Удалить коммит
Таким образом, мы можем иметь полный контроль над нашими коммитами. Если мы хотим удалить коммит, мы можем просто drop’нуть его.
Или, если мы хотим объединить несколько коммитов вместе, чтобы получить более чистую историю, нет проблем!
Interactive rebasing дает вам большой контроль над коммитами, которые вы пытаетесь rebase’нуть, даже в текущей активной ветке.
Merge
Наличие нескольких веток чрезвычайно удобно для того, чтобы новые изменения были отделены друг от друга, а также чтобы вы случайно не запушили несанкционированные или поврежденные изменения в продакшин. Как только изменения будут одобрены, мы хотим получить эти изменения в нашей прод ветке!
Один из способов получить изменения из одной ветки в другую — выполнить ! Есть два типа менж команд, которые может выполнять Git: fast-forward или no-fast-forward
Fast-forward ()
Fast-forward merge когда текущая ветка не имеет дополнительных коммитов по сравнению с веткой, которую мы мержим. Git у нас ленив и сначала попытается выполнить самый простой вариант: Fast-forward! Этот тип менжа не создает новый коммит, а скорее объединяет коммит(ы) в ветку, которую мы объединяем прямо в текущей ветке
Отлично! Теперь у нас есть все изменения, которые были сделаны в ветке , в ветке . Итак, что же такое no-fast-forward?
No-fast-foward ()
Хорошо, если ваша текущая ветка не имеет каких-либо дополнительных коммитов по сравнению с веткой, которую вы хотите смержить, но, к сожалению, это случается редко! Если мы зафиксировали изменения в текущей ветке, которых нет в ветке, которую мы хотим объединить, git выполнит объединение без fast-forward merge. При слиянии без fast-forward Git создает новый коммит мержа в активную ветку. Родительский коммит указывает на активную ветку и ветку, которую мы хотим объединить!
Merge конфликты
Хотя Git хорошо решает, как объединять ветки и добавлять изменения в файлы, он не всегда может принять это решение сам по себе. Это может произойти, когда две ветки, которые мы пытаемся смержить, имеют изменения в одной строке в одном и том же файле, или если одна ветка удалила файл, который изменила другая ветка, и так далее.
В этом случае Git попросит вас помочь решить, какой из двух вариантов мы хотим сохранить. Допустим, что в обеих ветках мы отредактировали первую строку в файле README.md.
Если мы хотим смержить в , это приведет к конфликту: хотите, чтобы заголовок был Hello! или hey!?
При попытке объединить ветки, Git покажет вам, где происходит конфликт. Мы можем вручную удалить изменения, которые не хотим сохранять, сохранить изменения, снова добавить файл и закоммитить изменения.
Теоретические сведения
Слияние веток
Основная цель создание дополнительных веток – проработать отдельный элемент проекта автономно а затем присоединить его к проекту. Поэтому в большинстве случаев эти ветки в конечном итоге объединяются с веткой . Процедура объединения веток называется слияние (merge).
Для слияния текущей ветки с какой-либо другой используется команда
В результате выполнения этой команды в текущей ветке появится новый коммит. Этот коммит будет иметь два предка – последние коммиты обоих веток, участвующих в слиянии. Содержимое этого коммита будет включать содержимое коммитов обоих веток.
Изменения, происходящие с репозиторием при выполнении команды схематично показаны на рисунках 3 и 4.
Рисунок 3 – Состояние репозитория до слияния
Рисунок 4 – Состояние репозитория после слияния
После слияния веток одна из них становится лишней. Ее можно удалить командой .
Типичная последовательность действий, которой завершается работа над какой-либо задачей проекта в отдельной ветке (назовем ее для примера ), будет такой:
Конфликты при слиянии
При слиянии может возникнуть ситуация, когда фрагмент в каком-либо файле проекта в различных ветках отредактирован по разному. Такая ситуация называется конфликт (conflict).
В случае возникновения конфликтов git заносит в создаваемый при объединении коммит файл, содержащий текст обоих версий. Начало конфликтного фрагмента помечается строкой, начинающиеся с символов и содержащей имя первой ветки, а заканчивается строкой, начинающиеся с символов и содержащей имя вливаемой ветки. Версии из каждой ветки разделяются строкой с символами . Такой файл получает статус не объединенный (unmerged).
При возникновении конфликта пользователь должен в ручном режиме его устранить. Посмотреть список не объединенных файлов можно с помощью команды . После редактирования конфликтной области и сохранения файла, нужно сообщить git о разрешении конфликта с помощью индексирования этого файла (после чего он перейдет в состояние «измененный»).
Перемещение
В СКВ git существует альтернативный способ объединения веток. Он называется Перемещение (rebase, часто переводиться как перебазирование).
В общем случае операции слияния и перемещения формируют одинаковый результирующий коммит. Отличаются эти операции внутренними алгоритмами получения результирующего коммита, а также тем, что операция перемещения изменяет историю коммитов. Схематично результат перемещения для приведенного выше репозитория представлен на рисунке 5.
Рисунок 5 – Состояние репозитория после перемещения
В случае операции слияния имеющиеся в репозитории коммиты не изменяются, в частности, не изменяются их предки. Операция перемещения изменяет историю коммитов, размещая коммиты ветки, в которой происходит слияния, после коммитов ветки, с которой происходит слияние. В результате коммит, возникающий при операции перемещения, имеет одного предка, а обе ветки, участвующие в слиянии, состоят из одной цепочки коммитов.
Какой способ объединения веток использовать (слияние или перемещение) во многом является делом вкуса. Слияние сохраняет историю коммитов, а перемещение позволят прибраться в структуре коммитов. Единственное существенное ограничение, которое накладывается на выбор операции, состоит в том, что перемещение нельзя использовать для коммитов, для которых есть копии в других репозиториях или на сервере (подробнее копирование репозиториев рассмотрено в следующей работе).
Назад к Базе (Back to Base)
Хитрость заключается в том, что Listing 1 не даёт вам полную информацию, необходимую для совершения корректного слияния. На самом деле, в процессе слияния участвуют четыре важных части информации (состояния), три из которых просто необходимы для успешного разрешения конфликта. В случае Listing 1, Git предоставил вам только два состояния.
Следующая диаграмма иллюстрирует эти четыре состояния:
Состояния (B) и (C) относятся к текущим положениям (head) веток master и beta соответственно, эти два состояния как раз таки и отражены в Listing 1. Состояние (D) это результат слияния, то что вы хотите получить/сгенерировать в конечном итоге (в большинстве случаев Git автоматически генерирует состояние (D)). Состояние (А) на самом верху, представляет собой базу (основу) слияния веток master и beta. База слияния (A) это последний общий предок веток master и beta, и пока предположим что это база слияния уникальна. Как мы увидим позже состояние (A) играет ключевую роль в разрешении конфликтов. На диаграмме я также отразил дельты 1 и 2, которые представляют изменения между состояниями (A)-(B), и (A)-(C) соответственно. Зная состояния (A), (B) и (C) дельты 1 и 2 могут быть легко получены (вычислены)
Обратите внимание, что дельты 1 и 2 могут состоять из более чем одного коммита. Но для наших целей будем считать что все дельты монолитны
Чтобы понять, как получить состояние (D), вы должны понимать что же операция слияния пытается сделать. Состояние (D) должно представлять собой сочетание изменений, внесённых в ветку master и beta соответственно. Т.е. другими словами сочетание дельт 1 и 2. Идея проста на поверхности и большую часть времени не требует вмешательства со стороны человека, за исключением особых случаев когда дельты затрагивают наслаиваемые (пересекающиеся) части файла. В такой ситуации вам требуется помочь машине сгенерировать результат (D), путём сравнения дельт 1 и 2.
Голубые Розы (Roses are Blue)
Давайте предположим что вашей команде поручили писать поэмы в отведённом для этих целей репозитории. (Какой кошмар!) А вам доверили самое главное — делать слияния последних фиксов из ветки master в ветку beta. Итак, вы переключаетесь в ветку beta и выполняете следующую команду:
Ого, это конфликт. Вы решаете просмотреть файл на который ссылается git:
Listing 1
123456789101112 |
Замечательно! Весь файл, как показывает Listing 1, находится в конфликтном состоянии. Какой же вариант файла является более корректным? Оба варианта выглядят корректно. Верхний вариант написан в хакер-стиле с элементами цветовой кодировки в стиле HTML и с использованием только строчных букв. Нижний вариант выглядит более натурально, с использованием пунктуации и заглавных букв.
Если бы это был ваш проект, вы бы могли просто выбрать один вариант и покончить с этим слиянием. Но проблема в том, что это не ваша поэма, вы никогда не читали эту поэму раньше, не были ответственны за написание или редактирование, и вы отлично понимаете что в случае не верного решения чья-то тяжёлая работа может кануть в небытие. Однако вас всё же назначили ответственным по слиянию этих веток. Что же вам делать?
Reflog
Все делают ошибки, и это совершенно нормально! Иногда может показаться, что вы испортили свой репозиторий Git настолько сильно, что просто хотите полностью его удалить.
— очень полезная команда для отображения журнала всех выполненных действий! Это включает в себя слияния, перезагрузки, возвраты: в основном, любые изменения в вашей ветке.
Если вы допустили ошибку, вы можете легко отменить ее, сбросив HEAD на основе информации, которую нам предоставляет reflog.
Скажем, мы на самом деле не хотели мержить origin ветку. Когда мы выполняем команду , мы видим, что состояние репозитория до мержа- . Давайте выполним , чтобы указать HEAD туда, где он был.
Что такое мердж или слияние веток
Это перенос кода из одной ветки в другую. Например, когда мы заканчиваем работу над веткой, например, сделали новый функционал или поправили багу, мы сливаем ее в мастер. В мастере код проверяется еще раз и выкладывается на боевой сервер.
Сливать друг в друга можно любые ветки. Технически, с точки зрения git нет никакой разницы, сливается ветка с новым функционалом в мастер или наоборот. Для нас мастер — это основная ветка разработки, а для git это просто ветка.
Разница логическая
Мы будем говорить, что новая ветка сливается в мастер, когда мы закончили работать над функционалом и хотим выложить его на боевой сайт. Тогда мы сливаем ветку в мастер и выкладываем мастер в продакшен.
Мастер сливается в ветку или мастер подтягивается в ветку, когда мы продолжаем работать над веткой, но хотим периодически подтягивать новые коммиты с сервера — новый код, который написали наши коллеги. Если работа над веткой идет довольно долго, есть смысл подтягивать изменения из мастера хоть каждый день.
Следует четко различать мердж своей ветки в мастер и мердж мастера в свою ветку.