La suite de pruebas lleva tres semanas en verde. Hoy npm test falla en una aserción que no tiene nada que ver con lo que acabas de cambiar, y git log v1.4.0..HEAD muestra 340 commits de cuatro personas desde la última release que alguien confirmó que funcionaba. Leer cada diff te cuesta un día entero. Hacer checkout de commits al azar es más rápido, pero sigue sin ser un plan.
git bisect convierte eso en una búsqueda binaria. Le das un commit que sabes que es bueno y otro que sabes que está roto; hace checkout del commit a mitad de camino entre ambos y espera a que pruebes ese commit y reportes el resultado. Cada respuesta reduce el rango a la mitad. 340 commits se resuelven en un máximo de 9 pruebas, porque 2^9 = 512.
Cómo git bisect reduce la búsqueda a la mitad en cada paso
Imagina los 340 commits en una línea, del más antiguo al más nuevo, el tag bueno conocido en un extremo y el HEAD roto en el otro. git bisect no recorre esa línea. Salta al medio, espera un veredicto y luego descarta la mitad que ese veredicto descarta:
| |
Supongamos que el commit del medio pasa. El bug está en la mitad más reciente, así que la mitad más antigua desaparece para siempre y en esta sesión no volverás a ver esos commits. Repite: mitad de lo que queda, prueba, descarta la mitad. Tras aproximadamente log2(n) rondas, el rango se reduce a un solo commit — aquel en el que el comportamiento pasó de funcionar a fallar. No siempre es el commit que “causó” el bug en un sentido más profundo. Es el commit donde el comportamiento cambió, que casi siempre es justo lo que necesitas.
Cómo iniciar una sesión de git bisect
Ejecuta esto desde un árbol de trabajo limpio. Bisect hace checkout de commits por ti, y se niega a arrancar si hay cambios sin confirmar.
| |
Git hace checkout del punto medio y te dice cuánto queda:
| |
Ejecuta tu prueba en ese commit y luego reporta lo que encontraste:
| |
o bien
| |
Git hace checkout del nuevo punto medio automáticamente después de cada respuesta. Sigue probando y reportando hasta que ya no quede nada que reducir:
| |
Esa es tu respuesta. Lee ese diff, no los otros ocho commits que lo rodean. git bisect reset te devuelve al branch y al commit desde donde empezaste, y los checkouts que hizo bisect por el camino no dejan rastro.
| |
Automatizar el ciclo con git bisect run
Responder good o bad a mano está bien para un bug que detectas en unos segundos. Para algo que necesita una ejecución de pruebas real, delega todo el ciclo a un script:
| |
bisect-test.sh solo tiene que salir con el código correcto: 0 significa bueno, cualquier código del 1 al 127 salvo 125 significa roto, y 125 significa “no puedo probar este commit, sáltalo”.
| |
La mayoría de los test runners ya salen con 0 cuando pasan y con un código distinto de cero cuando fallan, así que a menudo el script se reduce a llamar al runner. git bisect run hace checkout de cada punto medio, ejecuta el script, lee el código de salida y reporta el resultado él mismo, imprimiendo al final el mismo resumen de “first bad commit”.
El script puede hacer más que llamar a un test runner. Si la regresión solo se ve en un artefacto construido, un script que ejecuta docker build y luego pone a prueba la imagen es algo normal para pasarle a git bisect run — los pasos de build de Construir y Publicar una Imagen Docker con GitHub Actions funcionan en un script tan bien como en un workflow. Cuenta con ello: cada ronda ahora cuesta una build completa de la imagen.
Hay una condición para el propio script. Mantenlo fuera del rango sobre el que haces bisect, o asegúrate de que no ha cambiado dentro de ese rango. Si el script vive en la historia que estás buscando, un commit antiguo hace checkout de una versión antigua del script junto con el código antiguo, y dejas de ejecutar la misma prueba en cada paso.
Saltar commits que no puedes probar con git bisect skip
Algunos commits no se pueden probar por sí solos: un commit a medio refactorizar que no compila, una migración de esquema que necesita un estado de base de datos que no tienes. Forzar un veredicto good/bad en uno de esos envenena el resultado. Exclúyelo en su lugar:
| |
Bisect hace checkout de un commit cercano y sigue rodeando el hueco. En bisect run, el código de salida 125 hace lo mismo automáticamente, así que un script capaz de detectar “este commit ni siquiera compila” puede saltarlo en vez de reportar un falso bad. Si saltas demasiados commits adyacentes, bisect se queda sin terreno donde probar: la respuesta final se convierte en “el bug viene de uno de estos N commits” en vez de un solo commit.
Cuándo git bisect no es la herramienta adecuada
Bisect asume que tu prueba es determinista: mismo commit, mismo resultado, siempre. Una prueba inestable rompe esa suposición sin avisar. Bisect no puede distinguir “este commit está roto” de “esta prueba falló por inestabilidad”, y una respuesta equivocada al principio manda cada ronda posterior a la mitad equivocada. Arregla o excluye la aserción inestable antes de hacer bisect, no después.
También asume que un commit produjo un único cambio de comportamiento limpio. Un bug que se coló a lo largo de varios commits, o uno que depende de un estado acumulado en vez de un solo diff, no converge en nada útil. Un historial con muchos rebases es una versión más leve del mismo problema (mira Git Rebase vs Merge: diferencias y cuándo usarlos): el primer commit malo que encuentra bisect puede ser un commit aplastado que agrupa cinco cambios lógicos. Correcto, pero demasiado grueso para revisarlo por separado.
Ninguno de los dos casos pide una herramienta distinta, solo un punto de partida distinto. Primero haz que la prueba sea fiable, luego elige un commit good lo bastante atrás como para estar seguro de que el bug todavía no estaba ahí.
La próxima vez que aparezca una regresión sin causa evidente, marca los dos extremos y responde good o bad unas cuantas veces — o escribe el script de tres líneas y deja que git bisect run responda por ti.