Практическое руководство по Stacked PR: как избавиться от недельных задержек ревью из-за гигантских PR
С внедрением AI-инструментов скорость генерации кода невероятно возросла. Однако скорость код-ревью за ней не поспевает. Согласно отчету DORA (DevOps Research and Assessment) за 2024 год, в командах с высоким уровнем внедрения AI средний размер PR увеличился на 154%, а время на ревью выросло на 91%. В результате частота деплоев только замедлилась. Любой устанет, если ему предлагают проревьюить за один раз более 1 000 строк кода. Чтобы покончить с этим хаосом, нужно перейти на цепочечную структуру из связанных друг с другом PR — Stacked PRs.
Делим кусок в 1 000 строк на 3 ветки
Если просто так разрезать код, компиляция упадет или нарушатся зависимости. Нужен четкий критерий. Ограничьте единичный PR изменением не более чем 200 строк кода и менее чем 10 файлами, чтобы ревьюер мог закончить работу за 15 минут. Одна гигантская задача делится максимум на 3 дочерние ветки в цепочке:
- Layer 1: Схема БД, сущности, DTO
- Layer 2: Доменная логика, реализации сервисов
- Layer 3: Контроллеры, API-эндпоинты
Строго соблюдайте этот порядок. Названия веток также фиксируйте в формате feature/<название-фичи>/stack-<этап>-<описание>. Процесс декомпозиции уже запутанной ветки на 1 000 строк (legacy-giant-branch) в цепочку довольно прост.
Сначала обновите ветку main и сделайте бэкап оригинала.
`bash
git checkout main
git pull origin main
git checkout legacy-giant-branch
git branch backup/legacy-giant-branch
`
Затем ответвите Layer 1 от main и примените через cherry-pick только коммиты, относящиеся к схеме.
`bash
git checkout -b feature/order-refactor/stack-1-schema main
git cherry-pick
git push origin feature/order-refactor/stack-1-schema
`
Теперь, используя Layer 1 в качестве родительской ветки, последовательно создайте Layer 2 и Layer 3.
`bash
git checkout -b feature/order-refactor/stack-2-service feature/order-refactor/stack-1-schema
git cherry-pick
git push origin feature/order-refactor/stack-2-service
git checkout -b feature/order-refactor/stack-3-controller feature/order-refactor/stack-2-service
git cherry-pick
git push origin feature/order-refactor/stack-3-controller
`
Выбор инструментов тоже вызывает споры. Чистый Git CLI не требует затрат, но требует много ручной работы, а специализированные инструменты вроде Graphite удобны, но влекут за собой расходы и инфраструктурную зависимость.
| Критерий оценки |
Pure Git CLI |
GitHub CLI (gh stack) |
Graphite / External Tools |
| Отслеживание зависимостей |
Ручная настройка базы веток |
Локальный .git и метаданные GitHub |
Отдельный удаленный движок и локальная БД |
| Автоматизация ребейза |
Ручная работа с каждой веткой |
Выполнение команды gh stack rebase |
Автоматическая обработка дочерней цепочки при изменении коммитов |
| Кривая обучения |
Высокая (требуется понимание принципов Git) |
Низкая (установка расширения CLI) |
Средняя (требуется адаптация к отдельному UI) |
| Операционная стабильность |
100% совместимость со стандартным Git |
Статус Public Preview |
Очень высокая |
Предотвращение конфликтов ребейза при изменении родительского PR
При использовании Stacked PR самый неприятный момент — это запрос на исправление родительского PR. Поскольку хеши коммитов меняются, все дочерние ветки ниже ломаются. Если просто применить git rebase, коммиты добавятся дубликатами. Диапазон для извлечения нужно указывать с помощью конструкции git rebase --onto.
Если был изменен Layer 1, перейдите в Layer 2 и перенесите изменения с момента коммита старого Layer 1 до текущего HEAD на новую вершину Layer 1.
`bash
git checkout feature/order-refactor/stack-2-service
git rebase --onto feature/order-refactor/stack-1-schema feature/order-refactor/stack-2-service
git push --force-with-lease
`
Точно так же перенесите Layer 3 поверх нового Layer 2.
`bash
git checkout feature/order-refactor/stack-3-controller
git rebase --onto feature/order-refactor/stack-2-service feature/order-refactor/stack-3-controller
git push --force-with-lease
`
Если делать эту работу вручную каждый раз, человек обязательно допустит ошибку. Напишитеシェル-скрипт (shell-скрипт) для автоматизации последовательного ребейза.
`bash
#!/usr/bin/env bash
set -e
STACK_BRANCHES=(
"feature/order-refactor/stack-1-schema"
"feature/order-refactor/stack-2-service"
"feature/order-refactor/stack-3-controller"
)
TARGET_BASE="main"
git fetch origin
CURRENT_PARENT="$TARGET_BASE"
for BRANCH in "STACKBRANCHES[@]";dogitcheckout"BRANCH"
OLD_BASE=(gitmerge−base"CURRENT_PARENT" "BRANCH"2>/dev/null∣∣echo"")if[−n"OLD_BASE" ]; then
git rebase --onto "CURRENTPARENT""OLD_BASE" "BRANCH"∣∣exit1figitpush−−force−with−leaseorigin"BRANCH"
CURRENT_PARENT="$BRANCH"
done
`
Не забудьте включить функцию сохранения истории разрешения конфликтов в настройках Git. Она автоматически пропустит повторяющиеся конфликты.
`bash
git config --global rerere.enabled true
git config --global rerere.autoupdate true
git config --global rebase.autoStash true
git config --global rebase.updateRefs true
`
Предотвращение траты ресурсов CI-раннеров и путаницы в порядке слияния
Разделение PR на три части приводит к проблеме троекратного запуска CI-раннеров. Запускать тяжелые E2E-тесты для каждого PR не требуется. Настройте окружение GitHub Actions так, чтобы на нижних уровнях выполнялись только надежные юнит-тесты, а полная интеграционная проверка выполнялась только в PR верхнего уровня.
Добавьте условие позиции в стеке в .github/workflows/ci.yml.
`yaml
name: Stack-Aware CI Pipeline
on:
pull_request:
branches: [ main ]
jobs:
fast-lint-and-unit-test:
name: Fast Validation (All Layers)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Linter & Unit Tests
run: |
npm ci
npm run test:unit
heavy-integration-test:
name: Full E2E Test (Top Layer Only)
runs-on: ubuntu-latest
if: |
github.event.pull_request.stack == null ||
github.event.pull_request.stack.position == github.event.pull_request.stack.size
steps:
- uses: actions/checkout@v4
- name: Run Heavy Integration & E2E Tests
run: |
npm ci
npm run test:e2e
`
Настройка правил защиты ветки на уровне организации также обязательна.
- Включите функцию GitHub Merge Queue, чтобы принудительно объединять одобренные стэк-PR по порядку.
- Включите 'Require Linear History' в Repository Rulesets, чтобы предотвратить появление мусорных коммитов слияния (Merge Commit).
- Используйте 'Require Status Checks to Pass Before Merging', чтобы предотвратить ситуации, когда PR верхнего уровня попадает в
main до слияния нижнего PR.
Культура командного ревью и устранение проблем с UI после Squash Merge
Для полноценной работы со Stacked PR ревьюеры должны видеть общую картину. Укажите карту стека в файле .github/PULL_REQUEST_TEMPLATE.md.
`markdown
Stack Overview
Этот PR является частью цепочки Order Refactoring Stack.
- #101 - feature/order-refactor/stack-1-schema (DB Migration)
- [Current PR] #102 - feature/order-refactor/stack-2-service (Business Logic)
- #103 - feature/order-refactor/stack-3-controller (API Controller)
Current Layer Scope
- Реализация логики изменения статуса платежа в OrderService
- Добавление обработки публикации доменных событий и тестов интеграции с репозиторием
Dependency Status
- Base PR: #101 (Должен быть слит в первую очередь)
`
После того как нижний PR #101 объединяется с main через Squash Merge, Diff в пользовательском интерфейсе GitHub для дочернего PR #102 неожиданно взрывается. Это явление, при котором изменения уже слитого #101 начинают учитываться как изменения дочернего PR. Это происходит потому, что SHA Squash-коммита и SHA родительского коммита в графе коммитов Git стали разными. Не паникуйте, просто перенесите коммиты дочерней ветки поверх main в следующем порядке:
`bash
git log --graph --oneline
git checkout feature/order-refactor/stack-2-service
git fetch origin
git rebase --onto origin/main feature/order-refactor/stack-2-service
git push --force-with-lease origin feature/order-refactor/stack-2-service
`
Работа по разделению гигантского PR на 1 000 строк — это не просто вопрос владения технологией Git. Это инженерная задача по наведению порядка в процессе код-ревью команды и во всей цепочке деплоя. Столкнувшись с грязной устаревшей веткой, сначала сделайте бэкап и попробуйте разбить её на 3-уровневые слои. Вы не только снизите усталость ревьюеров, но и сами научитесь гораздо лучше контролировать код.