2. Ветвление в git

Цель работы

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

После выполнения работы ознакомьтесь с A simple git branching model.

Материал для подготовки к работе

  1. Branches in a Nutshell

  2. Git Branching — Rebasing

Команды:

Настройка отображения конфликтов

Git позволяет настроить более подробное отображение конфликтов:

git config --global merge.conflictstyle diff3

По умолчанию конфликты отображаются в виде:

<<<<<<< HEAD
Левая ветка
=======
Правая ветка
>>>>>>> c2392943

В формате diff3 отображается также состояние общего предка сливаемых веток:

<<<<<<< HEAD
Левая ветка
||||||| merged common ancestors
Общий предок
=======
Правая ветка
>>>>>>> c2392943

Руководство

Часть 1: создание веток

  1. Создать новый репозиторий branches-basics.

  2. В основной ветке master создать несколько коммитов.

  3. Создать новую ветку develop. Выполнить несколько коммитов.

  4. Вернуться на ветку master. Создать коммит.

  5. Внести изменения в рабочую копию репозитория, не коммитить. Переключиться на ветку develop

Часть 2: merge и rebase

В этой части нужно выполнить слияние веток одним из предложенных способов. Предварительно необходимо изучить историю каждой ветки репозитория и понять, какие изменения были сделаны. В результате слияния вы должны получить работоспособный исходный код, объединяющий в себе изменения, выполненные в ветках main и develop.

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

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

Дополнительные коммиты с исправлениями после мержа или ребейза нежелательны, поскольку замусоривают историю.

В случае ошибки вы можете откатить ветку к ее исходному состоянию с помощью команды git reset с опцией --hard.

  1. Склонируйте репозитории:

    git clone https://github.com/branches-sandbox/merge-simple.git  1-merge-ff
    git clone https://github.com/branches-sandbox/merge-simple.git  2-merge-no-ff
    git clone https://github.com/branches-sandbox/merge-dirty.git   3-merge-dirty
    git clone https://github.com/branches-sandbox/no-conflict.git   4-rebase-no-conflict
    git clone https://github.com/branches-sandbox/conflict-easy.git 5-merge-conflict-easy
    git clone https://github.com/branches-sandbox/conflict-easy.git 6-rebase-conflict-easy
    git clone https://github.com/branches-sandbox/conflict-hard.git 7-merge-conflict-hard
    git clone https://github.com/branches-sandbox/conflict-hard.git 8-rebase-conflict-hard
    

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

    $ ls -1
    1-merge-ff
    2-merge-no-ff
    3-merge-dirty
    4-rebase-no-conflict
    5-merge-conflict-easy
    6-rebase-conflict-easy
    7-merge-conflict-hard
    8-rebase-conflict-hard
    

1-merge-ff

Нужно выполнить простейшее слияние. История исходного репозитория:

* de873d1 (origin/develop, develop) Extract printing array to function
* 03b93ef Count array items with macro
* 5cbd705 (HEAD -> main, origin/main) Implement min_element function
* 04e3251 Initial commit

История после слияния:

* de873d1 (HEAD -> main, origin/develop, develop) Extract printing array to function
* 03b93ef Count array items with macro
* 5cbd705 (origin/main, origin/HEAD) Implement min_element function
* 04e3251 Initial commit

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

2-merge-no-ff

Исходный репозиторий такой же, как и в задании 1-merge-ff:

* de873d1 (origin/develop, develop) Extract printing array to function
* 03b93ef Count array items with macro
* 5cbd705 (HEAD -> main, origin/main) Implement min_element function
* 04e3251 Initial commit

Нужно выполнить слияние веток с опцией --no-ff. Результат:

*   a7f1782 (HEAD -> main) Merge branch 'develop' into main
|\
| * de873d1 (origin/develop, develop) Extract printing array to function
| * 03b93ef Count array items with macro
|/
* 5cbd705 (origin/main) Implement min_element function
* 04e3251 Initial commit

Мы получили историю, в которой:

  • У каждого коммита не более двух предков

  • Между началом ветки (5cbd705) и мерж-коммитом (a7f1782) коммиты присутствуют только в правой ветке.

Историю такого вида будем называть псевдолинейной.

Преимущества псевдолинейной истории:

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

  2. Появляется потенциальная возможность откатить задачу целиком. Для этого достаточно выполнить revert мерж-коммита.

  3. Мерж-коммит не содержит разрешения конфликтов, как это может быть при нелинейной истории.

  4. Поскольку история остается линейной, легко искать проблемный коммит методом бинарного поиска (git bisect).

Недостатки:

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

  2. Если требуется выполнить вливание нескольких веток подряд, то разработчикам приходится договариваться об очередности вливания, иначе приходится многократно выполнять ребейз фиче-ветки на main.

По соглашениям курса в лабораторных работах и курсовом проекте следует формировать псевдолинейную историю.

3-merge-dirty

Исходная история:

* 899302d (develop, origin/develop) Extract printing array to function
* 9513c27 Count array items with macro
| * fe2320b (HEAD -> main, origin/main, origin/HEAD) Expand abbreviation 'min' in message
|/
* 5855f29 Implement min_element function
* 155841b Initial commit

Поскольку коммит fe2320b (main) не является предком коммита 899302d (develop), в этой ситуации fast-forward merge невозможен.

Изучите историю изменений. Для этого воспользуйтесь коммантами git hist -p или gitk --all. Посмотрите изменения в каждом коммите и подумайте, какое поведение должно быть у приложения после слияния веток.

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

После слияния приложение должно выводить:

Array: 3 1 4 1 5 9 2
Minimum element: 1

Если у вас вывод отличается, выполните откат и повторите слияние.

Результат:

*   8edb092 (HEAD -> main) Merge branch 'develop' into main
|\
| * 899302d (origin/develop, develop) Extract printing array to function
| * 9513c27 Count array items with macro
* | fe2320b (origin/main, origin/HEAD) Expand abbreviation 'min' in message
|/
* 5855f29 Implement min_element function
* 155841b Initial commit

Мы получили историю, в которой:

  • У коммита может быть более одного предка.

  • Между началом ветки и мерж-коммитом как в левой, так и в правой ветке могут присутствовать другие коммиты.

Историю такого вида будем называть нелинейной.

Преимущества нелинейной истории:

  1. Сравнительно легко формировать.

Недостатки:

  1. Топология истории значительно усложняется.

  2. Нарушается принцип «один коммит — одно изменение», поскольку мерж-коммит может содержать:

    • Изменения из левой ветки

    • Изменения из правой ветки

    • Разрешения конфликтов

    • Случайные изменения, внесенные в процессе разрешения конфликтов

В лабораторных работах и курсовом не следует формировать нелинейную историю.

4-rebase-no-conflict

В исходной истории fast-forward merge невозможен:

* f5275c7 (origin/develop, develop) Extract printing array to function
* 3314b69 Count array items with macro
| * 4e235e6 (HEAD -> main, origin/main, origin/HEAD) Remove unused header
|/
* 41653c8 Implement min_element function
* 7e36880 Initial commit

В этой ситуации нужно:

  1. Переключиться на ветку develop.

  2. Выполнить ее rebase на main.

  3. Переключиться на ветку main.

  4. Выполнить merge.

В результате получим псевдолинейную историю:

*   d3a9b2f (HEAD -> main) Merge branch 'develop' into main
|\
| * b3f6746 (develop) Extract printing array to function
| * 155a81a Count array items with macro
|/
* 4e235e6 (origin/main, origin/HEAD) Remove unused header
| * f5275c7 (origin/develop) Extract printing array to function
| * 3314b69 Count array items with macro
|/
* 41653c8 Implement min_element function
* 7e36880 Initial commit

В склонированном репозитории у вас нет прав на push. При работе со своими репозиториями ветку фиче-ветку можно удалять после мержа, тогда история примет вид:

*   d3a9b2f (HEAD -> main, origin/main, origin/HEAD) Merge branch 'develop' into main
|\
| * b3f6746 Extract printing array to function
| * 155a81a Count array items with macro
|/
* 4e235e6 Remove unused header
* 41653c8 Implement min_element function
* 7e36880 Initial commit

Такой подход рекомендуется при выполнении лабораторных работ и курсового.

5-merge-conflict-easy

Нужно выполнить мерж и разрешить конфликты.

Исходная история:

* 330f3ad (origin/develop, develop) Separate elements with comma
* d9796db Count array items with macro
| * 7c1c684 (HEAD -> main, origin/main, origin/HEAD) Add more elements to array
|/
* 586e5bd Implement min_element function
* 9e8374a Initial commit

Результат (нелинейная история):

*   642abc8 (HEAD -> main) Merge branch 'develop' into main
|\
| * 330f3ad (origin/develop, develop) Separate elements with comma
| * d9796db Count array items with macro
* | 7c1c684 (origin/main, origin/HEAD) Add more elements to array
|/
* 586e5bd Implement min_element function
* 9e8374a Initial commit

Разультат работы приложения после мержа:

Array: 3, 1, 4, 1, 5, 9, 2, 6, 5, 3
Min element: 1

6-rebase-conflict-easy

Те же исходные данные, что и в 5-merge-conflict-easy, но нужно сформировать псевдолинейную историю.

Исходная история:

* 330f3ad (origin/develop, develop) Separate elements with comma
* d9796db Count array items with macro
| * 7c1c684 (HEAD -> main, origin/main, origin/HEAD) Add more elements to array
|/
* 586e5bd Implement min_element function
* 9e8374a Initial commit

Результат (псевдолинейная история):

*   41a27f4 (HEAD -> main) Merge branch 'develop' into main
|\
| * 495d413 (develop) Separate elements with comma
| * 4890b37 Count array items with macro
|/
* 7c1c684 (origin/main, origin/HEAD) Add more elements to array
| * 330f3ad (origin/develop) Separate elements with comma
| * d9796db Count array items with macro
|/
* 586e5bd Implement min_element function
* 9e8374a Initial commit

Повторим: после ребейза в истории не сохраняются разрешения конфликтов, это упрощает ее чтение.

7-merge-conflict-hard

  1. Прочитайте и осознайте изменения в каждом коммите.

  2. Прочитайте и осознайте изменения в каждом коммите. Вас предупредили дважды.

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

Исходная история:

* 2a5b4e7 (origin/develop, develop) Extract printing array to function
* 9284a54 Count array items with macro
| * 85c6c1b (HEAD -> main, origin/main, origin/HEAD) Add more elements to array
|/
* 96568b0 Implement max_element function
* c61eb1d Initial commit

Результат (нелинейная история):

*   b9f54e9 (HEAD -> main) Merge branch 'develop' into main
|\
| * 2a5b4e7 (origin/develop, develop) Extract printing array to function
| * 9284a54 Count array items with macro
* | 85c6c1b (origin/main, origin/HEAD) Add more elements to array
|/
* 96568b0 Implement max_element function
* c61eb1d Initial commit

Результат работы программы:

Array: 3 1 4 1 5 9 2 6 5 3
Max element: 9

8-rebase-conflict-hard

Исходные данные те же, что и в 7-merge-conflict-hard, но теперь разрешать конфликты придется дважды: при применении каждого коммита из ветки develop. Это также пример ситуации, в которой формирование псевдолинейной истории может быть более трудоемким, чем простой мерж.

Исходная история:

* 2a5b4e7 (origin/develop, develop) Extract printing array to function
* 9284a54 Count array items with macro
| * 85c6c1b (HEAD -> main, origin/main, origin/HEAD) Add more elements to array
|/
* 96568b0 Implement max_element function
* c61eb1d Initial commit

Результат (псевдолинейная история):

*   74c74d1 (HEAD -> main) Merge branch 'develop' into main
|\
| * 150787b (develop) Extract printing array to function
| * 2a05362 Count array items with macro
|/
* 85c6c1b (origin/main, origin/HEAD) Add more elements to array
| * 2a5b4e7 (origin/develop) Extract printing array to function
| * 9284a54 Count array items with macro
|/
* 96568b0 Implement max_element function
* c61eb1d Initial commit

Результат работы программы:

Array: 3 1 4 1 5 9 2 6 5 3
Max element: 9

Заключение

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

  2. На практике возможны процессы, в которых трудозатраты на операции rebase+merge слишком высоки, поэтому на уровне соглашений принято создавать нелинейную историю.

  3. Подробнее о соглашениях по работе с репозиторием можно почитать в A simple git branching model.

Контрольные вопросы

  1. Что такое ветка?

  2. Что такое HEAD?

  3. Способы создания веток.

  4. Как узнать текущую ветку?

  5. Как переключаться между ветками?

  6. Что такое merge? Что такое fast-forward merge?

  7. Что такое rebase? Как он работает?

  8. Как можно перемещать метки?