Git. Урок 5.
Слияние изменений и
продвинутая работа с ветками. Команды: merge, cherry-pick, rebase.

Разберемся, что такое слияние веток и зачем его делать, познакомимся с командами merge, cherry-pick и rebase.

Git. Урок 5.
Слияние изменений и продвинутая
работа с ветками.
Команды: merge, cherry-pick, rebase.

Разберемся, что такое слияние веток и зачем его делать, познакомимся с командами merge, cherry-pick и rebase.
Smartiqa Git cover
  • Урок: 5
  • Команды: merge, cherry-pick, rebase

Оглавление

1
Теоретический блок

1. Что такое слияние и зачем оно нужно.

2. Слияние в Git. Команда git merge.
2.1 Явное слияние
2.2 Неявное слияние (fast-forward)
2.3 В чем разница между fast-forward и явным слиянием?

3. Разрешение конфликтов слияния.
3.1. Ручное разрешение конфликта
3.2. Выбор одного из двух файлов
3.3. Тонкости разрешения merge-конфликтов.
3.4. Инструменты для разрешения merge-конфликтов.

4. Еще один способ объединения изменений из разных веток. Команда git rebase.

5. Интерактивный git rebase. Редактирование коммитов любой давности.

6. Берем вишенку с торта. Команда git cherry-pick.

Перейти
2
Практический блок
1. Задание
2. Решение
Перейти

ТЕОРЕТИЧЕСКИЙ БЛОК

Сегодня мы познакомимся с очень важным для работы в команде функционалом: слиянием веток, ребейзом коммитов и cherry-pick`ом. Также мы узнаем, чем слияние отличается от rebase и cherry-pick, и в каком случае какую команду использовать.
1

Что такое слияние и зачем оно нужно

Перед началом урока давайте освежим в памяти информацию из предыдущих:
  1. Ветка – независимая последовательность коммитов. Ветки нужны, чтобы тестировать новые функции и распараллеливать работу над проектом.
  2. Первоначально мы работаем в основной ветке. У нас она называется main.
Обычно в основной ветке находится та же версия кода, что и в продакшене. То есть если в основную ветку попадут какие-то непроверенные изменения, код может сломаться и повлечь за собой поломку приложения у конечного пользователя. Поэтому во всех командах, работающих над серьезными проектами, принято, что в основной ветке находятся только протестированные изменения, которые в идеале не придется экстренно исправлять.

То есть общий ход нашей работы выглядит следующим образом:
1. Решили добавить новую функцию – создали отдельную ветку. Дальше работаем в новой ветке.
2. Написали функцию, протестировали ее работу, внесли все необходимые исправления, еще раз протестировали и убедились, что функция работает исправно и не привнесла ошибок в остальной код.
3. Теперь нужно как-то перенести изменения с тестовой ветки на основную – в продакшн. Тут нам на помощь и приходит слияние: мы просто сливаем (т.е. переносим) изменения с нашей тестовой ветки в основную.

Итак, дадим определения:
Сливаемая ветка – та ветка, с которой мы берем изменения, чтобы влить их в целевую.
Целевая ветка – та ветка, в которую мы сливаем наши изменения.
Слияние веток – это перенос изменений с одной ветки на другую. При этом слияние не затрагивает сливаемую ветку, то есть она остается в том же состоянии, что позволяет нам потом продолжить работу с ней.
Теперь давайте перейдем к тому, как слияние реализовано в Git.
2

Слияние в Git. Команда git merge.

Чтобы выполнить мердж (от англ. merge – слияние), в Git предусмотрена команда git merge.

Команда git merge

Формат
git merge <сливаемая ветка>
Ключи
--ff , --no-ff и --ff-only
Эти ключи определяют стратегию слияния. Подробнее о стратегиях мы поговорим чуть ниже, а пока перечислим назначения каждого из ключей.
--ff – включить fast-forward, если это возможно,
--no-ff отключить fast-forward,
а --ff-only – остановить merge, если его невозможно сделать fast-forward.

--abort
Ключ, использующийся только при разрешении конфликтов. Позволяет прервать слияние и вернуть все к моменту начала операции.

--continue
Ключ, использующийся только при разрешении конфликтов. Позволяет продолжить слияние после разрешения всех конфликтов.
Что делает
Сливает изменения с переданной ветки в текущую.
Пример
# Переключимся на основную ветку
$ git checkout main
# Сольем изменения с ветки develop в ветку main
$ git merge develop
Обновление d078c8d..4a2f9b1
Fast-forward
main.py | 33 +++++++++++++++++++++++++++++++++
README.md | 23 +++++++++++++++++------
bot.py | 6 ++++++
3 files changed, 56 insertions(+), 6 deletions(-)
create mode 100644 main.py
create mode 100644 bot.py

Теперь, когда мы узнали, как выполнить простейшее слияние в Git, можно поговорить о нем подробнее. Давайте разберем стратегии слияния.
Стратегия слияния – это набор правил, которыми руководствуется Git при выполнении слияния.
Существует две основных стратегии слияния:
  1. Явное слияние
  2. Неявное слияние.
Их различие заключается в том, что при явном всегда создается новый коммит, а при неявном – используются существующие коммиты.

2. 1. Явное слияние

Во время явного слияния создается так называемый merge-коммит. Основное предназначение этого коммита состоит в том, чтобы "соединить" изменения двух веток. У этого коммита есть одна особенность: два родительских коммита. Один родитель – последний коммит сливаемой ветки, второй – последний коммит целевой ветки.

Допустим, у нас есть граф вида:
Граф Git перед слиянием
Граф Git перед слиянием
То есть у нас есть две ветки: main – основная и develop – ветка для разработки новых функций. Давайте посмотрим, что будет происходить, если мы выполним команду:
Git Bash

$ git checkout main
$ git merge --no-ff develop
Флаг --no-ff в данной ситуации необходим, поскольку мы хотим выполнить именно явное слияние. Подробнее о смысле этого флага мы поговорим чуть позже.

Итак, git merge делает следующие шаги:
  1. Проверяет, нет ли конфликтов, т.е. не удалят и не перепишут ли наши изменения какую-либо уже существующую информацию. Если возникает конфликт git merge останавливается, чтобы получить инструкции от пользователя, но этот случай мы рассмотрим ниже. А пока допустим, что конфликтов нет.
  2. Добавляет все изменения из коммитов 3-5 в индекс ветки main
  3. Делает коммит

После git merge граф репозитория будет выглядеть следующим образом:
 Граф Git после явного слияния
Граф Git после явного слияния
Полезно знать
Заметим, что в данном случае (то есть если не возникло конфликтов слияния), git merge эквивалентен командам git checkout develop * и git commit -a "Merge commit "– то есть копированию всех файлов с ветки develop в рабочую копию текущей ветки и последующему созданию коммита.

Кстати, коммит Merge commit действительно имеет двух родителей: Commit-5 с ветки develop и Commit-2 c ветки main. Это усложнит откат этого коммита, поэтому будьте предельно внимательны, выполняя git merge.

2.2. Неявное слияние

Во время неявного слияния не создается новых коммитов: используются только уже существующие. Суть этого слияния заключается в том, что из вливаемой ветки извлекаются несколько коммитов, а затем они применяются к последнему коммиту целевой ветки. Такое слияние называется fast-forward.

Давайте рассмотрим пример. Допустим, у нас есть все тот же граф репозитория:
Граф Git перед слиянием
Граф Git перед слиянием
Попробуем выполнить слияние, но уже без флага --no-ff:
Git Bash

$ git checkout main
$ git merge develop
Тогда git merge поступит так:
  1. Проверит, что в ветке main нет коммитов, сделанных после ответвления develop.
  2. Проверит, что не возникает конфликтов, если конфликты возникнут, Git попросит пользователя разрешить их.
  3. Перенесет указатель main на Commit-5. Теперь ветка develop как бы стала веткой main.

Графически ситуация выглядит следующим образом:
Граф Git после fast-forward слияния
Граф Git после fast-forward слияния
Как видно из рисунка, новый коммит действительно не был создан. Вместо него, Git "подставил" в ветку main уже существующие коммиты из ветки develop.

Стоит подробнее разобрать первый пункт в работе git merge. В нем говорится, что Git проверит, что в ветке main нет коммитов, после ответвления develop. Дело в том, что режим fast-forward возможен не всегда, например в случае такого репозитория:
Граф Git для которого невозможно выполнить fast-forward слияние
Граф Git для которого невозможно выполнить fast-forward слияние
Слияние в режиме fast-forward выполнить будет невозможно, поскольку в таком случае мы потеряем всю информацию о Коммите-6. Не будет активных ссылок, указывающих на этот коммит, или одного из его наследников: последующих коммитов, для которых Коммит-6 стал родителем. Поэтому в данном случае придется выполнять явный git merge с созданием merge-коммита.
Полезно знать
Кстати, по умолчанию git пытается выполнить слияние именно в режиме fast-forward. Поэтому когда мы разбирали явное слияние, нужно было указать флаг --no-ff: без него Git выполнил бы merge в режиме fast-forward.

Подытожим: В чем разница между fast-forward и явным слиянием?
Режим fast-forward считается более удобным, поскольку в нем не нужно создавать лишних merge-коммитов, засоряющих историю репозитория. С другой стороны, если мы продолжим пользоваться веткой develop после fast-forward слияния, потом будет довольно трудно разобраться в ее истории. Так что каждый раз выполняя слияние, задумайтесь, хотите ли вы, чтобы оно прошло в режиме fast-forward, или для вас лучше явно создать merge-коммит, собирающий все воедино.
3

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

Очень часто во время слияния веток оказывается, что ваши изменения удаляют или переписывают информацию в уже существующих файлах. Такая ситуация называется файловым конфликтом. Git останавливает выполнение слияния, пока вы не разрешите конфликт.

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

Давайте сразу начнем с примера и по ходу будем учиться разрешению конфликтов. Так будет нагляднее. Итак, допустим есть следующий репозиторий:
 Граф Git-репозитория, в котором мы будем выполнять слияние
Граф Git-репозитория, в котором мы будем выполнять слияние
Чтобы не перегружать себя лишней информацией, будем рассматривать всего один файл: Docs.md. Содержимое этого файла в различных коммитах приведено на рисунке, но для удобства продублируем его:
Содержимое Docs.md, C2

- This is documentation

Содержимое Docs.md, C6

- This is documentation
- It contains lots of info
Содержимое Docs.md, C5

- This is documentation
- New feature info
- It has lots of info

Наша задача состоит в том, чтобы слить ветку develop в ветку main. Давайте попробуем сделать это.
Git Bash

$ git merge develop
Auto-merging Docs.md
CONFLICT (content): Merge conflict in Docs.md
Automatic merge failed; fix conflicts and then commit the result.
Вот так Git и сообщает нам о конфликте: в файле Docs.md из коммита C5 вторая строка переписывает вторую строку фалйа Docs.md из коммита C6. Таким образом Git просит нас разобраться, какой файл оставлять. Давайте научимся делать это.

Общий подход к разрешению конфликтов такой:
  1. Непосредственно разрешить конфликт одним из двух рассмотренных немного ниже способов. Либо, если возникновение конфликта стало неожиданным для вас, можно выполнить git merge --abort. Эта команда прервет слияние и вернет все, как было.
  2. Сообщить Git, что мы разрешили конфликт, добавив все файлы с разрешенными конфликтами в индекс. Сделать это можно уже знакомой командой git add <конфликтный файл> для каждого конфликтного файла.
  3. Продолжить слияние, выполнив git merge --continue. Либо вручную создать merge-коммит уже знакомой командой git commit.

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

Разберем каждый из них по очереди.

3.1. Ручное разрешение конфликта

Для этого в любом текстовом редакторе откройте конфликтный файл (если файлов несколько, конфликт нужно устранять в каждом). Приведем содержимое файла Docs.md.
Содержимое Docs.md

- This is documentation
<<<<<<< HEAD
- It contains lots of info
=======
- New feature info
- It has lots of info
>>>>>>> develop

Видим, что Git оставил нам пометки, чтобы нам было проще устранять конфликт:
  1. Текст до <<<<<<< HEAD – это общая часть двух файлов, она не конфликтует. В нашем случае оба файла имеют одинаковую первую строку: - This is documentation
  2. Текст между <<<<<<< HEAD и ======= – это конфликтующее содержимое файла, на который указывает HEAD, то есть файла из целевой ветки. В нашем случае это вторая строка, именно она переписывается изменениями из ветки develop.
  3. Все, что находится между ======= и >>>>>>> develop – это содержимое файла из ветки develop. В нашем случае это вторая и третья строки: - New feature info и - It has lots of info.
Файл Docs.md в состоянии конфликта
Файл Docs.md в состоянии конфликта
Наша задача – объяснить Git, каким мы хотим видеть файл Docs.md. Для этого нам нужно вручную отредактировать файл Docs.md. Нам не обязательно выбирать один из двух приведенных вариантов – в этом вся прелесть ручного редактирования. Мы можем удалить вообще весь текст из файла, оставить часть первого файла и часть второго или вообще написать что-то свое. Не забудьте удалить строки, которые оставил Git, то есть <<<<<<< HEAD, ======= и >>>>>>>develop, сами собой они не пропадут. В качестве примера, мы отредактировали конфликтный файл таким образом:
Содержимое docs.md

- This is documentation
- It contains lots of info
- New feature info
Теперь рассмотрим второй способ.

3.2. Выбор одного из двух файлов

Если вы точно знаете, что вам нужно оставить только один из двух конфликтных файлов (вся информация из другого файла при этом потеряется), можно сказать об этом Git:
  1. Выполните git checkout --ours Docs.md, чтобы выбрать файл ветки main (то есть целевой ветки)
  2. Либо git checkout --their Docs.md, чтобы выбрать файл из ветки develop (то есть сливаемой ветки).
Эта команда скопирует в docs.md из рабочей копии содержимое одного из конфликтных файлов. То есть Git полностью заменит файл в рабочей копии выбранным вами файлом.
Git Bash

# Разрешим конфликт выбором файла из коммита, к которому мы откатываемся.
$ git checkout --their Docs.md
Updated 1 path from the index
Поздравляю, теперь конфликт разрешен! Кстати, если сейчас выполнить команду git status, нас ждет необычный вывод.
Git Bash

$ git status
On branch main
You have unmerged paths.
  (fix conflicts and run "git commit")
  (use "git merge --abort" to abort the merge)

Changes to be committed:
	new file:   new_feature.py

Unmerged paths:
  (use "git add <file>..." to mark resolution)
	both modified:   Docs.md
Как видим, Git добавил в индекс файл new_feature.py с ветки develop, поскольку этот файл не вызывал конфликтов. И вместе с тем пометил, что файл Doc s.md находится среди Unmerged paths, то есть Неслитых путей.

Конфликт разрешен, теперь нам осталось только сообщить о разрешении конфликта Git. Сделать это можно командой git add <filename> или git add -A, чтобы добавить в индекс сразу все файлы, конфликты которых разрешены. После этого следует сообщить Git, что мы можем продолжить слияние.
Git Bash

# Добавим наш файл в индекс.
$ git add Docs.md

# Ради интереса посмотрим статус
$ git status
On branch main
All conflicts fixed but you are still merging.
  (use "git commit" to conclude merge)

Changes to be committed:
	modified:   Docs.md
	new file:   new_feature.py
Видим, что Git сообщает нам, что все конфликты разрешены. Теперь можно либо вручную выполнить git commit, либо написать git merge --continue: разницы нет.

Дальше все стандартно: откроется редактор сообщения коммита. В сообщении следует указать, какие ветки вы сливали, и вкратце перечислить внесенные изменения.
Git Bash

# Продолжим слияние.
$ git merge --continue
[main a219fa1] L-04: Merge branch 'develop'
Теперь слияние полностью завершено.

3.3. Тонкости разрешения merge-конфликтов.

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

Допустим, вам дали репозиторий и поставили перед вами задачу: влить ветку develop в ветку main. Ну хорошо, это мы умеем делать, давайте выполним слияние.
Git Bash

$ git merge develop
Auto-merging colors.txt
CONFLICT (content): Merge conflict in colors.txt
Automatic merge failed; fix conflicts and then commit the result.
Вот мы и столкнулись с конфликтом. Давайте посмотрим на конфликтующий файл в любом текстовом редакторе.
Содержимое файла colors.txt

<<<<<<< HEAD
This color is red,
And this one is blue.
Check out the base
To make conflict solved by you!
=======
this color is (255, 36, 0)
and this one is (0, 191, 255)
check out the base
to make conflict solved by you
>>>>>>> develop
Видно, что файлы различаются, но из этого представления совсем непонятно, что именно должно войти в итоговый файл. Если бы это был ваш личный проект, вы бы прекрасно знали, какой из файлов выбрать, но это чужой репозиторий и цена ошибки велика. Конечно, можно потратить много времени, перебирая коммиты из обеих веток, разбираясь, как менялись файлы, чтобы понять, что должно быть в итоговом файле. Но можно поступить проще. Прежде всего, давайте изучим немного теории.

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

Эти состояния можно изобразить на диаграмме следующим образом.
Четыре состояния слияния
Четыре состояния слияния
Чтобы понять, что изображено на диаграмме, полезно будет вспомнить, что такое дельта.
Дельта – это разность каких-то двух состояний. То есть по сути это информация о том, какие изменения вы внесли в файлы с момента определенного коммита. Как мы помним из второго урока данного курса, коммиты хранят именно дельты, а не полностью файлы. Благодаря этому репозиторий Git занимает очень мало места.

На диаграмме выше Дельта-1 – это разность текущего состояния ветки main и базы слияния. Аналогично Дельта-2 – это разность текущего состояния ветки develop и базы слияния.

Перейдем к самим состояниям. В выводе файла выше нам были показаны только два состояния. На диаграмме они отмечены белым: это текущее состояние ветки main и текущее состояние ветки develop. То есть все, что находится между <<<<<<< HEAD и ======= – текущее состояние конфликтного файла в ветке main, и, соответственно, текст между ======= и >>>>>>> develop – состояние конфликтного файла в ветке develop. На диаграмме видно еще два состояния – базу слияния и результат слияния. Разберемся, что это за состояния.

Результат слияния нам как раз и нужно получить. Git пытается получить его автоматически, совмещая Дельту-1 и Дельту-2, но если эти дельты задевают одни и те же части одного и того же файла, возникает конфликт. При возникновении конфликта Git просит нас сравнить Дельту-1 и Дельту-2, чтобы составить из них третье состояние – результат слияния.

База слияния – это последний общий родитель конфликтных файлов. Говоря проще, это тот файл, применив к которому изменения из Дельты-1, мы получим наш файл в ветке develop, а применив изменения из Дельты-2, мы получим наш файл в ветке main. Именно база слияния является для нас самым важным состоянием при разрешении конфликта. Мы можем просмотреть ее, и это сильно поможет нам в понимании, какой из файлов нужно оставить.

Итак, чтобы просмотреть базу слияния, нам нужно поменять настройки Git, указав, что мы хотим, чтобы он выводил базу слияния при файловом конфликте. Сделать это можно уже знакомой командой git config.
    Git Bash
    
    $ git config --global merge.conflictstyle diff3
    
    
    Попробуем прервать слияние и снова выполнить его, чтобы Git составил для нас уже новый файл, в котором он отобразит базу слияния.
    Git Bash
    
    $ git merge --abort
    $ git merge develop
    
    А теперь посмотрим на содержимое файла colors.txt.
    Содержимое файла colors.txt
    
    <<<<<<< HEAD
    This color is red,
    And this one is blue.
    Check out the base
    To make conflict solved by you!
    ||||||| 8b5b982
    this color is red
    and this one is blue
    check out the base
    to make conflict solved by you
    =======
    this color is (255, 36, 0)
    and this one is (0, 191, 255)
    check out the base
    to make conflict solved by you
    >>>>>>> develop
    
    Видно, что появился новый текст. Все, что находится между ||||||| 8b5b982 и ======= – и есть наша база слияния. Иначе говоря, это содержимое файла из общего родителя двух последних коммитов в ветках main и develop. Кстати, 16-ричное число в строке ||||||| 8b5b982 – это хэш этого родителя, так что при необходимости можно посмотреть на него в логе репозитория.

    Теперь изменения в двух файлах нам понятны. В файл в ветке main добавили пунктуационные знаки и заглавные буквы, а в файле в ветке develop названия цветов заменили на их RGB-коды. Теперь мы понимаем, что в итоговом файле должны быть заглавные буквы, пунктуационные знаки и RGB-коды вместо привычных цветов. Итоговый файл выглядит так:
    Содержимое файла colors.txt
    
    This color is (255, 36, 0),
    And this one is (0, 191, 255).
    Check out the base
    To make conflict solved by you!
    
    Просмотр базы слияния очень часто помогает понять, как менялись файлы, и как правильно разрешать конфликты в больших репозиториях.

    3.4. Инструменты для разрешения merge-конфликтов.

    Существует множество графических инструментов для разрешения файловых конфликтов. Сегодня мы познакомимся с некоторыми. Итак, чтобы запустить графический инструмент для разрешения конфликтов, выполните
    Git Bash
    
    $ git mergetool
    
    
    При запуске программа спросит вас, какой утилитой вы бы хотели воспользоваться. Доступные варианты: meld, opendiff, kdiff3, tkdiff, xxdiff, tortoisemerge, gvimdiff, diffuse, diffmerge, ecmerge, p4merge, araxis, bc, codecompare, smerge, emerge, vimdiff, nvimdif. У каждой есть свои плюсы и минусы, а в интернете есть множество сравнений этих утилит. Так что оставляем выбор на ваш вкус. После выбора утилиты, откроется она сама. В ней вы сможете просмотреть подсвеченные изменения в конфликтных файлах в удобном формате, а затем внести необходимые изменения, чтобы разрешить конфликт.

    Использование графических утилит особенно удобно при сравнении больших файлов: почти во всех есть подсветка внесенных изменений, что поможет вам не запутаться в файле. Тем не менее при сравнении маленьких файлов часто можно обойтись одной консолью. Кстати, большинство IDE имеет встроенную утилиту для разрешения конфликтов, что сделает вашу работу с кодом еще более удобной.
    Подведем итог
    1. git merge – команда, выполняющая слияние веток.
    2. Иногда во время слияния возникают конфликты, в этом случае действовать нужно так:
    2.1. Разрешить конфликт.
    2.2. После устранения конфликта нужно добавить изменения в индекс командой git add <имя файла>.
    2.3. Выполнить git merge --continue, чтобы сообщить Git, что можно продолжать слияние, либо просто сделать коммит самостоятельно.
    2.4. Не забывать, что в любой момент можно выполнить git merge --abort. Эта команда остановит слияние и вернет все, как было.
    3. Разрешить конфликт можно двумя способами:
    3.1. Выбрать файл с целевой ветки или со сливаемой ветки, выполнив, соответственно, git checkout --ours <имя файла> или git checkout --theirs <имя файла>
    3.2. Открыть сам файл и отредактировать его вручную, записав туда что угодно.
    4. В сообщении merge-коммита следует указывать полезную информацию: какие ветки вы слили, по какому принципу вы сливали файлы, если были конфликты, какие изменения внесли и т.д.
    5. В случае, если вы работаете с большим репозиторием, коммиты в который делают много разработчиков, при возникновении конфликтов очень полезно будет посмотреть на базу слияния. Она поможет разобраться, как должен выглядеть слитый файл.
    4

    Еще один способ объединения изменений из разных веток. Команда git rebase.

    Итак, давайте познакомимся с командой git rebase. Она позволит нам не только объединять изменения с разных веток, но и менять историю репозитория, манипулируя коммитами.

    Если говорить кратко, git rebase переносит коммиты текущей ветки на вершину переданной. Но перед тем, как перейти непосредственно к команде, давайте разберем принцип ее действия на примере. Пусть у нас есть репозиторий со следующим графом.
      Граф репозитория
      Граф репозитория
      Если бы мы хотели выполнить слияние ветки develop в ветку main прямо сейчас, мы бы не смогли сделать его в режиме fast-forward. И нам бы пришлось создавать merge-коммит, засоряющий историю репозитория. На помощь приходит git rebase.

      Если вы примените команду git rebase main, находясь на ветке develop, репозиторий примет следующий вид.
      Граф репозитория после ребазирования
      Граф репозитория после ребазирования
      То есть команда git rebase перенесла коммиты ветки develop так, чтобы ветка develop брала свое начало на последнем коммите main. Попросту говоря, она перенесла коммиты ветки develop на вершину ветки main. Такое состояние очень выгодно нам тем, что в нем уже можно сделать fast-forward слияние. Таким образом, мы избавились от необходимости создания merge-коммита.
      Важно
      Заметьте, что коммиты D-new, E-new и F-new – это не коммиты D, E и F – их хеш-суммы отличаются, но дельты, которые эти коммиты несут в себе, в идеале должны быть одинаковыми.

      Теперь, когда мы получили интуитивное представление о работе команды git rebase, давайте рассмотрим ее синтаксис подробнее.

      Команда git rebase

      Формат
      git rebase <целевая ветка>
      Ключи
      -i
      --interative

      Эти ключи позволяют нам делать rebase в интерактивном режиме. Мы будем активно пользоваться ими при редактировании старых коммитов.

      --abort
      Ключ, использующийся только при разрешении конфликтов. Позволяет прервать ребейз и вернуть все к моменту до начала операции.

      --continue
      Ключ, использующийся только при разрешении конфликтов. Позволяет продолжить ребейз после разрешения всех конфликтов.

      --skip
      Ключ, использующийся только при разрешении конфликтов. Позволяет пропустить текущий коммит.
      Что делает
      Перемещает все коммиты: от общего коммита двух веток до последнего коммита текущей ветки на вершину переданной ветки.
      Пример
      # Переключимся на ветку для разработки
      $ git checkout develop
      # Перенесем коммиты ветки develop на верхушку ветку main
      $ git rebase main

      Теперь, когда мы познакомились с синтаксисом команды, давайте разберем ее работу поэтапно.
      Допустим, есть репозиторий с такой структурой:
       Граф репозитория
      Граф репозитория
      Давайте посмотрим, что происходит при выполнении команды
      Git Bash
      
      $ git rebase main
      
      
      Git rebase выполняет следующие действия:
      1. Первым делом, Git находит общий коммит двух веток. В нашем случае это коммит B.
      2. После этого Git начинает двигаться от коммита B к положению указателя HEAD, и делает следующее для каждой пары коммитов.
      2.1. Вычисляет дельту между B и D: delta_bd = D - B
      2.2. Пытается применить дельту к последнему коммиту ветки main. Так получается коммит D-new. Т.е. D-new = C + delta_bd. Если при этом возникает конфликт, Git останавливается, пока мы не разрешим конфликт.
      2.3. Вычисляет дельту между D и E: delta_de = E - D
      2.4. Применяет эту дельту к коммиту D-new и получает коммит E-new = D-new + delta_de
      2.5. Вычисляет дельту между коммитами E и F: delta_ef = F - E
      2.6. Применяет вычисленную дельту к коммиту E-new и получает коммит F-new = E-new + delta_ef
      3. Переносит указатель ветки develop на коммит F-new.
      4. Переносит HEAD на указатель ветки develop.
      5. Создает указатель ORIG_HEAD на коммите F.

      После выполнения всех вышеуказанных операций, граф репозитория будет выглядеть так.
      Граф репозитория после git rebase
      Граф репозитория после git rebase
      Как видно из схемы действий, HEAD переносится в самом конце, поэтому если во время выполнения одного из шагов по применению дельты возникнет конфликт, вы обнаружите, что находитесь в состоянии detached head. В это время в вашей рабочей копии будут все изменения дельты, кроме конфликтных. Не стоит пугаться, просто разрешите конфликты, добавьте изменения в индекс и продолжите ребейз командой:
      Git Bash
      
      $ git rebase --continue
      
      
      Иногда во время выполнения git rebase возможна следующая ситуация. Изменения, например, в коммите D полностью исключают изменения в коммите C. Причем вам нужны изменения именно из коммита C. В таком случае, после разрешения конфликта, вы обнаружите, что изменений в рабочей копии нет. Вы просто удалили все изменения коммита D, оставив только изменения коммита C. В конечном итоге, вы не добавили в рабочую копию ничего нового. В такой ситуации стоит просто пропустить коммит D, то есть не создавать коммит D-new, выполнив
      Git Bash
      
       git rebase --skip 
      
      
      во время возникновения конфликта. Также не стоит забывать, что как и с командой git merge, вы в любой момент можете отменить ребейз, выполнив
      Git Bash
      
      git rebase --abort
      
      
      Эта команда вернет все к состоянию до выполнения git rebase.

      Если же вы уже выполнили ребейз, но по какой-то причине решили все вернуть обратно, не стоит переживать - это легко сделать. Git оставил нам указатель ORIG_HEAD на последнем коммите первоначальной ветки develop. Давайте этим и воспользуемся. Итак, чтобы отменить git rebase уже после его завершения, просто выполните:
      Git Bash
      
      git reset --hard ORIG_HEAD
      
      
      находясь на ветке develop. Как вы помните из прошлого урока, эта команда перенесет наш указатель ветки вместе с HEAD на место указателя ORIG_HEAD, то есть ветка develop начнет указывать на коммит F как и раньше. По сути это и есть отмена действий git rebase.
      5

      Интерактивный git rebase.
      Редактирование коммитов любой давности.

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

      На самом деле git rebase можно выполнять для одной и той же ветки. В обычном режиме это нам ничего не даст, но вот в интерактивном режиме мы сможем поменять сообщения, содержимое и вообще манипулировать коммитами, как нам только вздумается, вплоть до удаления. Давайте на примере разберем, как происходит изменение коммитов. Итак, допустим у нас есть репозиторий, граф которого выглядит так:
      Граф репозитория
      Граф репозитория
      Допустим, нам нужно поменять сообщение коммита C. Как это сделать? Давайте попробуем взять все коммиты, начиная с коммита С и заканчивая коммитом Е, и перенести их на вершину той же ветки. Если бы мы не меняли сообщение коммита С, то такая манипуляция ничего не изменила бы. В этом и кроется небольшая хитрость: если мы поменяем коммит С, поменяется его хеш-сумма, и, соответственно, нам придется менять все последующие коммиты. А теперь вспомним, как работает rebase. На каждом шаге она заново вычисляет дельту и создает новый коммит на основе старого. Именно то, что нам нужно. Давайте поменяем сообщение коммита С. Для этого выполним:
      Git Bash
      
      git rebase -i HEAD~3
      
      
      Важно
      Заметьте, мы указываем все коммиты не с коммита C, а с коммита B. Если бы мы указали коммиты, начиная с коммита C, git rebase определил бы коммит C, как общий коммит ветки main и нашей подвыборки коммитов, а значит сам коммит C меняться бы не стал. Поэтому нужно указать коммит за один до того, который мы хотим изменить.

      Итак, когда мы выполним указанную команду, перед нами в консольном текстовом редакторе откроется примерно такой экран:
      Git Bash
      
       GNU nano 4.8                                  /home/smartiqa/Desktop/test/.git/rebase-merge/git-rebase-todo                                            
      pick f831285 C
      pick c06b382 D
      pick 0874316 E
      
      # Rebase f831285..0874316 onto 2a50e6e (3 commands)
      #
      # Commands:
      # p, pick <commit> = use commit
      # r, reword <commit> = use commit, but edit the commit message
      # e, edit <commit> = use commit, but stop for amending
      # s, squash <commit> = use commit, but meld into previous commit
      # f, fixup <commit> = like "squash", but discard this commit's log message
      # x, exec <command> = run command (the rest of the line) using shell
      # b, break = stop here (continue rebase later with 'git rebase --continue')
      # d, drop <commit> = remove commit
      # l, label <label> = label current HEAD with a name
      # t, reset <label> = reset HEAD to a label
      # m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
      # .       create a merge commit using the original merge commit's
      # .       message (or the oneline, if no original merge commit was
      # .       specified). Use -c <commit> to reword the commit message.
      #
      # These lines can be re-ordered; they are executed from top to bottom.
      #
      # If you remove a line here THAT COMMIT WILL BE LOST.
      #
      # However, if you remove everything, the rebase will be aborted.
      #
      
      Это так называемый TO-DO файл git rebase. В нем мы указываем, как хотим манипулировать коммитами. Чтобы что-то сделать с коммитом, нужно слово pick перед хешем соответствующего коммита заменить на одну из команд. Либо, если команда это позволяет, вставить ее между коммитами. Вы также можете менять порядок инструкций. Данный файл с инструкциями будет выполняться сверху вниз.

      Все доступные команды можно посмотреть в документации, мы же разберем только те, которые необходимы, чтобы изменять коммиты:
      1. p, pick <коммит> – просто использовать коммит, ничего не менять
      2. r, reword <коммит> – использовать коммит, но поменять его сообщение
      3. e, edit <коммит> – использовать коммит, но остановить ребейз, чтобы добавить в коммит больше файлов
      4. s, squash <коммит> – использовать коммит, объединив его с предыдущим
      5. f, fixup <коммит> – как squash, но удаляет информацию об объединенном коммите из истории.

      В нашем случае, если мы хотим поменять сообщение коммита С, нужно отредактировать файл следующим образом:
      Git Bash
      
      reword f831285 C
      pick c06b382 D
      pick 0874316 E
      
      # Rebase f831285..0874316 onto 2a50e6e (3 commands)
      #
      # Commands:
      # p, pick <commit> = use commit
      # r, reword <commit> = use commit, but edit the commit message
      # e, edit <commit> = use commit, but stop for amending
      # s, squash <commit> = use commit, but meld into previous commit
      # f, fixup <commit> = like "squash", but discard this commit's log message
      # x, exec <command> = run command (the rest of the line) using shell
      # b, break = stop here (continue rebase later with 'git rebase --continue')
      # d, drop <commit> = remove commit
      # l, label <label> = label current HEAD with a name
      # t, reset <label> = reset HEAD to a label
      # m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
      # .       create a merge commit using the original merge commit's
      # .       message (or the oneline, if no original merge commit was
      # .       specified). Use -c <commit> to reword the commit message.
      #
      # These lines can be re-ordered; they are executed from top to bottom.
      #
      # If you remove a line here THAT COMMIT WILL BE LOST.
      #
      # However, if you remove everything, the rebase will be aborted.
      
      То есть мы заменили команду pick на reword напротив коммита С. Сохраняем файл и выходим обратно в консоль. Перед нами сразу же откроется редактор сообщения коммита С. Вводим новое сообщение, сохраняем файл и выходим из редактора. Если мы теперь просмотрим историю, то обнаружим, что сообщение коммита С действительно поменялось, а вместе с ним поменялись и хэши последующих коммитов. Аналогичным образом можно объединять и удалять любые коммиты, а также добавлять в них новые файлы.

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

      В случае, если вы случайно удалили коммит, можно пойти двумя путями:
      1. Если вы все еще выполняете ребейз, следует выполнить git rebase --abort. Эта команда вернет все к первоначальному состоянию.
      2. Если ребейз уже закончен, переместите указатель ветки на вершину исходной последовательности коммитов, выполнив git reset --hard ORIG_HEAD
      Как вы могли убедиться, git rebase – очень мощный инструмент, позволяющий манипулировать историей нашего репозитория. Конечно не стоит забывать, что с большой силой приходит и большая ответственность: при неправильном использовании можно потерять коммиты, доступ к которым потом будет трудно восстановить (но, конечно, не невозможно).
      Подведем итог
      1. git rebase – команда, перемещающая все коммиты: от общего коммита двух веток до последнего коммита текущей ветки на вершину переданной ветки.
      2. Такое перемещение может быть необходимо, чтобы стало возможным слияние в fast-forward режиме.
      3. Помимо обычного git rebase, существует интерактивный режим этой команды: git rebase -i. В основном им пользуются, чтобы отредактировать, объединить или удалить старые коммиты. Стоит быть осторожным с этой командой, особенно работая в чужих репозиториях: изменение уже записанных в истории коммитов в некоторых случаях может повлечь непредвиденные последствия, поэтому хорошо подумайте, прежде чем выполнять ребейз.

      6

      Берем вишенку с торта. Команда git cherry-pick

      Последняя на сегодня команда тоже связана с манипуляцией коммитами в различных ветках. Дело в том, что иногда возникает ситуация, когда изменения внесенные другим разработчиком в своей ветке нужны вам в вашей ветке прямо сейчас. Конечно, можно сделать merge, но это только засорит историю и сломает логику репозитория: зачем делать слияние, если ветка другого разработчика еще к нему не готова. В этом случае нам на помощь придет команда git cherry-pick. Давайте разберемся, как ей пользоваться, а затем подробнее разберем, в каких именно случаях лучше всего применять cherry-pick, а в каких merge.

      Команда git cherry-pick

      Формат
      git cherry-pick <хеш коммита>
      git cherry-pick <хеш первого коммита> … <хеш последнего коммита>
      Ключи
      -e
      --edit

      С этим ключом вы сможете отредактировать сообщение коммита.

      -n
      --no-commit

      С этим ключом команда не создаст коммит на вашей ветке, а только скопирует все изменения в вашу рабочую копию. То есть с этим ключом данная команда идентичная git checkout <коммит> *

      --abort
      Ключ, использующийся только при разрешении конфликтов. Позволяет прервать операцию и вернуть все к моменту до начала операции.

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

      Также в команду можно передать первый и последний коммит последовательности, тогда та же операция будет выполнена для всех коммитов последовательности.
      Пример
      # Находясь на основной ветке
      $ git cherry-pick 48efdd9
      [main 6905e02] Add main.py
      Author: @Smartiqa <info@smartiqa.ru>
      Date: Sat Apr 10 20:42:22 2021 +0300
      1 file changed, 1 insertion(+)
      create mode 100644 main.py

      Никаких особых тонкостей или подводных камней данная команда не имеет. Это действительно мощный инструмент, который поможет вам в работе, однако не всегда хорошо использовать именно cherry-pick. Из-за чрезмерного использования этой команды у вас могут возникнуть коммиты-дубликаты, а история репозитория станет абсолютно нечитаемой. Поэтому нужно хорошо понимать, в каких случаях использовать cherry-pick, а в каких – merge. Всего можно выделить три случая, когда нужно использовать именно cherry-pick, а не merge. Давайте рассмотрим их по очереди.

      1. Случай первый. Работа в команде.
      Этот случай был описан во введении к данной части. Часто в команде несколько разработчиков работают над одним и тем же участком кода, но каждый – в своей ветке. Соответственно могут возникать ситуации, когда одному из разработчиков для реализации своей части задачи потребуется часть кода, написанная другим разработчиком. Merge в данном случае делать нерационально, поскольку ни одна из веток не пришла к своему логическому завершению, а вот cherry-pick – то, что надо.

      2. Случай второй. Быстрые исправления багов.
      Если в коде был обнаружен баг, очень важно как можно быстрее донести исправления до конечного пользователя. В таком случае, разработчик, обнаруживший ошибку, срочно создает коммит, в котором исправляет ее. Этот коммит может быть перенесен в основную ветку с помощью cherry-pick, чтобы не задерживать исправление бага слияниями в различные пре-релизные ветки.

      3. Случай третий. Восстановление утерянных коммитов.
      Иногда возникают ситуации, когда вы из-за каких-то манипуляций потеряли определенный коммит. Например, сделали git reset --hard, начали новую ветку, сделали несколько коммитов и только тут обнаружили пропажу. С помощью git log и git reflog вы можете узнать хеш утерянного коммита, а затем выполнить git cherry-pick, чтобы вернуть его обратно.

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

      ПРАКТИЧЕСКИЙ БЛОК

      1

      Задание. Библиотека Geometric Lib.


      И снова воспользуемся командой git clone (как и в прошлом уроке). Напомним, что она копирует удаленный репозиторий к вам на компьютер.

      Также напомним, что наш удаленный репозиторий представляет собой библиотеку на Python, которая позволяет высчитывать площади и периметры некоторых геометрических фигур.
      Ссылка на библиотеку Geometric Lib на GitHub: https://github.com/smartiqaorg/geometric_lib

      В этот раз в нашем репозитории появилась новая ветка release, теперь его структура выглядит следующим образом:
      Структура библиотеки geometric_lib
      
      # Ветка main (Основная стабильная ветка)
      geometric_lib
          ├── circle.py
          ├── square.py
          └── docs
               └── README.md
      
      # Ветка develop (Ветка разработки)
      geometric_lib
          ├── circle.py
          ├── square.py
          └── docs
               └── README.md
          └── calculate.py
      
      # Ветка feature (Ветка для новых функций)
      geometric_lib
          ├── circle.py
          ├── square.py
          └── docs
               └── README.md
          └── rectangle.py
      
      # Ветка release
      geometric_lib
          ├── circle.py
          ├── square.py
          └── docs
               └── README.md
          └── user_agreement.txt
      
      2

      Задание. Условие.

      1. Клонирование репозитория и знакомство с его структурой
      1.1. Выполните git clone https://github.com/smartiqaorg/geometric_lib. Эта команда создаст директорию geometric_lib/ на вашем компьютере и скопирует наш удаленный репозиторий. Не забудьте перейти в эту директорию командой cd geometric_lib, когда клонирование будет завершено.

      Кстати, когда вы склонируете к себе наш репозиторий, у вас будет только одна локальная ветка: main. Чтобы создать остальные, нужно выполнить git checkout <имя ветки>. Эта команда переключит вас на коммит, на который указывает удаленная ветка и создаст там локальную ветку с таким же именем. Эту команду нужно запустить для каждой ветки отдельно. То есть у вас получится два запуска: для ветки feature и ветки develop.

      1.2. Постройте полный граф истории, чтобы познакомиться со структурой комитов.
      2. Работа с веткой develop
      2.1. Влейте ветку develop в ветку main явным образом (с созданием merge-коммита).
      2.2. Удалите коммит слияния, чтобы затем выполнить слияние в fast-forward режиме.
      2.3. Влейте ветку develop в ветку main неявным образом (без создания merge-коммита - в режиме fast-forward).
      3. Работа с веткой release
      3.1. Выполните интерактивный ребейз ветки release на ветку main - объедините все коммиты в один и поменяйте их общее сообщение. Разрешите конфликты.
      3.2. Выполните fast-forward слияние ветки release в ветку main.
      3

      Задание. Решение.

      Решение - Git Bash
      
      # п 1. Клонируем репозиторий и активируем локальные ветки
      $ git clone https://github.com/smartiqaorg/geometric_lib.git
      $ cd geometric_lib/
      $ git checkout develop
      $ git checkout release
      $ git log --all --pretty=oneline --graph
      
      # п 2. Работа с веткой develop
      $ git checkout main
      $ git merge develop --no-ff
      $ git log --all --pretty=oneline --graph
      $ git reset --hard HEAD^
      $ git log --all --pretty=oneline --graph
      $ git merge develop --ff
      $ git log --all --pretty=oneline --graph
      
      # п 3. Работа с веткой release
      $ git config merge.conflictstyle diff3
      $ git config --global merge.tool meld
      $ git checkout release 
      $ git rebase -i main
      $ git mergetool
      $ git rebase --continue
      $ git mergetool
      $ git rebase --continue
      $ git log --all --pretty=oneline --graph
      $ git checkout main
      $ git merge release --ff
      $ git log --pretty=oneline --graph
      

      Более подробный разбор задания

      Также на странице Задания и Ответы по курсу Git мы даем более подробный разбор текущего задания.
      Приводим в нем не только команды Git, но и их консольный вывод, а также даем комментарии к каждой команде.
      Как вам материал?

      Читайте также