Самый избегаемый инструмент в Git
Спросите комнату разработчиков, какая команда Git заставляет их нервничать, и git rebase -i окажется в числе первых. У интерактивного rebase сложилась репутация: он переписывает историю, открывает странный todo list в вашем редакторе, и кажется, что сломать что-нибудь опасно легко. Поэтому большинство просто избегает его -- и их pull request приходят с commit вроде "wip", "fix typo" и "actually fix it".
И это обидно, потому что интерактивный rebase -- лучший инструмент для превращения захламлённой branch в чистую историю, удобную для ревью. В этом руководстве мы развеем страхи на одном конкретном сквозном примере: feature-branch с 9 беспорядочными commit, которые мы превратим в 3 чистых перед открытием pull request. Если вы всё ещё сомневаетесь, подходит ли rebase для вашего рабочего процесса, начните с нашего сравнения rebase и merge -- эта статья предполагает, что вы уже решили делать rebase и хотите делать его хорошо.
Почему чистая история важна
Приведение commit в порядок перед pull request -- это не вопрос эстетики. Аккуратная история окупается тремя вполне практическими способами:
- Удобство ревью. Ревьюер, читающий три логических commit -- "Add login form with validation", "Add remember me option", "Add error messages" -- может рассматривать каждое изменение изолированно. Тот же код, размазанный по девяти полуготовым commit, вынуждает его просматривать весь diff целиком, и качество ревью падает.
- git bisect. Когда баг проявляется спустя недели,
git bisectпроходит по истории, чтобы найти commit, который его внёс. Это работает только в том случае, если каждый commit собирается и имеет смысл сам по себе. История, полная commit "wip", которые даже не компилируются, делает bisect почти бесполезным. - Откат изменений. Если функциональность нужно убрать, revert одного самодостаточного commit -- дело тривиальное. Revert функциональности, размазанной по девяти переплетённым commit, -- это целый день археологии.
Наш сквозной пример: 9 беспорядочных commit
Вот branch, которую мы будем приводить в порядок. Это совершенно нормальная рабочая история -- именно так все и пишут код на самом деле:
$ git log --oneline main..feature/login
c9d0e1f oops forgot the css file
b8c9d0e add error messages
a7b8c9d wip 2
f6a7b8c add remember me checkbox
e5f6a7b actually fix it
d4e5f6a fix typo
c3d4e5f add form validation
b2c3d4e wip
a1b2c3d add login form skeleton
Нет ничего плохого в том, чтобы делать такие commit в процессе работы. Проблема только в том, если это уходит в таком виде. Наша цель -- получить три commit:
- Add login form with validation
- Add remember me option
- Add error messages with styling
Запускаем rebase: git rebase -i HEAD~9
Чтобы переписать последние 9 commit, выполните:
git rebase -i HEAD~9
HEAD~9 означает "commit на 9 шагов раньше текущего" -- всё после этой точки доступно для редактирования. Git открывает ваш редактор с todo list:
pick a1b2c3d add login form skeleton
pick b2c3d4e wip
pick c3d4e5f add form validation
pick d4e5f6a fix typo
pick e5f6a7b actually fix it
pick f6a7b8c add remember me checkbox
pick a7b8c9d wip 2
pick b8c9d0e add error messages
pick c9d0e1f oops forgot the css file
Две вещи, которые нужно понять, прежде чем что-либо трогать. Во-первых, это просто текстовый файл: ничего не происходит, пока вы его не сохраните и не закроете, а если удалить все строки или закрыть без сохранения значимых изменений, rebase будет безболезненно отменён. Во-вторых, порядок здесь от старых к новым -- противоположный git log. Git будет повторно применять commit сверху вниз, выполняя глагол в начале каждой строки.
Шесть глаголов
pick
Оставить commit ровно таким, какой он есть. Это значение по умолчанию для каждой строки.
reword
Сохранить изменения commit, но остановиться, чтобы вы могли отредактировать сообщение. Идеально, чтобы исправить "wip 2", не трогая код.
edit
Приостановить rebase на этом commit. Вы можете сделать amend, разбить его на несколько commit или протестировать, а затем продолжить командой git rebase --continue. Это самый продвинутый глагол, и пользоваться им вы будете реже всего.
squash
Слить этот commit с тем, что выше, и открыть редактор, чтобы вы могли объединить два сообщения commit в одно.
fixup
Как squash, но сообщение этого commit полностью отбрасывается. Commit выше сохраняет своё сообщение без изменений. Это то, что нужно для commit вида "fix typo" -- их сообщения не несут никакой информации, достойной сохранения.
drop
Полностью удалить commit вместе с его изменениями. Удаление строки из todo list даёт тот же эффект, но запись drop делает ваше намерение явным.
Чистим branch, шаг за шагом
Теперь применим всё это к нашим 9 commit. Нужны две операции: изменение порядка и смена глаголов.
Сначала изменим порядок. Если посмотреть на diff, "oops forgot the css file" -- это на самом деле стили для чекбокса remember me, а не для сообщений об ошибках. В todo list изменить порядок -- значит просто переместить строку: вырежьте строку c9d0e1f и вставьте её сразу после группы remember me. Git применит commit в новом порядке.
Затем глаголы. Всё до "actually fix it" относится к форме логина, поэтому сворачивается в первый commit. Мы используем squash для "add form validation", потому что хотим включить его сообщение в итоговое, и fixup для шумовых commit, чьи сообщения заслуживают исчезновения. Вот отредактированный todo list:
pick a1b2c3d add login form skeleton
fixup b2c3d4e wip
squash c3d4e5f add form validation
fixup d4e5f6a fix typo
fixup e5f6a7b actually fix it
pick f6a7b8c add remember me checkbox
fixup a7b8c9d wip 2
fixup c9d0e1f oops forgot the css file
reword b8c9d0e add error messages
Сохраните и закройте. Git повторно применяет commit и останавливается дважды: один раз -- чтобы вы написали объединённое сообщение для первой группы (введите "Add login form with validation"), и один раз -- для reword (введите "Add error messages with styling"). Для commit с remember me быстрый reword тоже сработал бы -- здесь мы оставили его как pick, и его исходное сообщение уже было приличным. Результат:
$ git log --oneline main..feature/login
3f4e5d6 Add error messages with styling
2e3d4c5 Add remember me option
1d2c3b4 Add login form with validation
Девять commit превратились в три, каждый из которых самодостаточен и удобен для ревью. Обратите внимание, что все хеши изменились -- это совершенно новые commit. Запомните эту деталь; она лежит в основе золотого правила, о котором речь ниже.
Приём профессионалов: --autosquash
Когда вы освоитесь, вы можете готовить чистку прямо в процессе работы, а не разбираться со всем в конце. Когда вы исправляете что-то в более раннем commit, зафиксируйте это так:
git commit --fixup a1b2c3d
Git создаёт commit с сообщением fixup! add login form skeleton. Позже выполните:
git rebase -i --autosquash main
Git предзаполняет todo list, где каждый commit fixup! уже перемещён под свой целевой commit и помечен как fixup. Вам остаётся только сохранить и закрыть. Чтобы сделать это поведением по умолчанию, настройте один раз:
git config --global rebase.autosquash true
Золотое правило: никогда не делайте rebase общих commit
Rebase не изменяет commit -- он заменяет их новыми. Если коллеги уже получили старые commit через pull и построили на них свою работу, переписывание этих commit создаёт две расходящиеся истории, и следующий merge превращается в кашу из дубликатов и конфликтов.
Правило простое: делайте rebase только тех commit, которые ещё не были отправлены через push, или которые живут на branch, где работаете только вы. Чистка собственной feature-branch перед открытием pull request -- хрестоматийный безопасный случай. Если вы уже отправили branch через push и по-прежнему являетесь её единственным автором, отправьте переписанную историю командой git push --force-with-lease, которая откажется перезаписывать работу, которую кто-то другой успел запушить за это время. Никогда не переписывайте main или любую branch, из которой ваша команда делает pull.
Когда что-то идёт не так
Посреди rebase могут произойти две вещи. Если повторно применяемый commit конфликтует с более ранним изменением, Git останавливается и просит вас разрешить конфликт, затем выполнить git add для файлов и запустить git rebase --continue. А если в какой-то момент вы почувствуете, что запутались, есть полноценная кнопка катапультирования:
git rebase --abort
Эта команда возвращает branch ровно в то состояние, в котором она была до начала, без лишних вопросов. Пока вы помните о существовании --abort, незавершённый rebase никогда не загонит вас в ловушку.
А что, если rebase завершился, и вы поняли, что результат неверный? Даже тогда ничего не потеряно. Старые commit всё ещё существуют -- Git хранит их неделями, а reflog записывает каждую позицию, на которую когда-либо указывала ваша branch, так что вы можете вернуться к состоянию до rebase одним reset. Мы подробно разбираем эту страховку в статье о восстановлении потерянных commit с помощью git reflog. А если ваша ошибка меньше -- нужно изменить только последний commit -- rebase вообще не нужен: смотрите как отменить commit в Git.
Интерактивный rebase в GitSquid
Todo list -- мощный инструмент, но редактирование глаголов в текстовом файле -- именно та часть, которая заставляет людей нервничать. GitSquid заменяет её визуальным интерактивным rebase: вы меняете порядок commit с помощью drag & drop и выбираете действие для каждого commit -- pick, squash, fixup, reword или drop -- из выпадающего списка. Весь план виден целиком ещё до того, как что-либо запустится, и это снимает большую часть страха.
GitSquid помогает ещё до начала: Conflict Predictor, входящий в бесплатный план, показывает, какие файлы вызовут конфликты, до запуска rebase, так что вы знаете, на что подписываетесь, вместо того чтобы обнаруживать конфликты на полпути. Rebase доступен прямо из контекстных меню branch и commit на графе commit.
Краткий справочник
| Команда / глагол | Что делает |
|---|---|
git rebase -i HEAD~n |
Интерактивно переписать последние n commit |
pick |
Оставить commit как есть |
reword |
Сохранить изменения, отредактировать сообщение |
edit |
Остановиться на этом commit, чтобы сделать amend или разбить его |
squash |
Слить с предыдущим commit, объединить сообщения |
fixup |
Слить с предыдущим commit, отбросить это сообщение |
drop |
Полностью удалить commit |
git commit --fixup <hash> |
Зафиксировать исправление, предназначенное для более раннего commit |
git rebase -i --autosquash |
Автоматически расставить commit fixup! в todo list |
git rebase --continue |
Продолжить после разрешения конфликта |
git rebase --abort |
Отменить и восстановить состояние до rebase |
Интерактивный rebase вознаграждает небольшую практику большой силой. Начните с одноразовой branch, держите в уме --abort, соблюдайте золотое правило -- и превращение "wip, fix typo, actually fix it" в чистую историю станет рутиной. А если вы предпочитаете видеть план, а не редактировать текстовый файл, скачайте GitSquid бесплатно и попробуйте визуальный rebase на своей следующей feature-branch.