Stacked diffs: цепочка зависимых PR

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

Stacked diffs: цепочка зависимых PR

Сообщение 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 и как из них выбираться
Боль: один гигантский PR, который никто не хочет ревьюить

Ты неделю пилил большую фичу. Получился PR на 40 файлов и +1800 строк: миграция БД, новый сервис, ручка API, фронтовый компонент, тесты. Ты вешаешь ревьюера, и дальше тишина. Через два дня прилетает "лгтм" без единого комментария - потому что осилить такую простыню честно невозможно. А ты-то знаешь, что в этой простыне затаился баг, и ревью был ровно для того, чтобы его поймать.

Это системная проблема, а не лень конкретного коллеги. Качество ревью падает обвально с ростом диффа: 200 строк человек читает вдумчиво, 1000 - проматывает, 1800 - аппрувит на доверии. Большой PR ещё и блокирует тебя самого: пока он висит, ты не можешь начать следующий кусок, не утащив за собой неотревьюенный код.

Решение называется stacked diffs (они же stacked PR, по-русски - цепочка pull request или зависимые PR). Идея простая до неприличия: не жди, пока вся фича будет готова, чтобы открыть один монолит. Бей задачу на цепочку маленьких зависимых PR, где каждый следующий стоит поверх предыдущего: A <- B <- C. Каждый PR ревьюится отдельно и быстро, сливаются они по порядку снизу вверх. Reviewer видит 150 осмысленных строк за раз и реально думает, а не листает.

Изображение

Что такое стек на самом деле: ветки поверх веток

Под капотом stacked diffs - это не магия, а обычные ветки Git, выстроенные в линию. Разберём руками, без всяких инструментов.

Обычно ты ветвишься от main. В стеке ты ветвишься от предыдущей ветки стека. Пусть фича "профиль пользователя" бьётся на три куска: миграция, сервис, API.

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

git switch main
git switch -c feat/profile-db        # A: миграция, ветвимся от main
# ... коммит A ...
git switch -c feat/profile-service   # B: сервис, ветвимся от A
# ... коммит B ...
git switch -c feat/profile-api       # C: ручка, ветвимся от B
# ... коммит C ...
Теперь история линейна, а ветки-ярлыки расставлены по ней как вехи:

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

* 7c1a9f0 (feat/profile-api)     C: добавил GET /api/profile
* 3e8b210 (feat/profile-service) B: ProfileService
* a4d0c55 (feat/profile-db)      A: миграция profiles
* 91ff002 (main)                 базовый коммит
В легенде наших схем: кружок-узел - это коммит, стрелка ведёт от коммита к его родителю, ярлык-узел (feat/profile-db, main) - это ветка. Стек - это просто три ветки, нанизанные на одну линию коммитов. Дальше каждую ветку ты пушишь и открываешь из неё PR, причём - внимание - база (target) у PR не main, а ветка ниже по стеку:
  • PR A: feat/profile-db -> main
  • PR B: feat/profile-service -> feat/profile-db
  • PR C: feat/profile-api -> feat/profile-service
Зачем такая база? Чтобы в диффе PR B показывались ТОЛЬКО изменения B, а не B плюс A. Если бы B целился в main, ревьюер увидел бы и миграцию из A, и сервис из B в одной куче - мы вернулись бы к монолиту. База на ветку ниже даёт чистый, узкий дифф на каждый слой.

Главная боль стека и git rebase --update-refs

Стек прекрасен ровно до первого ревью-замечания. Тебе говорят: "в миграции A неправильный тип колонки". Ты правишь самый нижний коммит A. И тут начинается ад: B стоял поверх СТАРОГО A, C поверх старого B. Поправив A, ты сделал коммит другим (новый SHA), и весь стек над ним повис в воздухе, указывая на устаревшие коммиты.

До Git 2.38 это лечилось вручную и мучительно: ребейзишь B на новый A, потом C на новый B, по одной ветке, легко перепутать. Именно эту боль закрыл флаг git update-refs - точнее, опция --update-refs у git rebase, появившаяся в Git 2.38 (октябрь 2022). Она и есть штатный механизм работы со стеком на голом Git в 2026.

Смысл: при интерактивном ребейзе Git сам видит ветки, сидящие на промежуточных коммитах внутри переписываемого диапазона, и переставляет их ярлыки на новые коммиты автоматически. Давай поправим нижний коммит и прогоним весь стек одной командой. Стоим на верхушке стека (feat/profile-api):

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

git rebase -i --update-refs main
Открывается todo-лист, и - ключевой момент - в нём появляются служебные строки update-ref:

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

pick a4d0c55 A: миграция profiles
update-ref refs/heads/feat/profile-db
pick 3e8b210 B: ProfileService
update-ref refs/heads/feat/profile-service
pick 7c1a9f0 C: ручка GET /api/profile
update-ref refs/heads/feat/profile-api
Меняем pick на edit у строки A, правим тип колонки, git commit --amend, git rebase --continue. Git перебазирует B и C поверх нового A и, наткнувшись на строки update-ref, сам передвинет ярлыки feat/profile-db, feat/profile-service на их новые коммиты. Вывод в конце:

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

Successfully rebased and updated refs/heads/feat/profile-api.
Updated the following refs with --update-refs:
  refs/heads/feat/profile-db
  refs/heads/feat/profile-service
Весь стек снова стоит ровно, одной командой. Чтобы не дописывать --update-refs каждый раз, включи навсегда:

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

git config --global rebase.updateRefs true
Тонкость, на которой все спотыкаются: --update-refs чинит ЛОКАЛЬНЫЕ ветки, но не пуш. После ребейза истории разъехались с remote, и обычный git push отлетит с non-fast-forward. Нужен форс - и только безопасный, с проверкой, что наверху удалёнки не появилось чужих коммитов:

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

git push --force-with-lease --all
Никогда не делай голый git push --force на ветки стека, особенно если по PR уже кто-то ревьюит: --force затрёт всё вслепую, а --force-with-lease (а ещё строже - --force-if-includes) откажется пушить, если на сервере с твоего последнего fetch что-то изменилось. Это та же дисциплина, что и при любом rewrite опубликованной истории.

Связь с rebase -i и --onto: когда нижний PR смержили

Вторая классическая ситуация: PR A прошёл ревью и его смержили в main (часто squash-мержем, тогда в main лёг ОДИН новый коммит, не равный твоему A). Теперь B и C всё ещё стоят на твоём локальном A, которого в истории main уже нет. Надо пересадить остаток стека (B и C) с исчезнувшего A прямо на main. Это ровно работа для git rebase --onto:

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

git fetch origin
git switch feat/profile-api
git rebase --onto origin/main feat/profile-db --update-refs
Читается так: возьми коммиты ПОСЛЕ feat/profile-db (то есть B и C) и пересади их на origin/main. Старый A выпадает из диапазона, потому что его содержимое уже в main. --update-refs заодно подвинет ярлык feat/profile-service. После этого у PR B надо в интерфейсе сменить базу с feat/profile-db на main - и стек сократился на один слой. Многие инструменты (ниже) делают эту перебазировку базы автоматически после мержа нижнего PR.

Здесь же видно, почему стек требует уверенного владения rebase -i (урок про интерактивный ребейз) и --onto: stacked diffs - это не отдельная фича Git, а дисциплина поверх ребейза. Голый Git даёт фундамент: ветки, --update-refs, --onto, --force-with-lease. Всё остальное - автоматизация рутины.

Инструменты 2026: Graphite, GitHub gh-stack, jj и компания

Вести стек руками на трёх ветках реально. На стеке из восьми PR ты утонешь в ребейзах и смене баз. Поэтому 2026-й - год, когда code review заметно сдвинулся в сторону стеков, и инструменты подтянулись.

Graphite (graphite git - частый запрос) - самый зрелый специализированный продукт. Его CLI gt держит модель стека в голове: gt create делает новую ветку поверх текущей, gt submit пушит весь стек и открывает/обновляет связанные PR с правильными базами, gt sync после мержа нижнего PR сам перебазирует остаток и подчищает слитые ветки. Killer-фича 2026 - stack-aware merge queue: очередь понимает, что PR в стеке зависимы, гоняет CI на всю пачку параллельно и фаст-форвардит её разом, не перетестируя каждый слой по отдельности. Платно для команд.

GitHub нативные stacked PR - свежак апреля 2026. Появилось расширение gh-stack к gh CLI и поддержка стеков в самом интерфейсе: карта стека в UI для навигации между слоями, CI и branch protection проверяются против ФИНАЛЬНОЙ целевой ветки (main), а не только против прямой базы. Это важный сигнал: стеки официально перестали быть нишевой экзотикой.

GitLab stacked diffs - экспериментальная поддержка через glab CLI, тема активно пилится. У GitLab свой давний козырь для последовательного мержа - Merge Trains.

Из легковесного и опенсорсного: git-branchless (команды git sync, git move, умный smartlog - переосмысление работы со стеком поверх обычного Git), spr (Stacked Pull Requests, изначально из Phabricator-культуры, один локальный коммит = один PR) и Sapling от Meta - целый VCS с родной моделью стека и веб-ревью ISL.

Jujutsu (jj) - вот где стек становится не дисциплиной, а естественным режимом. В jj нет staging area и stash, каждое изменение - это change со стабильным change-id, который переживает переписывания. Стек в jj - это просто линия изменений, и когда ты правишь нижнее, все верхние автоматически перебазируются поверх него - без всякого --update-refs, без ручного ребейза, это встроенное поведение. Колоцированный jj живёт прямо поверх .git, его коммиты - настоящие git-коммиты, а мощный operation log даёт undo на любую операцию. Если стеки станут твоим основным стилем работы, присмотрись к jj: то, ради чего в Git мы городим --update-refs и --onto, там бесплатно из коробки. Мосты к GitHub-стекам типа jj-stack и jj-spr уже есть.

Типичные грабли
  • Забыл --update-refs (и не включил rebase.updateRefs) - после ребейза нижнего слоя верхние ветки молча указывают на старые коммиты. Дифф PR раздувается, ревьюер видит чужие изменения. Лечение: включи флаг глобально раз и навсегда.
  • Голый git push --force вместо --force-with-lease - затёр ревью-правки коллеги, которые он успел запушить в твою ветку стека. Всегда --force-with-lease.
  • Неправильная база PR - открыл PR B против main вместо ветки A. Ревьюер видит A+B в одном диффе, весь смысл стека потерян. Проверяй target каждого PR.
  • Слишком глубокий стек - 12 PR, каждый по 30 строк. Накладные расходы на ведение стека и переключение контекста у ревьюера превышают пользу. Стек хорош на 2-5 осмысленных слоях.
  • Squash-мерж нижнего PR ломает родство - в main лёг новый коммит, не равный твоему A. Не пытайся ребейзить "на A", пересаживай через git rebase --onto origin/main.
Мини-лаба: собери и почини стек руками

Десять минут в пустой папке, без всяких сервисов - только Git.
  • 1. git init -b main && git commit --allow-empty -m "base"
  • 2. git switch -c a; добавь файл a.txt, закоммить "A". git switch -c b; файл b.txt, коммит "B". git switch -c c; файл c.txt, коммит "C". Стек из трёх веток готов.
  • 3. git log --oneline --all --decorate - убедись, что a, b, c стоят линией на разных коммитах.
  • 4. Сломай нижний слой: git switch a, измени a.txt, git commit --amend --no-edit. Теперь git log --oneline --all - b и c висят на старом A.
  • 5. Почини одной командой: git switch c && git rebase -i --update-refs main. В todo НЕ трогай pick, просто сохрани и закрой. Прочитай блок "Updated the following refs".
  • 6. git log --oneline --all --decorate ещё раз - все три ветки снова на одной линии. Стек выровнен.
  • 7. Бонус: смоделируй мерж нижнего. git switch main && git merge a. Затем git switch c && git rebase --onto main a --update-refs - пересади b и c на main, A растворился.
Контрольные вопросы
  • 1. Почему в стеке база PR B - это ветка A, а не main? Что увидит ревьюер, если ошибиться с базой?
  • 2. Что именно делают строки update-ref в todo-листе интерактивного ребейза и почему без них верхние ветки стека "повисают"?
  • 3. Почему после ребейза стека нужен --force-with-lease, и чем он безопаснее голого --force?
  • 4. Нижний PR смержили squash-мержем в main. Почему обычный rebase на твою локальную ветку A не сработает и какая команда решает задачу?
Итог

Stacked diffs - это лекарство от непосильных PR: бьёшь фичу на цепочку маленьких зависимых PR A <- B <- C, каждый ревьюится отдельно и быстро, сливаются по порядку. Механика - обычные ветки поверх веток, а ключевой инструмент голого Git в 2026 - git rebase --update-refs (включи rebase.updateRefs глобально), плюс --onto для пересадки стека после мержа и --force-with-lease для безопасного пуша. Когда стеков много, бери Graphite, нативные gh-stack или присматривайся к jj, где стек - родной режим. Стек окупается на 2-5 содержательных слоях; не плоди по 12 крошечных PR ради красоты.
👍2 ❤️2 🔥 😄 🤔
Аватара пользователя
middleraccoon
Сообщения: 1
Зарегистрирован: 11 май 2026, 03:37

Re: Stacked diffs: цепочка зависимых PR

Сообщение middleraccoon »

Долго не мог понять, почему мои стеки разъезжались после правок - оказалось не включал rebase.updateRefs. Поставил глобально, и git rebase -i теперь сам двигает ветки. Спасибо за todo с update-ref, наглядно.
👍1 ❤️ 🔥 😄 🤔1
Аватара пользователя
jojohno
Сообщения: 1
Зарегистрирован: 31 май 2026, 13:57

Re: Stacked diffs: цепочка зависимых PR

Сообщение jojohno »

А подскажите по jj: если коммиты у него настоящие git-коммиты и стек перебазируется сам, есть ли смысл вообще учить --update-refs? Или это на случай когда в команде только голый Git и Graphite не завезли?
👍3 ❤️1 🔥 😄 🤔
Ответить
← Предыдущая глава
git cherry-pick: перенос отдельных коммитов
Следующая глава →
Массовое переписывание истории: git filter-repo и git replace

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

Поделиться темой: ✈ Telegram VK

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

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

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