Команда git rebase заслужила репутацию магической - какой-то невероятный фокус Git, которого должны избегать новички, и которая значительно облегчает совместную разработку в команде при аккупатном использовании. В этой статье (перевод документации [1]) будут сравниваться возможности git rebase с похожей по назначению командой git merge, и будут описаны все потенциальные возможности команды rebase для включения в обычный рабочий процесс Git.
[Обзор концепции merge и rebase]
Первое, что нужно понимать для git rebase - эта команда предназначена для той же цели, что и git merge. Обе эти команды разработаны для интекрации изменений из одной ветки в другую, только делают они это разными способами.
Рассмотрим ситуацию, что происходит, когда вы начали работать над новой функцией в выделенной для этого ветке (пусть это будет ветка feature, которая была получена от основной ветки проекта main), а другой участник команды разработки после этого обновил основную сетку (main) проекта новыми коммитами. Итак, сейчас в проекте существует 2 параллельно разрабатываемые ветки - main и feature. В результате дальнейшей работы получится разветвленная история изменений, с которой сталкиваются повсеместно пользователи, использующие Git как инструмент управления версиями и поддержки совместной разработки.
Теперь предположим, что новые коммиты в ветке main затрагивают также и код, над которым вы работали в своей отдельной ветке feature. Для внедрения новых коммитов из ветки main в ветку feature, у вас есть 2 опции: merge или rebase.
Merge. Это самая простая опция слить ветку main в ветку feature, используя команды наподобие следующих:
$ git checkout feature
$ git merge main
Или можно выполнить эти команды одной строкой:
Это создаст новый "коммит слияния" (merge commit) в ветке feature, который объединит друг с другом истории обоих веток, в результате получится структура веток наподобие следующей:
Слияние веток командой merge хорошо тем, что в ней отсутствуют деструктивные операции. Существующие ветки никаким образом не изменяются. Это позволяет избежать потенциальных проблем, связанных с rebase (что будет рассмотрено далее).
С другой стороны это также означает, что ветка feature получит дополнительную фиксацию слияния (merge commit; далее для простоты фиксацию будем называть коммитом - устоявшийся термин среди разработчиков) всякий раз, когда нужно слить друг с другом добавление в разработке новой функции и основной внешний код проекта. Если в основной ветке проекта (веика main) велась очень активная разработка, это может несколько загрязнить историю изменений в вашей ветке feature. Хотя проблему можно решить с помощью расширенных опций команды git log, это может затруднить для других разработчиков понимание истории проекта.
Rebase. Как альтернативу для слияния (merge) вы можете сделать перебазирование (rebase) ветки feature на ветку main с помощью следующих команд:
# Переключение на ветку feature:
$ git checkout feature
# Текущая ветка feature будет перебазирована на самый свежий
# коммит ветки main:
# git rebase main
Последняя команда перенесет всю ветку feature на кончик ветки main, эффективно внедряя все новые изменения из коммитов ветки main. Однако вместо использования merge commit перебазирование перезапишет историю проекта таким образом, что создаются новые коммиты для каждого коммита в оригинальной ветке.
Основное достоинство rebase в том, что вы получаете намного более чистую историю проекта. Во-первых, это устраняет нежелательные фиксации слияния (merge commit), требуемые для команды git merge. Во вторых, как вы можете видеть на диаграмме выше, rebase также приводит к исключительно линейной истории проекта - вы можете продолжить разработку своей новой фичи с головы (HEAD) основной ветки main без каких-либо ответвлений. Это упрощает навигацию по проекту командами наподобие git log, git bisect и gitk.
Однако существует 2 компромисса, на которые приходится идти с этой чистой истории коммитов: безопасность и трассировка. Если вы не следуете Золотому Правилу для git rebase, то перезапись истории проекта может быть потенциальной катастрофой для совместного процесса разработки. И, что не так важно, rebase выбрасывает контекст, который предоставляет merge commit вы не сможете увидеть в истории изменения внешней ветки, когда встроите их в свою ветку feature.
Интерактивный rebase. Это режим перебазирования, который дает вам возможность изменить коммиты, когда они переносятся в новую ветку. Это более мощная функция, чем автоматический rebase (или стандартный rebase), поскольку дает полный контроль на историей коммитов ветки. Обычно это используется для подчистки запутанной истории перед слиянияем ветки feature в ветку main (более подробно про стандартный и интерактивный rebase см. [2]).
Чтобы начать интерактивную сессию перебазирования, передайте опцию -i в команду git rebase:
$ git checkout feature
$ git rebase -i main
Это откроет текстовый редактор, где будут перечислены все коммиты, которые будут перемещаться в процессе перебазирования:
pick 33d5b7a Сообщение для коммита #1
pick 9480b3d Сообщение для коммита #2
pick 5c67e61 Сообщение для коммита #3
Этот листинг точно определяет, как будет выглядеть ветка после выполнения rebase. Путем изменения команды pick и/или переупорядочивания строк в этом списке вы можете добиться любого варианта вида истории, какой захотите. Например, если 2-й коммит зафиксирует небольшую проблему в 1-ом коммите, то вы можете решить сжать их в один коммит командой fixup:
pick 33d5b7a Сообщение для коммита #1
fixup 9480b3d Сообщение для коммита #2
pick 5c67e61 Сообщение для коммита #3
Когда вы сохраните и закроете этот файл, Git выполнит rebase в соответствии с вашими инструкциями, в результате чего проект будет выглядеть следующим образом:
Устранение малозначимых коммитов, как было сделано в этом примере, сделает вашу историю проекта более простой для понимания. Это как раз то, чего не может делать команда git merge.
[Золотое Правило rebase]
После того, как мы уяснили, чем является rebase, самое важное научиться, когда не нужно его делать. Золотое правило для git rebase состоит в том, что никогда не нужно его делать для публичных веток.
Например давайте подумаем, что произойдет, если вы сделаете rebase ветки main на вашу ветку feature:
Это перебазирование переместит все коммиты main в HEAD ветки feature. Проблема тут в том, что это произойдет только в вашем локальном репозитории. Все другие разработчики продолжат как ни в чем ни бывало работать с оригинальной веткой main. Поскольку rebase приведет к совершенно новым коммитам, то Git будет думать, что история вашей ветки main разошлась со всеми остальными.
Единственный способ синхронизации двух ответвлений main - слить их обратно друг в друга, что приведет к дополнительному merge commit, и два набора коммитов будут содержать одинаковые изменения (в оригинальной ветке main и вашей перебазированной ветке). Что и говорить, получается довольно запутанная ситуация.
Так что перед тем, как запустить git rebase, всегда задавайте себе вопрос: "Еще кто-нибудь работает с этой веткой?". Если ответ ДА, то уберите ваши руки подальше от клавиатуры, и задумайтесь над менее деструктивным способом применить свои изменения (например, команду git revert). В противном случае вы как угодно сможете перезаписать историю проекта.
Принудительный push. Если вы попытаетесь выгрузить (push) перебазированную ветку main обратно в сетевой репозиторий, то Git не даст вам это сделать, потому что получится конфликт с веткой main сетевого репозитория. Однко вы все еще можете сделать принудительную выгрузку передачей в push флага --force (или -f):
# Будьте очень осторожны с этой командой!
$ git push -f
Эта команда перезапишет ветку main в сетевом репо на перебазированную версию из вашего репозитория, что приведет в шок остальных участников команды разработки (если они вдруг сделают git pull, то все результаты их работы потеряются). Поэтому перед выполнением принудительного push нужно хорошо понимать, что вы делаете.
Одним из случаев применения принудительного push может быть ситуация, когда вы выполнили локальную очистку после того, как локальная ветка feature была перенесена в сетевой репозиторий (например, с целью бэкапа). Это как если бы сказать: "Ой, я на самое деле не хочу делать push той оригинальной ветки feature. Вместо этого возьмите текущую версию". И снова важно отметить, чтобы никтр не отрабатывал фиксации из оригинальной версии ветки feature.
[Описание рабочего процесса]
Перебазирование может быть внедрено в рабочий процесс Git вашей команды настолько, насколько это удобно. В этой секции мы рассмотрим, насколько полезен может быть rebase на различный стадиях разработки новой фичи (feature).
Первый шаг для любого рабочего процесса, где может использоваться git rebase, состоит создание выделенной ветки для каждой новой разрабатываемой фичи. Это даст необходимую структуру веток проекта, позволяющую безопасно применять rebase:
Локальная очистка. Один из лучших способов внедрить rebase в ваш рабочий процесс - очистка локальных, находящихся в разработке фич проекта. Периодическими запусками интерактивного rebase вы можете гарантировать, что каждый коммит в вашей фиче сфокусирован на достижении главной цели и вносит значимые изменения. Это позволяет писать код, не беспокоясь о том, чтобы разбивать его на изолированные коммиты - вы сможете это починить постфактум.
При вызове git rebase у вас есть 2 опции для создания новой базы: переместить веку в голову родительской ветки (например на самый свежий коммит ветки main), либо на предыдущий коммпит вашей ветки feature. Первый вариант мы уже в примере выше из секции "Интерактивный rebase". Второй вариант хорош, когда нужно исправить только несколько последних коммитов. Например, следующая команда начнет интерактивный rebase только последних трех коммитов.
$ git checkout feature git rebase -i HEAD~3
Если указать HEAD~3 в качестве ссылки на новую базу, то фактически ветка не перемещается - вы просто интерактивно перезапишете 3 коммита, которые идут за ней. Обратите внимание, что при этом не будут внедрены внешние изменения в ветку feature.
Если вы хотите перезаписать таким способом всю feature, то может быть полезна команда git merge-base, чтобы найти оригинальную базу ветки feature. Следующая команда возвратит идентификатор фиксации (commit ID) оригинальной базы, который вы можете затем передать в команду git rebase:
$ git merge-base feature main
Такое использование интерактивного перебазирования - хороший способ внедрения git rebase в ваш рабочий процесс, поскольку это влияет только на локальные ветки. Единственное, что увидят другие разработчики - только ваш конечный продукт, что должно создать для всех чистую, простую для понимания историю веток проекта.
И снова, это работает только для приватных веток фичи. Если вы работаете совместно с другими разработчиками на одной же ветке фичи, то эта ветка публичная, и вам нельзя просто так перезаписать её историю.
Не существует альтернативы для git merge, когда очищаются локальные коммиты с интерактивным rebase.
Внедрение внешних изменений в feature. Выше в секции "Обзор концепции merge и rebase" мы видели, как в ветку feature могут быть внедрены изменения внешних изменений из ветки main с использованием git merge или git rebase. Слияние (merge) это безопасный вариант, который сохранит всю историю вашего репозитория, в то время как rebase создаст линейную историю путем перемещения вашей ветки feature в голову ветки main.
Это использование git rebase подобно локальной очистке (и может быть выполнено одновременно), но в процессе интегрируются внешние коммиты из main.
Следует иметь в виду, что вполне нормально делать rebase не ветку сетевого репозитория вместо main. Это может быть, когда происходит совместная работа над одной и той же фичей с другим разработчиком, и вам нужно внедрить его изменения в свой локальный репозиторий.
Например, если вы и другой разработчик (Джон) добавили свои коммиты в ветку feature, то ваш репозиторий после извлечения ветки feature Джона может выглядеть следующим образом:
Вы можете разобраться с этим разветвлением таким же способом, как интегрировали бы внешние изменения из main: либо слиянием (merge) вашей локальной ветки feature с веткой feature Джона, либо перебазированием (rebase) вашей локальной ветки feature в голову ветки feature Джона (john/feature).
Обратите внимание, что этот rebase не нарушает Золотое Правило Перебазирования, потому переносятся что только только коммиты вашей локальной ветки - все до этого осталось нетронутым. Это примерно как сказать: "добавь мои изменения к тому, что Джон уже сделал". В большинстве случаев это является более интуитивно понятным, чем синхронизация с сетевой веткой через коммит слияния.
По умолчанию команда git pull выполняет слияние (merge), однако можно заставить его интегрировать сетевую ветку с rebase передачей опции --rebase.
Просмотр изменений feature с запросом pull. Если вы используете запросы pull как часть процесса оценки вашего кода, то вам нужно избегать исопользования git rebase после создания запроса pull. Как только вы сделаете запрос pull, другие разработчики увидят ваши коммиты, т. е. ветка станет публичной. Перезапись истории сделает невозможным для Git и ваших участников команды разработки отследить и следовать коммитам, добавленным в ветку feature.
Любые измененние других разработчиков должны быть внедрены через git merge вместо git rebase.
По этой причине обычно хорошей идеей будет очистить ваш код с помощью интерактивного rebase перед выдачей запроса pull.
Интеграция одобренной функции (Approved Feature). После того, как ветка feature была одобрена вашей командой, у вас есть опция сделать rebase ветки feature в голову ветки main перед использованием git merge для интеграции feature в базу кода main.
Это аналогично ситуации, когда внешние внешние изменения внедряются в ветку feature, но поскольку вам не разрешено перезаписывать коммиты в ветке mainh, вы должны время от времени использовать git merge для интеграции feature. Однако путем выполнения rebase перед merge вы будете уверены, что merge будет быстро перенаправлено (fast-forwarded), в результате получится исключительно чистая история. Это также даст вам возможность скрещивать (squash) любые последующие коммиты, добавленные запросом pull.
Если в не чувствуете себя полностью в комфорте для использования git rebase, то всегда можете выполнять rebase во временную ветку. Таким способом, если вы вдруг случайно потеряете историю вашей ветки feature, то сможете извлечь нужный коммит из оригинальной ветки. Например:
$ git checkout feature
$ git checkout -b temporary-branch
$ git rebase -i main
# [Очистка истории]
$ git checkout main
$ git merge temporary-branch
Вместо заключения. Это все, что вам нужно знать, чтобы начать использовать rebase для ваших веток. Если вы предпочитаете иметь чистую, линейную историю, свободную от малозначимых коммитов слияния (merge commits), то должны обратиться к использованию git rebase вместо git merge, когда интегрируете изменения из одной ветки в другую.
С другой стороны, если вы хотите сохранить полную историю всех шагов по разработке в вашем проекте, и избежать риска risk перезаписи публичных коммитов, то можете придерживаться к стратегии git merge. Любой из этих вариантов отлично работает, просто крайней мере вы теперь хорошо знаете их достоинства и недостатки, и сможете теперь вполне оченить возможности git rebase.
[Ссылки]
1. Merging vs. Rebasing site:atlassian.com. 2. git rebase. 3. Краткий справочник по Git. |