Rebase мастерски
Зачем это нужно
Глава «Git: push, PR и командная работа» познакомила с rebase на минималках: он перекладывает твои коммиты поверх чужих, история становится прямой линией, а у переставленных коммитов меняются хеши. Этого достаточно, чтобы не пугаться слова в выводе git. Но rebase — инструмент с двойным дном: в одних руках он делает историю проекта читаемой, как оглавление книги, в других — устраивает команде разъехавшиеся ветки и потерянные коммиты. Разница между этими руками — ровно два знания: когда переписывать историю можно, а когда нельзя, и как пользоваться интерактивным режимом, который превращает пачку коммитов «поправил», «ещё поправил», «опечатка» в один осмысленный коммит.
Эта страница — углубление для тех, кто уже прошёл главы про ветки и про push: слова merge, конфликт, force и origin дальше используются без объяснений.
- Зачем это нужно
- Вспомнить механику: rebase не двигает — он копирует
- Золотое правило: что можно и что нельзя
- Интерактивный rebase: навести порядок перед PR
- Конфликты при rebase и git pull --rebase
- Как это выглядит вживую
- Одна и та же задержка — два разных решения
- Три кнопки на GitHub — это тот же rebase
--onto: пересадка ветки на другую базу--autosquash: fixup без ручной правки плана- Практика: отрепетируй в симуляторе
- Типичные грабли новичка
- Страховка: git reflog
- Реальный рабочий цикл: от черновика до вливания
- Шпаргалка
- На чемпионате: за что снимают баллы
- Что должен уметь
- Куда дальше: проверенные ресурсы
Вспомнить механику: rebase не двигает — он копирует
Короткое повторение, потому что на нём стоит всё остальное. git rebase main, выполненный на твоей ветке, берёт каждый твой коммит и создаёт его копию поверх свежего main: то же содержимое, то же сообщение, но другой родитель — а значит, другой хеш. Старые коммиты никуда не исчезают мгновенно: они становятся «ничьими» (на них больше не указывает ни одна ветка) и живут в репозитории ещё недели, пока их не приберёт сборщик мусора.
Из этого факта выводится всё поведение 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). Содержимое и сообщения одинаковые, но это буквально другие коммиты — со своими новыми хешами, посчитанными от новой цепочки родителей.
Золотое правило: что можно и что нельзя
Официальная документация формулирует это как «Опасности перемещения», а в народе оно живёт как золотое правило 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-запушил общую ветку, а у тебя локально уже есть коммиты поверх старой версии), паника не помогает — помогает порядок действий:
- Ничего не пуш и не мержи. Первое движение с перепугу — попробовать
git pullещё раз — только всё усложнит: появится ещё один запутанный merge-коммит поверх уже запутанного. - Посмотри в свой reflog. Твои коммиты никуда не делись —
git reflogпокажет их хеши, даже если веткаfeatureтеперь указывает на совсем другую историю. - Спроси, что именно случилось. Если коллега сделал
rebase -iи что-то убрал (drop/fixup) осознанно — твои старые коммиты нужно применить поверх новой версии заново. Если он просто синхронизировался сmain— обычно достаточно самому сделатьgit rebase origin/featureна своих ещё не отправленных коммитах. git cherry-pick— точечный инструмент починки. Если нужно перенести не всю историю, а два-три конкретных коммита со старой ветки на новую,git cherry-pick <хеш1> <хеш2>применит именно их — иногда это быстрее, чем разбираться с полным rebase.- Договоритесь про
--force-with-leaseна будущее. Разовая проблема чинится руками, повторяющаяся — только правилом: на общих ветках force push либо запрещён вовсе, либо всегда идёт с предупреждением в чат до, а не после.
Ключевая мысль: golden rule не про запрет операции газом в пол — она про то, что цена ошибки для общей ветки платится всей командой, а не одним человеком, который её сделал.
Интерактивный 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 не понадобится.
Если вместо 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 — отменить всё и вернуть ветку ровно в состояние до начала. Запомни второй: это кнопка «как было», работающая в любой момент.
Конфликты при 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.
Практика: отрепетируй в симуляторе
В учебном GitSim команды rebase нет — он остаётся для настоящего терминала (и раздела «Углубиться»). Зато в нём отлично репетируется то, что rebase производит: прямая история против ветвистой. Задание в свободном режиме: создай ветку, сделай в ней коммит и получи оба вида слияния — сначала fast-forward (ровно так выглядит история после rebase: прямая линия), потом настоящий merge-коммит с развилкой (то, что было бы без rebase). Сравни граф в обоих случаях — это и есть выбор «rebase или merge» глазами.
В настоящем терминале разницу видно быстрее с алиасом на красивый граф: git config --global alias.lg "log --oneline --graph --all". После этого команда git lg в одну строку показывает ту самую картину — прямая линия или развилка, — которую тренажёр ниже даёт увидеть руками.
Как это устроено под капотом
А команды самого 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
reset --hard — команда без сети безопасности (она затирает незакоммиченные изменения), поэтому пользуйся ей осознанно и только по хешу, который ты глазами увидел в reflog. Но сам факт, что она существует, меняет отношение к rebase: это не хождение по канату, а редактор с кнопкой отмены.
Реальный рабочий цикл: от черновика до вливания
Собери всю главу в один сценарий — так, как это выглядит в обычной рабочей неделе.
- Начал ветку
feature/paymentsот свежегоmain, накоммитил черновиков: «добавил парсер», «поправил парсер», «опечатка», «ещё раз опечатка». - Перед тем как открывать PR, прибрался:
git rebase -i HEAD~4,fixupтри последних в первый — в истории остался один коммит «добавил парсер конфига». - Открыл PR. Ревьюер попросил переименовать переменную. Поправил, снова прибрался:
git commit --amend(это тоже маленький rebase — переписывает последний коммит с новым хешем) иgit push --force-with-lease. - Пока шёл review, в
mainвлили чужой PR — твоя ветка отстала на два коммита. Синхронизируешься без лишнего merge-коммита:git rebase main, конфликта не случилось,git push --force-with-leaseснова. - Ревью одобрено. Дальше решает не автор PR, а настройка репозитория: если у команды включена кнопка Squash and merge — история твоих локальных fixup'ов уже не важна, сервер сплющит всё в один коммит поверх main. Если включена Rebase and merge — твой единственный аккуратный коммит уедет в main как есть, без merge-коммита.
Ни разу за весь цикл не был нарушен золотое правило: каждый rebase происходил на ветке, где кроме тебя никто не коммитил, и force-push всегда использовался с --force-with-lease. Это и есть «уметь работать с rebase, как в реальной команде» — не одна команда, а последовательность решений, каждое из которых из этой главы.
Шпаргалка
| Команда | Что делает |
|---|---|
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-ить сколько угодно относительно апстрима, даже если сам форк технически «публичный» (виден всем) — золотое правило касается не видимости, а того, работает ли с этой историей ещё кто-то. Если над форком кроме тебя никто не коммитит, он ничем не отличается от локальной ветки.
Дальше по теме:
- git-rebase — официальная документация — все режимы и флаги, включая --onto и --autosquash
- Pro Git: Перебазирование — та же тема по-русски, с разделом «Опасности перемещения», из которого выросло золотое правило
Сводная проверка по всей главе: 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 — официальная документация — первоисточник: точное описание каждого действия плана и каждого флага.
-
Pro Git: Перебазирование — глава официальной книги по-русски: базовый и продвинутый rebase с картинками графов.
-
git-reflog — официальная документация — как устроен журнал ссылок и синтаксис HEAD@{n}.
-
GitHub Docs: О слиянии pull request — как кнопка Merge связана с rebase и squash: те же идеи на стороне сервера.
-
Видео и материалы сообщества — ниже — ролики по теме и ссылки от студентов и преподавателей смотри в самом низу страницы.
Команда git rebase --abort — редкий случай, когда самая важная команда инструмента ничего не делает с твоим кодом. Опытные пользователи git советуют выучить её раньше самого rebase: смелость экспериментировать берётся из знания, где выход.
Комментарии
Обсуждение живёт в GitHub Discussions и грузится с чужого домена — поэтому только по нажатию.