Tu CI está en rojo en main, pero ayer estaba en verde. En algún lugar entre entonces y ahora, se coló un bug. Podrías desplazarte por 200 commits, leyendo diffs y adivinando. Podrías preguntar en Slack y esperar que alguien recuerde haber tocado el código relevante. O podrías dejar que Git haga el trabajo por ti.

git bisect es una herramienta de búsqueda binaria para tu historial de commits. Marcas un commit como bad y otro como good. Git hace checkout del punto medio. Lo pruebas, lo marcas good o bad, y Git repite. En 8 pasos, puede aislar un solo commit de 256. En 12 pasos, puede encontrar la aguja en 4,096. Es lo más cercano que tiene Git a un debugger para el historial mismo.

Por qué la bisección manual es una pérdida de tiempo

Los desarrolladores ya biseccionan manualmente sin darse cuenta. Haces checkout de un commit más antiguo, ejecutas los tests y piensas “todavía roto, tengo que ir más atrás.” Luego haces checkout de uno aún más antiguo, y los tests pasan. Ahora sabes que el bug está en algún lugar entre esos dos puntos. Así que eliges uno en el medio y sigues reduciendo.

Eso es búsqueda binaria. git bisect automatiza la contabilidad para que no pierdas la pista de qué commits ya has probado. Más importante aún, previene la heurística perezosa de “probablemente estaba en ese gran refactor del martes” que te envía por el agujero de conejo equivocado durante dos horas.

El valor real no es la velocidad, aunque es más rápido. El valor real es la corrección. Cuando estás frustrado y cazando un bug a las 6 PM, cometerás errores. Olvidarás reconstruir después del checkout. Probarás el commit equivocado dos veces. Leerás mal un resultado de test y marcarás un commit bad como good, destruyendo tu espacio de búsqueda. git bisect enforce la disciplina que no tienes cuando estás molesto.

Cómo funciona realmente la búsqueda binaria

Git no busca cronológicamente. Busca topológicamente, recorriendo el grafo de commits para encontrar el punto medio entre tus commits known-good y known-bad. Esto importa cuando tu historial tiene merges, porque el punto medio cronológico podría ni siquiera ser alcanzable desde ambos extremos.

Esto es lo que pasa bajo el capó. Inicias el bisect y proporcionas límites:

git bisect start
git bisect bad HEAD          # current commit is broken
git bisect good v2.1.0       # this release was fine

Git calcula el número de commits entre esos dos puntos. Hace checkout del que está exactamente en el medio y espera a que lo pruebes. Ejecutas tu reproducción, ves si el bug está presente, y le dices a Git:

git bisect bad   # this commit has the bug
git bisect good  # this commit is clean

Git descarta la mitad del espacio de búsqueda y repite. Cuando solo queda un commit, se detiene y te muestra el primer commit bad. La salida incluye el hash del commit, el autor, la fecha y el mensaje. No hay ambigüedad. Este commit introdujo el bug, punto final.

Un recorrido concreto con comandos reales

Supón que tus integration tests empezaron a fallar esta mañana. Sabes que pasaron en el último release etiquetado, v1.4.0. Aquí está la sesión completa:

# Start the session
git bisect start

# Mark the current HEAD as bad
git bisect bad HEAD

# Mark the last known good release
git bisect good v1.4.0

# Git checks out a midpoint commit automatically
# You run your reproduction script or test suite:
npm test -- --grep "checkout flow"

# Tests fail. Mark it bad.
git bisect bad

# Git checks out another midpoint. Run tests again.
npm test -- --grep "checkout flow"

# Tests pass. Mark it good.
git bisect good

# Repeat until Git tells you:
# "<commit-hash> is the first bad commit"

Al final, Git te deja en el commit bad. Puedes inspeccionarlo con git show, crear un arreglo, y luego limpiar:

git bisect reset

Esto te devuelve a la branch en la que estabas antes de empezar. Si olvidas resetear, te quedarás en un HEAD detached y te preguntarás por qué tu siguiente commit no está en tu branch. Yo he hecho esto. Es vergonzoso.

Automatizando todo el proceso con un script

La versión manual todavía requiere que ejecutes tests y escribas good o bad repetidamente. Si tu reproducción es un solo comando que sale con 0 para éxito y distinto de cero para fallo, puedes entregarle todo a Git:

git bisect start
git bisect bad HEAD
git bisect good v1.4.0

# Hand over control to an automated script
git bisect run npm test -- --grep "checkout flow"

Git hará checkout de commits, ejecutará tu comando y clasificará los resultados automáticamente. Cuando termina, obtienes la misma salida de “first bad commit” sin tocar tu teclado. Aquí es donde git bisect pasa de útil a indispensable.

Tu script no necesita ser una test suite. Puede ser cualquier cosa ejecutable que devuelva códigos de salida significativos. Aquí hay un shell script mínimo que comprueba un mensaje de log específico:

#!/bin/bash
# reproduce-bug.sh
# Exit 0 if the bug is NOT present (good)
# Exit 1 if the bug IS present (bad)

if curl -s http://localhost:3000/api/health | grep -q "database_timeout"; then
    exit 1  # bug is present
fi
exit 0  # bug is not present

Ejecútalo:

chmod +x reproduce-bug.sh
git bisect run ./reproduce-bug.sh

El contrato de códigos de salida es estricto. Salir con 0 significa “good”, salir con 1 a 124 significa “bad”, y salir con 125 significa “saltar este commit, no es testeable.” Salir con 125 es útil cuando un commit no compila, o el servidor no puede arrancar debido a un cambio de configuración no relacionado. Git saltará ese commit y buscará alrededor de él.

Dónde el bisect falla: los trucos

git bisect asume que tu bug es monotónico. Una vez que un commit lo introduce, cada commit descendiente también es bad. Si el bug parpadea, apareciendo y desapareciendo entre commits, la búsqueda binaria se desmorona. Obtendrás resultados sin sentido o Git se quejará de que no puede aislar un solo commit.

Los bugs no deterministas son los peores culpables. Una race condition que falla el 10% del tiempo marcará ocasionalmente un commit bad como good, corrompiendo la búsqueda. Si tu reproducción es flaky, arregla la flakiness primero. O ejecuta el test múltiples veces en tu script y solo marca un commit como good si pasa cada ejecución. Esto es más lento pero más confiable.

Los build artifacts son otra trampa. Si cambias de un commit que cambia una versión de herramienta de build a uno que no, tus node_modules obsoletos o binarios compilados podrían no coincidir con el código extraído. Siempre limpia y reconstruye dentro de tu script de reproducción si tu proyecto tiene un paso de build.

#!/bin/bash
# safer-reproduce.sh
rm -rf node_modules dist
npm ci
npm run build
npm test -- --grep "checkout flow"

Esto agrega unos segundos por iteración, pero elimina toda una categoría de falsos positivos donde el bug está realmente en artifacts obsoletos.

Qué no hace bisect

git bisect encuentra el commit. No te dice por qué el commit es bad. Un refactor de 5,000 líneas que toca cuarenta archivos puede ser el primer commit bad, y todavía tienes que leer el diff para entender qué línea es la culpable. Bisect reduce tu alcance de “en algún lugar del último mes” a “en algún lugar de este diff.” El resto sigue siendo tu trabajo.

Tampoco ayuda con bugs que existen en el codebase pero nunca fueron atrapados por tests. Si tu test suite ya estaba en verde cuando se introdujo el bug, bisect no te salvará. Necesitas una reproducción primero. Bisect es una herramienta de búsqueda, no de detección.

Cuándo recurrir a bisect en lugar de blame o log

git blame es genial cuando ya sabes qué archivo contiene el bug. git log --grep es genial cuando recuerdas algo sobre el mensaje del commit. Bisect es para cuando no sabes ninguno de los dos. Solo sabes que funcionaba en el punto A y falla en el punto B, y el espacio entre ellos es demasiado grande para razonar sobre él.

Si tu equipo ejecuta CI en cada commit, a veces puedes biseccionar más rápido leyendo el historial de CI. Pero el CI solo atrapa lo que prueba. Una regresión de rendimiento, un bug visual o un error de lógica sutil que tus tests se pierden no aparecerán en los logs de CI. Bisect funciona para cualquier reproducer que puedas scriptear, independientemente de lo que valide tu CI.

Cómo hacer que bisect sea parte de tu workflow

No necesitas una configuración especial. Los comandos están integrados en Git. Pero hay dos hábitos que hacen que bisect sea más fluido cuando realmente lo necesitas.

Primero, tag tus releases. Bisect necesita un commit known-good, y las tags son la forma más fácil de proporcionar uno. Si tu último release fue hace tres semanas y sabes que estaba limpio, git bisect good v1.4.0 es un punto de anclaje de una línea.

Segundo, mantén tu reproducción mínima. Una reproducción que toma treinta segundos en ejecutarse es tolerable para ocho iteraciones. Una reproducción que toma diez minutos es tortura. Antes de biseccionar, dedica cinco minutos a reducir la reproducción al comando más pequeño posible. Tu yo futuro te lo agradecerá.

Cuando termines, ejecuta git bisect reset inmediatamente. Un HEAD detached con cambios no confirmados es una gran forma de perder trabajo. Aprendí esto a la mala para que tú no tengas que hacerlo.