Git
Chapters ▾ 2nd Edition

3.6 Ветвление в Git - Перебазирование

Перебазирование

В GIT есть два способа внести изменения из одной ветки в другую: слияние (merge) и перебазирование (rebase). В этом разделе вы узнаете, что такое перебазирование, как его осуществлять, почему это удивительный инструмент и в каких случаях вам не следует его использовать.

Простейшее перебазирование

Если вы вернетесь к более раннему примеру из Основы слияния, вы увидите, что разделили свою работу и сделали коммиты в две разные ветки.

История коммитов простого разделения.
Рисунок 35. История коммитов простого разделения

Простейший способ выполнить слияние двух веток, как мы выяснили ранее, — это команда merge. Она осуществляет трехстороннее слияние между двумя последними снимками (snapshot) сливаемых веток (C3 и C4) и самого недавнего общего для этих веток родительского снимка (C2), создавая новый снимок (и коммит).

Слияние разделенной истории коммитов.
Рисунок 36. Слияние разделенной истории коммитов

Тем не менее, есть и другой способ: вы можете взять те изменения, что были представлены в C4 и применить их поверх C3. В Git это называется перебазированием (rebasing). С помощью команды rebase вы можете взять все изменения, которые были зафиксированы (commited) в одной ветке и применить их к другой ветке.

В данном примере для этого необходимо выполнить следующее:

$ git checkout experiment
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: added staged command

Это работает следующим образом: берется общий родительский снимок (snapshot) двух веток (той, в которой вы находитесь, и той, поверх которой вы выполняете перебазирование); берется дельта (diff) каждого коммита той ветки, на который вы находитесь, эти дельты сохраняются во временные файлы; текущая ветка устанавливается на тот же коммит, что и ветка, поверх которой вы выполняете перебазирование; и, наконец, ранее сохраненные дельты применяются поочереди.

Перебазирование изменений из `C4` поверх `C3`.
Рисунок 37. Перебазирование изменений из C4 поверх C3

На этом моменте вы можете переключиться обратно на ветку master и выполнить слияние перемоткой.

$ git checkout master
$ git merge experiment
Перемотка ветки master.
Рисунок 38. Перемотка ветки master

Теперь снимок (snapshot), на который указывает C4' абсолютно такой же, как тот, на который указывал C5 в примере с трехсторонним слиянием. Нет абсолютно никакой разницы в конечном результате между двумя показанными примерами, но перебазирование делает историю коммитов чище. Если вы взглянете на историю перебазированной ветки, то увидите, что она выглядит абсолютно линейной: будто все операции были выполнены последовательно, даже если изначально они совершались параллельно.

Часто вы будете делать так для уверенности, что ваши коммиты могут быть бесконфликтно слиты в удалённую ветку — возможно в проект, куда вы пытаетесь внести вклад, но владельцем которого вы не являетесь. В этом случае вам следует работать в своей ветке и затем перебазировать вашу работу поверх origin/master, когда вы будете готовы отправить свои изменения в основной проект. Тогда владельцу проекта не придется делать никакой лишней работы — все решится простой перемоткой (fast-forward) или бесконфликтным слиянием.

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

Более интересные перемещения

Также возможно сделать так, чтобы при перемещении воспроизведение коммитов начиналось не от той ветки, на которую делается перемещение. Возьмём, например, Историю разработки с тематической веткой, ответвлённой от другой тематической ветки. Вы создаете тематическую ветку (server), чтобы добавить в проект некоторые функциональности для серверной части, и делаете коммит. Затем вы выполнили ответвление, чтобы сделать изменения для клиентской части, и несколько раз выполнили коммиты. Наконец, вы вернулись на ветку server и сделали ещё несколько коммитов.

A history with a topic branch off another topic branch.
Рисунок 39. История разработки с тематической веткой, ответвлённой от другой тематической ветки

Предположим, вы решили, что хотите внести свои изменения для клиентской части в основную линию разработки для релиза, но при этом хотите оставить в стороне изменения для серверной части до полного тестирования. Вы можете взять изменения из ветки client, которых нет в server (C8 и C9), и применить их на ветке master при помощи опции --onto команды git rebase:

$ git rebase --onto master server client

Это прямое указание “переключиться на ветку client, то есть взять изменения от общего предка веток client и server и повторить их на master”. Несмотря на некоторую сложность этого способа, результат впечатляет.

Rebasing a topic branch off another topic branch.
Рисунок 40. Перемещение тематической ветки, ответвлённой от другой тематической ветки

Теперь вы можете выполнить перемотку (fast-forward) для ветки master (см Перемотка ветки master для добавления изменений из ветки client):

$ git checkout master
$ git merge client
Fast-forwarding your master branch to include the client branch changes.
Рисунок 41. Перемотка ветки master для добавления изменений из ветки client

Представим, что вы решили добавить наработки и из ветки server. Вы можете выполнить перемещение ветки server на ветку master без предварительного переключения на эту ветку при помощи команды git rebase [осн. ветка] [тем. ветка], которая делает тематическую ветку (в данном случае server) текущей и применяет её изменения к основной ветке (master):

$ git rebase master server

Эта команда поместит результаты работы в ветке server в начало ветки master, как это показано на Rebasing your server branch on top of your master branch.

Rebasing your server branch on top of your master branch.
Рисунок 42. Rebasing your server branch on top of your master branch

После чего вы сможете выполнить перемотку основной ветки (master):

$ git checkout master
$ git merge server

Теперь вы можете удалить ветки client и server, поскольку весь ваш прогресс уже включен [в основную линию разработки], и больше нет нужны сохранять эти ветки. Полную историю вашего рабочего процесса отражает рисунок Окончательная история коммитов:

$ git branch -d client
$ git branch -d server
Final commit history.
Рисунок 43. Окончательная история коммитов

Опасности перемещения

Но даже перебазирование, при всех своих достоинствах, не лишено недостатков, которые можно выразить одной строчкай:

Не перемещайте коммиты, уже отправленные в публичный репозиторий

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

Когда вы что-то перемещаете, вы отменяете существующие коммиты и создаёте новые, похожие на старые, но являющиеся другими. Если вы выкладываете (push) свои коммиты куда-нибудь, и другие забирают (pull) их себе и в дальнейшем основывают на них свою работу, а затем вы переделываете эти коммиты командой git rebase и выкладываете их снова, ваши коллеги будут вынуждены заново выполнять слияние для своих наработок. В итоге, когда вы в очередной раз попытаетесь включить их работу в свою, вы получите путаницу.

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

Clone a repository, and base some work on it.
Рисунок 44. Клонирование репозитория и выполнение в нем какой-то работы

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

Fetch more commits, and merge them into your work.
Рисунок 45. Извлекаем ещё коммиты и сливаем их со своей работой

Далее тот, кто выложил содержащий слияние коммит, решает вернуться и вместо слияния (merge) переместить (rebase) свою работу; он выполняет git push --force, чтобы переписать историю на сервере. Когда вы извлекаете изменения (fetch) с сервера, вы извлекаете эти новые коммиты.

Someone pushes rebased commits, abandoning commits you’ve based your work on.
Рисунок 46. Кто-то выложил перемещенные (rebase) коммиты, отменяя коммиты, на которых основывалась ваша работа

Теперь вы оба в неловком положении. Если вы выполните git pull, вы создадите коммит слияния, включающий обе линии истории, и ваш репозиторий будет выглядеть следующим образом:

You merge in the same work again into a new merge commit.
Рисунок 47. Вы снова выполняете слияние для той же самой работы в новый коммит слияния

Если вы запросите git log, пока ваша история выглядит таким образом, вы увидите два коммита с одинаковыми авторами, датой, и сообщением, что может сбить с толку. Помимо этого, если вы отправите (push) в таком состоянии свою историю на удаленный сервер, вы вернете все эти перемещенные коммиты на центральный сервер, что ещё больше всех запутает. Довольно логично предположить, что разработчик не хочет, чтобы C4 и C6 были в истории, и именно поэтому она перебазируется в первую очередь.

Меняя базу, меняй основание

Если вы попадаете в ситуацию, подобную этой, у Git есть особая магия на такой случай. Если кто-то в вашей комаде форсирует отправку на сервер (push), изменений, переписывающих работу, на которых базировалась ваша работа; ваша задача будет состоять в том, чтобы определить, что именно было непосредственно ваше, а что было переписано ими.

Получается, что помимо контрольной суммы коммита SHA-1, Git также вычисляет контрольную сумму, основанную на патче, добавленом с коммитом. Это называется “patch-id”.

Если вы скачаете (pull) в свой локальный репозиторий работу, которая была переписана, и замените (rebase) ею новые коммиты вашего партнера, нередко Git может успешно определить, какие именно изменения были внесены именно вами, и вытащить их в начало новой ветки.

К примеру, если бы в предыдущем сценарии вместо слияния в Кто-то выложил перемещенные (rebase) коммиты, отменяя коммиты, на которых основывалась ваша работа мы выполним git rebase teamone/master, Git будет:

  • Определять, какая работа уникальна для вашей ветки (C2, C3, C4, C6, C7)

  • Определять, какие коммиты не были коммитами слияния (C2, C3, C4)

  • Определять, что не было перезаписано в тематическую ветку (только C2 и C3, поскольку C4 - это тот же патч, что и C4')

  • Применять эти коммиты к началу teamone/master

Таким образом, вместо результата, который мы можем наблюдать на Вы снова выполняете слияние для той же самой работы в новый коммит слияния, у нас получилось бы что-то вроде Перемещение в начало force-pushed перемещенной работы..

Rebase on top of force-pushed rebase work.
Рисунок 48. Перемещение в начало force-pushed перемещенной работы.

Это возможно, если C4 и C4', который был сделан вашим партнером, фактически является точно таким же патчем. В противном случае rebase не сможет сказать, что это дубликат, и создаст ещё один подобный C4 патч (который с большой вероятностью не удастся применить чисто, поскольку в нём уже присутствуют некоторые изменения).

Вы можете это упростить, применив git pull --rebase вместо обычного git pull. Также возможно осуществить это вручную с помощью git fetch, примененного после git rebase teamone/master.

Если вы используете git pull и хотите сделать --rebase по умолчанию, вы можете установить значение конфигурации pull.rebase чем-то вроде этого git config --global pull.rebase true.

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

Если в какой-то момент вы или партнер находите необходимость в этом, убедитесь, что все знают, как применять команду git pull --rebase, чтобы минимизировать ущерб от подобных действий.

Перемещение vs. Слияние

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

Одна из точек зрения заключается в том, что история коммитов в вашем репозиториии - это запись того, что на самом деле произошло. Это исторический документ, ценный сам по себе, и его нельзя подделывать. С этой точки зрения изменение истории коммитов почти кощунственно, вы лжете о том, что на самом деле произошло. Но что, если произошла путаница в коммитах слияния? Если это случается, репозиторий должен сохранить это для потомков.

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

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

В основном, стоит взять лучшее от обоих миров - использовать перемещение (rebase) для локальных изменений, ещё не отправленных на удаленный сервер (push), чтобы навести порядок в вашей истории; но никогда не перемещать (rebase) ничего, что уже было отправлено (push) куда-то.