Cómo implementar PRs apilados (Stacked PRs) en el trabajo real cuando las revisiones se retrasan una semana debido a PRs gigantescos
Con la introducción de herramientas de IA, la velocidad de generación de código se ha vuelto increíblemente rápida. Sin embargo, la velocidad de revisión no logra seguir el ritmo. Según el informe de DORA (DevOps Research and Assessment) de 2024, los equipos con una alta tasa de adopción de IA experimentaron un aumento promedio del 154% en el tamaño de los PRs y un incremento del 91% en el tiempo necesario para las revisiones. Como resultado, los ciclos de despliegue se han vuelto en realidad más lentos. Si le pides a alguien que revise de una sola vez más de 1.000 líneas de código, los ojos de cualquiera se fatigarán. Para poner fin a este caos, es necesario cambiar a una estructura de cadena de PRs apilados (Stacked PRs) donde el código se acumula por capas.
Dividir un bloque de 1.000 líneas en 3 ramas
Si cortas el código sin pensar, la compilación fallará o se romperán las dependencias. Se necesitan criterios claros. Limita un PR individual a menos de 200 líneas de código modificado y menos de 10 archivos modificados para que los revisores puedan terminar en 15 minutos. Una funcionalidad gigante se divide en un máximo de 3 cadenas de ramas hijas.
- Layer 1: Esquema de BD, Entidad, DTO
- Layer 2: Lógica de dominio, implementaciones de servicio
- Layer 3: Controlador, endpoints de API
Sigue este orden rigurosamente. Fija también los nombres de las ramas con el formato feature/<nombre-funcionalidad>/stack-<etapa>-<descripción>. El proceso de descomponer una rama de 1.000 líneas que ya está enredada (legacy-giant-branch) en una cadena es sencillo.
Primero, actualiza la rama main y haz una copia de seguridad del original.
`bash
git checkout main
git pull origin main
git checkout legacy-giant-branch
git branch backup/legacy-giant-branch
`
A continuación, crea la rama Layer 1 desde main y aplica mediante cherry-pick únicamente los commits relacionados con el esquema.
`bash
git checkout -b feature/order-refactor/stack-1-schema main
git cherry-pick
git push origin feature/order-refactor/stack-1-schema
`
Ahora, tomando Layer 1 como padre, puedes extender Layer 2 y Layer 3 secuencialmente.
`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
`
La elección de herramientas también es un factor a considerar. La CLI pura de Git no cuesta dinero pero requiere mucho trabajo manual, mientras que las herramientas dedicadas como Graphite son cómodas pero generan costos y dependencia de infraestructura.
| Criterio de Evaluación |
Pure Git CLI |
GitHub CLI (gh stack) |
Graphite / External Tools |
| Seguimiento de dependencias |
Configuración manual de la Base de la rama |
.git local y metadatos de GitHub |
Motor remoto separado y BD local |
| Automatización de rebase |
Trabajo manual por rama individual |
Ejecución del comando gh stack rebase |
Procesamiento automático de la cadena inferior al cambiar commits |
| Curva de aprendizaje |
Alta (requiere comprender los principios de Git) |
Baja (instalación de extensión CLI) |
Media (requiere adaptación a una interfaz de usuario dedicada separada) |
| Estabilidad operativa |
100% compatible con Git estándar |
En estado de vista previa pública |
Muy alta |
Prevenir conflictos de rebase cuando se modifica el PR padre
Al usar PRs apilados, el momento más doloroso es cuando llega una solicitud de modificación para el PR padre. Esto se debe a que, al cambiar el Hash del commit, todas las ramas hijas conectadas debajo se rompen. Si simplemente aplicas git rebase, los commits se duplicarán. Debes especificar el rango a recortar usando la sintaxis git rebase --onto.
Si se modificó Layer 1, ve a Layer 2 y reubica desde el punto de commit de Layer 1 antes de la modificación hasta el HEAD actual sobre el punto final del nuevo 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
`
Mueve Layer 3 de la misma manera sobre el nuevo 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
`
Si haces este trabajo manualmente cada vez, los humanos inevitablemente cometerán errores. Crea un script de shell para automatizar el rebase secuencial.
`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
`
No olvides activar la función de registro del historial de resolución de conflictos en la configuración de Git. Esto omitirá automáticamente los conflictos repetitivos.
`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
`
Bloquear el desperdicio de costos de los ejecutores de CI y el desorden en el orden de fusión
Dividir un PR en 3 genera el problema de que los ejecutores de CI también se ejecutan 3 veces. No es necesario ejecutar pruebas E2E pesadas en todos los PRs. Configura el entorno de GitHub Actions para que ejecute solo pruebas unitarias sólidas en las capas inferiores y realice la validación de integración completa únicamente en el PR superior.
Agrega una condición de ubicación de pila en .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
`
También es esencial configurar reglas de protección de ramas a nivel de organización.
- Activa la función GitHub Merge Queue para forzar que los PRs apilados aprobados se fusionen en orden.
- Activa 'Require Linear History' en Repository Rulesets para evitar commits de fusión residuales.
- Previene accidentes en los que un PR superior se mezcle en main antes de que el PR inferior haya sido fusionado usando 'Require Status Checks to Pass Before Merging'.
Cultura de revisión en equipo y manejo de la ruptura de la interfaz de usuario después de Squash Merge
Para hacer funcionar correctamente los PRs apilados, los revisores deben poder ver el panorama general. Especifica el mapa de la pila en .github/PULL_REQUEST_TEMPLATE.md.
`markdown
Stack Overview
Este PR es parte de la cadena 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
- Implementación de la lógica de cambio de estado de pago dentro de OrderService
- Adición de procesamiento de publicación de eventos de dominio y pruebas de integración con Repository
Dependency Status
- Base PR: #101 (Must be merged first)
`
Una vez que el PR inferior #101 se integra en main mediante un Squash Merge, el Diff de la interfaz de usuario de GitHub del PR hijo #102 explota repentinamente. Este es un fenómeno en el cual todo el código del #101 ya fusionado se captura como cambios del PR hijo. Ocurre porque el SHA del commit de Squash y el SHA del commit padre difieren en el gráfico de commits de Git. No te paniquees y simplemente eleva solo los commits de la rama hija sobre main en el siguiente orden:
`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
`
Dividir un PR gigante de 1.000 líneas no es simplemente un problema de manejo de tecnología Git. Está más cerca de una tarea de ingeniería para ordenar limpiamente todo el método de inspección de código y la cadena de despliegue del equipo. Si te encuentras con una rama heredada desordenada, crea primero una copia de seguridad y córtala en capas de 3 etapas. No solo se reducirá la fatiga del revisor, sino que tú también podrás controlar el código con mucha más claridad.