Trabajas con ramas permanentes (por ejemplo feature hacia develop, luego qa y por último main) y fusionas cada PR con "Squash and merge". Después de fusionar, aparecen tres síntomas:
- La rama local figura como divergida de
developaunque el contenido es idéntico. - El siguiente PR desde la misma rama lista archivos que ya habías fusionado, o informa conflictos.
- Un PR de
qahaciamainmarca conflictos en archivos quemainno ha tocado desde la última promoción.
El primer síntoma se ve así en Git. Ocurre, por ejemplo, con una rama creada con git switch -c feature/x origin/develop, que por defecto hereda origin/develop como rama de seguimiento, después de que develop recibió el squash de sus 2 commits:
On branch feature/x
Your branch and 'origin/develop' have diverged,
and have 2 and 1 different commits each, respectively.
Este artículo explica por qué ocurre, cómo comprobar que no se perdió nada y cómo resincronizar la rama para que los PR siguientes vuelvan a mostrar solo tus cambios.
Por qué pasa
Según la documentación de GitHub, "Squash and merge" combina todos los commits del PR en un único commit nuevo sobre la rama base. Ese commit tiene su propio hash y solo un padre: los commits originales de tu rama (F1, F2) nunca llegan a la rama base.
develop: B ---------------- S (S: un commit nuevo, sin relación con F1 ni F2)
\
feature: F1 -- F2 (siguen siendo commits distintos de S)
El contenido es el mismo, pero para Git son historias distintas. Git no compara commit por commit; al fusionar, según su documentación, solo considera las dos puntas y la base de fusión (el último ancestro común). Tras el squash, esa base sigue siendo B, el punto donde la rama se separó. De ahí salen los síntomas:
- GitHub muestra en un PR una diferencia de tres puntos: compara la base de fusión con la punta de la rama. Con la base antigua, los cambios ya fusionados vuelven a aparecer. La propia documentación de GitHub avisa de que, si sigues trabajando en la misma rama y abres otro PR hacia la misma base, los commits que ya combinaste se listan de nuevo y puede que debas resolver los mismos conflictos una y otra vez.
- Si después de la fusión vuelves a editar las mismas líneas, el cambio de la rama base (
S) y el tuyo parten de la versión antiguaB, así que Git ve dos ediciones distintas de la misma línea y marca conflicto. - En
develop,qaymain, que se reutilizan en cada ciclo, el efecto se repite en cada promoción: el squash que entra enmainnunca aparece en la historia deqa.
Por la misma razón, las herramientas que comparan commits no sirven para saber si algo ya está integrado: en la prueba, git cherry -v origin/develop HEAD marcó con + los dos commits, porque ninguno coincide con el commit de squash. Compara contenido, como en el primer paso de la solución.
Solución
-
Comprueba que el contenido es idéntico. Justo después de cada squash merge:
git fetch origin git diff origin/develop --statSi no imprime nada, tu rama y
developtienen exactamente los mismos archivos y puedes resincronizar sin perder trabajo. Si imprime archivos, hay cambios locales que aún no están endevelop: no resincronices a ciegas. -
Resincroniza la rama. Fusiona
origin/developen tu rama:git merge origin/developComo ambos lados contienen los mismos cambios, la fusión se completa sin conflictos (en la reproducción local así fue) y la base de fusión pasa a ser la punta de
develop. Los PR siguientes solo mostrarán los commits nuevos. Si no necesitas conservar los commits antiguos, también puedes crear la rama de nuevo desde el remoto:git switch -c feature/x-2 origin/developgit reset --hard origin/developdeja el mismo resultado, pero descarta cualquier cambio local sin guardar; úsalo solo cuando el paso 1 salió vacío y no tienes nada pendiente. -
Comprueba la fusionabilidad antes de pedir revisión. En local,
git merge-treesimula la fusión sin tocar tu directorio de trabajo ni el índice:git merge-tree --write-tree --name-only origin/develop HEADEl código de salida es 0 si la fusión es limpia y 1 si hay conflictos; en ese caso imprime los archivos en conflicto. (Según su documentación, no crea commits ni lee o escribe el directorio de trabajo o el índice; solo genera un objeto de árbol. El modo
--write-treeexiste en versiones recientes de Git; la prueba usó la 2.54.) En GitHub, justo después de abrir el PR:gh pr view NUMERO --json mergeable,mergeStateStatusSegún la referencia de la API GraphQL,
MERGEABLEindica que se puede fusionar,CONFLICTINGque tiene conflictos yUNKNOWNque aún se está calculando; enmergeStateStatus,CLEANsignifica que se puede fusionar y las comprobaciones pasan, yDIRTYque la fusión no se puede crear sin conflictos. Si vesUNKNOWN, repite la consulta unos segundos después. -
Para un PR de
qaamaincon conflictos, crea una rama de sincronización desdemain. Necesitas el SHA del commit deqaque se promovió la última vez (por ejemplo, el último commit del PR anterior). En Git Bash o WSL:git fetch origin ABSORBIDO=SHA_DE_QA_DE_LA_ULTIMA_PROMOCION git switch -c sync/qa-main origin/main git merge origin/qa git diff --name-only --diff-filter=U | while IFS= read -r f; do actual=$(git rev-parse -q --verify "origin/main:$f") absorbido=$(git rev-parse -q --verify "$ABSORBIDO:$f") if [ -n "$actual" ] && [ "$actual" = "$absorbido" ]; then git checkout --theirs -- "$f" && git add -- "$f" else echo "REVISAR a mano: $f" fi done git commit --no-editLa lógica: si el contenido de un archivo en
maines idéntico (mismo hash de blob) al que teníaqacuando se promovió por última vez,mainno lo ha cambiado desde entonces y la versión correcta es la deqa. En una fusión,--theirses la rama que fusionas (qa). Si los hashes difieren,maintiene cambios propios y debes resolver ese archivo a mano.Evita el atajo
git merge -X theirs origin/qa: según la documentación, resuelve a favor del otro lado todos los trozos en conflicto, incluidos los que contienen una corrección real hecha enmain. -
Cambia el método de fusión en las promociones entre ramas permanentes. Mientras
develop→qa→mainse fusionen con squash, el problema volverá en el ciclo siguiente. Con "Create a merge commit", GitHub añade todos los commits de la rama de origen a la base mediante un commit de fusión, de modo que la base de fusión avanza. Mantén "Squash and merge" parafeature→develop, donde la rama se descarta tras fusionar. Esto exige que el repositorio permita ese método de fusión y deja commits de fusión en la historia.
Cómo verificar que quedó resuelto
Tras resincronizar tu rama, el primer número de este comando (commits que solo están en develop) debe ser 0:
git rev-list --left-right --count origin/develop...HEAD
git diff --stat origin/develop...HEAD
El segundo comando es lo que verá el PR: en la reproducción local, antes de resincronizar listaba 2 archivos (uno de ellos ya fusionado por el squash) y, después de resincronizar, solo el archivo del commit nuevo. En el mismo caso, git merge-tree pasó de terminar con código 1 (conflicto en el archivo editado dos veces) a terminar con código 0.
En la rama de sincronización qa → main, comprueba que el árbol resultante coincide con el de qa:
git rev-parse "HEAD^{tree}"
git rev-parse "origin/qa^{tree}"
Los dos hashes deben ser iguales. En PowerShell las comillas son necesarias: sin ellas, HEAD^{tree} falló en la prueba con Windows PowerShell 5.1. Si main recibió cambios que qa no tiene (por ejemplo, una corrección urgente), los árboles diferirán justo en esos archivos; revisa con git diff --stat origin/qa HEAD que solo aparecen ellos. Por último, una vez abierto el PR de sincronización, gh pr view debe mostrar MERGEABLE y, si las comprobaciones pasan, CLEAN. Esa parte depende de GitHub y no se reprodujo en local: la reproducción usó repositorios locales que simulan el squash con git merge --squash.
Variantes y errores relacionados
error: the branch 'feature/x' is not fully merged
Después del squash, si la rama remota se borró (y se ejecutó git fetch --prune), git branch -d feature/x responde con ese error aunque el contenido esté en develop: así ocurrió en la prueba con Git 2.54, con develop actualizada y como rama actual. Comprueba primero que git diff develop feature/x --stat no imprime nada y entonces usa git branch -D feature/x.
Conflictos de tipo "modificado/eliminado"
Si un archivo se creó o se borró en uno de los lados, git rev-parse -q --verify no devuelve ningún hash para él. El script lo marca como "REVISAR a mano" en lugar de tomar una decisión por ti.
El mismo problema en develop → qa
No es exclusivo de main. Cualquier promoción por squash entre dos ramas que siguen vivas acumula la misma divergencia; la receta de la rama de sincronización sirve para cualquier par de ramas.
Resumen
- "Squash and merge" crea un commit nuevo en la rama base; tus commits originales no entran en su historia, aunque el contenido sea idéntico.
- Tras cada squash, ejecuta
git fetch originygit diff origin/develop --stat: vacío significa mismo contenido. - Resincroniza enseguida con
git merge origin/develop(o recrea la rama desde el remoto) para que los PR muestren solo tus archivos. - Para
qa→main, fusionaorigin/qaen una rama creada desdeorigin/mainy resuelve por archivo comparando hashes de blob; verifica con los hashes de árbol. - Para romper el ciclo, usa "Create a merge commit" en las promociones entre ramas permanentes.
Comentarios
Publicar un comentario