Испортили git rebase?

Источник: «Messed up a git rebase? Now What?»
Git rebase — мощный инструмент, помогающий перенести или объединить один, или несколько коммитов в новый базовый коммит, переписав историю проекта так, чтобы ваша ветка выглядела созданной из другого коммита. Он помогает поддерживать более чистую и линейную историю.

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

Помните, что эта статья посвящена Git, а не инструментам для совместной работы с Git-репозиториями, таким как Github или Gitlab, за исключением таких общих понятий, как одобрение кода, pull request, удалённый репозиторий, которые будут упоминаться по мере их появления.

Итак, давайте начнём.

git rebase: Основы

Зачем выполнять rebase, если есть команда git merge? Есть сценарии, когда rebase более удобен, чем слияние. Предположим, вы уже некоторое время работаете над новой функцией в отдельной ветке. За это время в ветке master появились новые коммиты, которых нет в вашей рабочей ветке, потому что она была создана до того, как эти коммиты были добавлены.

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

Я предпочитаю интерактивный инструмент для выполнения rebase. Если вы не знакомы с ним, то можете использовать его, добавив флаг -i или --interactive, например, git rebase -i master для ребазирования рабочей ветки в master.

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

Например:

// Предпочтительнее делать один коммит в ветку master
e184504 (HEAD -> master) feature A

// вместо отдельных коммитов, относящихся к одной и той же функции
e754500 (HEAD -> master) add Z
714fb43 add Y
8147fb0 add X

Интерактивный инструмент позволяет выполнять различные операции во время rebase, такие, как pick, drop, reword или squash. Если хотите узнать больше об интерактивном режиме, загляните на страницу: 7.6 Инструменты Git - Перезапись истории

Представьте, что работаете над проектом, в котором ветка master содержит два коммита:

e754500 (HEAD -> master) add About page
714fb43 add Login page

Затем вы создали ветку feature-1 из ветки master и работали над ней. Вот история новой ветки:

3ac2d3d (HEAD -> feature-1) fix bug with contact service
0a298d8 add fix bug in contact form
f91cc77 add Contact page
e754500 add About page
714fb43 add Login page

После того как было получено необходимое одобрение на Github, ветка готова к слиянию. Однако вы обнаружили, что другие члены команды вносили изменения в ветку master, пока вы работали над feature-1.

cdf9f3f (HEAD -> master) add Home page // в feature-1 нет этого коммита
902add0 add Blog page // в feature-1 нет этого коммита
e754500 add About page
714fb43 add Login page

Именно здесь на помощь приходит git rebase. Сначала необходимо получить последние изменения из master, проверив master и выполнив git pull origin master. Затем, если вы наберёте git rebase master, то сможете сделать так, чтобы рабочая ветка выглядела примерно так:

c1fbb22 (HEAD -> feature-1) fix bug with contact service
85d4677 add fix bug in contact form
701da95 add Contact page
cdf9f3f (master) add Home page
902add0 add Blog page
e754500 add About page
714fb43 add Login page

В интерактивном режиме можно использовать опцию squash, объединяющую коммиты в один. Этого можно сделать, заменив pick на squash или букву s:

pick c1fbb22 fix bug with contact service
squash 85d4677 add fix bug in contact form
squash 701da95 add Contact page

Это объединит три коммита из рабочей ветки в один:

fb8036f (HEAD -> feature-1) add Contact page
cdf9f3f (master) add Home page
902add0 add Blog page
e754500 add About page
714fb43 add Login page

Теперь можно слить ветку без каких-либо проблем.

Если хотите узнать больше о ребазировании, то недавно я написал статью: Руководство по merge, rebase, squash и cherry-pick.

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

Давайте рассмотрим более сложную ситуацию, когда всё пошло совсем не так.

Отмена git rebase

Что произойдёт, если по какой-то причине удалить коммит, который не должен был удаляться, или сжать коммит, не требующий сжатия? Одна из возможных ситуаций — когда требуется рефакторинг сервиса или класса для подготовки к новой функции, а вы не были к этому готовы.

В идеале следует создать отдельную ветку для рефакторинга и открыть Pull Request, чтобы ваши коллеги или руководитель команды могли просмотреть рефакторинг первыми, избегая лишнего шума. Рекомендую вам поступать так, когда это возможно.

Однако допустим, вы решили оставить рефакторинг в той же ветке. Если допустите ошибку, например, удалите или сожмёте неправильные коммиты, git revert не поможет, потому что они не будут отображаться в истории.

Разберём это на примере.

Вы работали над новой функцией для проекта. В процессе разработки столкнулись с препятствием — устаревшим кодом, требующим рефакторинга для поддержки вашей функции. Допустим, вы сохранили его в той же ветке, планируя создать для него отдельную ветку позже, когда работа над функцией будет завершена.

История коммитов выглядит следующим образом:

d600d8a (HEAD -> feature-1) fix bugs related to feature 1
37356d2 initial implementation of feature 1
92829c9 (refactor) prepare codebase for feature 1
fb8036f other commit

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

Таким образом, вы решили выполнить rebase ветки, чтобы получить последние изменения из master и воспользоваться возможностью сжать свои коммиты в процессе, дав им описательное имя. Вы сделали это, набрав git rebase -i master.

Вот вывод этой команды:

pick 92829c9 (refactor) prepare codebase for feature 1
pick 37356d2 initial implementation for feature 1
pick d600d8a fix bugs related to feature 1

Однако случайно вы указали Git'объединить вашу функцию с рефакторингом:

pick 92829c9 (refactor) prepare codebase for feature 1
squash 37356d2 initial implementation for feature 1 # this one should have remained as "pick"
squash d600d8a fix bugs related to feature 1

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

404824f (HEAD -> feature-1) add feature 1
fb8036f other commit

И тут вы заметили свою ошибку. Коммит с рефакторингом исчез. Три коммита были объединены, а вы намеревались объединить только два последних. Всегда можно извлечь изменения из удалённого репозитория и восстановить свою ветку, если только вы ранее запушили изменения до ребазирования ветки. Что ещё хуже, предположим, что вы запушили изменения с помощью git push -f. Теперь у нас проблемы.

Однако ещё можно кое-что попробовать.

git reflog

Интересная особенность Git'а заключается в том, что в нём сложно что-то удалить. Даже если удалённые коммиты больше не видны в истории, есть вероятность, что к ним всё ещё можно получить доступ, проверив журналы ссылок, также известные как reflog.

reflog записывает почти всё, что вы делаете в локальном репозитории, и предоставляет больше информации, чем такие команды, как git log. Доступ к reflog можно получить, набрав git reflog show. Вот часть моих рефологов:

404824f (HEAD -> feature-1) HEAD@{0}: rebase (finish): returning to refs/heads/feature-1
404824f (HEAD -> feature-1) HEAD@{1}: rebase (squash): add feature 1
df8d688 HEAD@{2}: rebase (squash): # This is a combination of 2 commits
92829c9 HEAD@{3}: rebase (start): checkout master
d600d8a HEAD@{4}: commit: fix bugs related to feature 1
37356d2 HEAD@{5}: commit: initial implementation for feature 1
92829c9 HEAD@{6}: commit: (refactor) prepare codebase for feature 1
fb8036f HEAD@{7}: checkout: moving from master to feature-1
...

Исходя из этого, можно сделать вывод, что rebase произошёл между HEAD и HEAD@{2}. Кроме того, HEAD@{3}, HEAD@{4}, HEAD@{5} и HEAD@{6} указывают на изменения, предшествующие rebase. Можно смело загружать любой из этих коммитов. Только имейте в виду, что любая операция checkout также генерирует записи в reflog, а это значит, что ссылка HEAD@{n} изменится для указанного коммита. Также можно выполнить checkout коммита по его хэшу.

После инспекции этих коммитов с помощью git checkout, кажется, что лучшим кандидатом является HEAD@{4} (d600d8a). Давайте восстановим нашу ветку.

Есть несколько способов сделать это. Один из них — удалить рабочую ветку и создать её заново из d600d8a, набрав:

git checkout d600d8a
git branch -D feature-1
git checkout -b feature-1

В качестве альтернативы можно сбросить рабочую ветку с помощью git reset --soft d600d8a. Я предпочитаю этот подход, но используйте то, что лучше подходит в вашем случае.

После git reset лог истории выглядит так:

d600d8a (HEAD -> feature-1) fix bugs related to feature 1
37356d2 initial implementation for feature 1
92829c9 (refactor) prepare codebase for feature 1
fb8036f other commit

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

d600d8a (HEAD -> feature-1) add feature 1
92829c9 (refactor) prepare codebase for feature 1
fb8036f other commit

Наконец, когда мы на 100% уверены, что rebase прошёл успешно, можно отправить изменения в удалённый репозиторий.

Заключение

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

В статье мы узнали, как комбинировать коммиты и переписывать историю логов с помощью git rebase. А также рассмотрели несколько примеров, когда что-то может пойти не так, и познакомились с git reflog как методом восстановления удалённых коммитов в рабочей ветке.

Библиография

Комментарии


Дополнительные материалы

Предыдущая Статья

CSS Веерное раскрытие с grid и @property

Следующая Статья

PHP 8.4: exit/die изменены из языковых конструкций в функции