Модуль 5: Перебазирование (Rebase)
Rebase — один из самых мощных и в то же время «опасных» инструментов Git. Он позволяет переписать историю коммитов, сделав её линейной и чистой. Но с силой приходит ответственность: неаккуратный rebase может создать проблемы всей команде.
В этом модуле мы разберём rebase досконально: когда его использовать, когда категорически нельзя, и как пошагово выполнять интерактивный rebase для «уборки» истории.
5.1 Rebase vs Merge: когда что использовать
И merge, и rebase решают одну задачу — объединение изменений из разных веток. Но делают это принципиально по-разному.
Merge: сохраняем историю как она есть
Merge создаёт новый коммит с двумя родителями. История остаётся правдивой — видно, когда ветка была создана, когда слита, какие коммиты туда входили.
Граф после merge
main: A --- B --- C --------- M
\ /
feature: D --- E --- F
Merge-коммит M явно показывает точку слияния.
Rebase: переписываем историю в линию
Rebase берёт ваши коммиты и «переигрывает» их поверх другой ветки. Старые коммиты заменяются новыми (с новыми хешами), но с тем же содержимым.
Граф после rebase
До rebase:
main: A --- B --- C
\
feature: D --- E --- F
После git rebase main (находясь в feature):
main: A --- B --- C
\
feature: D' --- E' --- F'
(D', E', F' — те же изменения, но новые коммиты с новыми хешами)
# Находимся в feature-ветке
git switch feature/auth
# Перебазируем поверх main
git rebase main
# Теперь feature/auth «начинается» от последнего коммита main
# История линейная, без ветвлений
Сравнительная таблица
Merge vs Rebase
- Merge: сохраняет полную историю, создаёт merge-коммит, безопасен для публичных веток
- Rebase: линейная история, нет merge-коммитов, переписывает хеши — опасен для публичных веток
- Merge: подходит для интеграции больших feature-веток, командной работы
- Rebase: подходит для «уборки» локальных веток перед PR, синхронизации с main
Золотое правило rebase: Никогда не делайте rebase веток, которые уже опубликованы и используются другими разработчиками. Rebase переписывает хеши коммитов — у коллег, которые основали свою работу на старых коммитах, всё сломается.
5.2 Интерактивный rebase: редактор истории
Интерактивный rebase (git rebase -i) — самый мощный режим. Он открывает редактор, в котором вы можете переставлять, объединять, удалять и редактировать коммиты.
Запуск интерактивного rebase
# Редактировать последние 5 коммитов
git rebase -i HEAD~5
# Или: все коммиты от точки ответвления от main
git rebase -i main
Что вы увидите в редакторе
Git откроет текстовый файл примерно такого содержания:
pick a1b2c3d feat: добавить модель пользователя
pick e4f5g6h fix: опечатка в модели
pick i7j8k9l wip: промежуточное сохранение
pick m0n1o2p feat: добавить API авторизации
pick q3r4s5t fix: исправить импорт
# Rebase abc1234..q3r4s5t onto abc1234 (5 commands)
#
# Commands:
# p, pick = использовать коммит как есть
# r, reword = использовать коммит, но изменить сообщение
# e, edit = использовать коммит, но остановиться для изменения
# s, squash = использовать коммит, объединить с предыдущим
# f, fixup = как squash, но отбросить сообщение этого коммита
# d, drop = удалить коммит
Пошаговый пример: чистим историю
Допустим, у нас 5 коммитов и мы хотим: объединить «wip» и «fix: опечатка» с основными коммитами, переименовать последний коммит.
# Шаг 1: Запускаем интерактивный rebase
git rebase -i HEAD~5
# Шаг 2: Редактируем файл (меняем pick на нужные команды):
pick a1b2c3d feat: добавить модель пользователя
fixup e4f5g6h fix: опечатка в модели # ← объединить с предыдущим
fixup i7j8k9l wip: промежуточное сохранение # ← объединить с предыдущим
pick m0n1o2p feat: добавить API авторизации
reword q3r4s5t fix: исправить импорт # ← изменить сообщение
# Шаг 3: Сохраняем и закрываем редактор
# Git начнёт переигрывать коммиты
# Шаг 4: Для reword Git откроет ещё один редактор
# Пишем новое сообщение: "fix: исправить импорт в модуле авторизации"
# Результат: было 5 коммитов, стало 3 чистых
Подробно о каждой команде
pick — оставить как есть
Коммит используется без изменений. Порядок pick определяет порядок коммитов — можно переставлять строки!
reword — изменить сообщение
Содержимое коммита не меняется, но Git откроет редактор для нового сообщения. Используйте, когда сообщение неинформативное (fix stuff → fix: исправить валидацию email).
edit — остановиться и изменить
Git применит коммит и остановится. Вы можете изменить файлы, разбить коммит на несколько, добавить файлы. После правок: git rebase --continue.
squash — объединить с предыдущим
Содержимое коммита объединяется с предыдущим. Git откроет редактор с обоими сообщениями — выберите итоговое.
fixup — объединить, отбросить сообщение
Как squash, но сообщение текущего коммита отбрасывается. Остаётся только сообщение предыдущего коммита. Идеально для мелких фиксов.
drop — удалить коммит
Коммит полностью удаляется из истории. Осторожно: если последующие коммиты зависят от удалённого, возникнут конфликты.
Проверяем результат
# Смотрим, что получилось
git log --oneline
# Было:
# q3r4s5t fix: исправить импорт
# m0n1o2p feat: добавить API авторизации
# i7j8k9l wip: промежуточное сохранение
# e4f5g6h fix: опечатка в модели
# a1b2c3d feat: добавить модель пользователя
# Стало:
# x1y2z3w fix: исправить импорт в модуле авторизации
# m0n1o2p feat: добавить API авторизации
# a1b2c3d feat: добавить модель пользователя
5.3 Конфликты при rebase
При rebase конфликты работают иначе, чем при merge. Поскольку Git «переигрывает» каждый коммит по очереди, конфликт может возникнуть на любом из них — и вам придётся разрешать его для каждого коммита отдельно.
Как выглядит процесс
# Начинаем rebase
git rebase main
# CONFLICT (content): Merge conflict in app.py
# error: could not apply a1b2c3d... feat: добавить валидацию
# Resolve all conflicts manually, mark them as resolved with
# "git add/rm <conflicted_files>", then run "git rebase --continue".
# Шаг 1: Разрешаем конфликт в файле
# (открываем app.py, убираем маркеры, выбираем правильную версию)
# Шаг 2: Помечаем как разрешённый
git add app.py
# Шаг 3: Продолжаем rebase (переходим к следующему коммиту)
git rebase --continue
# Если возникнет ещё конфликт — повторяем шаги 1-3
# Если этот коммит не нужен — пропускаем:
git rebase --skip
Экстренная отмена
# Отменить rebase и вернуться к состоянию до него
git rebase --abort
# Всё вернётся как было. Никаких последствий.
Совет: Если при rebase возникает много конфликтов, возможно, ваша ветка слишком сильно разошлась с main. В таком случае проще сделать обычный merge, а rebase использовать для коротких веток.
Важное отличие от merge: при merge-конфликте вы разрешаете все конфликты за один раз. При rebase — по одному коммиту за раз. Это может быть утомительно при 20 коммитах, но зато даёт более точный контроль.
5.4 Правила безопасного rebase
Rebase переписывает историю — создаёт новые коммиты с новыми хешами. Это безопасно для локальных веток, но опасно для публичных.
Правило 1: Не перебазируйте публичные ветки
Если вы запушили ветку и кто-то на ней работает — rebase запрещён. Коллеги построили свою работу на ваших коммитах. Если вы перепишете их, у коллег будут дубликаты и конфликты.
# ХОРОШО: rebase своей локальной ветки перед push
git switch feature/my-work
git rebase main
git push -u origin feature/my-work # первый push
# ПЛОХО: rebase после push (коллеги уже видят вашу ветку)
git rebase main
git push --force # ← ОПАСНО! Ломает историю у коллег
# КОМПРОМИСС: если вы единственный на ветке
git push --force-with-lease # безопаснее: откажет, если кто-то успел запушить
Правило 2: Обновляйте базу перед rebase
# Всегда сначала получите свежую версию main
git fetch origin
# Потом перебазируйте
git rebase origin/main
Правило 3: Используйте pull --rebase для синхронизации
# Вместо git pull (который делает merge):
git pull --rebase origin main
# Или настройте по умолчанию:
git config --global pull.rebase true
# Теперь git pull будет делать rebase вместо merge
Шпаргалка: когда rebase безопасен
- Безопасно: локальная ветка, ещё не запушена
- Безопасно: ваша личная ветка, только вы на ней работаете (push --force-with-lease)
- Опасно: ветка, на которой работают другие
- Запрещено: main, develop, release — общие ветки
5.5 Практика: «чистим» историю перед PR
Типичный сценарий: вы работали над фичей 3 дня, сделали 12 коммитов, среди которых wip, fix typo, oops. Перед созданием PR нужно навести порядок.
Пошаговый walkthrough
# Смотрим текущую историю
git log --oneline
# f1a2b3c fix: ещё раз поправить тесты
# d4e5f6g fix: поправить тесты
# a7b8c9d wip
# 1e2f3g4 feat: API эндпоинт для профиля
# 5h6i7j8 fix: опечатка
# 9k0l1m2 feat: модель профиля пользователя
# (+ 6 коммитов дальше — это уже main)
# Запускаем интерактивный rebase для 6 коммитов
git rebase -i HEAD~6
# В редакторе пишем:
pick 9k0l1m2 feat: модель профиля пользователя
fixup 5h6i7j8 fix: опечатка # ← вжать в предыдущий
pick 1e2f3g4 feat: API эндпоинт для профиля
fixup a7b8c9d wip # ← вжать
fixup d4e5f6g fix: поправить тесты # ← вжать
fixup f1a2b3c fix: ещё раз поправить тесты # ← вжать
# Сохраняем. Результат:
git log --oneline
# x1y2z3w feat: API эндпоинт для профиля
# a1b2c3d feat: модель профиля пользователя
# Чистая история из 2 логичных коммитов! Готово для PR.
Принцип уборки: каждый коммит в итоговой истории должен быть логической единицей. «Добавил модель» — один коммит. «Добавил API» — другой. «Поправил опечатку» — не отдельный коммит, а часть исходного.
5.6 autosquash и --fixup: автоматическая уборка
Git предоставляет умный механизм для автоматизации «уборки»: при коммите вы сразу помечаете, к какому коммиту относится фикс, а при rebase Git сам переставит и объединит.
Как использовать
# Допустим, вы нашли баг в коммите abc1234
# Вместо обычного коммита делаем fixup-коммит:
git commit --fixup=abc1234
# Git создаст коммит с сообщением: "fixup! feat: модель пользователя"
# Когда будете делать rebase:
git rebase -i --autosquash main
# Git автоматически переставит fixup-коммит под целевой
# и поставит ему команду "fixup"
Разница между --fixup и --squash
--fixup=<commit>— при rebase объединяет и отбрасывает сообщение фикса--squash=<commit>— при rebase объединяет и предлагает отредактировать объединённое сообщение
# Включить autosquash по умолчанию
git config --global rebase.autosquash true
# Теперь git rebase -i всегда будет автоматически
# переставлять fixup!/squash! коммиты
5.7 --rebase-merges: сохранение структуры
Обычный rebase «выпрямляет» всю историю в линию, теряя merge-коммиты. Опция --rebase-merges сохраняет структуру ветвлений при перебазировании.
# Обычный rebase — теряет merge-коммиты
git rebase main
# С сохранением merge-структуры
git rebase --rebase-merges main
Это полезно, когда ваша feature-ветка сама содержит merge-коммиты (например, вы мёржили в неё подветки) и хотите сохранить эту структуру при перебазировании на свежий main.
5.8 --update-refs: обновление связанных веток
Если у вас есть «стопка» зависимых веток (feature-1 → feature-2 → feature-3), rebase одной из них сломает остальные. Опция --update-refs автоматически обновляет все связанные ветки.
# Обновить все связанные ветки при rebase
git rebase --update-refs main
# Включить по умолчанию (Git 2.38+)
git config --global rebase.updateRefs true
5.9 Публичная история и спасение через reflog
Если вы случайно сделали rebase не там, где нужно — не паникуйте. Git хранит все перемещения HEAD в reflog, и вы можете откатиться.
# Посмотреть reflog — историю всех перемещений HEAD
git reflog
# abc1234 HEAD@{0}: rebase (finish): returning to refs/heads/feature
# def5678 HEAD@{1}: rebase (pick): feat: API авторизации
# 111aaa2 HEAD@{2}: rebase (start): checkout main
# 999bbb3 HEAD@{3}: commit: feat: API авторизации ← ВОТ ОНО!
# Вернуться к состоянию до rebase
git reset --hard HEAD@{3}
# Или по хешу
git reset --hard 999bbb3
# Готово! rebase отменён, как будто его не было
Важно: reflog — локальный журнал. Он не синхронизируется с удалённым репозиторием. Записи в reflog хранятся по умолчанию 90 дней, потом удаляются сборщиком мусора.
5.10 Rebase в монорепозиториях
В крупных монорепо (Google, Meta) rebase требует особой аккуратности: перебазирование может затронуть тысячи файлов из разных подпроектов.
Рекомендации
- Используйте
sparse-checkout, чтобы rebase затрагивал только нужные директории - Держите ветки короткоживущими — чем меньше коммитов, тем меньше конфликтов
- Договоритесь в команде о стратегии: rebase или merge. Смешивание обоих подходов в монорепо — рецепт хаоса
- Используйте
--update-refsесли у вас стопка зависимых веток
Практическое задание
Задание 1: Интерактивный rebase
- Создайте репозиторий с 2 коммитами в main
- Создайте ветку
featureи сделайте 5 коммитов:feat: A,wip,fix typo,feat: B,oops fix - Выполните
git rebase -i HEAD~5 - Объедините
wipиfix typoв первый коммит (fixup),oops fixв четвёртый - Проверьте результат:
git log --oneline— должно остаться 2 коммита
Задание 2: Rebase поверх main
- В репозитории из задания 1 добавьте 2 коммита в main
- Переключитесь на feature и выполните
git rebase main - Проверьте граф:
git log --oneline --graph --all - Feature должна «начинаться» после последнего коммита main
Задание 3: Спасение через reflog
- Сделайте rebase, который «портит» историю (например, удалите нужный коммит через drop)
- Используйте
git reflog, чтобы найти состояние до rebase - Откатитесь:
git reset --hard HEAD@{N} - Убедитесь, что история восстановлена