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делает это поведение поведением по умолчанию.