Интерактивный rebase: squash, fixup, reword, drop

Берём пачку последних коммитов и переписываем её по своему вкусу: склеиваем, переименовываем, переставляем и выкидываем.

Интерактивный rebase (git rebase -i) открывает редактируемый список последних коммитов — «todo-лист», где напротив каждого коммита вы указываете, что с ним сделать.

Базовый rebase (перенос ветки на свежую вершину) мы уже разбирали в «Полезных приёмах». Здесь — про другой режим той же команды: не «перенести ветку», а хирургически отредактировать собственную историю перед тем, как показать её другим. Это главный инструмент, которым причёсывают ветку перед открытием Pull Request.

Зачем это на практике

Пока вы пилите фичу, история выглядит честно, но некрасиво: «черновик», «фикс», «опять фикс», «забыл точку с запятой», «WIP». Ревьюеру такое читать тяжело, а в main этот мусор останется навсегда. Интерактивный rebase позволяет из десятка хаотичных коммитов собрать два-три осмысленных, с внятными сообщениями — будто вы сразу писали аккуратно.

Запуск и todo-лист

Указываете, сколько последних коммитов хотите редактировать. Чаще всего через HEAD~N:

git rebase -i HEAD~4   # редактируем 4 последних коммита

Откроется редактор с примерно таким содержимым (коммиты идут сверху вниз — от старых к новым, это обратный порядок относительно git log):

pick a1b2c3d Добавить форму логина
pick b2c3d4e фикс
pick c3d4e5f опять фикс
pick d4e5f6g поправить отступы

# Rebase 9f8e7d6..d4e5f6g onto 9f8e7d6 (4 команды)
#
# p, pick   = использовать коммит как есть
# r, reword = использовать коммит, но изменить его сообщение
# e, edit   = остановиться на коммите для правки
# s, squash = влить в предыдущий, сообщения объединить
# f, fixup  = как squash, но сообщение этого коммита выбросить
# d, drop   = удалить коммит

Вы редактируете только левую колонку — слово-команду. Сами строки можно переставлять местами (это меняет порядок коммитов), а ненужные удалять (равносильно drop). Сохранили файл, закрыли редактор — git проигрывает ваш сценарий.

Команды по одной

reword — переименовать коммит

Меняем только сообщение, содержимое не трогаем. Ставим reword (или r):

reword b2c3d4e фикс

После сохранения git остановится и откроет редактор сообщения этого коммита — пишете нормальный текст, сохраняете, rebase продолжается сам.

squash и fixup — склеить коммиты

Оба объединяют коммит с предыдущим (тем, что выше). Разница — в судьбе сообщения. squash предложит отредактировать объединённое сообщение (оба текста показываются вместе). fixup молча выбрасывает сообщение склеиваемого коммита и берёт сообщение верхнего. Для коммитов-заплаток вроде «фикс» нужен именно fixup:

pick a1b2c3d Добавить форму логина
fixup b2c3d4e фикс
fixup c3d4e5f опять фикс
fixup d4e5f6g поправить отступы

Результат — один коммит «Добавить форму логина», вобравший все четыре изменения. Из мусорной истории получилась чистая.

edit — остановиться и поправить

Самый мощный режим. Git применит коммит и замрёт, отдав вам управление прямо в этой точке истории. Теперь можно изменить файлы, разбить коммит на части, что-то добавить. Когда закончили:

git add .
git commit --amend      # вписать правки в текущий коммит
git rebase --continue   # двигаться дальше по списку

drop — выбросить коммит

Помечаете drop (или просто удаляете строку) — коммит исчезает из истории вместе со своими изменениями. Удобно вычистить случайный коммит с отладочным console.log.

Переупорядочивание коммитов

Поменяйте строки местами в редакторе — и коммиты применятся в новом порядке. Это полезно, чтобы сгруппировать связанные правки рядом перед squash. Учтите: если переставленные коммиты трогают одни и те же строки файла, git может попросить разрешить конфликт — порядок изменений влияет на результат.

Разбить один коммит на два

Частый сценарий: в одном коммите случайно смешались две разные задачи. Ставим ему edit, и когда rebase остановится — расцепляем:

git reset HEAD~        # вернуть изменения коммита в рабочую директорию (история на шаг назад)
git add api.py
git commit -m "Добавить эндпоинт"
git add ui.py
git commit -m "Обновить интерфейс"
git rebase --continue

Был один коммит — стало два аккуратных.

Как это работает под капотом

Rebase не «двигает» коммиты — он создаёт их заново. Git берёт начальную точку (onto), а затем по вашему todo-листу применяет изменения один за другим, формируя на каждом шаге свежий коммит. Поэтому у всех переписанных коммитов меняются хеши: даже если содержимое идентично, это технически другие объекты — у них другой родитель, другое время, другой SHA-1. Старые коммиты при этом не исчезают сразу: какое-то время они остаются доступны через git reflog и служат страховкой, если rebase пошёл не туда.

Золотое правило rebase

Не переписывайте историю, которой уже пользуются другие.

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

Частые ошибки

  • Редактируют текст коммита вместо команды. В todo-листе меняют только левое слово (picksquash и т.п.), а не сам заголовок коммита. Для смены заголовка есть reword.
  • Путают направление склейки. squash/fixup вливают коммит в верхний (более старый). Если хотите слить два — пометьте fixup нижний из пары, а верхний оставьте pick.
  • Берут слишком большой диапазон. HEAD~30 на ветке, часть которой уже в общей истории, затронет опубликованные коммиты. Считайте, сколько коммитов реально ваши и незапушенные.
  • Запаниковали при конфликте и закрыли терминал. Если что-то пошло не так, всегда есть аварийный выход: git rebase --abort возвращает ветку ровно в исходное состояние, как будто rebase и не запускали.
  • Принудительно пушат через --force. После rebase для отправки нужен force-push, но безопаснее --force-with-lease (разберём в уроке про очистку истории).

Итоги

  • git rebase -i HEAD~N открывает todo-лист последних N коммитов: команда слева, коммит справа, порядок — от старых к новым.
  • reword — сменить сообщение, squash — склеить с сохранением текста, fixup — склеить и выбросить текст, edit — остановиться для правки, drop — удалить.
  • Перестановка строк меняет порядок коммитов; edit + git reset HEAD~ позволяет разбить коммит на несколько.
  • Rebase пересоздаёт коммиты с новыми хешами — поэтому только для незапушенной истории.
  • Что-то сломалось — git rebase --abort вернёт всё назад.
Проверьте себя
1. В каком порядке идут коммиты в todo-листе git rebase -i?
AСнизу вверх, от старых к новым
BСверху вниз, от старых к новым (обратно git log)
CВ случайном порядке
DТочно так же, как показывает git log
2. Чем fixup отличается от squash?
Afixup удаляет коммит целиком, а squash сохраняет
BНичем, это синонимы
CОба склеивают коммит с предыдущим, но fixup выбрасывает его сообщение, а squash предлагает объединить тексты
Dfixup работает только с последним коммитом
3. Что нужно сделать, если интерактивный rebase пошёл не так и хочется всё вернуть?
Agit rebase --continue
Bgit rebase --abort
Cgit reset --hard origin/main
DЗакрыть терминал и начать заново