← назад к разделу

Ветка живёт две недели, каждое утро её обновляют из main, и через месяц git log наполовину состоит из «Merge branch main»: история в ромбах, в которой не найти ни одного содержательного коммита. merge объединяет ветки, честно сохраняя всё как было, отсюда и ромбы. rebase решает ту же задачу иначе: переносит ваши коммиты на новое основание, переписывая их. Результат — прямая, читаемая история без ромбов и merge-коммитов. Цена — правила безопасности, которые нужно знать твёрдо, поэтому они идут сразу за механикой. Дальше без религиозных войн «merge vs rebase».

git rebase main main a1b2c3d4было 9f074be1стало feature правки те же — идентификаторы новые

Ветка 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).
строка списка отдельный коммит pick склейка с сообщением 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 нет, минусом те, что уже там под другим хешем.

main 9f07 a1b2 фикс 4be1 release-2.3 7d1e копия a1b2

Перенесённый фикс лежит в двух ветках под разными идентификаторами, поэтому «долетел ли он» проверяют по содержимому командой 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.

Что почитать дальше