Перейти к основному содержимому
Отдельные темы03ГЛАВА 03Rebase мастерски

Rebase мастерски

Прочитано0%
Квизы0/6
Тренажёры0/3
Уровень 1 · Новичок

Зачем это нужно​

Глава «Git: push, PR и командная работа» познакомила с rebase на минималках: он перекладывает твои коммиты поверх чужих, история становится прямой линией, а у переставленных коммитов меняются хеши. Этого достаточно, чтобы не пугаться слова в выводе git. Но rebase — инструмент с двойным дном: в одних руках он делает историю проекта читаемой, как оглавление книги, в других — устраивает команде разъехавшиеся ветки и потерянные коммиты. Разница между этими руками — ровно два знания: когда переписывать историю можно, а когда нельзя, и как пользоваться интерактивным режимом, который превращает пачку коммитов «поправил», «ещё поправил», «опечатка» в один осмысленный коммит.

Эта страница — углубление для тех, кто уже прошёл главы про ветки и про push: слова merge, конфликт, force и origin дальше используются без объяснений.

Вспомнить механику: rebase не двигает — он копирует​

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

REBASE · КОПИРУЕТ, А НЕ ПЕРЕМЕЩАЕТбыло: mainтвоя веткаa1f9c3d · 4e21b77после git rebase mainкопии, новый хешa1f9c3d, 4e21b77 —ничьи, живут в reflogrebase создаёт копии твоих коммитов с другим родителем — старые не исчезают мгновенно
Rebase не двигает твои коммиты — создаёт их копии с другим родителем и новым хешем; старые становятся «ничьими», но не исчезают мгновенно.

Из этого факта выводится всё поведение rebase:

  • История после rebase — прямая линия без merge-коммитов: как будто ты начал работу позже, чем на самом деле.
  • Для git переставленные коммиты — другие коммиты. Сравнение веток, push, чужие копии — всё считает их новыми.
  • Пока «старая» версия коммитов ни к кому не уехала, замена проходит бесследно. Как только уехала — начинаются проблемы из следующего раздела.

Почему хеш вообще меняется, если содержимое файлов то же самое? Хеш коммита в git считается не только по содержимому — в него входит и хеш родителя. Формула утрированно выглядит так: хеш = f(содержимое, сообщение, автор, время, хеш_родителя). Меняется любая из этих составляющих — меняется и хеш, даже если diff коммита один в один совпадает с оригиналом. Rebase меняет ровно одну составляющую — родителя, — но этого достаточно, чтобы получился совершенно другой хеш, никак не похожий на старый.

Разница видна и в git log --oneline --graph. До rebase (или на merge-подходе) граф ветвится:

* f8a2c19 Merge branch 'feature' into main
|\
| * 90cc102 опечатка в README
| * 4e21b77 поправил парсер
| * a1f9c3d добавил парсер конфига
* | 7d4e001 правка в main
|/
* 2673310 общий предок

После git rebase main на ветке feature и обычного (fast-forward) вливания — та же работа, но линия прямая, будто ветвления не было вовсе:

* d92b6aa опечатка в README
* c1a0f55 поправил парсер
* e77a3bd добавил парсер конфига
* 7d4e001 правка в main
* 2673310 общий предок

Обрати внимание: хеши d92b6aa, c1a0f55, e77a3bd — не те же самые, что были до rebase (90cc102, 4e21b77, a1f9c3d). Содержимое и сообщения одинаковые, но это буквально другие коммиты — со своими новыми хешами, посчитанными от новой цепочки родителей.

MERGE ПРОТИВ REBASE · ФОРМА ИСТОРИИmergemerge-коммит, два родителяrebaseкопии с новым хешем — прямая линия, без развилки
Merge оставляет развилку и коммит слияния с двумя родителями; rebase распрямляет историю — копии твоих коммитов встают в конец.

Золотое правило: что можно и что нельзя​

Официальная документация формулирует это как «Опасности перемещения», а в народе оно живёт как золотое правило rebase: не перебазируй коммиты, которые уже отправлены туда, где их могли забрать другие. Разложим по типичным ситуациям:

СитуацияRebase?Почему
Локальная ветка, ещё не было push✅ можно свободнокопии заменяют оригиналы, о которых никто не знал
Своя ветка в PR, работаешь над ней один⚠️ можно, но push потом только --force-with-leaseсервер уже видел старые хеши; force переписывает их, а --force-with-lease страхует от затирания чужого
Ветка, над которой работает кто-то ещё❌ нельзяу коллеги останутся старые хеши — при следующем pull его git попытается «слить» историю саму с собой
main / общая ветка команды❌ никогдаэто история всей команды; переписал — сломал всем сразу

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

И парное правило про отправку: после законного rebase своей ветки обычный git push получит rejected — сервер видит, что твоя история «не продолжает» его версию. Здесь и нужен git push --force-with-lease: он переписывает ветку на сервере, но откажется это делать, если туда кто-то успел запушить, пока ты перебазировался. Голый --force затирает молча — в командной работе он не нужен практически никогда.

Важно

Реальный кейс из командной работы: два человека коммитят в общую ветку feature/payments. Один делает git rebase main и пушит форсом. У второго локально остаются старые хеши — его следующий git pull не может «продолжить» историю и молча создаёт merge-коммит, который сливает старую и новую версии одних и тех же коммитов друг с другом. В логе появляются задвоенные изменения, а иногда и обратный откат чужих правок — при слиянии git выбирает «победителя» по своим правилам, не по смыслу кода. Час на разбор получаса работы — типичная цена нарушения золотого правила, и это не гипотеза, а стандартный инцидент в командах, которые ещё не выучили это правило.

Если правило уже нарушено — как чинить​

Если сценарий из подсказки выше всё-таки случился (кто-то force-запушил общую ветку, а у тебя локально уже есть коммиты поверх старой версии), паника не помогает — помогает порядок действий:

  1. Ничего не пуш и не мержи. Первое движение с перепугу — попробовать git pull ещё раз — только всё усложнит: появится ещё один запутанный merge-коммит поверх уже запутанного.
  2. Посмотри в свой reflog. Твои коммиты никуда не делись — git reflog покажет их хеши, даже если ветка feature теперь указывает на совсем другую историю.
  3. Спроси, что именно случилось. Если коллега сделал rebase -i и что-то убрал (drop/fixup) осознанно — твои старые коммиты нужно применить поверх новой версии заново. Если он просто синхронизировался с main — обычно достаточно самому сделать git rebase origin/feature на своих ещё не отправленных коммитах.
  4. git cherry-pick — точечный инструмент починки. Если нужно перенести не всю историю, а два-три конкретных коммита со старой ветки на новую, git cherry-pick <хеш1> <хеш2> применит именно их — иногда это быстрее, чем разбираться с полным rebase.
  5. Договоритесь про --force-with-lease на будущее. Разовая проблема чинится руками, повторяющаяся — только правилом: на общих ветках force push либо запрещён вовсе, либо всегда идёт с предупреждением в чат до, а не после.

Ключевая мысль: golden rule не про запрет операции газом в пол — она про то, что цена ошибки для общей ветки платится всей командой, а не одним человеком, который её сделал.

Отвечено вопросов: 0 из 4
Почему после rebase у коммитов другие хеши?
Ты сделал rebase локальной ветки, которую вчера уже отправлял в свой PR (работаешь над ней один). Как правильно отправить результат?
Коллега тоже работает над веткой feature. Можно ли её перебазировать?
Чем --force-with-lease лучше голого --force?
Отвечено верно: 0 из 4

Интерактивный rebase: навести порядок перед PR​

Второе применение rebase не связано с чужими ветками вообще. git rebase -i (interactive) — это редактор собственной истории: перед тем как показать ветку людям, ты пересобираешь свои черновые коммиты в аккуратные.

Важная механика: план открывается в текстовом редакторе (том, что настроен как core.editor — по умолчанию часто Vim, если не менялся). Не пугайся: сохранить и выйти в Vim — Esc, затем :wq и Enter. Если Vim непривычен, редактор по умолчанию для git можно сменить командой git config --global core.editor "code --wait" (или любой другой редактор с флагом ожидания закрытия) — это разовая настройка, а не то, что нужно вспоминать каждый rebase.

Нажми на часть выражения

Команда открывает в редакторе план — список коммитов от старого к новому, каждый со словом-действием:

pick a1f9c3d добавил парсер конфига
pick 4e21b77 поправил парсер
pick 90cc102 опечатка в README

Меняешь слова — git выполняет план сверху вниз. Основные действия:

  • pick — оставить коммит как есть (значение по умолчанию).
  • reword — оставить, но остановиться и дать переписать сообщение.
  • squash — влить в предыдущий коммит, объединив сообщения (git даст их отредактировать).
  • fixup — влить в предыдущий, а сообщение выбросить: идеально для коммитов «поправил» и «опечатка».
  • drop — выбросить коммит совсем (то же самое — просто удалить строку).
  • edit — остановиться прямо на этом коммите, дать что-то доделать руками, потом продолжить.
  • Переставил строки местами — коммиты поменяют порядок.

edit — самое гибкое действие из всех: rebase доходит до отмеченного коммита, применяет его и останавливается, как будто это конфликт, только без конфликта. Дальше можно сделать что угодно: git commit --amend, если забыл файл, или даже расколоть один разбухший коммит на два — сначала git reset HEAD^ (это откатывает коммит, но не рабочие файлы — все изменения остаются в рабочей директории незакоммиченными), потом собрать из этих изменений два аккуратных коммита через git add -p и git commit. Когда всё готово — как обычно, git rebase --continue, и план поедет дальше.

План выше типично превращается в:

pick a1f9c3d добавил парсер конфига
fixup 4e21b77 поправил парсер
pick 90cc102 опечатка в README

— и вместо трёх коммитов в истории остаются два осмысленных. Правило хорошего тона: наводить такой порядок до отправки ветки в PR — тогда и force не понадобится.

GIT REBASE -I · ПЛАНпланpick a1f9c3dpick 4e21b77pick 90cc102правишь словаменяешь словаpick a1f9c3dfixup 4e21b77pick 90cc102git выполняетисториядобавил парсеропечатка в READMEfixup вливает 4e21b77 в предыдущий коммит без своего сообщения — было три коммита, стало два
Меняешь слово действия у коммита — fixup вливает его в предыдущий без отдельного сообщения: было три коммита, стало два.

Если вместо fixup в плане поставить reword, после выполнения плана до этого коммита rebase остановится второй раз — но не с конфликтом, а с открытым редактором сообщения:

поправил парсер

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.

Меняешь текст, сохраняешь — rebase сам продолжает дальше без отдельной команды --continue (она нужна только после явной остановки на конфликте или на edit). А если поставить squash вместо fixup, редактор откроется с сообщениями обоих объединяемых коммитов сразу — можно оставить оба, слить в одну строку или переписать заново:

# This is a combination of 2 commits.
# This is the 1st commit message:

добавил парсер конфига

# This is the commit message #2:

поправил парсер

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

Отвечено вопросов: 0 из 4
Чем fixup отличается от squash?
Что делает git rebase --abort?
Как выбросить коммит из истории через rebase -i?
Когда лучше всего наводить порядок в коммитах через rebase -i?
Отвечено верно: 0 из 4

Конфликты при rebase и git pull --rebase​

Конфликты в rebase случаются по той же причине, что и в merge, но обрабатываются иначе: rebase накладывает твои коммиты по одному, и останавливается на том, который не лёг. Дальше знакомый цикл: открыть файлы с маркерами <<<<<<<, оставить правильный вариант, git add — но вместо commit сказать git rebase --continue. Если конфликтов несколько, цикл повторится для каждого коммита. Стало страшно — git rebase --abort, и ты снова до начала.

Отдельный родственник — git pull --rebase. Обычный git pull при разъехавшихся историях делает merge-коммит «Merge branch...», который в логе только шумит. С флагом --rebase git вместо этого переложит твои локальные коммиты поверх свежепришедших — история останется прямой. Это самый безопасный повседневный rebase: перекладываются только те коммиты, которых на сервере ещё нет, так что золотое правило не нарушается по построению.

Совет

Самая частая ошибка новичка в цикле конфликта — после правки файлов сразу набрать git commit, по привычке из merge. При rebase так делать не нужно: коммит для текущего шага git создаст сам, когда ты скажешь git rebase --continue. Лишний commit посреди rebase не сломает всё бесповоротно, но добавит в историю случайный лишний коммит, который потом придётся вычищать.

Живой кейс: конфликт от начала до конца​

Разберём на конкретном примере, а не абстрактно. Ты в ветке feature, делаешь git rebase main, и внутри одного файла конфигурации твои правки и правки main пересеклись:

$ git rebase main
Rebasing (1/3)
CONFLICT (content): Merge conflict in src/config.py
error: could not apply 4e21b77... поправил парсер

Открываешь src/config.py, видишь маркеры:

<<<<<<< HEAD (текущий, из main)
TIMEOUT = 30
=======
TIMEOUT = 60
>>>>>>> 4e21b77 (поправил парсер)

Смысл разметки тот же, что в merge, но роли поменялись местами: HEAD здесь — это уже переложенная часть истории (то есть main, на который ты переезжаешь), а нижний блок — твой коммит, который git пытается применить поверх. Если непонятно, что вообще меняли по обе стороны конфликта, git status покажет список проблемных файлов, а посмотреть на исходные версии до маркеров можно так:

git status # список: "both modified: src/config.py"
git diff # сам конфликт с маркерами
git show HEAD:src/config.py # версия файла из main (куда переезжаем)
git show 4e21b77:src/config.py # версия файла из твоего коммита

Решаешь, какое значение верное (или пишешь третье), убираешь маркеры, сохраняешь. Дальше:

$ git add src/config.py
$ git rebase --continue

Если следующий коммит из плана тоже не ляжет — цикл «поправил → add → continue» повторится ещё раз. Когда rebase выполнит план целиком, ты увидишь Successfully rebased and updated refs/heads/feature. — тот самый первый ответ из следующего раздела.

Если конфликтов много и разбираться с ними руками неудобно, есть облегчающая, но необязательная надстройка — git mergetool: она открывает настроенный визуальный редактор конфликтов (например, встроенный в IDE) вместо голых маркеров в текстовом файле. Для одного-двух файлов обычно быстрее сделать всё руками; для десятка конфликтующих файлов сразу — mergetool экономит время.

Как это выглядит вживую​

Три сообщения, которые rebase печатает чаще всего, — узнавай их в лицо, чтобы не гадать, на каком ты этапе:

Successfully rebased and updated refs/heads/feature.

Всё прошло без остановок: коммиты переложены, ветка обновлена. Если перед этим был -i — план выполнен целиком.

CONFLICT (content): Merge conflict in src/config.py
error: could not apply 4e21b77... поправил парсер
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
hint: You can instead skip this commit: run "git rebase --skip".
hint: To abort and get back to the state before "git rebase", run "git rebase --abort".

Остановка на конфликтующем коммите — git сам перечисляет три выхода: разрешить и --continue, пропустить коммит через --skip (нужен редко), отменить всё через --abort. Хеш и сообщение в строке error говорят, какой именно из твоих коммитов не лёг.

interactive rebase in progress; onto 3b7e0aa
Last command done (1 command done):
pick a1f9c3d # добавил парсер конфига
Next commands to do (2 remaining commands):
reword 4e21b77 # поправил парсер
pick 90cc102 # опечатка в README
(use "git rebase --edit-todo" to view and edit)

Так git status отчитывается посреди интерактивного rebase: что уже сделано, что впереди. Потерялся — git status всегда расскажет, где ты и что git от тебя ждёт.

Одна деталь, которая сбивает с толку: хеш после onto (3b7e0aa) — не из плана. Это база, тот самый коммит, поверх которого всё перекладывается: для git rebase -i HEAD~3 это коммит, стоявший перед тремя пересобираемыми. А решётка перед сообщением коммита — просто разделитель во внутреннем файле плана, который git показывает как есть; в редакторе плана её не видно.

Одна и та же задержка — два разных решения​

Возьмём конкретную, максимально частую ситуацию: твоя ветка feature отстала от main на пару коммитов, и нужно подтянуть свежие изменения перед продолжением работы. Вот как выглядят оба пути бок о бок — с одинаковой стартовой точкой и разным результатом.

Путь merge:

git switch feature
git merge main

Если конфликтов нет — git создаст merge-коммит автоматически, с сообщением вроде Merge branch 'main' into feature. Граф в git log --graph покажет явную развилку и точку слияния. Твои старые коммиты не меняются вообще — ни один хеш не тронут.

Путь rebase:

git switch feature
git rebase main

Конфликтов нет — git молча перекладывает твои коммиты поверх свежего main, без лишнего коммита. Граф в git log --graph окажется прямой линией. Но у всех твоих коммитов на ветке feature теперь новые хеши — даже у тех, что были написаны неделю назад и уже мельком просмотрены коллегой в черновом PR.

Разница на бумаге небольшая — одна команда вместо другой, — но именно эта развилка запускает всё золотое правило: merge никогда не портит существующие хеши, поэтому им можно пользоваться на чужой территории без спроса. Rebase портит хеши всегда, поэтому перед его использованием нужно на секунду спросить себя: «а не увёл ли кто-то уже эти коммиты к себе?».

Три кнопки на GitHub — это тот же rebase​

Когда PR готов ко вливанию, на GitHub (и в похожих сервисах) обычно есть выбор из трёх кнопок, и все три — это ровно те концепции, что уже разобраны выше, просто сервер выполняет их за тебя одним кликом:

КнопкаЧто происходит на самом деле
Merge commitобычный git merge — создаётся merge-коммит с двумя родителями, история PR входит в main развилкой
Squash and mergeвсе коммиты PR сливаются в один (как один большой squash в интерактивном плане) и кладутся поверх main одним коммитом
Rebase and mergeровно git rebase — каждый коммит PR копируется поверх main по отдельности, без merge-коммита

Отсюда практический вывод: если в команде включена кнопка Squash, наводить порядок через rebase -i перед PR почти не нужно — сервер всё равно сплющит историю в один коммит. А если включена Rebase and merge, аккуратность истории внутри PR имеет значение: каждый твой коммит попадёт в main как есть. Первый вопрос в незнакомом проекте — не только «как принято сливать веток», но и какая из трёх кнопок настроена по умолчанию.

--onto: пересадка ветки на другую базу​

Обычный git rebase main перекладывает все коммиты текущей ветки. Иногда нужно точнее: переставить только часть истории на новое место, не трогая остальное. Для этого есть полная форма команды с тремя аргументами.

Нажми на часть выражения

Ситуация, где это спасает: ты начал ветку new-feature от чужой незавершённой old-feature, потому что она тебе была нужна для работы. Потом old-feature признали лишней и удалили, а твои коммиты остались висеть на её истории. Обычный git rebase main тут не поможет — он потащит с собой все коммиты old-feature тоже. Решение:

git rebase --onto main old-feature new-feature

Git берёт только те коммиты, что есть в new-feature, но не в old-feature, и пересаживает ровно их на свежий main. Коммиты чужой недописанной ветки в истории не остаются.

Родственный редкий флаг — git rebase -i --root: пересобирает историю с самого первого коммита ветки (у которого нет родителя вовсе, поэтому обычный HEAD~N до него не дотягивается). Нужен почти исключительно для того, чтобы поправить сообщение или содержимое самого первого коммита репозитория — в обычной работе не встречается месяцами, но когда встречается, вспомнить про --root дешевле, чем гуглить.

--autosquash: fixup без ручной правки плана​

Второй инструмент экономит рутину интерактивного rebase. Вместо того чтобы потом руками искать в плане нужную строку и менять pick на fixup, можно сразу пометить коммит как «правку» другого:

git commit --fixup a1f9c3d

Git создаст коммит с сообщением fixup! добавил парсер конфига (текст исходного сообщения git подставит сам по хешу a1f9c3d). А дальше вместо обычного -i запускаешь:

git rebase -i --autosquash HEAD~4

Открывшийся план уже готов: git сам нашёл коммит fixup! ... и подставил ему слово fixup прямо под коммитом-целью — редактировать ничего не нужно, достаточно сохранить план как есть. Это конвейер для тех, кто чинит опечатки в уже написанном коммите по ходу работы: commit --fixup, а в конце дня один rebase -i --autosquash.

Отвечено вопросов: 0 из 2
Чем git rebase --onto main old-feature new-feature отличается от обычного git rebase main?
Для чего нужен git commit --fixup <хеш>?
Отвечено верно: 0 из 2

Практика: отрепетируй в симуляторе​

В учебном GitSim команды rebase нет — он остаётся для настоящего терминала (и раздела «Углубиться»). Зато в нём отлично репетируется то, что rebase производит: прямая история против ветвистой. Задание в свободном режиме: создай ветку, сделай в ней коммит и получи оба вида слияния — сначала fast-forward (ровно так выглядит история после rebase: прямая линия), потом настоящий merge-коммит с развилкой (то, что было бы без rebase). Сравни граф в обоих случаях — это и есть выбор «rebase или merge» глазами.

Совет

В настоящем терминале разницу видно быстрее с алиасом на красивый граф: git config --global alias.lg "log --oneline --graph --all". После этого команда git lg в одну строку показывает ту самую картину — прямая линия или развилка, — которую тренажёр ниже даёт увидеть руками.

Квест «Получи оба вида слияния: перемотку (историю-линию, как после rebase) и merge-коммит (развилку)»: получи оба вида слияния — перемотку и merge-коммит
○ шаг 1 — fast-forward: слей ветку, пока main не двигался (без расхождения)○ шаг 2 — merge-коммит: вернись, создай расхождение (свой коммит и в main, и в ветке) и слей снова
Пустая папка проекта. Начни с git init. Полный список команд — help.
project $
Команды: git config · init · status · add · commit -m · diff · log · branch · switch · merge · restore · edit <файл> <текст> — изменить файл · help · clear
Файлы проекта
пока пусто — создай файл: edit README.md Привет
История коммитов
история пуста — сделай первый коммит
Как это устроено под капотом
Внутри — не настоящий git, а его модель в памяти: коммит — объект с хешем, сообщением, списком родителей и полным снимком файлов, а ветка — просто указатель на хеш коммита. Команды меняют этот граф объектов, при этом формулировки вывода сняты с настоящего git 2.x, чтобы глаз привыкал к реальному терминалу. Граф справа рисуется по тем же объектам: SVG-кружки — коммиты, линии — связи «родитель — потомок», а merge с расхождением создаёт коммит с двумя родителями, ровно как в настоящем git. Push и pull в режиме с origin — упрощённая пересылка недостающих коммитов между двумя такими репозиториями.

А команды самого rebase набери руками — пальцы должны помнить и --continue, и особенно --abort:

Цель: точность ≥ 90%

git rebase main

Отдельно потренируй цикл разрешения конфликта и продвинутые команды — набирать их руками нужно так же уверенно, как обычный git commit:

Цель: точность ≥ 90%

git add src/config.py

Типичные грабли новичка​

Четыре мелочи, на которых спотыкаются почти все в первую неделю работы с rebase — лучше споткнуться здесь, на странице с объяснением, чем в боевой ветке.

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

Незакоммиченные изменения мешают начать. git rebase (как и git checkout/switch) откажется стартовать, если в рабочей директории есть неcохранённые правки, которые могут быть перезаписаны:

error: cannot rebase: You have unstaged changes.
error: Please commit or stash them.

Сообщение исчерпывающее: либо git commit, если правки готовы, либо git stash, если ещё нет — после rebase спрятанное возвращается командой git stash pop.

git rebase --skip — редкая и опасная кнопка. Она не «пропускает конфликт» — она выбрасывает содержимое всего конфликтующего коммита целиком, как будто его никогда не было. Использовать стоит только когда точно понятно, что этот коммит стал не нужен (например, его правки уже попали в main другим путём). В сомнении — не skip, а честно разрешить конфликт руками.

Force push посреди чужого code review — предупреди в чате. Технически если ветка твоя и в PR работаешь один, --force-with-lease безопасен по золотому правилу. Но у ревьюера, который уже открыл вкладку с diff, после force push старые комментарии могут отвязаться от строк, а CI перезапустится с нуля. Формально не нарушение правила, но вежливость команды — короткое «форснул историю, гляньте заново» экономит чужое недоумение.

Важно

Порядок в плане rebase -i — старые коммиты сверху, новые снизу — противоположен привычному git log. Это единственная причина половины «я случайно снёс не тот коммит» в первую неделю. Сверяйся с сообщением коммита, а не с позицией строки.

Страховка: git reflog​

Последний страх, который стоит снять: «а вдруг rebase потеряет мои коммиты». Почти никогда не потеряет — а на «почти» есть git reflog: журнал всех перемещений HEAD за последние недели. Старые, «ничьи» коммиты после rebase остаются в репозитории, и reflog показывает их хеши. Вернуться к состоянию до неудачного rebase можно всегда:

git reset --hard ORIG_HEAD # самый короткий путь: git сам запомнил, где ветка была до rebase

ORIG_HEAD — служебная ссылка, которую git ставит перед каждой «крупной» операцией вроде rebase или merge. Если она уже затёрта следующей операцией, состояние ищут глазами в журнале:

git reflog # найти строку "rebase (start)" — нужна строка СРАЗУ ПОД ней
git reset --hard HEAD@{5} # вернуть ветку на состояние из журнала

Читать журнал нужно аккуратно: reflog печатает записи от самых новых к старым, сверху вниз. Строка rebase (start) — это уже первый шаг самого rebase, и хеш на ней — новая база, а не то, что тебе нужно. Состояние ветки до rebase записано строкой ниже:

2673310 HEAD@{4}: rebase (start): checkout main
b2593cf HEAD@{5}: checkout: moving from main to feature ← сюда возвращаться
2673310 HEAD@{6}: commit: правка в main
GIT REFLOG · ЖУРНАЛ ПЕРЕМЕЩЕНИЙ HEADновые записи сверху, старые снизуHEAD@{4}: rebase (start): checkout mainHEAD@{5}: checkout: moving from main to featureHEAD@{6}: commit: правка в main← сюдаgit reset --hard HEAD@{5}rebase (start) — уже первый шаг rebase; состояние ветки ДО него записано строкой ниже
Reflog читают сверху вниз, от новых записей к старым: строка rebase (start) — уже начало rebase, состояние ДО него записано строкой ниже.

reset --hard — команда без сети безопасности (она затирает незакоммиченные изменения), поэтому пользуйся ей осознанно и только по хешу, который ты глазами увидел в reflog. Но сам факт, что она существует, меняет отношение к rebase: это не хождение по канату, а редактор с кнопкой отмены.

Отвечено вопросов: 0 из 4
Чем разрешение конфликта при rebase отличается от разрешения при merge?
Что делает git pull --rebase?
После неудачного rebase кажется, что коммит пропал. Где его искать?
Почему git reset --hard требует осторожности?
Отвечено верно: 0 из 4

Реальный рабочий цикл: от черновика до вливания​

Собери всю главу в один сценарий — так, как это выглядит в обычной рабочей неделе.

  1. Начал ветку feature/payments от свежего main, накоммитил черновиков: «добавил парсер», «поправил парсер», «опечатка», «ещё раз опечатка».
  2. Перед тем как открывать PR, прибрался: git rebase -i HEAD~4, fixup три последних в первый — в истории остался один коммит «добавил парсер конфига».
  3. Открыл PR. Ревьюер попросил переименовать переменную. Поправил, снова прибрался: git commit --amend (это тоже маленький rebase — переписывает последний коммит с новым хешем) и git push --force-with-lease.
  4. Пока шёл review, в main влили чужой PR — твоя ветка отстала на два коммита. Синхронизируешься без лишнего merge-коммита: git rebase main, конфликта не случилось, git push --force-with-lease снова.
  5. Ревью одобрено. Дальше решает не автор PR, а настройка репозитория: если у команды включена кнопка Squash and merge — история твоих локальных fixup'ов уже не важна, сервер сплющит всё в один коммит поверх main. Если включена Rebase and merge — твой единственный аккуратный коммит уедет в main как есть, без merge-коммита.

Ни разу за весь цикл не был нарушен золотое правило: каждый rebase происходил на ветке, где кроме тебя никто не коммитил, и force-push всегда использовался с --force-with-lease. Это и есть «уметь работать с rebase, как в реальной команде» — не одна команда, а последовательность решений, каждое из которых из этой главы.

Отвечено вопросов: 0 из 3
Что на самом деле делает git commit --amend с точки зрения истории коммитов?
В репозитории включена кнопка Squash and merge. Стоит ли перед PR наводить порядок через rebase -i, вычищая коммиты «поправил», «опечатка»?
Ветка отстала от main на пару коммитов во время открытого PR. Какой вариант держит историю прямой и не создаёт лишний merge-коммит?
Отвечено верно: 0 из 3

Шпаргалка​

КомандаЧто делает
git rebase mainпереложить коммиты текущей ветки поверх main (копии, новые хеши)
git rebase -i HEAD~3интерактивно пересобрать три последних коммита
pick / reword / squash / fixup / drop / editоставить / переименовать / влить с сообщением / влить без / выбросить / остановиться и доделать руками
git rebase --continueпродолжить после разрешённого конфликта, reword или edit
git rebase --skipвыбросить текущий конфликтующий коммит целиком (редко, только когда он точно не нужен)
git rebase --abortотменить rebase целиком, вернуть как было
git rebase --onto new old branchпересадить только часть истории (между old и branch) на new
git commit --fixup <хеш> + git rebase -i --autosquashпометить коммит как правку другого и разложить план автоматически
git pull --rebaseзабрать чужое, переложив свои неотправленные коммиты наверх
git push --force-with-leaseотправить переписанную ветку со страховкой от затирания чужого
git reflogжурнал перемещений HEAD — найти «потерянный» коммит
git reset --hard ORIG_HEADвернуть ветку туда, где она была до rebase (или merge)
Золотое правилоне перебазировать то, что уже есть у других (и никогда — общие ветки)
Углубиться

Merge или rebase — вечный спор. Одни команды требуют прямую историю (rebase перед вливанием, squash-merge в кнопке PR), другие принципиально сохраняют merge-коммиты как честную летопись «что когда сливалось». Правильного ответа нет — есть соглашение конкретной команды, и первое, что стоит спросить в новом проекте: «как у вас принято вливать ветки?».

Почему «база»? Слово rebase буквально значит «сменить базу»: базой (base) называется коммит, от которого ветка растёт. rebase отцепляет ветку от старой базы и прищепляет к новой — вся терминология команды становится прозрачной, если помнить эту картинку.

Сколько живут «ничьи» коммиты? Настройка gc.reflogExpire по умолчанию — 90 дней для достижимых через reflog записей и 30 дней для недостижимых; именно поэтому в разделе про reflog сказано «недели», а не «навсегда». git gc можно вызвать и руками, но в обычной повседневной работе он срабатывает сам по неспешному расписанию — специально дожидаться, что твои «потерянные» коммиты вот-вот исчезнут, почти никогда не нужно.

Rebase и git revert — не конкуренты. revert создаёт новый коммит, который отменяет старый, ничего не переписывая — это единственный безопасный способ «отменить» то, что уже ушло в общую ветку или в main. Rebase для той же цели не подходит в принципе: он переписывает историю, а на публичной ветке это запрещено золотым правилом. Разные инструменты для разных моментов: rebase — пока история твоя личная, revert — когда она уже общая.

Форк на GitHub — тоже «своя» история, пока не спутана с чужой. Личный форк репозитория можно rebase-ить сколько угодно относительно апстрима, даже если сам форк технически «публичный» (виден всем) — золотое правило касается не видимости, а того, работает ли с этой историей ещё кто-то. Если над форком кроме тебя никто не коммитит, он ничем не отличается от локальной ветки.

Дальше по теме:

Сводная проверка по всей главе: 8 вопросов, 5:00 на всё, без подсказок по ходу. Вопросы показываются по одному, ответ изменить нельзя. Если время выйдет — экзамен завершится с тем, что успел ответить. Пересдавать можно сколько угодно раз.

На чемпионате: за что снимают баллы​

Rebase — ровно тот навык, где судьи чемпионата смотрят не только на конечный результат, но и на сам процесс: как выглядит история, а не только правильный ли код в итоге. Частые причины потери баллов на заданиях с git:

  • История дороже красоты, если задание требует показать процесс. Иногда постановка задачи прямо просит сохранить видимые шаги работы — тогда неуместный squash, стирающий промежуточные коммиты, снимает баллы за нарушение условия, даже если код в итоге правильный.
  • Force push без --lease на чужой ветке. Если задание подразумевает совместную работу (двое участников на одном репозитории), голый --force — это верный способ затереть работу напарника и получить штраф за потерянные коммиты, которые не восстановить без reflog напарника.
  • Забытый git add перед --continue. Rebase «зависает» на середине, задание не засчитывается как выполненное — время истекает, пока разбираешься, что вообще происходит. Быстрый диагноз всегда один: git status.
  • Rebase уже опубликованной командной ветки. В групповых заданиях это то самое золотое правило: перебазировал общую ветку — сломал репозиторий всем, кто с ней работает, а не только себе.

Практический вывод для тайминга: перед стартом задания на git — секунда, чтобы вслух проговорить, чья это ветка и кто ещё в ней коммитит. Дальше решение (rebase можно / нельзя, force с lease или без) принимается сразу, а не посреди паники, когда push уже выдал rejected.

Что должен уметь​

  • Объяснять, почему rebase создаёт новые коммиты и что происходит со старыми, включая роль хеша родителя.
  • Применять золотое правило: по описанию ситуации отвечать, можно ли перебазировать, и почему.
  • Пересобирать свою историю через git rebase -i: squash, fixup, reword, drop, edit, перестановка коммитов.
  • Разрешать конфликт в середине rebase и знать все выходы: --continue, --skip, --abort.
  • Объяснять, чем git pull --rebase отличается от обычного pull и почему он не нарушает золотое правило.
  • Отправлять переписанную ветку через --force-with-lease и находить «потерянные» коммиты через reflog.
  • Использовать --onto для точечной пересадки части истории и --autosquash для конвейера fixup-коммитов.
  • Соотносить кнопки Merge / Squash / Rebase на GitHub с командами, которые они выполняют под капотом.

Куда дальше: проверенные ресурсы​

Интересный факт

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

Видео по теме

ВидеоGIT. Rebase and SQUASH. Делаем красивые лаконичные commit-ыАргументика
ВидеоGit rebase interactive: интерактивный ребейзletsCode
ВидеоGIT. Интерактивный Rebase. Как с ним работать?Front-end Science із Сергієм Пузанковим
ВидеоGit Rebase Interactive (squash)Steven Lee
Видеоgit interactive rebase - Undo, Edit & Squash git commits with a single commandPhilomatics

Комментарии

Обсуждение живёт в GitHub Discussions и грузится с чужого домена — поэтому только по нажатию.