Ты неделю пилил большую фичу. Получился 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) базовый коммит
- PR A: feat/profile-db -> main
- PR B: feat/profile-service -> feat/profile-db
- PR C: feat/profile-api -> feat/profile-service
Главная боль стека и 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
Код: Выделить всё
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
Код: Выделить всё
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
Код: Выделить всё
git config --global rebase.updateRefs true
Код: Выделить всё
git push --force-with-lease --all
Связь с 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
Здесь же видно, почему стек требует уверенного владения 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 ради красоты.