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

В Git есть два способа внести изменения из одной ветки в другую:

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

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

Допустим, вы разделили свою работу и сделали коммиты в две разные ветки: main и experiment.

Простейший способ выполнить слияние двух веток, как вы выяснили ранее, это команда Ветвь > Объединить... (Branch > Merge...). Она осуществляет трехстороннее слияние между двумя последними снимками сливаемых веток (main-issue-58 и experiment-issue-57) и самого недавнего общего для этих веток родительского снимка (Добавил .gitignore), создавая новый снимок (и коммит).

Тем не менее, есть и другой способ: вы можете взять те изменения, что были представлены в experiment-issue-57 и применить их поверх main-issue-58. В Git это называется перебазированием.

C помощью команды Ветвь > Перемещение изменений из одной ветви в другую (Branch > Rebase Branch...) вы можете взять все изменения, которые были зафиксированы в одной ветке и применить их к другой ветке.

В данном примере для этого необходимо выполнить следующее:
  1. Сначала переключитесь на ветку experiment;
  2. После этого выполните Ветвь > Перемещение изменений из одной ветви в другую (Branch > Rebase Branch...) и в палитре команд выберите ту ветку, в которую нужно переместить изменения — main.

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

Важно заметить, что, в отличие от слияния, при котором оба сливаемых коммита сохраняются, при перебазировании старый коммит experiment-issue-57 удаляется и вместо него создается новый коммит experiment-issue-57 с новым id.

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

Теперь снимок, на который указывает коммит experiment-issue-57 абсолютно такой же, как тот, на который указывал Merge branch 'experiment' в примере с трехсторонним слиянием.

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

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

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

Более интересные перебазирования

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

Вы создаете тематическую ветку server, чтобы добавить в проект некоторые функции для серверной части, и делаете коммит server-issue-57.

Затем вы ответвляетесь в ветку client, чтобы сделать изменения для клиентской части, и выполняете пару коммитов: client-issue-58 и client-issue-59.

После этого вы снова переходите в ветку server и делаете коммиты server-issue-60 и server-issue-61.

Наконец, вы возвращаетесь в ветку main и делаете пару коммитов main-issue-62 и main-issue-63.

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

Вы хотите взять изменения из ветки client, которых нет в server (58 и 59), и применить их на ветке main.

Для этого воспользуйтесь командной строкой и командой git rebase с параметром --onto:

> git rebase --onto main server client  
Successfully rebased and updated refs/heads/client.

В этой команде говорится: «Переключись на ветку client, найди изменения относительно ветки server и примени их для ветки main». Несмотря на некоторую сложность этого способа, результат впечатляет.

Теперь вы можете выполнить перемотку (fast-forward) для ветки main.

> git checkout main
Switched to branch 'main'

> git merge client
Updating 8b1c036..f87377b
Fast-forward
 bitdepth.sbsl | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-) 

Теперь вы решили добавить наработки и из ветки server. Вы можете выполнить перебазирование ветки server относительно ветки main без предварительного переключения на server при помощи команды git rebase main server, которая извлечет тематическую ветку (в данном случае server) и применит изменения в ней к базовой ветке (main).

> git rebase main server
Successfully rebased and updated refs/heads/server.

Это повторит работу, сделанную в ветке server поверх ветки main.

После этого вы сможете выполнить перемотку основной ветки main.

> git checkout main     
Switched to branch 'main'

> git merge server      
Updating f87377b..95bb204
Fast-forward
 bitdepth.sbsl | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

Теперь вы можете удалить ветки client и server, поскольку весь ваш прогресс уже интегрирован и тематические ветки больше не нужны.

> git branch -d client
Deleted branch client (was f87377b).

> git branch -d server
Deleted branch server (was 95bb204).

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

Опасности перебазирования

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

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

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

Когда вы что-то перемещаете, вы отменяете существующие коммиты и создаете новые, похожие на старые, но являющиеся другими. Представьте, что вы отправили свои коммиты куда-нибудь (Отправка (Push)), и другие получили их себе (Принесение (Fetch)), и на их основе выполнили свои доработки. Теперь вы переделываете эти коммиты командой Ветвь > Перемещение изменений из одной ветви в другую (Branch > Rebase Branch...), и выкладываете их снова. Это значит, что ваши коллеги будут вынуждены заново выполнять слияние для своих доработок. В итоге, когда вы в очередной раз попытаетесь включить их работу в свою, вы получите путаницу.

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

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

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

Далее Василий решает вернуться, и вместо слияния (Ветвь > Объединить... (Branch > Merge...)) выполнить перебазирование своей работы (Ветвь > Перемещение изменений из одной ветви в другую (Branch > Rebase Branch...)).

Он выполняет это и отправляет изменения на сервер, перезаписывая его историю.

> git push --force origin main
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 12 threads      
Compressing objects: 100% (3/3), done.        
Writing objects: 100% (3/3), 291 bytes | 291.00 KiB/s, done.
Total 3 (delta 2), reused 0 (delta 0), pack-reused 0 (from 0)
remote: Resolving deltas: 100% (2/2), completed with 2 local objects.
To https://github.com/1C-EDT-Developer/coldevex
 + dbc49d8...3192a5e main -> main (forced update)

Когда вы получаете изменения с сервера (Принесение (Fetch)), вы извлекаете этот новый коммит (bug-97).

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

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

Перебазирование или слияние

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

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

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

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

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

По материалам книги Pro Git (авторы Scott Chacon и Ben Straub, издательство Apress). Книга распространяется по лицензии Creative Commons Attribution Non Commercial Share Alike 3.0 license.