Stacked pull requests: por dentro solo son rebases en cascada
El 30 de julio de 2026 GitHub puso los stacked pull requests en preview público. La idea no es nueva: Meta lleva más de una década trabajando así con Phabricator y luego con Sapling, y herramientas como Graphite o git-spice existen justamente porque git nunca dio una respuesta cómoda a esto.
Lo que quiero contarte no es cómo hacer clic en la interfaz. Es el mecanismo, porque todas las herramientas de stacking hacen la misma cosa por debajo: una cascada de rebases. Cuando entiendes qué se rompe y por qué, decidir si necesitas una herramienta se vuelve trivial, y cuando la herramienta falla sabes dónde mirar.
El problema no es el tamaño del PR
La recomendación de partir los PRs es vieja y está bien fundamentada. La guía de code review de Google es explícita: los cambios pequeños se revisan más rápido y con más profundidad, porque "es más fácil para un revisor encontrar cinco minutos varias veces que reservar un bloque de 30 minutos". Y da un número: 100 líneas suele ser razonable, 1000 suele ser demasiado.

El problema es que, con el flujo normal de PRs, partir el trabajo te castiga. Abres el PR uno, y hasta que alguien lo apruebe y se mergee no puedes empezar el dos, porque el dos necesita el código del uno. Así que tienes dos opciones malas:
- Esperar, y quedarte de brazos cruzados hasta que llegue la revisión.
- Meter todo en un PR gigante, que nadie revisa de verdad, y llevártelo aprobado con un "LGTM" después de tres días.
El stacking rompe ese falso dilema. Sigues escribiendo el PR dos mientras el uno espera revisión, y el PR dos se abre contra el uno en lugar de contra main. El revisor ve exactamente los cambios de esa capa, no el acumulado.
Qué es un stack, literalmente
No hay magia en la estructura. Un stack es una cadena de ramas donde cada PR apunta como base a la rama del PR de abajo, en vez de apuntar a main:
main
└── 01-schema ← PR #1, base: main
└── 02-api ← PR #2, base: 01-schema
└── 03-ui ← PR #3, base: 02-api
| PR | Rama | Base |
|---|---|---|
| #1 | 01-schema |
main |
| #2 | 02-api |
01-schema |
| #3 | 03-ui |
02-api |
Eso es todo. Puedes montarlo hoy, sin instalar nada, cambiando el desplegable de "base" al abrir cada pull request. GitHub ya calcula el diff contra la base, así que el PR #2 muestra solo los cambios de la capa de API.
La regla de diseño que ordena el stack es la dirección de la dependencia, y lo dice la propia documentación de GitHub: si el código de una capa depende del de otra, la dependencia tiene que estar en la misma rama o en una más baja. Esquemas y utilidades compartidas abajo; rutas, vistas y consumidores arriba. Si te sale un stack donde la capa 3 necesita algo de la capa 1 y la 1 necesita algo de la 3, el corte está mal hecho, no el stack.
Por qué se rompe
Hasta aquí es cómodo. Se rompe en el segundo movimiento, y se rompe por una razón sola: git identifica los commits por SHA, y el SHA depende del padre.
Un revisor te pide un cambio en 01-schema. Lo haces, amendas el commit o rebasas para limpiar el historial, y ahora 01-schema tiene commits con SHAs nuevos. Los commits de 02-api siguen colgando de los SHAs viejos. Para git, 02-api ya no está sobre 01-schema: está sobre una versión de 01-schema que nadie va a mergear.
Y no se queda ahí. Rebasas 02-api sobre el nuevo 01-schema, lo cual cambia los SHAs de 02-api, lo cual rompe 03-ui. Rebasas 03-ui, lo cual rompería 04- si existiera. Esa es la cascada, y hacerla a mano en un stack de cinco ramas es exactamente el trabajo tedioso que hace que la gente abandone el flujo a los dos días.
Lo mismo pasa al mergear. Si tu repo usa squash merge (que es lo normal), al mergear el PR #1 los tres commits de 01-schema se convierten en un commit nuevo en main, con un SHA que nunca existió en tu rama. El PR #2 se queda apuntando a commits que ya no le sirven de base, y su diff empieza a mostrar cosas que no son suyas.
Los dos flags que resuelven la cascada
Git tiene la pieza que falta desde la versión 2.38, y casi nadie la conoce. Es --update-refs:
Automatically force-update any branches that point to commits that are being rebased.
Es decir: rebasas la rama de arriba, y git arrastra los punteros de todas las ramas intermedias que iban montadas en esos commits. Un solo comando en vez de N. Puedes dejarlo activado siempre:
git config --global rebase.updateRefs trueEl otro flag es --onto, que te deja separar dos cosas que normalmente van juntas: qué commits se mueven y adónde van.
git rebase --onto <destino> <upstream> <rama>De la documentación de git-rebase: --onto es el "starting point at which to create the new commits", y <upstream> es lo que decide qué commits entran. Los commits que se replican son el rango upstream..rama.
Con eso, el caso del squash merge se arregla así. Mergeaste el PR #1, y quieres que el resto del stack se apoye en main sin arrastrar los commits viejos de 01-schema:
git fetch origin
git rebase --update-refs --onto origin/main 01-schema 03-uiLéelo de derecha a izquierda: toma los commits que van de 01-schema (excluido) hasta 03-ui, y ponlos encima de origin/main. Como 02-api apunta a uno de esos commits, --update-refs lo mueve con ellos. Un comando reordena el stack entero.
Y para publicarlo:
git push --force-with-lease origin 02-api 03-ui--force-with-lease y no --force: si alguien tocó una de esas ramas mientras trabajabas, el push falla en vez de pisar su trabajo. En un flujo donde reescribes historia todos los días, esa diferencia deja de ser teórica.
El cambio a mitad de stack, y la trampa
El otro caso frecuente es que el revisor pida un cambio en la capa de abajo. Aquí hay una trampa que parece la solución evidente y no funciona:
# esto NO funciona
git checkout 01-schema
# corriges y amendas
git checkout 03-ui
git rebase --update-refs 01-schemaFalla por exactamente la razón de la sección anterior. Al amendar, el commit viejo de 01-schema desaparece de la rama, pero sigue vivo en el historial de 02-api. Así que el rango 01-schema..03-ui todavía lo incluye, y git intenta reaplicarlo encima de su propia versión corregida:
CONFLICT (add/add): Merge conflict in schema.txt
error: could not apply c1d537c... schema
La forma correcta es no salir del stack. Te pones en la punta y editas el commit de abajo desde ahí, en una sola operación:
git checkout 03-ui
git rebase -i --update-refs mainMarca edit en el commit que quieres corregir. Git te deja parado en él, haces el cambio, git commit --amend, git rebase --continue. Al terminar te dice qué punteros movió:
Updated the following refs with --update-refs:
refs/heads/01-schema
refs/heads/02-api
El stack entero reconstruido, con un comando y sin conflictos. Esta es la operación que las herramientas de stacking te automatizan.
Lo que añade el soporte nativo de GitHub
Con los dos comandos de arriba ya puedes trabajar en stacks. Lo que faltaba era la mitad del lado del servidor, y eso es lo que acaba de llegar.
Antes de esto, GitHub tenía una pieza suelta: el retargeting automático, que existe desde 2020. Si mergeas un PR y borras su rama, GitHub cambia la base de los PRs que apuntaban a ella para que apunten a donde apuntaba el PR mergeado, en vez de cerrarlos. Es útil, pero es solo el puntero: retargeting no es rebase. Los commits de tu rama siguen colgando de donde colgaban, y el diff del PR sigue enseñando ruido hasta que rebasas de verdad.
Lo que el preview añade:
| Pieza | Qué hace |
|---|---|
| Stack como objeto de primera clase | GitHub sabe que esos PRs son una serie ordenada, y lo muestra como tal |
| Rebase en cascada en el servidor | Lo disparas desde el PR, sin clonar ni tocar la terminal |
| Merge de varias capas | Mergeas la capa más alta que esté lista y aterrizan todas las de abajo en una sola operación |
| Rebase automático al mergear | Al mergear la capa de abajo, las ramas restantes se rebasan y la siguiente pasa a apuntar a la rama por defecto |
gh stack |
Extensión de CLI para hacer lo mismo en local |
| Reglas existentes | Respeta branch protections, checks requeridos y requisitos de merge, sin configuración aparte |
El flujo con la extensión, según el quickstart oficial:
gh extension install github/gh-stack
gh stack init # crea el stack y la primera rama
gh stack add -Am "api routes" # commitea todo y abre la capa siguiente
gh stack submit # publica y abre los PRs con la base correcta
gh stack view # el estado del stack de un vistazo
gh stack rebase # la cascada, cuando algo de abajo se movióDos límites que conviene saber antes de montarlo en un repo:
- No funciona entre forks. Todas las ramas del stack tienen que vivir en el mismo repositorio, así que el flujo típico de contribución externa en open source se queda fuera.
- GitHub Desktop no lo soporta. Web, CLI y la app móvil sí.
El soporte de merge queue, que es donde el stacking se vuelve realmente cómodo en equipos grandes, sigue desplegándose de forma progresiva.
El panorama de herramientas
Si ya usabas algo, la pregunta obvia es si esto lo reemplaza. Depende bastante de dónde vive tu código:
| Herramienta | Modelo | Nota |
|---|---|---|
git rebase --update-refs |
Ramas, sin estado extra | Ya lo tienes instalado. Suficiente para stacks de 2 o 3 capas |
gh stack |
Ramas, con el stack registrado en GitHub | Nativo, gratis, solo GitHub y solo dentro del mismo repo |
| git-spice | Ramas, estado local | Open source y multiplataforma: GitHub, GitLab, Bitbucket, Gitea y Forgejo |
| Graphite | Ramas, con interfaz web y merge queue propia | Comercial, la capa de producto más completa |
| Sapling / ghstack | Commits, no ramas | Vienen de Meta. Cambian el modelo mental: la unidad de revisión es el commit |
La diferencia interesante de la última fila: en Sapling la unidad no es la rama, es el commit. Un stack es simplemente tu historial local, y cada commit se convierte en una unidad de revisión. Es más limpio conceptualmente y es más difícil de adoptar, porque deja de parecerse a git.
Si tu repo está en GitHub y tus stacks son cortos, empieza por lo nativo. Si trabajas en varias forjas, git-spice es la respuesta obvia.
Lo que te van a costar
Esta parte casi nunca aparece en los anuncios, y es la que decide si el flujo aguanta más de una semana.
La CI se multiplica. Cada capa es un PR, y cada PR corre la suite. Un stack de cinco son cinco pipelines, y vuelven a correr los cinco cada vez que rebasas la base. Si tu CI tarda 20 minutos y se paga por minuto, el stacking tiene un precio literal.
Los comentarios de revisión se quedan huérfanos. Rebasar reescribe SHAs, y al hacer force-push los hilos anclados a esas líneas se marcan como obsoletos. En un stack rebasas mucho más que en un flujo normal, así que la conversación de la revisión se degrada más rápido.
Los conflictos se vuelven a pelear en cada capa. Un conflicto que resolviste al rebasar la capa 2 puede volver a aparecer al rebasar la 3 y la 4. Esto tiene arreglo, y es de lo más rentable que puedes activar en git:
git config --global rerere.enabled truererere (reuse recorded resolution) guarda cómo resolviste cada conflicto y lo vuelve a aplicar solo cuando reaparece el mismo. En un flujo con rebases en cascada deja de ser una curiosidad y pasa a ser infraestructura.
El revisor tiene que leer de abajo hacia arriba. El orden no es opcional, y no es evidente si le llegan tres notificaciones a la vez. Una descripción de PR que diga "capa 2 de 3, va después de #481" cuesta diez segundos y ahorra bastante confusión.
Si la capa de abajo se cae, se cae todo. Un cambio de dirección en el PR #1 no invalida un PR: invalida el stack. Por eso lo que va abajo tiene que ser la parte menos discutible del trabajo, no la más ambiciosa.
La profundidad tiene techo. Cuatro o cinco capas es lo que se maneja bien. Un stack de diez es un PR gigante disfrazado, con la contabilidad de diez PRs encima.
La regla mental
El stacking no es una técnica para escribir PRs más pequeños. Es la técnica que te quita el castigo por escribirlos pequeños. Si ya trabajas en cambios de una sola cosa y el bloqueo por revisión no te afecta, no tienes el problema que esto resuelve.
Y antes de instalar nada, prueba esto:
- Activa
rebase.updateRefsyrerere.enabled. - Monta un stack de dos ramas a mano, cambiando la base al abrir el segundo PR.
- Mergea el de abajo y arregla el de arriba con
git rebase --update-refs --onto origin/main <rama-de-abajo> <rama-de-arriba>.
Si eso te resulta natural, cualquier herramienta de stacking te va a parecer cómoda desde el primer día, porque vas a saber qué está haciendo por ti. Si te lo saltas y empiezas por la herramienta, el primer conflicto en mitad de la cascada te va a dejar en un estado que no sabes leer, y esa es la forma más común de abandonar este flujo.