git rebase: как работает, интерактивный режим и squash коммитов
git rebase переносит коммиты текущей ветки на новую базу: Git временно снимает ваши коммиты, переставляет ветку на конец указанной и применяет их заново как новые коммиты. Так получают линейную историю без merge-коммитов, а через git rebase -i коммиты можно склеить (squash), переименовать или удалить; переписывать так можно только коммиты, которые ещё не опубликованы для других.
Что делает git rebase
git rebase «пересаживает» вашу ветку на другую точку истории. Допустим, вы ответвили feature от main неделю назад, а main с тех пор ушёл вперёд. После rebase ваша ветка выглядит так, будто вы начали работу только что — от самого свежего коммита main.
git switch feature
git fetch origin
git rebase origin/main
Под капотом происходит три шага. Git находит общего предка веток, по очереди сохраняет изменения каждого вашего коммита после него, передвигает ветку на origin/main и применяет сохранённые изменения заново, один за другим. Получаются новые коммиты с тем же содержанием и сообщениями, но с другими хешами.
До: A---B---C---F---G main
\
D---E feature
После: A---B---C---F---G main
\
D'---E' feature
Штрихи у D' и E' — не украшение. Это другие объекты с другими хешами, у них другой родитель. Отсюда главное правило rebase, о котором ниже.
Золотое правило: не перебазируйте опубликованное
Rebase переписывает историю. Пока коммиты есть только у вас, это безопасно. Но если ветку уже скачали коллеги, после rebase у них останутся старые D и E, у вас — новые D' и E', и при следующем pull история задвоится, а конфликты придётся решать повторно.
Практическое правило: rebase применяют к своим локальным веткам и к личным feature-веткам, в которых работает один человек. Общие ветки — main, develop, релизные — не перебазируют никогда.
Как отправить ветку после rebase
Если ветка уже лежала на сервере, обычный git push будет отклонён: локальная история больше не продолжает удалённую. Нужен принудительный push, причём в безопасном варианте:
git push --force-with-lease
В отличие от --force, флаг --force-with-lease откажется перезаписывать ветку, если на сервере появились коммиты, которых вы не видели. Так вы не сотрёте чужую работу случайно.
Конфликты при rebase
Поскольку коммиты применяются по одному, конфликт может возникнуть на любом из них — и даже несколько раз подряд. Git останавливается и сообщает, какой коммит не удалось применить.
$ git rebase origin/main
Auto-merging src/api.js
CONFLICT (content): Merge conflict in src/api.js
error: could not apply 4e1f9a2... Добавить кэш запросов
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
Порядок действий:
- Откройте конфликтный файл, выберите правильный вариант и уберите маркеры
<<<<<<<,=======,>>>>>>>. - Отметьте файл как решённый:
git add src/api.js. Коммит делать не нужно. - Продолжите:
git rebase --continue. Git применит следующий коммит.
Остальные команды управления процессом:
| Команда | Что делает |
|---|---|
git rebase --continue | Продолжить после разрешения конфликта |
git rebase --skip | Пропустить текущий коммит целиком (его изменения потеряются) |
git rebase --abort | Прервать rebase и вернуть ветку в исходное состояние |
Обратите внимание на путаницу с «нашей» и «их» стороной. Во время rebase HEAD — это база, на которую вы переносите ветку, а «входящие» изменения — ваши собственные коммиты. Поэтому git checkout --ours при rebase берёт версию из main, а --theirs — вашу. При merge всё наоборот.
Повторяющиеся конфликты: rerere
Если вы регулярно перебазируете долгую ветку и снова и снова решаете один и тот же конфликт, включите git config --global rerere.enabled true. Git запомнит, как вы разрешили конфликт, и в следующий раз применит решение сам.
Интерактивный rebase: правим историю перед публикацией
Самое ценное в rebase — интерактивный режим. Он позволяет привести в порядок свои коммиты, прежде чем показывать их на ревью: склеить «fix», «fix typo», «ещё фикс» в один осмысленный коммит, переписать сообщения, поменять порядок или выбросить лишнее.
git rebase -i HEAD~4 # последние 4 коммита
git rebase -i origin/main # все коммиты ветки, которых нет в main
Git откроет редактор со списком коммитов — от старых к новым (в отличие от git log):
pick 1a2b3c4 Добавить форму входа
pick 5d6e7f8 fix
pick 9a0b1c2 Валидация e-mail
pick 3d4e5f6 опечатка в валидации
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash" but keep only the previous commit's log message
# d, drop = remove commit
Меняете слово в начале строки, сохраняете и закрываете редактор — Git выполнит план сверху вниз.
| Команда | Эффект |
|---|---|
pick | Оставить коммит как есть |
reword | Оставить изменения, но переписать сообщение |
edit | Остановиться на коммите, чтобы изменить его содержимое (git commit --amend) или разбить на несколько |
squash | Влить в предыдущий коммит, сообщения объединить и отредактировать |
fixup | Влить в предыдущий коммит, сообщение этого коммита отбросить |
drop | Удалить коммит вместе с изменениями |
Перестановка строк меняет порядок коммитов, удаление строки равносильно drop.
Git squash: как объединить коммиты в один
Отдельной команды git squash в Git нет. «Сквошить» коммиты можно тремя способами, и интерактивный rebase — самый гибкий из них.
Способ 1. Интерактивный rebase
Для примера выше превратим четыре коммита в два аккуратных:
pick 1a2b3c4 Добавить форму входа
fixup 5d6e7f8 fix
pick 9a0b1c2 Валидация e-mail
fixup 3d4e5f6 опечатка в валидации
Первый коммит в группе должен остаться pick: squash и fixup вливают коммит в предыдущий, и самому первому вливаться некуда.
Способ 2. fixup-коммиты и autosquash
Если заранее знаете, какой коммит правите, создайте исправление сразу помеченным:
git commit --fixup=1a2b3c4
git rebase -i --autosquash origin/main
Git сам поставит fixup-коммит под нужный и пометит его как fixup — останется только сохранить план. Включить это поведение по умолчанию можно через git config --global rebase.autoSquash true.
Способ 3. Сжать всю ветку в один коммит
git reset --soft $(git merge-base HEAD origin/main)
git commit -m "Вход по e-mail с валидацией"
reset --soft откатывает указатель ветки к точке ответвления, но все изменения оставляет в индексе — остаётся сделать один коммит. Ещё вариант — git merge --squash при слиянии в основную ветку или кнопка «Squash and merge» в pull request.
Полезные варианты rebase
git pull --rebase
Обычный pull сливает удалённую ветку в вашу и создаёт merge-коммит, если вы коммитили параллельно с коллегами. git pull --rebase вместо этого переставляет ваши локальные коммиты поверх свежих удалённых. Сделать поведением по умолчанию: git config --global pull.rebase true. Это та самая безопасная зона rebase — ваши локальные коммиты ещё никто не видел.
--onto: перенести часть ветки
Вы ответвили feature-b от feature-a, а теперь хотите, чтобы feature-b шла прямо от main, без коммитов feature-a:
git rebase --onto main feature-a feature-b
Читается так: взять коммиты feature-b, которых нет в feature-a, и пересадить на main.
--exec: проверить каждый коммит
git rebase -i --exec "npm test" origin/main
После каждого применённого коммита запустятся тесты; если они упадут, rebase остановится на проблемном коммите. Удобно, когда важно, чтобы каждый коммит истории собирался.
--update-refs: стопка зависимых веток
Если вы держите цепочку веток, зависящих друг от друга, флаг --update-refs (Git 2.38+) при перебазировании верхней ветки передвинет и промежуточные.
Как отменить rebase
Пока процесс идёт — git rebase --abort. Если rebase уже завершён и результат не понравился, старые коммиты никуда не делись: их помнит журнал ссылок.
$ git reflog
e5f6a7b HEAD@{0}: rebase (finish): returning to refs/heads/feature
e5f6a7b HEAD@{1}: rebase (pick): Валидация e-mail
...
c3d4e5f HEAD@{5}: commit: опечатка в валидации
$ git reset --hard HEAD@{5}
Найдите в git reflog запись до начала rebase и сбросьте ветку на неё. Сразу после rebase работает и короткий путь: git reset --hard ORIG_HEAD. Reflog хранится локально и по умолчанию держит записи около 90 дней, так что паниковать после неудачного rebase не нужно.
Типичные ошибки
- Перебазировали общую ветку и сделали force push — у коллег расползлась история. Исправление: договориться, восстановить ветку из reflog того, у кого она правильная, и запретить force push в защищённые ветки на сервере.
- Используют
--forceвместо--force-with-leaseи затирают коммит, который коллега успел запушить в ту же ветку. - Squash в первой строке интерактивного плана — Git ответит ошибкой «cannot 'squash' without a previous commit».
- Начали rebase с грязным рабочим деревом. Git откажется запускаться. Закоммитьте правки, уберите их в
git stashили используйтеgit rebase --autostash. - Решили конфликт и сделали
git commitвместоgit rebase --continue. Получится лишний коммит; после--continueего можно убрать повторным интерактивным rebase.
Итог
git rebase переносит ваши коммиты на новую базу и создаёт их заново, поэтому история получается линейной и чистой. Интерактивный режим превращает черновые коммиты в аккуратную историю: squash и fixup склеивают правки, reword исправляет сообщения, drop убирает лишнее. Главное правило одно — перебазируйте только то, что не успели опубликовать для других, а после rebase своей удалённой ветки отправляйте её через --force-with-lease. Работа с rebase и чистка истории коммитов разобраны на практике в курсе по Git.
Частые вопросы
Меняет ли git rebase содержимое кода?
Итоговое содержимое файлов при отсутствии конфликтов будет тем же, что и при слиянии, но сами коммиты пересоздаются с новыми хешами и новыми родителями. Если при конфликте вы выбрали другой вариант, промежуточные коммиты будут отличаться от исходных.
Как перебазировать ветку на самый первый коммит репозитория?
Используйте git rebase -i --root: в план попадут все коммиты, включая корневой. Это нужно редко, например чтобы переписать сообщение самого первого коммита.
Сохраняет ли rebase автора и дату коммитов?
Автор и дата авторства сохраняются, а коммиттер и дата коммита меняются на текущие. Флаг --committer-date-is-author-date оставляет дату коммита равной дате авторства.
Можно ли перебазировать merge-коммиты?
По умолчанию rebase выпрямляет историю и отбрасывает merge-коммиты. Чтобы сохранить структуру слияний, используйте git rebase --rebase-merges.
Как узнать, что rebase сейчас в процессе?
git status покажет строку interactive rebase in progress или You are currently rebasing и подскажет, какие команды доступны. Многие оболочки и IDE также показывают это в строке приглашения.
Эта тема подробно разобрана с практикой в курсе «Git с нуля до уверенного разработчика».
Перейти к курсу