Ветка живёт две недели, каждое утро её обновляют из main, и через месяц git log наполовину состоит из «Merge branch main»: история в ромбах, в которой не найти ни одного содержательного коммита. merge объединяет ветки, честно сохраняя всё как было, отсюда и ромбы. rebase решает ту же задачу иначе: переносит ваши коммиты на новое основание, переписывая их. Результат — прямая, читаемая история без ромбов и merge-коммитов. Цена — правила безопасности, которые нужно знать твёрдо, поэтому они идут сразу за механикой. Дальше без религиозных войн «merge vs rebase».
Ветка feature отошла от main раньше, чем туда приехали чужие коммиты. Rebase переприменяет её правки поверх свежей верхушки: содержимое то же, а коммиты новые — a1b2 и c3d4 заменились на 9f07 и 4be1. Закладка feature переезжает на новую цепочку, старая остаётся в стороне без ветки и со временем исчезает.
Что делает rebase
Вы работали в ветке, main ушёл вперёд. Вместо вливания main в себя (merge-коммит в вашей ветке) — переставим вашу ветку на свежий main:
git switch main && git pull # сначала обновить сам main
git switch feature-export
git rebase main
Первая строка тут не для красоты. git rebase main переносит ветку на вашу локальную закладку main, а она обновляется только по вашей команде — без pull вы аккуратно перенесёте работу на вчерашнее основание. Тот же смысл в одну строку: git fetch && git rebase origin/main.
Git по одному «переприменяет» ваши коммиты поверх верхушки main. Внешне изменения те же, но история выглядит так, будто вы начали работу от свежего main. Важно понимать: это новые коммиты — с новыми идентификаторами; старые заменены. Конфликты возможны, как при merge, и решаются так же — только всплывать они могут на каждом переносимом коммите по очереди: разобрали, git add, git rebase --continue; передумали на середине — git rebase --abort вернёт ветку как было.
Тот же механизм в ежедневном git pull --rebase: ваши локальные коммиты переносятся поверх свежих серверных — без мусорных merge-коммитов «Merge branch main into main».
Когда rebase остановился посреди пути, первое, что нужно знать, это где вы. git status отвечает прямо: interactive rebase in progress; onto 4be18c, (2/5) и список файлов с конфликтом; полностью посмотреть очередь можно в .git/rebase-merge/done и git-rebase-todo. Выходов из остановки три, а не один: --continue после разбора конфликта, --abort, чтобы вернуть всё как было, и --skip, чтобы выбросить текущий коммит целиком, что правильно, когда его правки уже есть в main и конфликт состоит из «то же самое с обеих сторон». Если один и тот же конфликт всплывает на каждом из десяти переносимых коммитов, включают git config --global rerere.enabled true: Git запоминает, как вы разрешили конфликт в первый раз, и дальше применяет то же решение сам.
Отдельный случай, когда ветку отвели не оттуда: начали feature-b от feature-a, а feature-a влили через squash или её вообще выбросили, и теперь git rebase main тащит за собой чужие коммиты. Спасает --onto, у которого три аргумента: новое основание, старое основание и что переносить:
git rebase --onto main feature-a feature-b # коммиты feature-b после feature-a переставить на main
Читается так: «возьми всё, что в feature-b после feature-a, и поставь на main». Это единственный способ вытащить ветку из-под неправильного основания, не перенося лишнего.
Золотое правило
Не переписывайте историю, которую уже отправили в общую ветку. Rebase создаёт новые коммиты — у коллег, успевших забрать старые, история разойдётся с вашей, и распутывание будет мучительным.
Практическая формула безопасности: переписывать можно свою ветку до слияния (даже уже запушенную — тогда git push --force-with-lease, это ваша личная ветка); нельзя — main, develop и любые ветки, от которых работают другие. Отсюда и ответ на «merge или rebase»: в личной ветке — rebase на здоровье; общие ветки объединяются merge. --force-with-lease отличается от --force тем, что сверяет серверную ветку с вашей копией origin/* и отказывается затирать, если на сервере появилось что-то, чего вы не видели.
У этой сверки есть слабое место, о котором стоит знать заранее. Копию origin/* освежает любой fetch — в том числе тот, что редактор делает в фоне сам, ни о чём не спрашивая. После такого фонового обновления Git считает, что чужой коммит вы уже «видели», и защита тихо снимается. Подстраховка появилась в Git 2.30 — флаг --force-if-includes: он дополнительно проверяет, что серверная вершина действительно доехала до вашей ветки, а не просто мелькнула в фоновом обновлении. В паре это выглядит так:
git push --force-with-lease --force-if-includes
Подтянуть main в свою ветку: merge или rebase
Ветка живёт три дня, а main за это время ушёл вперёд на двадцать коммитов. «Регулярно подтягивать main» звучит в каждой статье, а команды две, и у них разные последствия.
git fetch origin
git merge origin/main # вариант 1: merge-коммит «Merge branch 'main' into feature»
git rebase origin/main # вариант 2: ваши коммиты переписаны поверх свежего main
Merge ничего не переписывает: ваши коммиты остаются как были, к ним добавляется коммит слияния. Это безопасно для ветки, которую уже смотрят в PR, но после трёх таких подтягиваний история ветки читается как клубок. Rebase даёт прямую историю, будто ветку начали сегодня, но меняет хеши ваших коммитов; поэтому после него обычный git push отклоняется, и нужен git push --force-with-lease (не --force: --force-with-lease откажется затирать чужие коммиты, если кто-то успел запушить в вашу ветку).
Обычная договорённость: пока ветка личная и открытый PR ещё не обсуждали построчно, подтягивать через rebase; когда ревьюеры уже оставили замечания к конкретным строкам, через merge, чтобы их комментарии не отвязались от переписанных коммитов. Конфликты в обоих вариантах те же самые, разница лишь в том, что при rebase их решают по одному коммиту.
Интерактивный rebase: приборка перед ревью
Реальная работа порождает черновую историю: «wip», «поправил опечатку», «ещё раз fix». Перед pull request её можно превратить в аккуратную:
git rebase -i main
Откроется файл со списком ваших коммитов. Два сюрприза сразу: сверху самый старый коммит (порядок обратный привычному git log), и все строки начинаются словом pick — Git ничего за вас не решил, он просто перечислил, что собирается перенести:
pick a1b2c3 feat: экспорт заказов в CSV
pick d4e5f6 wip
pick 9f07ab поправил опечатку
pick 4be18c добавил тесты экспорта
Работа — заменить слова слева и сохранить файл. Например, так:
pick a1b2c3 feat: экспорт заказов в CSV
squash d4e5f6 wip
fixup 9f07ab поправил опечатку
reword 4be18c добавил тесты экспорта
pick— оставить как есть;reword— поправить сообщение;squash— приклеить к предыдущему (сообщения объединятся);fixup— приклеить, выбросив сообщение;- строки можно переставлять и удалять (
drop).
Разница squash и fixup видна справа: оба приклеивают коммит к предыдущему, но squash сохраняет его сообщение, а fixup выбрасывает.
Итог: два коммита вместо четырёх. wip и «поправил опечатку» приклеились к «feat: экспорт заказов в CSV», причём сообщение wip вошло в его текст, а «поправил опечатку» выброшено; «добавил тесты экспорта» остался отдельным коммитом с поправленным сообщением. Если команда сливает через squash — усердствовать не надо; если через rebase/merge — приборка делает вашу историю подарком для будущих читателей.
И практическая деталь, на которой спотыкаются все. Если ветка уже была на сервере, обычный git push после приборки отклонят: там лежат старые коммиты, а у вас теперь другие. Отправлять надо git push --force-with-lease — и это ровно тот случай, когда так можно: ветка ваша, её ещё никто не сливал. Почему «ваша» здесь ключевое слово — в золотом правиле выше. И одно последствие для открытого pull request: построчные комментарии ревьюеров привязаны к коммитам, а после rebase -i этих коммитов больше нет. Сервер покажет их как «устаревшие» и свернёт, ревьюеру придётся искать, куда делась строка, и проверять, что замечание учли, станет труднее. Поэтому приборку делают до того, как позвали ревьюеров, или в самом конце, когда все замечания закрыты и осталось только слить; в разгар обсуждения историю не трогают.
cherry-pick: перенести один коммит
Иногда нужна не ветка целиком, а один коммит из неё — классика: багфикс из main нужно доставить в ветку релиза 2.3.
git switch release-2.3
git cherry-pick a1b2c3
Git применяет изменения указанного коммита к текущей ветке (как отдельный новый коммит). Здесь атомарность коммитов из статьи про историю окупается буквально: атомарный багфикс переносится одной командой, а «фикс внутри коммита-свалки» — только руками.
Перенесённый коммит — тоже новый: правки те же, идентификатор другой. Чтобы связь не потерялась, в релизные ветки переносят с ключом -x: он дописывает в сообщение строку (cherry picked from commit a1b2c3), и через год видно, откуда исправление пришло и попало ли оно в main. Несколько коммитов подряд переносят диапазоном, git cherry-pick a1b2c3^..d4e5f6 (крышка включает первый), а при конфликте команды те же, что у rebase: разобрать, git add, git cherry-pick --continue, либо --abort.
При последующем слиянии веток перенесённые правки встретятся дважды — обычно молча, потому что Git видит одинаковое изменение с обеих сторон, иногда конфликтом, когда рядом успели поменять что-то ещё. Во втором случае конфликт разбирают в пользу той стороны, где правка свежее, и это единственный раз, когда «оставить свою» осмысленно. Проверить заранее, какие коммиты ветки ещё не долетели до main по содержимому, а не по идентификатору, умеет git cherry -v main release-2.3: плюсом помечены коммиты, чьих правок в main нет, минусом те, что уже там под другим хешем.
Перенесённый фикс лежит в двух ветках под разными идентификаторами, поэтому «долетел ли он» проверяют по содержимому командой git cherry -v, а не по хешу.
Коротко
- Rebase переносит ваши коммиты на новое основание: история прямая, но коммиты — новые. Конфликты решаются как в merge (
--continue/--abort). pull --rebase— чистая ежедневная синхронизация без мусорных merge-коммитов.rebase -i— приборка ветки перед ревью: squash/fixup/reword/drop; отправлять после неё —push --force-with-lease.- Золотое правило: не переписывать отправленное в общие ветки. Своя ветка — можно (
--force-with-lease); main — никогда. - cherry-pick переносит отдельный коммит — атомарные коммиты делают это возможным.
- Подтянуть
main:git rebase origin/main, пока ветка личная, иgit merge origin/main, когда PR уже обсуждают построчно; после rebase только--force-with-lease. - Застрявший rebase:
git statusговорит, на каком коммите вы (2/5), выходы--continue,--abortи--skip; повторяющийся конфликт запоминаетrerere; ветку из-под чужого основания вытаскиваетrebase --onto. - После
rebase -iпострочные комментарии в PR отвязываются: прибираться до ревью или после закрытия замечаний. - В релизные ветки переносят с
-x, диапазонa^..b, конфликт через--continue; что ещё не долетело доmainпо содержимому, показываетgit cherry -v.
Что почитать дальше
- Отмена и спасение — reset, revert, reflog: страховочная сетка, если rebase пошёл не туда.
- Pull request и код-ревью — где приборка истории встречается с процессом команды.
- Модели ветвления — финал цикла: как всё это собирается в процесс.