git rebase: пересадка истории и золотое правило

Рейтинг: 72.2% · 10 голосов
Самый подробный курс по Git - от первого коммита до объектной базы и packfiles. Терминал и первый репозиторий, ветки и слияние, конфликты, удалённая работа и форджи (GitHub, GitLab), rebase и переписывание истории, спасение кода через reflog и fsck, stash и worktree, bisect и blame, подмодули и монорепо, хуки и подпись коммитов, внутреннее устройство и масштаб. Для новичков и профи. Актуально на 2026 (Git 2.54, дорожная карта 3.0).
Ответить
Аватара пользователя
Pavel_Git
Сообщения: 52
Зарегистрирован: 11 май 2026, 05:31

git rebase: пересадка истории и золотое правило

Сообщение Pavel_Git »

Оглавление курса (52)
  1. Что такое контроль версий и почему победил Git
  2. Установка Git на Windows, macOS и Linux и первая настройка
  3. Терминал и как спрашивать у Git: выживание в командной строке
  4. Ментальная модель Git: три состояния, коммит-снимок и граф
  5. Первый репозиторий: init, add, commit, status
  6. Просмотр изменений: status, diff и индексация по частям
  7. История проекта: git log, диапазоны и поиск
  8. .gitignore: что не должно попасть в репозиторий
  9. .gitattributes: окончания строк, бинарники и атрибуты
  10. Ветки как указатели: branch, switch и detached HEAD
  11. Слияние веток: fast-forward против трёхстороннего merge
  12. Конфликты слияния: анатомия и уверенное разрешение
  13. Практикум: твой первый день с Git от нуля до push
  14. Удалённые репозитории: remote, fetch, базовый pull и refspec
  15. git push: безопасная отправка и защита от перезаписи
  16. Клонирование: shallow, partial clone, bundle и backfill
  17. GitHub, GitLab и форки: pull request, rulesets и защита веток
  18. Рабочие процессы команды: GitFlow, GitHub Flow, trunk-based
  19. Merge queue и merge trains: безопасное вливание в trunk
  20. Культура code review и Conventional Commits
  21. git reset: три дерева и отмена индексации и коммитов
  22. git revert: безопасная отмена в общей истории
  23. Правка последнего коммита: git commit --amend
  24. git rebase: пересадка истории и золотое правило (вы здесь)
  25. Интерактивный rebase: переписать серию коммитов
  26. git cherry-pick: перенос отдельных коммитов
  27. Stacked diffs: цепочка зависимых PR
  28. Массовое переписывание истории: git filter-repo и git replace
  29. git reflog: журнал, который помнит всё
  30. Восстановление утерянных коммитов, веток и файлов
  31. Откат опасных операций: reset --hard, сломанный rebase, плохой merge
  32. git stash: тайник для незавершённой работы
  33. git worktree: несколько рабочих деревьев одного репозитория
  34. git bisect: бинарный поиск регрессии
  35. git blame, mailmap и археология кода
  36. Теги, релизы, archive и bundle
  37. Подмодули: внешние репозитории внутри проекта
  38. Subtree, монорепозитории и sparse-checkout
  39. Git LFS и большие файлы
  40. Вспомогательные команды: clean, mv, rm, restore
  41. Псевдонимы и кастомизация .gitconfig
  42. Хуки Git: автоматизация на клиенте и сервере
  43. Подпись коммитов и тегов: SSH, GPG и Sigstore
  44. Внутреннее устройство I: объектная база Git
  45. Внутреннее устройство II: ссылки, HEAD, notes и reftable
  46. Внутреннее устройство III: индекс, packfiles, gc и обслуживание
  47. От SHA-1 к SHA-256: переход на новый хеш
  48. Огромные репозитории: partial clone, FSMonitor, Scalar
  49. Git в CI/CD: shallow, кеш, merge_group и безопасность
  50. Куда движется Git: дорожная карта 3.0 и экосистема
  51. Профессиональный setup: собираем рабочее окружение Git
  52. 30+ типичных ошибок Git и как из них выбираться
Ты доделал фичу. Пока ты копался в коде три дня, в main прилетело двадцать чужих коммитов. Открываешь Pull Request - а там багровая надпись "This branch is out of date". Дальше две дороги. Первая: git merge main в свою ветку, и в историю врезается merge-коммит "Merge branch main into feature-x", а граф расползается в косичку. Вторая: git rebase, и твои коммиты будто бы написаны только что, поверх свежего main, история прямая как рельса. Этот урок - про вторую дорогу, про её механику до винтика, и про одно правило, нарушение которого ломает рабочий день всей команде.

Что физически делает git rebase

Слово "перебазирование" буквально про это: rebase меняет базу ветки. База - это коммит, от которого ветка начала расходиться с другой (общий предок). Merge соединяет две линии новым коммитом. Rebase поступает иначе: он берёт твои коммиты, отвязывает их от старой базы и заново прикладывает поверх новой.

Ключевое слово - "заново". Коммит в git неизменяем: его SHA-1 (а в новых репозиториях с Git 2.51 - SHA-256) считается от содержимого дерева, родителя, автора, времени и сообщения. Меняешь родителя - меняется хеш. Поэтому rebase не двигает старые коммиты, он создаёт новые с тем же содержимым изменений (патчем), но другим родителем, и поэтому другим SHA. Старые коммиты остаются висеть в объектной базе сиротами, пока их не уберёт сборка мусора.

Разберём на графе. Легенда простая: кружок-узел - коммит, стрелка - связь коммит->родитель, ярлык - ветка или HEAD.

Код: Выделить всё

До rebase:

      A---B---C   feature
     /
D---E---F---G   main

После git switch feature; git rebase main:

              A'--B'--C'   feature
             /
D---E---F---G   main
Видишь штрихи у A, B, C? Это и есть новые коммиты. Содержимое изменений то же, родитель новый (теперь цепочка растёт от G, а не от E), хеши другие. Старые A, B, C никуда из базы не делись, но на них больше не указывает ни одна ветка - они доступны только через reflog. Главное: история стала линейной. Нет вилки, нет слияния, читается сверху вниз как книга.

Механически rebase делает примерно следующее: вычисляет список коммитов, которые есть в feature, но нет в main (git log main..feature), запоминает их патчи по порядку, перематывает feature на main, а потом проигрывает патчи один за другим как git cherry-pick. С Git 2.34 за это отвечает merge-движок ort - тот же, что в обычном merge, так что трёхстороннее применение патча умное, а не тупой текстовый diff.

Изображение

git merge vs rebase: главный спор и где правда

"git merge vs rebase" - один из топовых вечных холиваров. Сразу скажу: правильного ответа "всегда так" нет, есть контекст. Разложим честно.

Merge сохраняет историю как она была. Каждый merge-коммит - это документ: "вот тут ветка X влилась в main, вот её два родителя". Ты видишь, что фича разрабатывалась параллельно три дня. Ничего не переписывается, существующие коммиты неприкосновенны. Минус - граф со временем превращается в спагетти: десятки merge-коммитов, параллельные дорожки, и git log без флагов читается тяжело.

Rebase даёт линейную историю git: одна прямая линия, git log читается как changelog, git bisect по ней работает идеально (нет merge-коммитов, в которых непонятно, что именно сломалось). Минус - ты переписываешь историю. Коммиты получают новые хеши, реальная хронология теряется (твои коммиты выглядят так, будто написаны после чужих, хотя физически были раньше).

Философски разница вот в чём. Merge отвечает на вопрос "что произошло на самом деле". Rebase отвечает на вопрос "как должна читаться история, чтобы её было удобно изучать потом". Первое - про точность протокола, второе - про чистоту нарратива. Команды выбирают по вкусу и по тому, важна ли им археология или читабельность.

Практический компромисс, который прижился в индустрии: rebase локально, merge публично. Свою личную ветку перед вливанием причёсываешь rebase'ом (она ещё никому не нужна), а в общий main вливаешь через merge или squash. Это подводит нас к самому важному.

Золотое правило rebase: не трогай опубликованную историю

Запиши это и повесь над столом.
Никогда не делай rebase коммитов, которые ты уже опубликовал и которыми пользуется кто-то ещё.
Почему это смертельно? Rebase меняет хеши. Хеш - это идентичность коммита для всего git. Допустим, ты запушил ветку feature с коммитами A, B, C. Коллега сделал git fetch и завязал на C свою работу - коммит D с родителем C. Теперь ты передумал, сделал rebase, и у тебя появились A', B', C' с новыми хешами. Ты делаешь force-push. На сервере теперь C', а старого C нет в ветке.

Что у коллеги? Его D всё ещё указывает на исчезнувший C. Когда он сделает pull, git увидит две расходящиеся истории - твою новую (C') и его старую (C->D) - и предложит их слить. Коллега, не разобравшись, сольёт. И в историю вернутся старые A, B, C, которые ты выкинул, плюс дубликаты-двойники A', B', C'. Каждое изменение теперь присутствует дважды. Это и есть "клоны разъезжаются": репозитории команды расходятся, и собрать их обратно - боль на полдня.

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

Есть нюанс с "своей" feature-веткой в PR, которую видишь только ты. Технически она опубликована, но раз на неё никто не завязан, rebase допустим - но force-push делай только через --force-with-lease, а не голым --force:

Код: Выделить всё

git push --force-with-lease origin feature
--force-with-lease сначала проверяет, что удалённая ветка указывает туда, куда ты ожидаешь. Если кто-то успел запушить туда, пока ты ребейзил, push откажется - и ты не сотрёшь чужую работу. Голый --force затирает молча. Возьми за рефлекс: для перезаписи своих веток - всегда --force-with-lease.

git rebase --onto: хирургическая пересадка истории

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

Синтаксис: git rebase --onto НОВАЯ_БАЗА СТАРАЯ_БАЗА [ВЕТКА]. Читается так: "возьми коммиты после СТАРАЯ_БАЗА (не включая её саму) вплоть до ВЕТКА и пересади их на НОВАЯ_БАЗА".

Классический сценарий. Ты отвёл ветку topic от ветки feature, а feature - от main. Потом понял, что topic к feature отношения не имеет, её надо повесить прямо на main, выкинув коммиты feature из-под неё.

Код: Выделить всё

              H---I---J   topic
             /
    E---F---G   feature
   /
A-B-C-D   main
Тебе нужны только H, I, J, но без E, F, G под ними. Команда:

Код: Выделить всё

git rebase --onto main feature topic
Перевод: новая база - main, старая база - feature (то есть отрезаем всё до feature включительно), переносим ветку topic. Результат:

Код: Выделить всё

          H'--I'--J'   topic
         /
A-B-C-D   main
H, I, J переехали на кончик main, а E, F, G под ними испарились из ветки topic. Вот реальный вывод:

Код: Выделить всё

$ git rebase --onto main feature topic
Successfully rebased and updated refs/heads/topic.
Второй частый случай --onto - выкинуть коммит из середины. Допустим, в ветке A-B-C-D коммит B оказался лишним, а C и D нужны:

Код: Выделить всё

git rebase --onto A B D
"Поставь на A всё, что после B до D" - то есть C и D приклеятся прямо к A, минуя B. Тут, конечно, легко словить конфликт, если C опирался на изменения из B - об этом ниже.

Запомни мантру для --onto: куда, откуда-не-включая, что. Три аргумента в этом порядке, и магия становится понятной.

Конфликты при rebase: continue, skip, abort

Rebase проигрывает твои патчи по одному поверх новой базы. Если патч не ложится чисто - rebase останавливается ровно на проблемном коммите и отдаёт тебе руль. Это не катастрофа, это нормальная часть процесса. Вывод выглядит так:

Код: Выделить всё

$ git rebase main
Auto-merging service.py
CONFLICT (content): Merge conflict in service.py
error: could not apply 4f2a1c9... add retry logic
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 rebase --continue - после того как ты открыл файл, убрал маркеры <<<<<<< ======= >>>>>>>, оставил правильный код и сделал git add. Git доделывает текущий коммит и идёт к следующему патчу. Это твой основной путь.
  • git rebase --skip - выбросить текущий коммит совсем. Используй осознанно: если изменение уже присутствует в новой базе (например, тот же фикс уже влили в main), патч становится пустым, и skip - правильный ход. Случайный skip = потеря изменений.
  • git rebase --abort - паническая кнопка. Откатывает всё к состоянию до rebase, ветка возвращается на старую базу как будто ничего не было. Если запутался в конфликтах - не геройствуй, жми abort и подумай.
Важный момент про множественные конфликты. При rebase один и тот же конфликт может вылезти несколько раз подряд - в каждом коммите, который трогал ту же строку. Это бесит новичков ("я же только что это решил"). Помогает git rerere (reuse recorded resolution): включи git config --global rerere.enabled true, и git запомнит, как ты разрулил конфликт, и применит то же решение автоматически в следующий раз. На длинных rebase это спасает.

Где ты сейчас в процессе - всегда видно. git status во время rebase честно пишет "interactive rebase in progress" и сколько коммитов осталось.

git pull --rebase и --ff-only: тонкости, обещанные ранее

Теперь про pull - команду, которую все используют и почти никто не настроил правильно. git pull - это не одна операция, а две: git fetch (скачать чужие коммиты) плюс что-то, чтобы их пристыковать. И вот это "что-то" по умолчанию - merge. А значит, дефолтный pull плодит мелкие merge-коммиты "Merge branch main of origin..." каждый раз, когда твоя локальная ветка разошлась с удалённой. Это мусор, который замусоривает историю.

Есть три режима, и ты обязан выбрать осознанно.

Код: Выделить всё

git pull --rebase   # fetch + rebase: твои локальные коммиты переедут поверх чужих
git pull --ff-only   # fetch + только перемотка, никаких слияний
git pull --no-rebase  # fetch + merge (старый дефолт, плодит merge-коммиты)
pull rebase (git pull --rebase) - когда у тебя есть локальные неотправленные коммиты, а в origin прилетели чужие. Вместо слияния git перебазирует твои поверх свежескачанных. История остаётся линейной, лишнего merge-коммита нет. Это то, что нужно 90% времени в командной работе.

--ff-only (fast-forward only) - самый строгий и безопасный. Он соглашается только на перемотку: если у тебя нет своих локальных коммитов и ветку можно просто продвинуть вперёд - ок. А если истории разошлись - git откажется и заставит тебя явно решить, rebase или merge. Вывод при расхождении:

Код: Выделить всё

$ git pull --ff-only
fatal: Not possible to fast-forward, aborting.
Это не баг, это фича: pull больше не делает тихо ничего неожиданного. Современный git вообще ругается, если режим не настроен, и просит выбрать.

Настрой раз и навсегда в глобальном конфиге. Рекомендую такую связку:

Код: Выделить всё

git config --global pull.rebase true   # pull всегда ребейзит
git config --global pull.ff only     # но если ребейз не нужен - только fast-forward
git config --global rebase.autoStash true  # спрятать незакоммиченное на время rebase
С pull.rebase=true команда git pull автоматически становится git pull --rebase - больше не надо помнить флаг. autoStash сам спрячет твои незакоммиченные правки в stash перед rebase и вернёт после, чтобы pull не упирался в "your local changes would be overwritten".

Линейная история: за и против, и когда merge честнее

Линейная история git - это красиво и удобно: git log, git bisect, git blame работают как часы, ревью читается по коммитам. Многие команды настраивают сервер на rebase-merge или squash-merge для PR, чтобы main был идеально прямым.

Но у медали есть оборот. Rebase врёт о времени: коммиты, написанные в понедельник, после rebase в пятницу выглядят как пятничные, поставленные после чужих изменений. Если важна точная археология ("в каком порядке реально велась работа", "когда именно слили релизную ветку") - merge честнее, потому что merge-коммит фиксирует факт слияния и обоих родителей навсегда.

Когда merge честнее rebase:
  • Слияние долгоживущих веток (release, develop в main) - тут merge-коммит это значимая веха, а не шум.
  • Когда на ветку уже завязались другие - золотое правило, rebase запрещён.
  • Когда важна аудируемость: кто, когда, что именно влил - регуляторика, безопасность.
Когда rebase уместнее: причесать свою личную ветку перед PR, подтянуть свежий main под фичу (git pull --rebase), выкинуть отладочные коммиты, переставить кусок истории через --onto. Везде, где история ещё твоя личная.

Мини-лаба: пощупай rebase и --onto руками

Десять минут, всё локально, ничего не пушим. Повтори по шагам.

Код: Выделить всё

# 1. Песочница
mkdir rebase-lab && cd rebase-lab
git init -b main
git config user.email you@example.com
git config user.name You

# 2. Базовая история на main
echo base > file.txt && git add . && git commit -m "base"
echo more >> file.txt && git commit -am "main work"

# 3. Ветка feature с двумя своими коммитами
git switch -c feature
echo feat1 > feat.txt && git add . && git commit -m "feat 1"
echo feat2 >> feat.txt && git commit -am "feat 2"

# 4. Тем временем main ушёл вперёд
git switch main
echo newmain >> file.txt && git commit -am "new main commit"

# 5. Посмотри вилку
git log --oneline --graph --all

# 6. Перебазируй feature на свежий main
git switch feature
git rebase main
git log --oneline --graph --all   # теперь прямая линия

# 7. Поиграй с --onto: отведи topic от feature
git switch -c topic
echo t1 > t.txt && git add . && git commit -m "topic 1"
# пересади topic прямо на main, убрав коммиты feature из-под него
git rebase --onto main feature topic
git log --oneline --graph --all
После шага 6 убедись, что хеши коммитов feature изменились - сравни git log до и после. После шага 7 в ветке topic под коммитом "topic 1" должен быть сразу main, без коммитов feature. Если в шаге 7 будет конфликт - попрактикуй --continue/--abort, это и есть цель упражнения.

Типичные грабли
  • Force-push голым --force в общую ветку - классика разрушения. Всегда --force-with-lease, и только в своих ветках.
  • Rebase ветки, на которую кто-то завязан - нарушение золотого правила, дубли коммитов у всей команды. Сначала спроси, не отвёл ли кто-то от тебя ветку.
  • Паника при конфликте и хаотичный --skip - skip выбрасывает коммит. Если не уверен - --abort и начни заново на свежую голову.
  • Путаница в аргументах --onto - порядок "куда откуда что", и "откуда" не включается. Перечитай мантру.
  • Дефолтный pull без настройки - тихо плодит merge-коммиты. Поставь pull.rebase=true и pull.ff=only один раз.
Контрольные вопросы
  • Почему после rebase у коммитов меняются хеши, хотя изменения в коде те же самые?
  • Сформулируй золотое правило rebase и объясни, что конкретно сломается у коллеги, если его нарушить.
  • У тебя ветка topic отведена от feature, а нужно повесить её прямо на main без коммитов feature. Какая команда и в каком порядке аргументы?
  • Чем git pull --ff-only отличается от git pull --rebase и в какой ситуации каждый из них уместен?
Итог

Rebase пересаживает твои коммиты на новую базу, создавая новые коммиты с новыми хешами и линейную историю. Merge сохраняет правду о том, что было, ценой ветвистого графа. Выбор - это спор "точность против читабельности", и ответ зависит от контекста: rebase локально и приватно, merge публично и для долгих веток. --onto даёт хирургическую пересадку любого куска истории по схеме "куда, откуда-не-включая, что". Pull настрой раз: pull.rebase=true плюс pull.ff=only, и больше никаких случайных merge-коммитов. И главное - золотое правило: опубликованную историю не ребейзят. Запомнишь только это - уже сбережёшь команде кучу нервов.
👍2 ❤️3 🔥 😄 🤔
Аватара пользователя
LinuxMaster
Сообщения: 1
Зарегистрирован: 11 май 2026, 23:13

Re: git rebase: пересадка истории и золотое правило

Сообщение LinuxMaster »

Дошло наконец почему force-push ломает команду - старый C исчезает а у коллеги D на него смотрит. Спасибо за пример с дублями.
👍2 ❤️1 🔥 😄 🤔1
Аватара пользователя
semyon123
Сообщения: 1
Зарегистрирован: 26 май 2026, 21:45

Re: git rebase: пересадка истории и золотое правило

Сообщение semyon123 »

Поставил pull.rebase true и pull.ff only как советуешь, история перестала зарастать merge-коммитами. А --onto всё ещё перечитываю по три раза, мантра помогает.
👍1 ❤️1 🔥 😄 🤔1
Ответить
← Предыдущая глава
Правка последнего коммита: git commit --amend
Следующая глава →
Интерактивный rebase: переписать серию коммитов

Все главы курса «Git профессионально: от первого коммита до внутреннего устройства»

Поделиться темой: ✈ Telegram VK
Похожие запросы: git merge или rebase: что выбратьgit rebase: перебазированиеgit filter-repo: удалить файл из истории

Вернуться в «Git профессионально: от первого коммита до внутреннего устройства»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 1 гость