amend и autosquash: аккуратная история

Учимся незаметно править коммиты: подшивать забытое в последний через amend и адресно — в любой старый через fixup + autosquash.

git commit --amend заменяет последний коммит новым, а связка --fixup + rebase --autosquash позволяет так же аккуратно поправить любой коммит в недавней истории.

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

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

Жизненный сценарий: сделали коммит, и через минуту замечаете опечатку в сообщении, забытый файл или лишний print. Заводить отдельный коммит «фикс предыдущего» — плодить мусор. Гораздо чище — дописать правку в тот самый коммит, будто опечатки и не было. Для последнего коммита это --amend, для коммита поглубже — autosquash.

git commit --amend

amend заменяет самый последний коммит. Два главных применения.

Поправить сообщение

git commit --amend -m "Добавить валидацию email"

Старое сообщение заменяется новым. Без -m откроется редактор с прежним текстом.

Дописать файлы в последний коммит

Забыли что-то добавить? Доделайте, застейджите и сделайте amend без правки сообщения:

git add forgotten.py
git commit --amend --no-edit   # подшить в последний коммит, сообщение не трогать

Коммит остаётся «один», но теперь включает и забытый файл. Флаг --no-edit говорит: сообщение оставить как есть.

Что amend делает на самом деле

amend не редактирует существующий коммит — коммиты в git неизменяемы. Он собирает новый коммит (старое содержимое + ваши добавки) и переставляет ветку на него; прежний коммит отцепляется и остаётся доступен лишь через reflog. Практический вывод тот же, что у rebase: хеш меняется. Значит, amend опубликованного коммита, которым уже пользуются другие, — нарушение золотого правила. amend хорош для последнего, ещё не запушенного коммита.

--fixup и --squash: адресная заплатка

amend чинит только верхушку. А если опечатка в коммите, под которым уже три новых? Делать rebase -i и руками тащить заплатку наверх — муторно. Решение: создать специальный коммит-заплатку, помеченный целевым коммитом.

Сначала узнаём хеш проблемного коммита (git log --oneline), затем коммитим исправление с привязкой к нему:

git add login.py
git commit --fixup a1b2c3d   # заплатка к коммиту a1b2c3d

git автоматически создаст коммит с сообщением fixup! <заголовок целевого коммита>. Аналогично --squash a1b2c3d создаёт squash!-коммит — разница та же, что у fixup и squash в rebase: squash даёт объединить тексты сообщений, fixup сообщение заплатки выбрасывает.

rebase --autosquash: автоматическая сборка

Заплатки накопились в конце ветки. Теперь одной командой расставляем их по местам:

git rebase -i --autosquash HEAD~6

git сам распознаёт префиксы fixup! / squash!, переставляет каждую заплатку прямо под её целевой коммит и проставляет ей действие fixup/squash. В редакторе todo-лист уже разложен правильно — обычно остаётся просто сохранить:

pick    a1b2c3d Форма логина
fixup   e5f6g7h fixup! Форма логина
pick    b2c3d4e Профиль пользователя
squash  f6g7h8i squash! Профиль пользователя

После сохранения заплатки бесшумно вливаются в свои коммиты. Чтобы не дописывать --autosquash каждый раз, включите его глобально:

git config --global rebase.autosquash true

Полный рабочий цикл

Как это выглядит в реальной работе над PR:

git commit -m "Форма логина"     # основной коммит
git commit -m "Профиль"          # ещё работа поверх
# ревью нашло баг в форме логина
git add login.py
git commit --fixup a1b2c3d       # заплатка, привязанная к нужному коммиту
git rebase -i --autosquash HEAD~3  # заплатка сама встаёт на место и вливается

История остаётся идеально чистой, хотя баг чинился задним числом.

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

Никакой магии: --fixup — это просто соглашение об именовании. Коммит-заплатка — обычный коммит, чьё сообщение начинается с fixup! и точного заголовка цели. --autosquash при старте rebase сканирует сообщения, находит такие префиксы, сопоставляет их с заголовками обычных коммитов в диапазоне и заранее переставляет/помечает строки todo-листа. То есть autosquash экономит вам ровно ту ручную работу, которую вы делали бы в rebase -i сами. И, как всякий rebase, он пересоздаёт затронутые коммиты с новыми хешами.

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

  • amend запушенного коммита, затем обычный push. Хеш сменился — push отвергнут или, хуже, создаст расхождение. Для уже отправленной ветки нужен осознанный --force-with-lease, а для общей — лучше вовсе не amend'ить.
  • amend с непустым индексом «не туда». amend подтягивает всё застейдженное в последний коммит. Если в индексе лежит лишнее, оно тоже окажется в коммите — проверьте git status до amend.
  • Ошиблись хешем в --fixup. autosquash просто не найдёт цель и оставит заплатку как обычный fixup!-коммит — не страшно, поправьте хеш и повторите.
  • Забыли --autosquash при rebase. Без флага git не распознает fixup!-коммиты и оставит их отдельными строками pick. Либо добавляйте флаг, либо включите rebase.autosquash в конфиге.
  • Используют --no-edit, хотя сообщение пора обновить. Если правка меняет смысл коммита, дайте отредактировать сообщение — не прячьте крупные изменения за старым заголовком.

Итоги

  • git commit --amend заменяет последний коммит: -m правит сообщение, --no-edit + предварительный git add подшивает забытые файлы.
  • amend и autosquash пересоздают коммиты и меняют хеши — только для незапушенной истории.
  • git commit --fixup <хеш> создаёт привязанную заплатку к старому коммиту, не требуя ручного rebase прямо сейчас.
  • git rebase -i --autosquash сам расставляет fixup!/squash!-коммиты по местам и вливает их.
  • git config --global rebase.autosquash true делает это поведение поведением по умолчанию.
Проверьте себя
1. Вы забыли добавить файл в только что сделанный коммит. Как подшить его, не меняя сообщение?
Agit add forgotten.py && git commit --amend --no-edit
Bgit commit -m 'фикс'
Cgit revert HEAD
Dgit reset --hard HEAD~1
2. Что делает git commit --fixup a1b2c3d?
AСразу вливает изменения в коммит a1b2c3d
BСоздаёт коммит-заплатку с сообщением 'fixup! <заголовок a1b2c3d>', который autosquash потом поставит на место
CУдаляет коммит a1b2c3d
DОткатывает историю до a1b2c3d
3. Почему amend нельзя применять к уже опубликованному коммиту?
Aamend работает только локально технически
Bamend пересоздаёт коммит с новым хешем, что расходится с историей у коллег
CGitHub блокирует amend
DПосле amend нельзя пушить совсем