git rebase: порядок в локальных ветках
Самый частый вопрос, с которым я сталкиваюсь в разговорах про git: «Зачем нужен rebase и как им пользоваться?»
Основная задача rebase — наведение порядка.
Если вы часто делаете коммиты-фиксы, а потом ужасаетесь, какой бардак
творится в ветке — эта команда станет вашей любимой.
Важно! Rebase стоит делать, только если вы ещё не запушили свои изменения
в удалённую ветку.
Эта операция переписывает SHA (id) коммитов, поэтому другие пользователи
не смогут так просто спулить ваши правки.
Давайте посмотрим, как можно «причесать» историю.
Изменить сообщение к коммиту

Сообщения к коммитам ничем не отличаются от комментариев в коде.
Когда через полгода–год вам понадобится разобраться в своей (или чужой)
работе, хорошие объяснения здорово сэкономят время.
Проблема в том, что иногда нам некогда писать подробные комментарии.
А иногда мы к тому же делаем опечатки.
Если перед мёрджем/отправкой пулл-реквеста у вас нашлась минутка,
помогите будущему себе и коллегам-разработчикам — поправьте свои сообщения.
Допустим, у нас есть репозиторий Х с такой историей:
$ git log -5 --pretty=%s --graph
* Ipdate docs
* Fix
* Add simple benchmarks
* Fix some bug
* New spinner
Изменить комментарий к последнему коммиту можно с помощью amend
:
$ git commit --amend -m 'Update docs'
Если же поправить нужно не самый последний коммит
(или сразу несколько), нам поможет reword
.
Для начала определимся, какие коммиты нужно отредактировать.
В нашем случае это первый, второй и четвёртый.
Просим гит начать rebase, взяв за основу последние 4 коммита:
$ git rebase -i HEAD~4
О различиях между HEAD~
и HEAD^
можно прочитать на git-scm.com.
В ответ на это откроется текстовый редактор:
pick Fix some bug
pick Add simple benchmarks
pick Fix
pick Ipdate docs
Обратите внимание на обратный порядок: сверху идут старые коммиты,
снизу — свежие.
Заменяем у нужных коммитов pick
на r
или reword
:
r Fix some bug
pick Add simple benchmarks
r Fix
r Ipdate docs
И поочерёдно правим каждое сообщение.
В итоге получим такую историю:
$ git log -5 --pretty=%s --graph
* Update docs
* Fix #42: Notifications in IE
* Add simple benchmarks
* Fix #132: URL parser
* New spinner
Удалить ненужный коммит
Бывает, что вы сделали какую-то работу, но она становится не нужна.
Или вы хотите вынести пару коммитов, сделанных неделю назад,
в отдельную ветку, а из текущей удалить.
Последние N коммитов легко убрать с помощью reset
.
Например, удалить последний коммит:
$ git reset --hard HEAD~
Удалить последние 7:
$ git reset --hard HEAD~6
Если же нужный коммит затесался между другими, запускаем rebase
:
$ git rebase -i HEAD~3
pick Add simple benchmarks
pick Fix #42: Notifications in IE
pick Update docs
И просто удаляем ненужную строчку (для нашего примера возьмём самую верхнюю):
pick Fix #132: one more try
pick Update docs
$ git log --pretty=%s --graph
* Update docs
* Fix #42: Notifications in IE
* Fix #132: URL parser
* New spinner
Объединить несколько коммитов в один
I need a script which, when I am making a Git commit, will automatically use the previous commit’s message, suffixed with with “for real.”
— Jeff Croft (@jcroft) June 21, 2013
Наверняка у вас бывало такое:
$ git log --pretty=%s --graph
* WTF #4
* Another fix for #9
* One more fix for #4
* Fixed #9
* Finally fixed #4
* Fixed #4
Вроде поправили баг, но нашлось что-то новое.
И ещё раз.
И опять.
С одной стороны, можно оставить всё как есть.
С другой, если вам когда-нибудь понадобится откатить изменения,
откатывать придётся всю пачку коммитов вместо одного.
Да и история выглядит не очень аккуратно.
git commit --amend
— хорошая команда, но подходит
только для обновления последнего коммита.rebase
, в свою очередь, умеет объединять несколько коммитов,
даже если они не следуют друг за другом по порядку.
Например, сделать из 6 коммитов 2 довольно просто.
Запускаем rebase
:
$ git rebase -i HEAD~6
pick Fixed #4
pick Finally fixed #4
pick Fixed #9
pick One more fix for #4
pick Another fix for #9
pick WTF #4
В гите есть два вида слияния: fixup
и squash
.
Разница между ними в том, что squash
склеивает все комментарии
в один, а fixup
использует только комментарий основного коммита (того, в
который вливаются остальные).
Если вы попросите гит смёрджить коммит, по умолчанию он вольёт его в предыдущий.
Так как нас это не устраивает, сначала поменяем очерёдность коммитов.
Для этого просто меняем строчки местами:
pick Fixed #4
pick Finally fixed #4
pick One more fix for #4
pick WTF #4
pick Fixed #9
pick Another fix for #9
Следующее действие похоже на переименование: мы заменяем pick
на f
(fixup
) или s
(squash
), в зависимости от нужного результата.
pick Fixed #4
f Finally fixed #4
f One more fix for #4
f WTF #4
pick Fixed #9
s Another fix for #9
Если вы выбрали squash
, гит даст вам ещё одну возможность
отредактировать сообщение для итогового коммита.
В результате получаем красивую историю:
$ git log --pretty=%s --graph
* Fixed #9
* Fixed #4
Автоматизировать слияние
Процесс объединения можно упростить.
Допустим, ваша история сейчас состоит из двух коммитов:
$ git log --pretty=%s --graph
* Fixed #9
* Fixed #4
Вы поправили пару файлов и хотите влить эти изменения в коммит Fixed #4
.
Чтобы не заниматься ручной перестановкой, можно сделать так:
$ git commit --fixup=123456
123456
в данном случае = SHA коммита, в который мы хотим влить изменения.
Гит создаст коммит с сообщением fixup! Fixed #4
.
Сделаем ещё несколько изменений, но на этот раз подготовим коммит для сквоша:
$ git commit --squash=567890
Если всё готово, запускаем rebase:
$ git rebase -i --autosquash HEAD~4
pick Fixed #4
fixup fixup! Fixed #4
pick Fixed #9
squash squash! Fixed #9
Смотрите: гит самостоятельно расставил все коммиты в нужном порядке и заодно
заменил флаг pick
на нужный.
История получится такая же чистая:
$ git log --pretty=%s --graph
* Fixed #9
* Fixed #4
Разделить один коммит на несколько
Противоположная предыдущей ситуация: вы сделали кучу изменений, закрыли 8 тасков, но накидали это всё в 2 коммита:
$ git log --pretty=%s --graph
* Update tests, fix #6, #7, #8
* Fix #1, #2, #3, #4, #5
Откатить их в случае необходимости будет сложно — слишком много зависимостей.
Да и ревьюеру кода не позавидуешь.
Решение простое: разделяем коммиты на логические части:
$ git rebase -i HEAD~2
pick Fix #1, #2, #3, #4, #5
pick Update tests, fix #6, #7, #8
Отмечаем коммит, который хотим разбить, словом edit
:
edit Fix #1, #2, #3, #4, #5
pick Update tests, fix #6, #7, #8
Гит откатится к коммиту Fix #1...
и остановится.
Далее у нас полная свобода действий.
Отменяем коммит:
$ git reset HEAD~
Выборочно добавляем файлы, связанные с первым таском:
$ git add ./task-1-file.txt
Делаем первый коммит:
$ git commit -m 'Fix #1: Update docs'
Повторяем по вкусу:
$ git add ./task-2-file.txt
$ git commit -m 'Fix #2: New notification system'
...
Когда результат нас удовлетворит, возобновляем rebase:
$ git rebase --continue
Повторив эти шаги несколько раз, можно получить в меру подробную историю:
$ git log --pretty=%s --graph
* Update tests
* Fix #9
* Fix #8
* Fix #7
...
Заключение
Rebase — чудесный и мощный инструмент.
Главное — не бояться.
Описанные выше приёмы помогут вам сделать историю понятной,
а поиск по ней лёгким.
Попробуйте применить их перед следующим пушем.