Москва: проверенные б/у ноутбуки с гарантией и проверкой перед оплатой
+7 906 722-94-45

Главная → Статьи → Git rebase и история коммитов

Git rebase на практике: чистая история без сломанных веток

Когда rebase уместен: interactive rebase локальных веток, fixup/squash, rebase вместо merge, правило публичных веток, reflog как страховка и разрешение конфликтов.

Прямой ответ

Rebase пересаживает серию коммитов на новую базу, переписывая их хеши — отсюда главное правило, снимающее 90% проблем: ребейзить можно только свои локальные коммиты, которые никто не успел забрать (публичная ветка после rebase требует force-push — у коллег история разъезжается — «пропавшие» коммиты, дубли при merge; защита — --force-with-lease и политика репозитория против force-push в общие ветки). Рабочие сценарии, где rebase незаменим: interactive rebase локальной ветки перед PR (git rebase -i main — fixup мелких коммитов в осмысленные, переформулировка сообщений — ревьюер видит чистую последовательность вместо «fix typo, fix again, wip»); синхронизация с main (git pull --rebase / rebase main — свои коммиты поверх свежего main без merge-коммита — линейная история — стратегии ветвления); наведение порядка в собственных экспериментах до публикации. Страховка, которую стоит знать до, а не после: reflog (git reflog — журнал перемещений HEAD — любой «потерянный» при rebase коммит восстанавливается git reset --hard <hash> — rebase обратим, пока не истёк gc); и понимание: rebase не «лучше merge» — это другой инструмент (переписывание против слияния), и в релизных моделях с общей историей merge-коммиты — законная картина.

Interactive rebase: наведение порядка

Механика git rebase -i HEAD~5 (или -i main): git открывает список коммитов с командами — pick (оставить как есть), reword (изменить сообщение), squash (слить с предыдущим, объединив сообщения), fixup (слить, отбросив сообщение — «wip/fix typo» растворяются в основном коммите), edit (остановиться для правки содержимого), drop (удалить); порядок строк можно менять (git применит в новом — осторожно с зависимыми коммитами); exec — выполнить команду после коммита (прогон тестов между точками истории — проверка, что каждый коммит рабочей последовательности зелёный — бисект-дружественная история). Типовой паттерн перед PR: ветка из 12 коммитов («начал, поправил, опять поправил, merge main, фикс») сворачивается в 2–3 логических (подготовка интерфейса / реализация / тесты) — reword делает сообщения осмысленными (conventional-формат — релизы по коммитам работают только с осмысленными типами). Конфликты в interactive rebase решаются по коммиту (git rebase --continue после разрешения; --abort — полный откат к исходному состоянию — безопасный выход в любой момент); rerere (reuse recorded resolution) запоминает решения конфликтов — повторный rebase той же серии не спрашивает дважды.

Rebase против merge и pull-дисциплина

Когда что: rebase — личная ветка синхронизируется с main (линейно, конфликты решаются один раз по коммиту, история без «шума»); merge — ветка вливается в общую (сохраняет контекст «фича разрабатывалась параллельно» — merge-коммит как граница; в squash-модели PR — вообще один коммит — вопрос rebase/merge на вливании снимается платформой); merge для синхронизации долгоживущей публичной ветки (ветку видят другие — merge main в неё безопаснее rebase — своя копия истории коллег не ломается). Дисциплина git pull: дефолтный pull = fetch+merge порождает merge-коммиты «Merge branch 'main' of github.com:...» на каждом пуле — шум; git config pull.rebase true (или --rebase per-pull) — свои незапушенные коммиты поверх — линейность без усилий; pull.ff only — строгий вариант (дивергенция требует явного решения — не мерджить и не ребейзить молча). Особый случай — rebase с незакоммиченными изменениями (отказывается работать — stash перед, pop после — worktree-практика как альтернатива stash-жонглированию) и rebase коммитов с подписями/хуками (переписанные коммиты переподписываются; pre-commit-хуки при rebase — --no-verify осознанно, не по привычке — хуки).

Reflog и восстановление: rebase обратим

Сценарии «всё пропало» после rebase почти всегда лечатся: git reflog показывает перемещения HEAD (включая состояние до rebase — HEAD@{5}: rebase (start) и выше — исходная точка); восстановление — git reset --hard HEAD@{N} (ветка возвращается в доребейзное состояние целиком) или git branch rescue HEAD@{N} (спасательная ветка для разбора без риска). Отдельные случаи: «уронил коммит при rebase» — тот же reflog по hash; «поребейзил публичную ветку и запушил» — коллеги восстанавливаются из своей локальной копии (их fetch не затронул историю — git push --force-with-lease от них вернёт оригинал — аргумент против паники и за коммуникацию); gc-срок — reflog-записи живут ~90 дней (по умолчанию) — восстановление возможно долго, но не вечно. Профилактика тяжёлых случаев: бэкап-ветка перед большим rebase (git branch backup/pre-rebase — копия в одну команду, удаляется после успеха — дешевле любого восстановления); на сложных сериях — rebase по частям, не «12 коммитов через 3 месяца расхождения за раз» (частая синхронизация с main делает каждый rebase маленьким — короткие ветки снова выигрывают).

Практический сценарий: ветка-неделя перед PR

Исходник: feature-ветка прожила 6 дней — 19 коммитов («wip», «fix linter», два merge main, «revert предыдущего»), дифф 600 строк; ревьюер открывает — в истории не разобраться, конфликты с main накопились. Наведение (локально, ветка не пушилась — rebase безопасен): (1) git branch backup/pre-rebase — страховка; (2) git rebase -i main — план: wip/fix-коммиты — fixup в логические родители, два merge main растворяются (rebase поверх свежего main их устраняет естественно), «revert» схлопывается с отменённым (drop обоих — суммарный эффект ноль); (3) конфликты — три раза по мелочи (одна функция двигалась в main — rerere запомнил после первого); (4) результат — 4 коммита: «добавлен интерфейс X», «реализация Y», «тесты Z», «миграция схемы» — reword под conventional (feat/test — changelog-автоматика подхватит типы); (5) exec pytest -q между коммитами подтвердил — каждый коммит зелёный (бисект по ветке будет работать); (6) push, PR — ревьюер видит четыре осмысленных шага; замечания ревью — новые коммиты → финальный fixup-проход перед squash-мержем. Контрпример из той же команды: коллега поребейзил общую ветку интеграции и сделал force-push — у двоих «пропали» коммиты (восстановили из reflog за 10 минут, но час переписки и нервотрёпки) — правило «rebase только личного» после случая записали в CONTRIBUTING.

FAQ

rebase или squash-merge в платформе — не дублирование? Разные этапы: interactive rebase — наведение порядка внутри ветки до ревью (логические коммиты, читаемая история разработки); squash-merge платформы — схлопывание ветки в один коммит при вливании в main; вместе дают: осмысленные коммиты в ветке (для ревью и понимания) и чистый main (одна фича — один коммит); при squash-merge ветки история ветки в main не попадает — rebase ветки всё равно полезен для ревью.

Можно ли rebase после того, как ветку уже ревьюили? Да, с оговорками: платформа покажет diff прежней/новой версий (GitHub force-push history), но ревьюер теряет контекст «что изменилось с прошлого взгляда» частично — мелкие fixup по замечаниям допустимы, крупная перекройка после аппрува — согласуется; в squash-модели промежуточная история не критична — вопрос снимается.

Что делать, если rebase просит разрешить один конфликт несколько раз? Это коммиты, последовательно трогающие одно место — каждый применяется заново; варианты: включить rerere (запись и автоприменение решений), предварительно посхлопывать конфликтующие коммиты (squash до rebase — одно разрешение вместо пяти), или пройти через merge main (конфликт решается один раз — ценой merge-коммита).

Нужен ли rebase при работе через worktree? Worktree меняет раскладку рабочих копий, не механику истории — правила те же (локальное — можно, публичное — нет); удобство — rebase одной ветки, пока другая worktree-копия собрана и не тронута (параллельность без stash — worktree-практика).

Источники: git-rebase/git-reflog docs, Git Book (Rebasing) — ссылки в шапке.