При командной разработке проекта всегда существует необходимость соединения друг с другом различных наработок. В git существуют 2 соответствующих инструмента, специализирующихся на интеграции изменений одной ветки проекта в другую ветку - git rebase и git merge. Самая простая из них это merge, которая позволяет легко слить ветки проектов в одну ветку в случае, если между исходными ветками нет конфликтов (под конфликтом подразумевается ситуация, когда в один и тот же файл разными разработчиками были внесены разные изменения). Merge всегда является движущейся вперед записью изменения.
Утилита rebase более продвинутая, она предоставляет возможность слияния веток с пошаговым, удобным методом разрешения конфликтов. Альтернативно rebase обладает мощными функциями перезаписи истории. Более подробный разбор merge и rebase см. в документации [2]. Сама утилита Rebase имеет 2 основных режима: "manual" и "interactive". Далее разные режимы Rebase будут рассмотрены более подробно.
[Что такое "git rebase"?]
Rebase это процесс переноса комбинации последовательности фиксаций (commit, далее фиксации будем для простоты называть коммитами) в новый базовый коммит. Общий процесс можно визуализировать следующим образом:
С точки зрения содержимого проекта процесс rebase это перемещение (перебазирование) начальной точки (базы) вашей ветки с одного коммита на другой, как если бы ваша метка начала свое ветвление из другого коммита. Внутри себя Git скрытно от пользователя делает новый, временный коммит, и применяет его для указанной базы (нового начала перебазируемой ветки). Очень важно понимать, что хотя перебазированная ветка выглядит так же, она теперь составлена из совершенно новых коммитов.
[Использование rebase]
Основная причина для применения операции rebase - обеспечить линейную историю развития проекта. Например, рассмотрим ситуацию, когда происходит непрерывная разработка главной ветви проекта (main), и в какой-то момент произошло ответвление от этой ветки в новую ветвь feature. Это обычная практика, применяемая при разработке, когда нужно в проект добавить новую фичу. Над веткой main работает один сотрудник, а над веткой feature другой, и работа над проектом ведется параллельно.
Предположим, что вы хотели бы получить последние обновления из ветки main, чтобы их применить к ветке feature, но хотите при этом сохранить историю изменения вашей ветки чистой и последовательной, как будто вы начали ветвление ветки feature от последнего коммита ветки main. В будущем это даст преимущество для чистого слияния (merge) вашей ветки feature обратно в ветку main. Почему нам так важно поддерживать "чистую историю" git? Выгоды наличия чистой истории становятся ощутимыми при поиске багов, когда приходится проводить исследования для попыток отката на предыдущие коммиты. Более реалистичный сценарий может выглядеть следующим образом:
1. В главной ветке main обнаружен баг. Ветка feature, которая до этого работала нормально, теперь оказывается поврежденной.
2. Разработчик исследует историю изменений (командой git log) ветки main, и тогда "чистая история" изменений позволит быстрее найти причину появления бага в проекте.
3. Разработчик с помощью команды git log не смог определить, когда появился баг. Поэтому разработчик выполняет команду git bisect.
4. Поскольку история git является чистой, git bisect дает уточненный набор коммитов для сравнения при поиске регрессии. Разработчик быстро находит коммит, который ввел ошибку, и получает возможность действовать соответствующим образом.
Более подробно про команды git log и git bisect см. соответствующие странички документации [4, 5].
У вас есть 2 опции для интеграции вашей ветки feature в основную ветку main проекта: прямое слияние (git merge), если это сработает, либо перебазирование (git rebase) с последующим слиянием (git merge). Первая опция приведет к трем операциям merge и затем merge commit, в то время как последняя опция (rebase) приведет к "быстрому слиянию вперед" и сохранению исключительно чистой истории коммитов. На следующей диаграмме демонстрируется процесс rebase на основную ветку main, упрощающий последующую процедуру fast-forward merge.
Процесс rebase это общий метод интеграции внешних изменений проекта в ваш локальный репозиторий. Подтягивание эволюционных изменений с помощью merge приводит к избыточной фиксации слияния (merge commit) всякий раз, когда вы хотите увидеть, как происходила эволюция проекта. С другой стороны операция rebase это примерно как фраза "Хочу, чтобы мои изменения основывались на том, что все уже к настоящему моменту сделали".
Не делайте rebase опубликованной истории. Как мы обсуждали выше, когда упоминалась перезапись истории, никогда не следует выполнять rebase для коммитов, которые уже были опубликованы в публичный репозиторий. Процесс rebase заменит старые коммиты на новые, и это будет выглядеть так, что как будто часть вашей истории проекта вдруг исчезла.
Rebase Standard против Rebase Interactive. Интерактивный rebase происходит, когда git rebase принимает аргумент -i (или --interactive). Это и означает "Interactive". Без каких-либо аргументов команда работает в стандартном режиме. В обоих случаях подразумевается, что будет создана отдельная временная ветка в виде копии feature, пристыкованная в голову (HEAD) ветки, на которую делается rebase.
# Создание ветки feature как копии ветки main:
git checkout -b feature main
# Здесь мы редактируем файлы, вносим изменения. После внесенных изменений
# делаем их добавление (-a) и фиксацию с комментарием (-m):
git commit -a -m "Добавление новой фичи"
Git rebase в стандартном режиме автоматически возьмет ваши коммиты в текущей рабочей ветке и пристыкует их в голову указанной базовой ветки (в этом примере main):
git rebase main
Эта команда автоматически "перебазирует" текущую ветку в начало ветки main (поверх её самого свежего коммита). В команде rebase вместо "main" может быть указана также любая ссылка на коммит (например ID, имя ветки, тег, или ссылка относительно HEAD).
Запуск git rebase с флагом -i начнет сессию интерактивного перебазирования ветки. Вместо того, чтобы тупо перенести изменения всех коммитов в новую базу (внесенные изменения в файлах снабжаются специальными маркерами, чтобы изменения можно было впоследствии проанализировать), интерактивный даст вам возможность влиять на применение отдельных коммитов. Это позволяет очистить историю путем удаления, разделения и изменения существующей серии коммитов. Это как git commit --amend на стероидах.
git rebase --interactive main
Эта команда также перебазирует текущую ветку поверх main (или любой другой указанной базы), но будет это делать в интерактивной сессии. Автоматически откроется настроенный по умолчанию редактор, где вы можете вводить команды (описанные ниже) для каждого перебазируемого коммита. Эти команды будут определять, как отдельные коммиты перекочуют на новую базу. Вы также сможете реорганизовать список коммитов, чтобы поменять порядок следования самих коммитов. После того, как вы укажете команды для каждого коммита в rebase, Git начнет проигрывать применение коммитов в командах rebase. Редактирование команд rebase выглядит следующим образом:
pick 2231360 какой-то старый коммит
pick ee2adc2 Добавление новой фичи
# Rebase 2cf755d..ee2adc2 onto 2cf755d (9 команд)
#
# Команды:
# p, pick = использовать этот коммит
# r, reword = использовать коммит, но отредактировать его сообщение
# e, edit = использовать коммит, но остановить для изменения
# s, squash = использовать коммит, но "вплавить" его в предыдущий коммит
# f, fixup = наподобие "squash", но отбросить log-сообщение этого коммита
# x, exec = запустить команду (указанную далее) используя шелл
# d, drop = удалить коммит
Как описано в документации по перезаписи истории git [6], rebase может использоваться для изменения старых и нескольких коммитов, попавших в коммит файлов и нескольких сообщений. Хотя это наиболее распространенные применения, у git rebase также есть дополнительные опции, которые могут быть полезны в более сложных вариантах использования.
git rebase -d означает, что во время проигрывания коммит будет отброшен из конечного комбинированного блока коммитов.
git rebase -p оставит коммит как есть. Это не поменяет сообщение коммита или его содержимое, и он останется как отдельный коммит в истории веток.
git rebase -x во время проигрывания выполняется шелл-скрипт командной строки на каждом помеченном коммите. Полезным примером может быть запуск вашей системы проверки кодовой базы на определенных коммитах, которая может помочь в идентификации регрессий во время rebase.
Итак, интерактивный rebase дает вам полный контроль над тем, как выглядит ваша история проекта. Это дает разработчикам много свободы коммитить "грязную" историю, фокусируясь на написании кода, чтобы потом вернуться обратно и задним числом все подчистить.
Многие разработчики любят интерактивный rebase как функцию "полировки" ветки перед тем, как влить её в основную базу кода. Это дает им возможность склеить друг с другом незначимые коммиты, удалить устаревшие коммиты, и убедиться в том, что все в порядке перед тем, как закоммитить ветку в "официальную" историю проекта. Для всех остальных это будет выглядеть так, что вся перебазируемая ветка (feature) была разработана как одна последовательность хорошо спланированных коммитов.
Реальная сила интерактивного rebase может быть отмечена при просмотре истории результирующей основной ветки (main). Все будут думать, что вы супер-разработчик, который никогда не ошибается и все делает чисто и четко, когда добавляет в проект новую фичу. Т. е. интерактивный rebase позволяет сохранять историю изменений проекта чистой и осмысленной.
Есть несколько свойств для rebase, которые можно установить командой git config. These options will alter the git rebase output look and feel.
rebase.stat: boolean, установленный по умолчанию в false. Опция переключает отображение содержимого визуального diffstat, которое показывает, что изменилось с момента последнего rebase.
rebase.autoSquash: boolean, переключающий поведение --autosquash.
rebase.missingCommitsCheck: может быть установлено в несколько значений, которые меняют поведение rebase, касающееся пропущенных коммитов:
warn напечатает предупреждающее сообщение в интерактивном режиме, оповещающее об удаленных коммитах. error остановит rebase, и напечатает предупреждающее сообщение удаленных коммитов. ignore если это установлено по умолчанию, то предупреждения о пропущенных коммитах будут игнорироваться.
rebase.instructionFormat: строка формата git log, используемая для форматирования format string that will be used for formatting interactive rebase display
Опция --onto разрешает более мощную форму, которая позволяет специальные ссылки на точки перебазирования ветки. Рассмотрим пример репозитория с ветками наподобие следующих:
o---o---o---o---o main
\
o---o---o---o---o featureA
\
o---o---o featureB
Здесь featureB основана на featureA, однако мы при этом уверены, что featureB не зависит ни от каких изменений в featureA и могла бы просто быть пристыкована к самому свежему коммиту ветки main.
git rebase --onto main featureA featureB
В этой команде featureA соответствует параметр стараябаза. Ветка main это новаябаза, и featureB ссылка на ветку, которая должна быть перебазирована на HEAD ветки новаябаза (т. е. main). В результате получится:
o---o---o featureB
/
o---o---o---o---o main
\
o---o---o---o---o featureA
[Риски rebase]
Использование Git Rebase может привести к слишком частым конфликтам слияния во время рабочего процесса. Это произойдет, если есть долго живущая ветка, которая давно ответвилась от основной ветки (main). В конце концов вы можете захотеть отказаться от main, поскольку для слияния в неё будет слишком много конфликтов, которые придется разрешать вручную. Эту проблему будет проще решить, если более часто делать rebase на ветку main, и чаще делать коммиты. Команды --continue или --abort можно передать вместе с git rebase, чтобы при разрешении конфликтов соответственно продолжать разрешать слияние коммитов или сбросить rebase.
Еще большая опасность rebase заключается в потере коммитов из интерактивной перезаписи истории. Запуск rebase в интерактивном режиме и выполнение подкоманд наподобие squash (вводом символа s редакторе) или drop (ввод символа d) будет удалять коммиты из непосредственного лога вашей ветки. На первый взгляд может показаться, что коммиты исчезли навсегда. Использованием git reflog эти коммиты можно восстановить, в весь rebase можно отменить. Для дополнительной информации по команде git reflog для поиска потерянных коммитов посетите страничку документации [7].
Git Rebase сам по себе при правильном использовании не очень опасен. Реальная опасность возникает при выполнении перезаписи истории в интерактивном rebase и принудительной выгрузке результата (командой git push -f) на сетевую ветку, которую совместно используют и другие разработчики. Такого шаблона поведения следует избегать, поскольку она может перезаписать результаты работы других людей, когда они сделают git pull для восстановления корректного для них состояния ветки репозитория.
Восстановление из upstream rebase. Если другой пользователь выполнил rebase, и сделал принудительный push на ветку, куда вы делали свои коммиты, то ваш git pull перезапишет любые ваши локальные коммиты, которые базировались на предыдущей ветви. К счастью, с помощью git reflog вы можете получить reflog сетевой ветки. На сетевой ветке reflog может найти ссылку на ветку до момента, когда она прошла rebase. Тогда вы сможете сделать повторный rebase вашей ветки сетевой ссылке с помощью опции --onto, как было описано выше во врезке "Продвинутое использование rebase".
[Словарик]
branch (ветка) основное средство для добавления в проект новых фич. Когда в проект нужно добавить новую функцию, из основной ветки делается копия в новую ветку (командой git checkout -b имяновойветки), и вся разработка ведется уже в ней, параллельно и независимо от основной ветки.
commit (фиксация) запоминание изменений в ветке проекта в виде датированной точки, снабженной хешем, датой и комментарием. В результате получается история - набор последовательных сохранений изменений, который называется историей (история фиксаций просматривается командой git log).
merge (слияние) команда, предназначенная для автоматического слияния двух веток друг с другом. Такое слияние возможно в случае, если внесенные изменения в каждой из сливаемых веток не пересекаются друг с другом (т. е. в разных ветках не было разных изменений в одном и том же файле проекта).
pull (вытягивание) закачка внешних изменений проекта (по умолчанию) в текущую локальную ветку.
push (выгрузка) локальных изменений (по умолчанию) ветки во внешний
rebase (перебазировать) команда, которая переносит побочную ветку в начало истории фиксаций и вносит в каждый файл проекта изменения из обоих веток (со специальными текстовыми метками, которые позволяют разрешать конфликты вручную).