Git history: удобнее править старые коммиты
Меня всегда напрягало одно в Git: чтобы исправить опечатку в коммите трехнедельной давности, нужно танцевать с бубном вокруг rebase -i. Помните это чувство, когда вы открываете интерактивный rebase и внезапно осознаете, что одно неверное движение — и ваша ветка истории улетает в космос?
Так вот, в августе 2024 года в Git добавили команду, которая решает эту проблему изящнее. Называется она git history — и это одна из тех фичей, о которых знают единицы, хотя лежат они буквально у всех под носом.
Что за git history
git history — это экспериментальная команда, которая появилась в двух релизах Git: версия 2.54 (апрель 2024) принесла подкоманды reword и split, а версия 2.55 (июнь 2024) добавила fixup. Забавно, что каждый релиз получил порцию внимания в день выхода, а потом… тишина. И это при том, что команда уже сейчас дает многое из того, ради чего народ обычно смотрит в сторону альтернатив вроде jj.
Главное преимущество — атомарность. git history никогда не оставит ваше дерево в полу-сломленном состоянии. Как? Очень просто: команда просто откажется выполнять операцию, если есть риск конфликта. Меньше магии, чем в jj, но зато предсказуемо.
Три кита: fixup, reword, split
Fixup — чиним старые коммиты
Допустим, вы сделали коммит B, а потом поверх него еще несколько (C, D). И тут выясняется, что в B была ошибка — опечатка в сообщении логина, кривой форматирование, в общем, что-то, что нужно поправить.
Раньше вы бы делали git commit --fixup, потом git rebase -i --autosquash, молились, чтобы не было конфликтов, и надеялись, что ветки правильно встанут на место.
Теперь? Стадите исправление как обычно (git add), а потом:
git history fixup B
И всё. Магия в том, что git history не просто squashит изменения — он автоматически перенесет все ваши ветки, которые содержали этот коммит.
ДО git history fixup B
───────────────────────
feat-1
▼
main ── A ── B ── C ── D
▲
feat-2
ПОСЛЕ git history fixup B
──────────────────────────
feat-1
▼
main ── A ── B* ── C* ── D*
▲
feat-2
B* — это B с исправлением
C*, D* — пересозданы наверху
Ветки feat-1 и feat-2 поехали следом
Если быть точным, git history находит все локальные ветки, которые произошли от коммита B, и перемещает их вместе с исправлением. Это дальше, чем git rebase --update-refs, который двигает только рефы внутри диапазона активного rebase.
Есть ложка дегтя: команда не работает с merge-коммитами. Если вы активно их используете — это может стать проблемой.
Reword — правим сообщения
Иногда код остается тем же, а вот понимание задачи меняется. Особенно это случается в long-running-ветках, где дизайн эволюционирует, а commit message по умолчанию остается «WIP» или «fix stuff».
git history reword — это fixup, но для сообщений:
git history reword B
Команда откроет редактор с текущим сообщением коммита B, вы его исправите, сохраните — и всё остальное перестроится сверху. Работает чисто на графе коммитов, не трогая ваш working tree или индекс. Можно даже редактировать сообщение на коммите в ветке, которую сейчас не выбрана.
Split — разделяем коммиты
Бывает, что вы сделали коммит, в котором оказались два независимых изменения. Или, что хуже, сделали идеальный коммит, а потом вспомнили, что часть изменений относится к другой задаче.
git history split открывает интерактивный режим построчно:
git history split B
Вы видите diff коммита B и выбираете, какие куски войдут в первый новый коммит, а какие — во второй. Это эквивалент git add -p, но без необходимости делать reset и снова собирать историю.
ДО split: один коммит с двумя изменениями
─────────────────────────────────────────
main ── A ── B (рефакторинг + новый эндпоинт) ── C
ПОСЛЕ split: два отдельных коммита
──────────────────────────────────
main ── A ── B1 (рефакторинг)
B2 (новый эндпоинт) ── C*
Сравнение с jj
Тут нужно быть честным. jj (или jujutsu, если вам так привычнее) — это действительно другой уровень. Операционный лог с легким undo, модель working copy как коммита, возможность протаскивать конфликты через rebase — всё это git history не умеет и не пытается.
Автор статьи (Lalit Maganti) описывает свой опыт точно: каждые три месяца он пробует jj, неделю пытается встроить в рабочий процесс, а потом возвращается к git. Я себя узнаю в этом описании.
Но git history дает часть преимуществ jj без необходимости менять весь workflow. Не нужно ничего устанавливать — команда уже в вашем Git. Не нужно переучиваться — синтаксис знакомый. И работает атомарно, что снимает главный страх переписывания истории.
Практический вывод
git history — это не замена jj и не революция. Это эволюция внутри привычного Git, которая делает болезненные операции менее болезненными.
- Если вы работаете с несколькими ветками одновременно
- Если часто вспоминаете, что забыли что-то добавить в старый коммит
- Если хотите чистую историю без «oops fix typo» коммитов
Попробуйте git history fixup. Пара вечеров практики — и выйдете на новый уровень работы с историей.
Git потихоньку впитывает лучшие идеи из альтернатив. И это, как по мне, хороший знак.
Ссылки
- Оригинальная статья: The git history command — статья Lalit Maganti
- Документация Git 2.54 — заметки о релизе с reword и split
- Документация Git 2.55 — заметки о релизе с fixup
- jj (Jujutsu) — альтернативная система контроля версий
Дмитрий Полухин — продуктовый дизайнер. Пишу про разработку, AI и дизайн интерфейсов. Обо мне, контакты и профили.