Git: ветки, слияния и разрешение конфликтов

18 мая 2015
Предыдущая статья: Введение в Git
PDF-книга на русском «Про Гит» от Скотта Чакона: https://github.com/downloads/GArik/progit/progit.ru.pdf
Официальная документация: https://git-scm.com/docs

Ветки

Ветка (branch) – это цепочка коммитов. Помимо главной ветки master, Гит  позволяет создавать неограниченное число других веток. 

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

В случае, когда над проектом трудятся сразу несколько человек, то порядок работы с Гитом может быть организован другим образом.

Удобный вариант:
  1. Ветка master никем не правится;
  2. От ветки master отходят ветки develop и release
  3. Ветка develop служит для работы над правками и разворачивается на отладочный версии сайта;
  4. Ветка release служит для хранения боевого (релизного) состояния, заливается в ветку master, которая после этого разворачивается на боевой версии сайта;
  5. Все правки выполняются в своих отдельных ветках, но только каждая ветка начинается от ветки release, а после заливается сначала в develop, а после успешной проверки и утверждения на выкладку заливается в release, затем автоматически правка идёт в master и от туда на боевой сайт;
  6. Ветки с правками должны иметь однородные названия и содержать номер задачи. Например, если это задача 31547, то ветку можно назвать task/31547;
  7. Описанный подход даёт более гибкий механизм работы с правками, но требует чуть больше действий. В рутине к ним быстро привыкаешь и потом не понимаешь как можно было без этого обходиться.

Простой вариант для тестирования слияний:
  1. Конкретным разработчиком под свою задачу на тестовом репозитории создаётся новая ветка, например, dev_1_task_34;
  2. В ветку dev_1_task_34 разработчик коммитит изменённые файлы с реализацией задачи;
  3. Далее нужно перейти в ветку master и объединить её с веткой dev_1_task_34;
  4. Ответственный разработчик (желательно один человек) заливает ветку master тестового репозитория в аналогичную ветку master, находящуюся в боевом репозитории работающего проекта/сайта.

Теперь разберём вопрос детальнее на примере. У нас есть выдуманный боевой сайт silos-12345.ru с развёрнутым боевым репозиторием. Для разработки и тестирования используется выдуманный тестовый сайт test-site.ru. Оба сайта находятся на локальном сервере. Стоит задача внести правки на сайт silos-12345.ru

Шаг 1 - разворачивание резервной копии боя на тесте

Делаем резервную копию silos-12345.ru и разворачиваем её в чистой корневой директории сайта test-site.ru

Шаг 2 - копирование репозитория боя на тест

Открываем консоль Git Bash и вводим команду cd C:/OSPanel/domains/test-site.ru/www/, которая переместит нас в корневую директорию тестового сайта.

Далее с помощью команды git clone C:/OSPanel/domains/silos-12345.ru/www/ . копируется репозиторий боевого сайта silos-12345.ru в текущую директорию (в корень тестового сайта C:/OSPanel/domains/test-site.ru/www/)


Введём команду git log --oneline для просмотра истории коммитов


Тут мы видим красные origin/master и origin/HEAD, где:
  • origin – стандартное символьное имя удалённого сервера, которое присваивается адресу, с которого мы делали копию репозитория. В нашем случае это путь к корню боевого сайта;
  • origin/master – ссылка на ветку master из репозитория-исходника;
  • origin/HEAD – ссылка на HEAD из репозитория-исходника.

Для просмотра всех удалённых серверов вводим команду git remote -v, которая  выведет имена привязанных к репозиторию удалённых серверов, а так же их адреса для считывания изменений (команда fetch) и записи изменений (команда puch)


Шаг 3 - поступила задача на правку

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

Теперь стоит вопрос, куда отправлять коммиты – в основную ветку или создать новую? За основную выступает меньшее количество движений, но вдруг выполнение растянется и мы захотим делать другую задачу? Тогда правки могут смешаться. Ещё больше усугубит ситуацию внезапная необходимость обновления основной ветки.

Иногда для задач легче создать отдельную ветку и спокойно в ней работать, а по завершению влить ветку с реализованной правкой в основную ветку master. Так мы и поступим в рамках примера.

Шаг 4 - создание ветки под правку

Для создания новой ветки вводим команду git branch dev_1_task_34, где dev_1_task_34 это название ветки. Можно указывать любые названия.

Ветка создана, но мы пока не переключились на неё, HEAD продолжает оставаться на ветке master. Для перехода в ветку вводим команду git checkout dev_1_task_34


Шаг 5 - вносим правку на сайт и отправляем коммит

Выполняем задачу и вносим правки, в ходе чего у нас изменился условный файл index.php. С помощью команды git commit -a -m "правка 34: Изменил вывод служебных сообщений из статичной формы в форму шаблонов" добавляем изменённые файлы в индекс и отправляем коммит.

Далее выведем дерево коммитов с помощью команды git log --all --graph --oneline 

 
Затем нам нужно влить ветку dev_1_task_34 в master. Но что бы чуть разнообразить демонстрацию, давайте переключимся на master, сделаем новую правку и отправим коммит.

Переключаюсь на master командой git checkout master, вношу изменения в файл index.php и отправляю коммит командой git commit -a -m "рефакторинг: Добавил описание скрипта"


Снова выведем дерево коммитов с помощью команды git log --all --graph --oneline


Графический вид дерева:


Слияние веток - теория

Слияние - это процесс объединения последних изменений из нескольких веток в один новый коммит. Чаще сливают две ветки, но Гит позволяет сливать и большее количество веток.

Имеется несколько алгоритмов слияния, в Гите они называются стратегиями. Для двух веток автоматически выбирается стратегия рекурсии (параметр -s recursive). Полный список и описание стратений можно посмотреть в документации на странице https://git-scm.com/docs/merge-strategies

Слияние веток - практика

Для объединения изменений двух веток используется операция слияния git merge название-добавляемой-ветки. Перед слиянием нужно зайти в принимающую ветку (в нашем примере это master, мы в ней уже находимся). Затем выполняем непосредственное слияние, введя команду git merge dev_1_task_34

Готово, изменения в файлах ветки dev_1_task_34 были успешно и без конфликтов объединены с содержимым ветки master, а в конце автоматически создался результирующий коммит слияния.


Снова посмотрим дерево коммитов, используя команду git log --all --graph --oneline. На скриншоте красным овалом я выделил место, где появляется вторая  ветка и которая потом заходит обратно (сливается с master).


Иногда после слияний удаляют указатель залитой ветки. В качестве примера, давайте сделаем это. Вводим команду git branch -d dev_1_task_34, которая удалит указатель ветки dev_1_task_34. Затем привычной командой git log --all --graph --oneline выводим дерево коммитов:


Конфликты слияния

В прошлом слиянии у нас не возникло конфликтов, попробуем их сделать и потом разрешить. Давайте откатимся на момент до слияния. Для этого нужно сделать два действия:

Откат. Действие 1 

Необходимо восстановить удалённый указатель ветки dev_1_task_34, а для этого нужно узнать последний коммит этой ветки. 

С помощью команды git reflog выведем журнал действий (в журнал попадают хеши коммитов, на которые переключался указатель HEAD). На скриншоте мы видим, что после перехода с ветки dev_1_task_34 на master указатель HEAD стал указывать на коммит 9ea5a4f


Ниже коммита 9ea5a4f находится коммит a47096c с правкой №34. Отлично, это и есть последний коммит ветки dev_1_task_34. Теперь вводим команду для восстановления ветки git branch dev_1_task_34 a47096c, а после выводим дерево коммитов через git log --all --graph --oneline

Видим, что на дереве снова появился указатель ветки dev_1_task_34


Откат. Действие 2

После восстановления удалённого указателя нужно отменить слияние. Как вариант, можно не отменять слияние, а просто восстановить состояние ветки master к коммиту до слияния. Делаем это, вводим команду git reset --hard f286e5b, которая откатит состояние текущей ветки к коммиту f286e5b. Затем опять выводим дерево коммитов git log --all --graph --oneline

Как видим, мы восстановили состояние дерева как оно было до слияния.


Создание ситуации конфликта

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

В ветке master изменим содержимое всего файла index.php на нижний код и отправим коммит. Код на PHP:
<?php
/**
* Скрипт выводит приветствие
*/

$a = 'Привет мир';
echo $a;

?>

В ветке dev_1_task_34 файл index.php имеет следующее содержимое (там мы ничего не меняем, новые коммиты не шлём): 
<?php

$template = '
<div style="padding: 5px 10px; color: #fff; background: black;">
<b>[[MESSAGE_TEXT]]</b>
</div>
';
$message_text = 'Привет мир 2';

$message_box = str_replace(array(
	'[[MESSAGE_TEXT]]'
), array(
	$message_text
), $template);

?>

Файл index.php в двух ветках координально различается, поэтому при слиянии Git вряд ли сможет объединить изменения.

Конфликт

Пробуем выполнить слияние master с dev_1_task_34 привычной командой git merge dev_1_task_34. По результату видим, что операция не удалась, выводится ошибка с конфликтом слияния файлов index.php: Automatic merge failed; fix conflicts and then commit the result


Файл index.php в рабочей области изменился, теперь он содержит черновик объединения файлов index.php и включает фрагменты файла из веток master, dev_1_task_34 и фрагмент из коммита, который является последним общим коммитом обеих веток (единый предок).

Содержимое файла index.php в рабочей области после конфликта:

 
Теперь нам нужно разрешить конфликт. Есть несколько путей, как можно это сделать, попробуем некоторые из них:

Вариант решения №1 - предпочитаем изменения из текущей ветки

Если для всех конфликтов мы предпочитаем изменения из текущей ветки, то делаем следующее:
  1. С помощью команды git merge --abort отменяем предыдущую неудачную попытку слияния;
  2. Командой git merge -Xours dev_1_task_34 выполняем слияние, которое через параметр -Xours действует так, что в случае конфликта автоматически будут выбраны изменения из текущей ветки (master).
  3. Готово, в завершении автоматически появится коммит слияния с комментарием вида Merge branch 'dev_1_task_34'


Содержимое итогового файла index.php после слияния:
<?php
/**
* Скрипт выводит приветствие
*/

$a = 'Привет мир';
echo $a;

?>

Вариант решения №2 - предпочитаем изменения из сливаемой ветки

Если для всех конфликтов мы предпочитаем изменения из сливаемой (добавляемой) ветки, то схема аналогична предыдущей, только вместо параметра -Xours указываем параметр -Xtheirs. Итак:
  1. С помощью команды git merge --abort отменяем предыдущую неудачную попытку слияния;
  2. Командой git merge -Xtheirs dev_1_task_34 выполняем слияние, которое через параметр -Xtheirs настроено так, что в случае конфликтов будут автоматически выбраны изменения из сливаемой (добавляемой) ветки dev_1_task_34.
  3. Готово, в завершении автоматически появится коммит слияния с комментарием вида Merge branch 'dev_1_task_34'


Содержимое итогового файла index.php после слияния:
<?php
/**
* Скрипт выводит приветствие
*/

$template = '
<div style="padding: 5px 10px; color: #fff; background: black;">
<b>[[MESSAGE_TEXT]]</b>
</div>
';
$message_text = 'Привет мир 2';

$message_box = str_replace(array(
	'[[MESSAGE_TEXT]]'
), array(
	$message_text
), $template);

?>

Вариант решения №3 - ручная правка конфликтных файлов через WinMerge

Когда конфликт нельзя решить сплошным предпочтением определённой ветки, конфликтные файлы начинают править вручную. Делается это с помощью специальных программ, которые находят различия в файлах и по кусочкам разработчик составляет правильный объединённый файл. Для своих нужд я использую программу WinMerge. В статье Подключение WinMerge к Git для разрешения конфликтов слияния описывается добавление WinMerge в Гит как стандартного инструмента по работе со слияниями файлов.

Теперь вернёмся к ситуации конфликта, где пишется ошибка Automatic merge failed; fix conflicts and then commit the result. Вводим команду git mergetool, которая откроет конфликтные файлы index.php в стандартном инструменте сравнения и объединения. В нашем случае таким инструментом установлена программа WinMerge


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


Вот так выглядит конечный результат:


Код вручную объединённого файла index.php:
<?php
/**
* Скрипт выводит приветствие
*/

$template = '
<div style="padding: 5px 10px; color: #fff; background: black;">
<b>[[MESSAGE_TEXT]]</b>
</div>
';
$message_text = 'Привет мир 2';

$message_box = str_replace(array(
	'[[MESSAGE_TEXT]]'
), array(
	$message_text
), $template);

echo $template;

?>

После составления объединённого файла закрываем окно WinMerge. Программа спросит, нужно ли сохранить изменения? Нажимаем ОК

 
Окно WinMerge закроется и нас переключит обратно в консоль Git Bash. Вводим команду git merge --continue, которая попытается завершить слияние, учтя наши изменения. Готово, действие завершилось успешно. Выводим дерево коммитов командой git log --all --graph --oneline. Как видим, всё нормально


На этом рассказ о процессах слияния завершён