El commit payment.spec.js, que había pasado en la CI anoche, falló en la compilación matutina. Hay 200 commits fusionados desde la última etiqueta de release estable. Desplazarse hacia arriba en git log y revisar el diff visualmente equivale a una búsqueda lineal, por lo que compilar y ejecutar pruebas en cada commit consume una cantidad significativa de tiempo. Supongamos que este no es un caso experimentado directamente, sino un escenario hipotético en el que ocurre dicha regresión.

En este momento, git bisect adopta un enfoque de "excluir la mitad cada vez" en lugar de "verificar uno por uno". Al establecer como límites un commit confirmado como correcto (good) y un commit donde se detectó una falla (bad), Git hace checkout del commit intermedio del conjunto de candidatos. El usuario debe probar ese commit y responder si es good o bad. Dado que el número de candidatos se reduce a la mitad en cada decisión, incluso con 1,000 commits, el rango puede reducirse en aproximadamente 10 iteraciones como máximo (log₂1000 ≈ 10) 1. Con 200 commits, converge en alrededor de 8 iteraciones.

Sin embargo, este cálculo solo se sostiene si cada decisión refleja realmente el comportamiento del commit. Si un commit no puede ejecutarse porque la compilación está rota y se juzga erróneamente como bad, o si un script de automatización ni siquiera puede ejecutarse y devuelve un código de salida 127, la situación cambia. Git selecciona la mitad incorrecta o marca todos los commits como bad, haciendo que el resultado carezca de sentido 2.

Preparación de las condiciones de reproducción y de los límites antes de iniciar la búsqueda

Para que git bisect funcione correctamente, el estado de los dos puntos debe ser claro. Que el HEAD actual sea bad significa que la regresión se reproduce, y que una etiqueta pasada sea good significa que no había regresión en ese momento. Si esta premisa es débil, todo el proceso de búsqueda se desvía.

Nodelog Engineer señala que los resultados pueden distorsionarse si el punto elegido como good estaba en realidad ya fallando. Por lo tanto, es preferible elegir como good "una etiqueta claramente anterior a la aparición del bug" en lugar de "el commit que parecía correcto recientemente", eligiendo un punto suficientemente atrás en el tiempo 1.

Cuando las decisiones las toma un script y no una persona, la ubicación desde donde se ejecuta el script también influye en el resultado. tms-outsource explica que es más seguro colocar el script de decisión fuera del repositorio. La razón es que los archivos de prueba añadidos posteriormente no existen en los commits anteriores, lo que podría provocar fallos por una causa errónea: la ausencia de dichos archivos de prueba 2. Si se intenta ejecutar payment.spec.js, añadido 200 commits más tarde, en un commit pasado incluido en el rango de búsqueda, no se puede distinguir si la falla se debe a un problema en el código de ese commit o a la presencia o ausencia del archivo de prueba.

Por consiguiente, el comando que verifica la regresión debe situarse en una ruta externa al repositorio o prepararse de forma aislada, incluyendo la instalación de dependencias (como npm ci, entre otras) 1. Esto evita que residuos del entorno de commits anteriores causen juicios erróneos. También es recomendable limpiar el espacio de trabajo antes de iniciar la búsqueda. Dado que los cambios no commiteados pueden interferir con los checkouts, se recomienda usar git stash u otras herramientas para aislar los cambios 2.

Establecer los límites de good y bad para reducir el rango de búsqueda

Establecer los límites de good y bad para reducir el rango de búsqueda

La sesión comienza con git bisect start. El commit actual donde se detectó la falla se registra como git bisect bad, y el punto confirmado como correcto se registra como git bisect good. La documentación oficial de GitLab resume este procedimiento como "identificar el commit bad y el commit good para especificar los valores límite" 3.

bash
git bisect start
git bisect bad          # 현재 HEAD = 회귀 재현
git bisect good v1.4.0  # 태그·커밋 해시·상대참조 모두 가능

También puede utilizarse una forma abreviada que pasa ambas revisiones de una vez al comando de inicio. Al especificar primero el bad y después el good, como en git bisect start HEAD v1.4.0, ambos límites quedan definidos simultáneamente 2.

Existen además recursos para reducir el propio espacio de búsqueda. Se puede añadir un pathspec para limitar los candidatos únicamente a los commits que modifican rutas específicas. En los ejemplos de la documentación de Git aparece la técnica de restringir los candidatos solo a los cambios en un directorio o archivo concreto, por ejemplo mediante git bisect start -- arch/i386 include/asm-i386 2. En un ejemplo hipotético de lógica de pago, esto permitiría excluir del conjunto de candidatos modificaciones de documentación o de configuración de CI ajenas al problema.

Asimismo, especificar varios commits known-good estrecha aún más el espacio de búsqueda. Al enumerar varios commits correctos tras el commit bad, el rango puede reducirse todavía más 2. Cuanto mayor sea el rango de búsqueda, mayor es el beneficio del cálculo log₂, de modo que estos preprocesamientos resultan efectivos para reducir el "ancho de los límites" más que el "punto de falla".

Inspección del commit intermedio, evasión mediante skip y confirmación visual

Inspección del commit intermedio, evasión mediante skip y confirmación visual

Una vez definidos los dos límites, Git hace checkout automáticamente del commit en el punto medio del conjunto de candidatos. En ese momento, Git muestra el número de candidatos restantes y los pasos estimados.

Bisecting: 63 revisions left to test after this (roughly 6 steps)

Esta salida permite al usuario anticipar cuántas decisiones deberá tomar. En los ejemplos de la documentación oficial también se observa esta dinámica de reducción de los candidatos a la mitad, como en "675 revisions left, roughly 10 steps" 12.

El usuario compila el commit con checkout realizado, confirma si la regresión se reproduce e introduce únicamente el resultado.

bash
# 회귀가 재현되면
git bisect bad
# 정상이면
git bisect good

Si se responde bad al commit intermedio, se indica que "el culpable está antes (o aquí)", por lo que Git excluye el segmento posterior. Si se responde good, se indica que "el culpable está después", por lo que Git excluye el segmento anterior. Esta regla elimina candidatos en pasos log₂ 2. Finalmente, se muestra un único hash con la lista de archivos modificados por ese commit, como en b9f0e1a2c3... is the first bad commit 1.

Pueden darse commits donde la propia compilación falle y no sea posible determinar good/bad. En esos casos, es posible omitir el commit actual con git bisect skip. Git se desvía a un commit cercano y continúa la búsqueda 1. También es posible omitir un rango mediante git bisect skip v2.5..v2.6 1.

No obstante, skip tiene un coste: la pérdida de precisión. Según la explicación de tms-outsource, que cita el manual oficial de Git, si se omite un commit adyacente al culpable, Git no puede señalar con exactitud "cuál de los dos vino primero" 2. Aunque skip es un recurso para no detener la búsqueda, puede reducir la precisión. Si los candidatos omitidos son adyacentes e indistinguibles, Git imprime la lista de candidatos y se detiene. En tal caso, puede recuperarse el proceso usando git bisect visualize para inspeccionar visualmente en gitk los candidatos sospechosos restantes y emitir manualmente una decisión sobre un commit correcto adyacente elegido a mano 1.

Automatización: git bisect run y los códigos de salida determinan el resultado

Automatización: git bisect run y los códigos de salida determinan el resultado

En lugar de que una persona introduzca good/bad en cada paso, la decisión puede automatizarse mediante el código de salida de un script. git bisect run <스크립트> ejecuta un comando en cada commit con checkout realizado e interpreta el código de salida como decisión.

Christian Couder (LWN.net, contribución del 3 de febrero de 2009) precisó que un código de salida 0 se interpreta como good y cualquier otro valor (salvo 125 y los valores iguales o mayores que 128) como bad. Asimismo, propuso el patrón de solicitar skip con 125 para los commits en los que la falla de compilación impide determinar si hay regresión 4.

bash
git bisect start
git bisect bad
git bisect good v1.4.0
git bisect run npm test -- payment.spec.js

El significado de los códigos de salida se recoge en la siguiente tabla 12.

Código de salidaDecisiónSituación
0goodLa prueba pasó
1~124, 126, 127badLa prueba falló
125skipNo verificable, por ejemplo por fallo de compilación
128~255abortInterrupción de la búsqueda (señal, salida intencional, etc.)

Conviene señalar aquí dos trampas que requieren atención.

La primera es el código de salida 127. El shell POSIX devuelve 127 cuando no encuentra un comando. Si hay un error tipográfico en la ruta del script y el comando ni siquiera llega a ejecutarse, aparece 127. Git lo interpreta como "bad", marca todos los commits como bad y la búsqueda termina con un resultado sin sentido 2. La causa es que "la prueba falló" y "el script no existe y no se pudo ejecutar" quedan agrupados como el mismo bad. Por ello, antes de automatizar, debe comprobarse que el script se encuentre en estado ejecutable 2.

La segunda es la distinción entre fallo de compilación y fallo de regresión. Si un error de compilación —no un fallo de prueba— devuelve un código de salida 1 o 2, ese commit queda registrado como bad. Sin embargo, la causa de la falla podría ser el entorno de compilación y no una regresión. Para aclarar el skip en caso de fallo de compilación, Couder propone el patrón make || exit 125 4.

bash
#!/usr/bin/env bash
# 저장소 밖에 두는 편이 안전합니다 [S13]

make build || exit 125    # 빌드 실패 = 검증 불가 → skip

if ./run-app --check-payment; then
  exit 0                  # good
else
  exit 1                  # bad (회귀 재현)
fi

Esta cláusula condicional de una sola línea es el mecanismo clave que separa "fallo de compilación" de "regresión". Si este script se ubica en una ruta externa al repositorio, también se reducen los juicios erróneos provocados por la ausencia de archivos de prueba en commits antiguos 2.

La automatización también tiene requisitos previos. En la contribución de Couder, Andreas Ericsson (desarrollador de Git) explicó que "para utilizar git bisect, todos los commits deben mantenerse pequeños y en un estado bisectable", y que es necesario un flujo de trabajo en el que se escriba, commit a commit, una prueba que revele la regresión 4. La automatización elimina el esfuerzo de introducir las decisiones, pero exige mantener siempre preparado un script de reproducción. Ericsson informó de que, según los criterios de su tracker interno, el ciclo medio de informe-corrección se redujo de 142,6 horas a 16,2 horas; sin embargo, se trata de cifras experimentales medidas en un sistema de seguimiento concreto y dependen de las condiciones previas de segmentar los commits y escribir pruebas de regresión 4.

Reset tras finalizar la búsqueda e interpretación del resultado

Reset tras finalizar la búsqueda e interpretación del resultado

Cuando la búsqueda termina, debe ejecutarse git bisect reset. Durante bisect, Git hace checkout del commit candidato mediante su hash, de modo que se queda en estado detached HEAD. Sin un reset, se continúa trabajando en un commit intermedio. Nodelog Engineer indica que conviene ejecutarlo "de forma habitual", y Pro Git advierte que "si se omite el reset final, el repositorio queda en un estado extraño" 12.

Si se pasa un argumento a git bisect reset, el destino cambia.

bash
git bisect reset          # 세션 시작 전 HEAD로 복귀
git bisect reset bisect/bad  # first bad commit으로 복귀
git bisect reset HEAD      # 탐색 중 커밋에 머무름

Si se ejecuta sin argumentos, se vuelve a la rama previa a la búsqueda 2.

El first bad commit mostrado informa únicamente de que en ese commit "se introdujo el cambio"; no explica "por qué" se produjo la regresión. La salida final muestra el hash del commit y la lista de archivos que modificó, y la lectura del diff para determinar la causa queda a cargo del ser humano 2.

El first bad commit también puede ser un commit de merge. En ese caso, siguiendo las indicaciones de Nodelog Engineer, puede volver a bisectarse el interior de esa rama para afinar el commit concreto 1.

Existe además una vía de recuperación ante decisiones erróneas durante la búsqueda. Con 10.928 candidatos (unas 14 etapas), la primera decisión determina la mitad que explorarán las otras 13 etapas. Un error puede desviar en sentido equivocado todas las etapas posteriores. En tal caso, es posible guardar el historial de decisiones con git bisect log > bisect.log, borrar la línea incorrecta y ejecutar git bisect reset e git bisect replay bisect.log para reanudar la búsqueda 12.

Conclusión: la calidad de las decisiones determina la exactitud de la búsqueda

git bisect es una herramienta que, tomando como referencia los límites de good y bad, reduce los candidatos a la mitad en cada etapa; con 200 commits, permite acotar el first bad commit mediante aproximadamente 8 decisiones 1. No obstante, este resultado solo es válido cuando cada decisión refleja realmente si hay o no regresión en el commit correspondiente.

Si un fallo de compilación se marca como bad, queda la mitad equivocada, y si un error tipográfico en la ruta del script produce un código de salida 127, Git confunde todos los commits con bad 2. Por tanto, al automatizar, conviene separar los fallos de compilación con el código de salida 125 para que se traten como skip, ubicar el script de verificación fuera del repositorio y, una vez finalizada la búsqueda, volver de detached HEAD a la rama original con git bisect reset; estos procedimientos condicionan la fiabilidad del resultado 42.

Si se fija un límite good con margen y se preparan de antemano las condiciones de reproducción, incluida la aislamiento de las pruebas, el hash del commit y la lista de archivos modificados señalados por Git son suficientemente exactos como punto de partida para indicar "aquí se introdujo la regresión" 1.

Referencias

  1. ↩
  2. ↩
  3. ↩
  4. ↩