Git

Модуль 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 stufffix: исправить валидацию 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

  1. Создайте репозиторий с 2 коммитами в main
  2. Создайте ветку feature и сделайте 5 коммитов: feat: A, wip, fix typo, feat: B, oops fix
  3. Выполните git rebase -i HEAD~5
  4. Объедините wip и fix typo в первый коммит (fixup), oops fix в четвёртый
  5. Проверьте результат: git log --oneline — должно остаться 2 коммита

Задание 2: Rebase поверх main

  1. В репозитории из задания 1 добавьте 2 коммита в main
  2. Переключитесь на feature и выполните git rebase main
  3. Проверьте граф: git log --oneline --graph --all
  4. Feature должна «начинаться» после последнего коммита main

Задание 3: Спасение через reflog

  1. Сделайте rebase, который «портит» историю (например, удалите нужный коммит через drop)
  2. Используйте git reflog, чтобы найти состояние до rebase
  3. Откатитесь: git reset --hard HEAD@{N}
  4. Убедитесь, что история восстановлена

Настройки

Цветовая схема

Тема