Git: ветки, слияния и разрешение конфликтов
18 мая 2015
Предыдущая статья: Введение в Git
PDF-книга на русском «Про Гит» от Скотта Чакона: https://github.com/downloads/GArik/progit/progit.ru.pdf
Официальная документация: https://git-scm.com/docs
Если вы делаете проект единолично и проект ваш личный, то вам может быть станет достаточно единственной ветки master. Вы спокойно работаете в одной ветке на тестовом репозитории и по необходимости переносите изменения в действующий проект путём заливки ветки master тестового репозитория в такую же ветку master, но находящуюся в боевом репозитории действующего сайта.
В случае, когда над проектом трудятся сразу несколько человек, то порядок работы с Гитом может быть организован другим образом.
Удобный вариант:
Простой вариант для тестирования слияний:
Теперь разберём вопрос детальнее на примере. У нас есть выдуманный боевой сайт silos-12345.ru с развёрнутым боевым репозиторием. Для разработки и тестирования используется выдуманный тестовый сайт test-site.ru. Оба сайта находятся на локальном сервере. Стоит задача внести правки на сайт silos-12345.ru
Далее с помощью команды 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, где:
Для просмотра всех удалённых серверов вводим команду git remote -v, которая выведет имена привязанных к репозиторию удалённых серверов, а так же их адреса для считывания изменений (команда fetch) и записи изменений (команда puch)
Теперь стоит вопрос, куда отправлять коммиты – в основную ветку или создать новую? За основную выступает меньшее количество движений, но вдруг выполнение растянется и мы захотим делать другую задачу? Тогда правки могут смешаться. Ещё больше усугубит ситуацию внезапная необходимость обновления основной ветки.
Иногда для задач легче создать отдельную ветку и спокойно в ней работать, а по завершению влить ветку с реализованной правкой в основную ветку master. Так мы и поступим в рамках примера.
Ветка создана, но мы пока не переключились на неё, HEAD продолжает оставаться на ветке master. Для перехода в ветку вводим команду git checkout dev_1_task_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
Готово, изменения в файлах ветки 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 выводим дерево коммитов:
С помощью команды 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
Как видим, мы восстановили состояние дерева как оно было до слияния.
В ветке master изменим содержимое всего файла index.php на нижний код и отправим коммит. Код на PHP:
В ветке dev_1_task_34 файл index.php имеет следующее содержимое (там мы ничего не меняем, новые коммиты не шлём):
Файл index.php в двух ветках координально различается, поэтому при слиянии Git вряд ли сможет объединить изменения.
Файл index.php в рабочей области изменился, теперь он содержит черновик объединения файлов index.php и включает фрагменты файла из веток master, dev_1_task_34 и фрагмент из коммита, который является последним общим коммитом обеих веток (единый предок).
Содержимое файла index.php в рабочей области после конфликта:
Теперь нам нужно разрешить конфликт. Есть несколько путей, как можно это сделать, попробуем некоторые из них:
Содержимое итогового файла index.php после слияния:
Содержимое итогового файла index.php после слияния:
Теперь вернёмся к ситуации конфликта, где пишется ошибка Automatic merge failed; fix conflicts and then commit the result. Вводим команду git mergetool, которая откроет конфликтные файлы index.php в стандартном инструменте сравнения и объединения. В нашем случае таким инструментом установлена программа WinMerge
В третьей колонке находится техническая версия файла index.php, с фрагментами конфликта из обоих веток. Нам нужно отредактировать техническую версию в конечный законченный файл
Вот так выглядит конечный результат:
Код вручную объединённого файла index.php:
После составления объединённого файла закрываем окно WinMerge. Программа спросит, нужно ли сохранить изменения? Нажимаем ОК
Окно WinMerge закроется и нас переключит обратно в консоль Git Bash. Вводим команду git merge --continue, которая попытается завершить слияние, учтя наши изменения. Готово, действие завершилось успешно. Выводим дерево коммитов командой git log --all --graph --oneline. Как видим, всё нормально
На этом рассказ о процессах слияния завершён
PDF-книга на русском «Про Гит» от Скотта Чакона: https://github.com/downloads/GArik/progit/progit.ru.pdf
Официальная документация: https://git-scm.com/docs
Ветки
Ветка (branch) – это цепочка коммитов. Помимо главной ветки master, Гит позволяет создавать неограниченное число других веток.Если вы делаете проект единолично и проект ваш личный, то вам может быть станет достаточно единственной ветки master. Вы спокойно работаете в одной ветке на тестовом репозитории и по необходимости переносите изменения в действующий проект путём заливки ветки master тестового репозитория в такую же ветку master, но находящуюся в боевом репозитории действующего сайта.
В случае, когда над проектом трудятся сразу несколько человек, то порядок работы с Гитом может быть организован другим образом.
Удобный вариант:
- Ветка master никем не правится;
- От ветки master отходят ветки develop и release;
- Ветка develop служит для работы над правками и разворачивается на отладочный версии сайта;
- Ветка release служит для хранения боевого (релизного) состояния, заливается в ветку master, которая после этого разворачивается на боевой версии сайта;
- Все правки выполняются в своих отдельных ветках, но только каждая ветка начинается от ветки release, а после заливается сначала в develop, а после успешной проверки и утверждения на выкладку заливается в release, затем автоматически правка идёт в master и от туда на боевой сайт;
- Ветки с правками должны иметь однородные названия и содержать номер задачи. Например, если это задача 31547, то ветку можно назвать task/31547;
- Описанный подход даёт более гибкий механизм работы с правками, но требует чуть больше действий. В рутине к ним быстро привыкаешь и потом не понимаешь как можно было без этого обходиться.
Простой вариант для тестирования слияний:
- Конкретным разработчиком под свою задачу на тестовом репозитории создаётся новая ветка, например, dev_1_task_34;
- В ветку dev_1_task_34 разработчик коммитит изменённые файлы с реализацией задачи;
- Далее нужно перейти в ветку master и объединить её с веткой dev_1_task_34;
- Ответственный разработчик (желательно один человек) заливает ветку 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 - предпочитаем изменения из текущей ветки
Если для всех конфликтов мы предпочитаем изменения из текущей ветки, то делаем следующее:- С помощью команды git merge --abort отменяем предыдущую неудачную попытку слияния;
- Командой git merge -Xours dev_1_task_34 выполняем слияние, которое через параметр -Xours действует так, что в случае конфликта автоматически будут выбраны изменения из текущей ветки (master).
- Готово, в завершении автоматически появится коммит слияния с комментарием вида Merge branch 'dev_1_task_34'
Содержимое итогового файла index.php после слияния:
<?php
/**
* Скрипт выводит приветствие
*/
$a = 'Привет мир';
echo $a;
?>
Вариант решения №2 - предпочитаем изменения из сливаемой ветки
Если для всех конфликтов мы предпочитаем изменения из сливаемой (добавляемой) ветки, то схема аналогична предыдущей, только вместо параметра -Xours указываем параметр -Xtheirs. Итак:- С помощью команды git merge --abort отменяем предыдущую неудачную попытку слияния;
- Командой git merge -Xtheirs dev_1_task_34 выполняем слияние, которое через параметр -Xtheirs настроено так, что в случае конфликтов будут автоматически выбраны изменения из сливаемой (добавляемой) ветки dev_1_task_34.
- Готово, в завершении автоматически появится коммит слияния с комментарием вида 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. Как видим, всё нормально
На этом рассказ о процессах слияния завершён