git

Git rebase: интерактивный режим, squash коммитов и решение конфликтов

Git rebase переписывает историю коммитов вашей ветки так, будто вы изначально начали работу от свежей вершины main. Это даёт линейную и читаемую историю без шумных merge-коммитов, но требует понимания, когда rebase безопасен, а когда сломает рабочий процесс команде. Разберём интерактивный режим, склейку коммитов, разрешение конфликтов и аварийный выход.

Сравнение поведения git merge и git rebase: merge добавляет коммит-слияние, rebase делает историю линейной
git merge сохраняет точку ветвления, git rebase — переписывает коммиты поверх свежего main

rebase vs merge: в чём принципиальная разница

git merge объединяет две ветки и создаёт merge-коммит с двумя родителями. История остаётся честной: видно, где начали ветку, что в ней делали и когда влили. Минус — на крупном проекте дерево превращается в спагетти.

git rebase снимает ваши коммиты с ветки, переключается на новую базу (например, обновлённый main) и поочерёдно применяет каждый коммит заново. SHA-хеши при этом меняются — это уже другие коммиты, пусть и с тем же содержимым.

# Сценарий: вы работаете в feature, в main за это время появились новые коммиты
git checkout feature
git fetch origin

# Вариант 1: merge — появится merge-коммит
git merge origin/main

# Вариант 2: rebase — линейная история
git rebase origin/main

Практика большинства команд такая: feature-ветки rebase’им поверх main перед пушем, а саму main никогда не rebase’им — иначе у коллег сломается локальная история.


Интерактивный rebase: pick, squash, fixup, reword, edit

Флаг -i открывает редактор со списком коммитов, которые будут применены. Каждую строку можно изменить — это и есть «переписывание истории».

# Перебрать последние 5 коммитов
git rebase -i HEAD~5

# Перебрать всё, что отличается от main
git rebase -i main

В редакторе вы увидите примерно следующее:

pick a1b2c3d Добавил модель User
pick e4f5a6b Опечатка в комментарии
pick c7d8e9f Тесты для User
pick 0a1b2c3 fix
pick 4d5e6f7 ещё один fix

# Команды:
# p, pick   = использовать коммит как есть
# r, reword = использовать коммит, но изменить сообщение
# e, edit   = остановиться и дать поправить
# s, squash = склеить с предыдущим, спросить новое сообщение
# f, fixup  = склеить с предыдущим, выбросить сообщение
# d, drop   = выбросить коммит

Типичная задача — собрать в один коммит работу над фичей и три «fix» коммита поверх. Меняем pick на squash или fixup:

pick a1b2c3d Добавил модель User
fixup e4f5a6b Опечатка в комментарии
pick c7d8e9f Тесты для User
fixup 0a1b2c3 fix
fixup 4d5e6f7 ещё один fix

После сохранения файла получится два чистых коммита: «Добавил модель User» и «Тесты для User». Разница между squash и fixup — первый спросит новое сообщение коммита, второй молча выбросит сообщение склеиваемого.

reword и edit: чинить сообщения и содержимое

reword нужен, когда содержимое коммита нормальное, а сообщение хочется переписать (опечатка, неверный тикет, нет глагола):

reword a1b2c3d Добавил модель User
pick c7d8e9f Тесты для User

После сохранения git откроет редактор с сообщением старого коммита — правите, сохраняете, rebase идёт дальше.

edit мощнее: rebase остановится перед применением коммита и даст вам что-то изменить. Например, забыли добавить файл в коммит:

# После остановки rebase
git add forgotten_file.php

# Доклеиваем к текущему коммиту, не создавая нового
git commit --amend --no-edit

# Продолжаем rebase
git rebase --continue

Если же нужно полностью разобрать один большой коммит на несколько маленьких:

# На остановке "edit" откатываем изменения в индекс, оставляя файлы
git reset HEAD^

# Теперь добавляем по частям и коммитим отдельно
git add app/Models/User.php
git commit -m "Добавил модель User"
git add tests/UserTest.php
git commit -m "Тесты для User"

git rebase --continue

autosquash: ускоряем работу с fixup-коммитами

Готовить fixup-коммиты вручную, а потом скрупулёзно расставлять их в редакторе — утомительно. Git умеет это автоматически. Делаете правку и коммитите её с флагом --fixup, указывая SHA коммита, к которому относится правка:

# Нашли баг в коммите a1b2c3d, исправили
git add app/Models/User.php
git commit --fixup=a1b2c3d

# Создастся коммит с сообщением "fixup! Добавил модель User"

Когда наберётся пачка таких коммитов — запускаем rebase с --autosquash:

git rebase -i --autosquash main

Git сам переставит fixup-коммиты после их «родителей» и пометит как fixup. Останется только сохранить файл. Чтобы не писать --autosquash каждый раз, включите глобально:

git config --global rebase.autoSquash true

Решение конфликтов в процессе rebase

При перебазировании каждый коммит применяется отдельно — а значит, конфликты тоже могут прилетать по одному. Git остановится и покажет файлы с конфликтами:

$ git rebase -i main
Auto-merging app/Service/Order.php
CONFLICT (content): Merge conflict in app/Service/Order.php
error: could not apply c7d8e9f... Поддержка скидок в заказе
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".

Алгоритм действий:

  1. git status — смотрим, какие файлы в конфликте
  2. Открываем файл, ищем маркеры <<<<<<<, =======, >>>>>>>, оставляем нужный вариант
  3. git add <файл> — помечаем как разрешённый
  4. git rebase --continue — продолжаем
# Удобная команда: открыть в редакторе все файлы с конфликтами
git diff --name-only --diff-filter=U | xargs $EDITOR

# Использовать графический mergetool (например, meld, kdiff3, vscode)
git mergetool

# После правок
git add .
git rebase --continue

Если в череде из 10 коммитов один и тот же кусок кода правится несколько раз, придётся разрешать один и тот же конфликт несколько раз. Чтобы git запомнил решения и применил их в следующих коммитах, включите rerere (reuse recorded resolution):

git config --global rerere.enabled true

После включения git сохраняет ваши решения конфликтов и автоматически применяет их при повторных столкновениях — экономит часы на длинных rebase’ах.


Отмена rebase: –abort, –skip и git reflog

Если в процессе rebase что-то пошло не так, у вас есть три уровня отката.

1. Отменить весь rebase и вернуться к состоянию до запуска:

git rebase --abort

Эта команда работает только пока rebase в процессе — после --continue или завершения она недоступна.

2. Пропустить проблемный коммит (если он, например, стал не нужен):

git rebase --skip

3. Откатить уже завершённый rebase через reflog. Это спасательный круг, когда вы поняли, что зря склеили коммиты или потеряли важную правку:

# Смотрим, где была ветка до rebase
git reflog

# Пример вывода:
# 4d5e6f7 HEAD@{0}: rebase (finish): returning to refs/heads/feature
# 0a1b2c3 HEAD@{1}: rebase (pick): Тесты для User
# c7d8e9f HEAD@{5}: rebase (start): checkout main
# a1b2c3d HEAD@{6}: commit: ещё один fix      <-- сюда хотим вернуться

# Откатываемся
git reset --hard HEAD@{6}

Reflog хранит все перемещения HEAD за последние 90 дней по умолчанию — потеряться по-настоящему в git довольно сложно.


Когда rebase опасен: золотое правило публичных веток

Главное правило, которое стоит выгравировать на клавиатуре: никогда не делайте rebase ветки, которая уже запушена и используется кем-то ещё. После rebase коммиты получают новые SHA, и для остальных это выглядит как «появилась параллельная история, старая исчезла». Когда коллега попытается запушить свои изменения поверх старой версии, git будет уговаривать его сделать merge — и в итоге история превратится в кошмар с дубликатами коммитов.

Безопасные сценарии rebase:

  • Локальная feature-ветка, которую вы ещё не пушили
  • Ваша личная ветка на удалёнке, с которой больше никто не работает (тогда после rebase нужен git push --force-with-lease)
  • PR-ветка перед мержем в main — для красивой истории

Опасные сценарии:

  • Rebase main, develop, release/* — это коллективные ветки
  • Rebase ветки, по которой уже открыт PR и ревьюеры оставили комментарии (потеряются якоря на конкретные строки)
  • Rebase в общей ветке, в которую коммитят несколько разработчиков

Для push после rebase используйте --force-with-lease, а не --force. Первый сначала проверит, что на удалённой ветке нет чужих коммитов поверх вашей старой версии, и только потом перезапишет. Это спасает от случайного затирания чужой работы:

# Безопасный force-push: откажется, если кто-то успел запушить раньше
git push --force-with-lease origin feature

# Опасный force-push: молча перезатрёт чужие коммиты
git push --force origin feature

Чеклист по работе с rebase

  • Перед rebase сделайте git status — рабочая директория должна быть чистой
  • Запомните или запишите текущий SHA — пригодится для git reset --hard, если что-то сломается
  • Не делайте rebase публичных веток — только своих локальных или личных feature-веток
  • Используйте fixup + --autosquash вместо ручного редактирования todo-листа
  • Включите rerere, чтобы не разрешать одни и те же конфликты дважды
  • Пушьте после rebase только с --force-with-lease, никогда с --force
  • Если зашли в тупик — git rebase --abort, выдохните, попробуйте снова

Освоив интерактивный rebase, вы перестанете бояться «переписывания истории» и начнёте сдавать PR с чистой линейной серией коммитов, по которой ревьюер пробежится за минуту вместо получаса.