12k
All articles

Uso de git bisect para localizar un commit defectuoso

Usa git bisect para localizar el commit que causó una regresión. Elige referencias fiables, automatiza las pruebas, gestiona resultados inestables y verifica el commit responsable.

OpenReplay Team
OpenReplay Team
Uso de git bisect para localizar un commit defectuoso

git bisect localiza mediante búsqueda binaria el commit que introdujo una regresión. Marcas un commit que sabes que funciona y otro que sabes que falla, y cada prueba reduce a la mitad el rango restante, de modo que 300 commits requieren unas ocho o nueve comprobaciones.

La funcionalidad funcionaba el mes pasado. Desde entonces se han integrado 300 commits en main, nadie recuerda haber tocado el código del carrito y revisar cada diff llevaría toda la tarde.

5 Git Commands Beyond Commit and Push presenta start, good, bad, run, skip y reset. Este artículo va más allá: cómo elegir un commit bueno fiable, cómo escribir un script para git bisect run cuyos códigos de salida no puedan confundir a Git y qué hacer cuando un test inestable o una marca errónea desvían la búsqueda.

Puntos clave

  • Cada vez que se duplica el rango de bisect se añade una sola comprobación. El verdadero coste de elegir un commit bueno muy antiguo son los commits viejos que ya no compilan y hay que omitir.
  • Con git bisect run, el código de salida 0 marca un commit como bueno, 125 lo omite, cualquier otro código entre 1 y 127 lo marca como malo, y 128 o superior aborta el bisect.
  • Ante un fallo intermitente, marca un commit como malo si falla cualquiera de varias ejecuciones y como bueno solo si todas pasan.
  • Una marca errónea no obliga a empezar de cero: guarda git bisect log, elimina la línea incorrecta y todas las posteriores, y ejecuta git bisect reset y git bisect replay.
  • Confirma el resultado probando el commit señalado y su padre. El padre debe pasar y el commit señalado debe fallar.

El ciclo manual de git bisect

El ciclo manual requiere tres comandos para empezar: git bisect start, luego git bisect bad sobre un commit defectuoso y después git bisect good <ref> sobre uno que funcione. A partir de ahí, pruebas cada commit que Git extrae y lo marcas hasta que Git identifica al culpable. El capítulo de Pro Git sobre depuración con Git describe el mismo flujo. Si aún te estás familiarizando con Git, 10 Git commands every developer should know cubre los fundamentos del día a día.

git bisect start
git bisect bad              # HEAD has the bug
git bisect good v4.12.0     # last release known to work
Bisecting: 149 revisions left to test after this (roughly 7 steps)
[9e41c07b2d85a3f16c0e7b94d2a58f3e1c6b0d27] Refactor cart line-item formatter

Git ha extraído el punto medio. Ejecuta tu prueba e indica el resultado:

git bisect bad
Bisecting: 74 revisions left to test after this (roughly 6 steps)
[2b7f0d9a4c16e83b5f2a07d9c4e1b68a3f05c9d1] Update currency util types

Sigue probando y marcando. Cuando solo queda un candidato, Git muestra el resultado (los SHA y nombres siguientes son ficticios):

3c9d2e7a51f04b8e96d1a2c7f05e4b3d8a6c1f92 is the first bad commit
commit 3c9d2e7a51f04b8e96d1a2c7f05e4b3d8a6c1f92
Author: Example Dev <dev@example.com>
Date:   <date>

    Round line totals before applying discount

 src/cart/total.ts | 4 ++--

¿Cómo se elige el commit bueno?

El mejor commit bueno para git bisect es el tag de versión más reciente que sepas que se publicó sin el bug, y debes probarlo antes de marcarlo. Una referencia “buena” que en realidad es mala sigue produciendo una respuesta aparentemente segura, normalmente un commit justo posterior a esa referencia, y esa respuesta es incorrecta.

Prueba el tag con la misma comprobación que usará bisect (Vitest es aquí solo un ejemplo de test runner):

git switch --detach v4.12.0
npm ci && npx vitest run src/cart/total.test.ts   # must pass
git switch -

No intentes reducir el rango a costa de la certeza. Cada duplicación añade solo una comprobación: unas 8 o 9 para 300 commits, 9 o 10 para 600 y 11 o 12 para 2400. Retroceder mucho tiene otro coste: los commits antiguos pueden requerir una versión anterior de Node, una dependencia que ya se ha retirado del registro o un formato de lockfile distinto. Cada commit que no compila hay que omitirlo, y un tramo largo de commits omitidos puede ocultar al culpable.

Las grabaciones de sesión (session replays) muestran cuándo una regresión llegó por primera vez a usuarios reales, lo que permite acotar el commit bueno a la versión desplegada justo antes.

¿Cómo automatiza git bisect run la búsqueda?

git bisect run <script> ejecuta tu script sobre cada commit candidato e interpreta su código de salida como veredicto. La documentación de git-bisect define la correspondencia:

Código de salidaSignificado
0Bueno
1–127, excepto 125Malo
125Omitir (no se puede probar)
128 o superiorAbortar el bisect

Guarda el script fuera del repositorio. Así, extraer commits antiguos no puede modificarlo y los comandos de limpieza como git clean -fdx no pueden borrarlo:

cat > ../bisect-test.sh <<'EOF'
#!/usr/bin/env bash
npm ci --silent        || exit 125   # can't install: skip
npm run build --silent || exit 125   # can't build: skip
npx vitest run src/cart/total.test.ts || exit 1
EOF
chmod +x ../bisect-test.sh
git bisect run ../bisect-test.sh

Si la instalación o la compilación fallan, el script sale con 125, de modo que un fallo ajeno al problema se omite en lugar de contarse como malo. El || exit 1 final convierte cualquier fallo del test en un 1, para que un test runner que casualmente devuelva 125 o 128+ no pueda omitir un commit ni abortar la ejecución por accidente.

Los códigos de salida 126 (no ejecutable) y 127 (comando no encontrado) normalmente cuentan como malos. Por tanto, una ruta de script errónea o la falta del permiso de ejecución podrían marcar todos los commits como malos. Las notas de la versión 2.36 de Git describen la salvaguarda: Git intenta detectar un script que no puede ejecutarse y se detiene antes de tiempo en lugar de señalar un culpable. Si git bisect run se detiene antes de lo esperado, revisa la ruta del script y sus permisos antes de examinar el código.

Problemas habituales: skip, reset y un árbol de trabajo con cambios

La mayoría de las interrupciones se deben a tres causas: un commit que no puedes probar, una sesión que necesitas terminar o cambios locales que estorban.

git bisect skip

git bisect skip aparta el commit actual y Git pasa a uno cercano. Si el culpable acaba dentro de una serie de commits omitidos, Git enumera los candidatos en lugar de señalar uno solo.

git bisect reset              # back to the branch you started on
git bisect reset 3c9d2e7      # end the session on a specific commit

Haz commit o stash de los cambios locales antes de git bisect start. Puedes limitar la búsqueda a determinadas rutas con git bisect start -- src/cart/. git bisect visualize abre gitk con los sospechosos restantes, o recurre a git log cuando Git no detecta una sesión de escritorio gráfica.

¿Cómo se gestionan los tests inestables y las marcas erróneas?

Para hacer bisect de un fallo intermitente, ejecuta el test varias veces en cada commit. Márcalo como malo si falla cualquier ejecución y como bueno solo si todas pasan. El motivo es que una sola marca errónea envía la búsqueda a la mitad equivocada, y aun así Git informa de un primer commit malo. Un commit realmente malo puede pasar por suerte, pero un commit bueno nunca debería fallar esta prueba.

#!/usr/bin/env bash
npm ci --silent        || exit 125
npm run build --silent || exit 125
for i in $(seq 1 10); do
  npx vitest run src/cart/total.test.ts || exit 1   # any failure = bad
done
exit 0                                              # all passed = good

Veamos los números en un caso hipotético: si un commit malo falla el 20 % de las veces, la probabilidad de obtener diez ejecuciones limpias por azar es 0,8¹⁰ ≈ 0,11. Más ejecuciones reducen ese riesgo. Omitir los commits ambiguos no resuelve nada, porque el resultado poco fiable sigue ahí y la respuesta final simplemente se amplía. Esta regla parte de que la inestabilidad es nueva. Si el test ya era inestable antes de la regresión, estabilízalo o escribe primero una comprobación más acotada; de lo contrario, marcará commits buenos como malos.

Si ya has cometido una marca errónea, puedes corregirla sin empezar de cero:

git bisect log > ../bisect.log
# edit ../bisect.log
git bisect reset
git bisect replay ../bisect.log

El log guardado también incluye líneas de comentario; aquí solo se muestran las líneas de comandos. Antes de editar:

git bisect start
git bisect bad 8d1f...
git bisect good 5a0c...
git bisect good 9e41...    <- wrong: this commit was flaky
git bisect bad 2b7f...

Tras la edición, elimina la línea incorrecta y todas las posteriores:

git bisect start
git bisect bad 8d1f...
git bisect good 5a0c...

git bisect replay restaura la sesión hasta la última marca correcta y puedes continuar desde ahí.

¿Cómo se confirma el resultado?

Considera el commit que señala git bisect como una pista hasta que lo hayas verificado. Revisa el diff con git show 3c9d2e7 y luego prueba el padre del commit y el propio commit:

git switch --detach 3c9d2e7^   # parent: test should pass
git switch --detach 3c9d2e7    # culprit: test should fail

Si el padre pasa y el commit falla, has encontrado la regresión. A partir de aquí, usa git blame para ver por qué cambiaron las líneas circundantes y el historial de Git para corregir commits que aún no se hayan subido. En la próxima regresión, parte de un tag de versión probado y de un script que ejecute el test varias veces en cada commit.

Preguntas frecuentes

¿Puede git bisect encontrar el commit que corrigió un bug en lugar del que lo introdujo?

Sí. Usa los términos old y new en lugar de good y bad. Ejecuta git bisect start, marca un commit corregido con git bisect new y un commit anterior defectuoso con git bisect old, y Git informará del primer commit new, que es la corrección. Para etiquetas más claras, ejecuta git bisect start --term-old broken --term-new fixed y marca los commits con git bisect fixed y git bisect broken.

¿Cómo gestiona git bisect los merge commits de ramas de funcionalidad?

Por defecto, git bisect también prueba commits del interior de las ramas fusionadas, incluidos commits de trabajo en curso que quizá no compilen. Ejecutar git bisect start --first-parent, disponible desde Git 2.29, mantiene la búsqueda en la línea principal del historial. En main, se detiene en el merge que introdujo el bug y nunca prueba los commits propios de la rama. Un segundo bisect sobre esa rama localiza el commit exacto.

¿Qué ocurre si el commit bueno no es ancestro del commit malo?

Git extrae una merge base de ambos commits y te pide que la pruebes primero. Si la merge base es buena, la bisección continúa con normalidad sobre los commits entre ella y el commit malo. Si la merge base ya es mala, Git se detiene en lugar de buscar, porque el commit bueno se encuentra en una línea de historial separada donde el bug no existe o fue corregido.

¿Qué diferencia hay entre git bisect y git blame?

git blame indica, línea por línea, qué commit modificó por última vez un archivo, por lo que solo responde quién editó por última vez una línea concreta. git bisect prueba el comportamiento a lo largo de los commits, de modo que encuentra una regresión incluso cuando la causa es una actualización de dependencia, un cambio de configuración o un archivo distinto del que muestra el síntoma. Usa bisect para encontrar el commit y después blame para rastrear las líneas que modificó.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.