Carla abre una incidencia con un título incómodo: "El botón de borrar tarea no borra nada". Lo reproduce en tres navegadores. Pulsas la papelera, la tarea parpadea y sigue ahí.
Lo desconcertante es que funcionaba. En la versión v1.0.0, que el equipo etiquetó hace dos meses (lección 05-05), el borrado funcionaba perfectamente: hay una demo grabada que lo demuestra. Entre aquella etiqueta y main hay 214 commits.
Las opciones evidentes son malas. Leerse los 214 diffs es un día de trabajo y una garantía de no ver nada. Buscar por palabras clave con git log -S "borrar" (lección 02-06) da veintitantos candidatos, y ninguno parece culpable. Y preguntar en el chat del equipo solo produce teorías.
Pero hay una propiedad del problema que lo cambia todo: el historial está ordenado, y en algún punto de esa secuencia el comportamiento pasó de "funciona" a "no funciona". Eso es exactamente el escenario de una búsqueda binaria, y Git trae una implementación integrada: git bisect. Con ella, esos 214 commits se reducen a ocho pruebas.
Contenido
- La idea: búsqueda binaria sobre el historial
- Cuántos pasos hacen falta de verdad
- Una sesión manual completa
git bisect skip: commits que no se pueden probargit bisect logyreplay: no perder el trabajo- Automatizar con
git bisect run - El script de prueba para
gestor-tareas - Los códigos de salida, con detalle
--first-parent: saltarse el interior de las fusionesgit bisect terms: cuando no es "bueno/malo"- Qué hace que un historial sea bisecable
- La idea: búsqueda binaria sobre el historial
La premisa de bisect es una sola:
Existe un commit concreto a partir del cual el comportamiento cambió. Antes de él, bien; desde él, mal.
Si eso se cumple, no hace falta probar los commits uno a uno. Basta con probar el del medio:
- Si el del medio está mal, el culpable está en la primera mitad. La segunda mitad se descarta entera.
- Si el del medio está bien, el culpable está en la segunda mitad. La primera se descarta entera.
Y se repite sobre la mitad que queda. Cada prueba elimina la mitad de los candidatos.
flowchart TD
A["214 candidatos<br/>v1.0.0 (bien) ... main (mal)"] --> B["Probar el commit del medio"]
B -- "funciona → good" --> C["107 candidatos<br/>(mitad superior)"]
B -- "no funciona → bad" --> D["107 candidatos<br/>(mitad inferior)"]
C --> E["Probar el nuevo medio"]
D --> E
E --> F["54 → 27 → 13 → 7 → 3 → 2 → 1"]
F --> G["Commit culpable identificado"]
Git hace todo el trabajo de logística: calcula qué commit probar, hace el checkout por ti (en detached HEAD, lección 03-02), lleva la cuenta de los descartados y al final te dice el hash exacto. Tú solo respondes una pregunta binaria en cada paso: ¿funciona o no?
- Cuántos pasos hacen falta de verdad
El número de pruebas es el logaritmo en base 2 del número de candidatos, redondeado hacia arriba. La diferencia con la búsqueda lineal es brutal y merece verse en una tabla:
| Commits entre bueno y malo | Pruebas con bisect (log₂) |
Pruebas una a una (peor caso) |
|---|---|---|
| 8 | 3 | 8 |
| 32 | 5 | 32 |
| 100 | 7 | 100 |
| 214 | 8 | 214 |
| 1 000 | 10 | 1 000 |
| 10 000 | 14 | 10 000 |
| 1 000 000 | 20 | 1 000 000 |
Ese es el titular: duplicar el tamaño del historial añade una sola prueba. Un repositorio con un millón de commits se biseca en veinte pasos. Es la razón por la que bisect sigue siendo útil por muy grande que sea el proyecto.
El propio Git te lo dice al empezar:
"106 revisiones por probar, aproximadamente 7 pasos". No es una estimación optimista: es matemática.
- Una sesión manual completa
Carla empieza. Lo primero, dejar el árbol de trabajo limpio: bisect va a hacer muchos checkout, y no puede si hay cambios sin confirmar. Si los tiene, git stash (lección 05-04).
Paso 1: iniciar la sesión.
No responde nada. Git ha entrado en modo bisección: ha creado referencias internas en .git/refs/bisect/ y un fichero .git/BISECT_LOG.
Paso 2: marcar los dos extremos.
git bisect bad # el commit actual (main) está mal
git bisect good v1.0.0 # la etiqueta v1.0.0 estaba bienBisecting: 106 revisions left to test after this (roughly 7 steps) [9f3c7a1e5b2d8c4f6a0e3b7d9c1f5a8e2b4d6c9f] Extraer la validación del formulario a su función
Fíjate en lo que ha pasado: Git ha hecho checkout del commit intermedio. El árbol de trabajo de Carla es ahora el del commit 9f3c7a1, y HEAD está desacoplado:
HEAD detached at 9f3c7a1 You are currently bisecting, started from main. (use "git bisect reset" to get back to the original branch) nothing to commit, working tree clean
Paso 3: el bucle. Carla abre index.html en el navegador, crea una tarea, pulsa la papelera:
Funciona. Lo marca:
Bisecting: 53 revisions left to test after this (roughly 6 steps) [4e8b2c9d7f1a3e5c6b0d8f2a4c7e9b1d3f5a7c8e] Añadir el filtro por estado de la tarea
De 106 a 53 de un golpe. Y así sucesivamente:
Bisecting: 26 revisions left to test after this (roughly 5 steps) [7a1f5c3e9b2d6a8c4f0e7b3d5a9c1e6f2b8d4a7c] Reescribir el renderizado del listado
Bisecting: 12 revisions left to test after this (roughly 4 steps) [2c6e9a4f8b1d7c3e5a0f9b2d6c8e4a1f7b3d5c9e] Extraer los estilos del listado a estilos.css
Carla continúa: good, bad, good, bad... Cinco pruebas más tarde:
8d4f2a7c1e6b9d3f5a8c0e2b4d7f9a1c3e5b8d6f is the first bad commit commit 8d4f2a7c1e6b9d3f5a8c0e2b4d7f9a1c3e5b8d6f Author: Bruno Salas <[email protected]> Date: Mon Jun 22 11:14:38 2026 +0200 Delegar los eventos del listado en el contenedor app.js | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-)
Ocho pruebas. 214 commits. Un culpable con nombre, fecha y diff.
Paso 4: mirar el diff.
diff --git a/app.js b/app.js
--- a/app.js
+++ b/app.js
@@ -78,18 +78,16 @@
- document.querySelectorAll('.borrar').forEach((boton) => {
- boton.addEventListener('click', (e) => {
- borrarTarea(e.target.dataset.id);
- });
- });
+ lista.addEventListener('click', (e) => {
+ if (e.target.classList.contains('borrar')) {
+ borrarTarea(e.target.dataset.id);
+ }
+ });Ahí está. Bruno cambió los escuchadores individuales por delegación de eventos en el contenedor, que es una mejora legítima. Pero el botón de borrar contiene un <svg> con el icono de papelera, así que al pulsar, e.target es el <svg>, no el botón: classList.contains('borrar') da false y no pasa nada. Con e.target.closest('.borrar') funcionaría.
Sin bisect, encontrar eso habría llevado horas. Con él, ocho pruebas y un diff de doce líneas.
Paso 5: salir. Y este paso no es opcional:
Previous HEAD position was 8d4f2a7 Delegar los eventos del listado en el contenedor Switched to branch 'main'
reset limpia el estado de bisección y devuelve a HEAD a donde estaba al empezar. Si lo olvidas, te quedas en detached HEAD sobre un commit antiguo, y el siguiente git status será una sorpresa desagradable.
Resumen de los comandos de la sesión:
| Comando | Qué hace |
|---|---|
git bisect start |
Inicia la sesión |
git bisect bad [<sha>] |
Marca un commit como defectuoso (por defecto, HEAD) |
git bisect good [<sha>] |
Marca un commit como correcto |
git bisect start <malo> <bueno> |
Atajo: inicia y marca los dos extremos de una vez |
git bisect skip |
"No puedo probar este" (apartado 4) |
git bisect log |
Muestra la sesión hasta ahora |
git bisect replay <fichero> |
Reproduce una sesión guardada |
git bisect visualize (o view) |
Abre gitk/log con los candidatos restantes |
git bisect run <comando> |
Automatiza toda la sesión (apartado 6) |
git bisect reset [<ref>] |
Termina y vuelve al punto de partida |
El atajo del inicio ahorra dos comandos y es el que se usa en la práctica:
git bisect skip: commits que no se pueden probar
git bisect skip: commits que no se pueden probarEn la sexta prueba, Carla se encuentra esto:
python3 -m http.server 8000
# La página aparece completamente en blanco. La consola del navegador dice:
# Uncaught SyntaxError: Unexpected token '}' en app.js:96Ese commit está roto por otro motivo. No es que el borrado no funcione: es que la aplicación entera no arranca. Marcarlo bad sería mentirle a bisect y podría llevarlo por el camino equivocado; marcarlo good, peor todavía.
La respuesta correcta es:
Bisecting: 6 revisions left to test after this (roughly 3 steps) [5b8e2d4a9c7f1e3b6d0a8c2f4e7b9d1a3c5f8e2b] Corregir la llave sobrante en app.js
skip le dice a Git: "este commit no me da información". Git elige otro candidato cercano y sigue. Si acabas saltándote demasiados y no puede aislar uno solo, te lo dirá:
There are only 'skip'ped commits left to test. The first bad commit could be any of: 3e7a1c5 ... 8d4f2a7 ... b2c9e4f ... We cannot bisect more!
En ese caso te da un conjunto pequeño de sospechosos, que ya es infinitamente mejor que 214.
También se pueden saltar rangos enteros, si sabes que toda una zona no compila:
Motivos habituales para skip: el commit no compila, faltan dependencias que aún no estaban en el package.json, o el commit es de un cambio de formato masivo que hace imposible ejecutar nada. skip es tu amigo; mentir a bisect no.
git bisect log y replay: no perder el trabajo
git bisect log y replay: no perder el trabajoA mitad de una bisección larga puede pasar de todo: te equivocas al marcar, se cierra el terminal, o tienes que atender otra cosa. git bisect log guarda la sesión:
git bisect start # status: waiting for both good and bad commits # bad: [c8f2a1e...] Añadir el contador de tareas pendientes git bisect bad c8f2a1e... # status: waiting for good commit(s), bad commit known # good: [1a5c9f3...] Version 1.0.0 git bisect good 1a5c9f3... # good: [9f3c7a1...] Extraer la validación del formulario a su función git bisect good 9f3c7a1... # bad: [4e8b2c9...] Añadir el filtro por estado de la tarea git bisect bad 4e8b2c9...
Guardarlo y recuperarlo después:
git bisect log > /tmp/sesion-borrado.txt
git bisect reset # dejarlo por hoy
# Mañana, o en otra máquina:
git bisect replay /tmp/sesion-borrado.txtreplay reconstruye la sesión hasta donde llegaste y te deja en el siguiente commit por probar.
Y aquí está su otro uso, el más valioso: corregir un error de marcado. Si te das cuenta de que dijiste good donde debías decir bad, no hay que empezar de cero:
git bisect log > /tmp/sesion.txt
# Editar /tmp/sesion.txt: quitar la línea equivocada y las posteriores
git bisect reset
git bisect replay /tmp/sesion.txt
- Automatizar con
git bisect run
git bisect runLa sesión manual funciona, pero ocho vueltas de "abrir el navegador, crear una tarea, pulsar, marcar" son ocho oportunidades de equivocarse y veinte minutos de la vida de Carla.
Si la comprobación se puede escribir como un comando que devuelve 0 cuando está bien y algo distinto de 0 cuando está mal, git bisect run lo hace todo solo:
Git prueba, ejecuta el comando, interpreta el código de salida, marca, avanza y repite hasta encontrar el culpable. Sin intervención humana.
Con una batería de pruebas, es literalmente esto:
Y para un solo caso, puede bastar un comando de una línea:
# ¿Existe todavía la función borrarTarea en app.js?
git bisect run grep -q "function borrarTarea" app.jsgrep -q devuelve 0 si encuentra, 1 si no. Es exactamente el contrato que bisect run espera.
- El script de prueba para
gestor-tareas
gestor-tareasComo el fallo de Carla es de comportamiento en el navegador, hace falta un script algo más elaborado. El equipo tiene una prueba automatizada con Node:
#!/usr/bin/env bash
#
# herramientas/probar-borrado.sh
# Devuelve 0 si el borrado de tareas funciona, 1 si no.
# Pensado para 'git bisect run'.
set -uo pipefail # OJO: sin -e, queremos controlar los códigos a mano
# 1. ¿Están los ficheros que necesitamos? Si no, este commit no es evaluable.
if [ ! -f app.js ] || [ ! -f index.html ]; then
echo "→ Faltan ficheros del proyecto: commit no evaluable"
exit 125
fi
# 2. Dependencias: se instalan sin ruido; si falla, no es culpa del código
if [ -f package.json ]; then
npm ci --silent >/dev/null 2>&1 || {
echo "→ npm ci ha fallado: commit no evaluable"
exit 125
}
fi
# 3. ¿El JavaScript es sintácticamente válido? Si no, tampoco es evaluable
if ! node --check app.js >/dev/null 2>&1; then
echo "→ app.js no es sintácticamente válido: commit no evaluable"
exit 125
fi
# 4. La prueba de verdad
if node herramientas/prueba-borrado.mjs >/dev/null 2>&1; then
echo "→ El borrado FUNCIONA"
exit 0
else
echo "→ El borrado NO funciona"
exit 1
fiY la prueba en sí, que simula el DOM mínimo necesario:
// herramientas/prueba-borrado.mjs
// Prueba mínima: crear una tarea, borrarla y comprobar que desaparece.
import { JSDOM } from 'jsdom';
import { readFileSync } from 'node:fs';
const html = readFileSync('index.html', 'utf8');
const dom = new JSDOM(html, { runScripts: 'outside-only' });
const { document } = dom.window;
// Exponer el entorno del navegador que espera app.js
globalThis.window = dom.window;
globalThis.document = document;
globalThis.localStorage = dom.window.localStorage;
// Cargar la aplicación
dom.window.eval(readFileSync('app.js', 'utf8'));
// 1. Crear una tarea
const campo = document.querySelector('#nueva-tarea');
const formulario = document.querySelector('#formulario-tarea');
campo.value = 'Tarea de prueba del bisect';
formulario.dispatchEvent(new dom.window.Event('submit'));
if (document.querySelectorAll('.tarea').length !== 1) {
console.error('La tarea no se ha creado: la prueba no es concluyente');
process.exit(125); // no evaluable, no es el fallo que buscamos
}
// 2. Pulsar el icono de la papelera (el <svg> dentro del botón)
const icono = document.querySelector('.tarea .borrar svg')
?? document.querySelector('.tarea .borrar');
icono.dispatchEvent(new dom.window.MouseEvent('click', { bubbles: true }));
// 3. ¿Ha desaparecido?
const quedan = document.querySelectorAll('.tarea').length;
process.exit(quedan === 0 ? 0 : 1);Los tres puntos que hacen que este script funcione bien con bisect:
- Prueba una sola cosa. Solo el borrado. Si la prueba fallara también por otros motivos,
bisectencontraría el primer commit que rompe cualquier cosa, que no es lo que buscamos. - Distingue "no funciona" de "no se puede evaluar". Ese es el papel del
125, y es lo que veremos ahora. - Es silencioso y determinista. Sin salida interactiva, sin depender de la red, sin fallos intermitentes. Una prueba que a veces falla envenena toda la bisección.
Lanzándolo:
running './herramientas/probar-borrado.sh' → El borrado FUNCIONA Bisecting: 53 revisions left to test after this (roughly 6 steps) [4e8b2c9] Añadir el filtro por estado de la tarea running './herramientas/probar-borrado.sh' → El borrado NO funciona Bisecting: 26 revisions left to test after this (roughly 5 steps) ... 8d4f2a7c1e6b9d3f5a8c0e2b4d7f9a1c3e5b8d6f is the first bad commit commit 8d4f2a7c1e6b9d3f5a8c0e2b4d7f9a1c3e5b8d6f Author: Bruno Salas <[email protected]> Date: Mon Jun 22 11:14:38 2026 +0200 Delegar los eventos del listado en el contenedor app.js | 22 ++++++++++------------ bisect run success
Cuarenta segundos, sin tocar el teclado. Y una advertencia sobre el #!: el script debe ser ejecutable (chmod +x) y estar fuera del árbol de trabajo o presente en todos los commits. Si vive dentro del repositorio y en los commits antiguos no existía, bisect no lo encontrará al hacer checkout de esos commits. La solución habitual es copiarlo a /tmp y ejecutarlo desde allí:
- Los códigos de salida, con detalle
Este es el contrato completo de git bisect run, y conocerlo es la diferencia entre una automatización que funciona y una que da resultados falsos:
| Código de salida | Significado para bisect |
Cuándo usarlo |
|---|---|---|
| 0 | El commit es bueno | La comprobación pasa |
| 1–124 | El commit es malo | La comprobación falla |
| 125 | Saltar este commit (skip) |
No se puede evaluar: no compila, faltan dependencias |
| 126 | Malo (el comando no es ejecutable) | Error de permisos: revísalo |
| 127 | Malo (comando no encontrado) | Error de ruta: revísalo |
| 128–255 | Aborta la bisección | Error grave; Git se detiene y avisa |
Tres consecuencias prácticas:
125es la pieza clave. Es el equivalente automático degit bisect skip. Sin él, un commit que no compila se marcaría como "malo" ybisectte señalaría un culpable equivocado.126y127son trampas mortales. Si el script no es ejecutable o la ruta está mal,bisectmarcará todos los commits como malos y "encontrará" como culpable el primero del rango. Si el resultado te parece absurdo, comprueba primero que el script se ejecuta a mano:./mi-script.sh; echo $?.- Evita
exit 128y superiores. Unkill -9de un proceso da 137, por ejemplo. Si tu script puede terminar así, envuélvelo para normalizar el código de salida.
Comprobación previa obligatoria, siempre, antes de lanzar un bisect run:
# En un commit que sabes BUENO
git switch --detach v1.0.0
./herramientas/probar-borrado.sh; echo "código: $?" # debe ser 0
# En un commit que sabes MALO
git switch --detach main
./herramientas/probar-borrado.sh; echo "código: $?" # debe ser 1Treinta segundos que evitan biseccionar durante diez minutos hacia una respuesta inventada.
--first-parent: saltarse el interior de las fusiones
--first-parent: saltarse el interior de las fusionesEl historial de gestor-tareas tiene fusiones (lección 03-03), y bisect por defecto explora todos los commits alcanzables, incluidos los internos de cada rama fusionada. Eso tiene dos inconvenientes:
- Los commits intermedios de una rama en desarrollo suelen estar a medias: puede que ni compilen, y acabas haciendo muchos
skip. - A efectos de
main, lo que importa es en qué fusión entró el problema, no cuál de los seis commits de la rama lo introdujo.
Con --first-parent, Git solo considera la línea principal: los commits directos de main y los commits de fusión, sin entrar dentro de las ramas. Es exactamente el mismo concepto de --first-parent que veremos en git log en la lección 06-04, y el mismo "primer padre" que elegía -m 1 al revertir una fusión (lección 05-06).
gitGraph commit id: "v1.0.0" commit id: "a1" branch funcionalidad/eventos commit id: "f1" commit id: "f2" commit id: "f3" checkout main commit id: "a2" merge funcionalidad/eventos id: "M1" commit id: "a3"
Sin --first-parent, los candidatos son a1, f1, f2, f3, a2, M1, a3. Con --first-parent, solo a1, a2, M1, a3.
Cuándo usar cada uno:
| Situación | Modo |
|---|---|
El equipo fusiona ramas y main siempre está sano |
--first-parent: más rápido, menos skip |
| Quieres el commit exacto dentro de la rama culpable | Sin --first-parent |
| El historial es lineal (rebase antes de integrar) | Da igual: no hay fusiones |
| Muchos commits intermedios rotos | --first-parent, casi obligado |
La táctica de dos fases es la mejor de las dos: primero --first-parent para identificar la fusión, después una segunda bisección dentro de esa rama:
git bisect start --first-parent main v1.0.0
git bisect run /tmp/probar.sh
# → resulta ser la fusión M1
git bisect reset
git bisect start M1^2 M1^1 # dentro de la rama: malo=punta, bueno=base
git bisect run /tmp/probar.shM1^2 es el segundo padre (la punta de la rama fusionada) y M1^1 el primero (la base en main), con la notación de la lección 02-06.
git bisect terms: cuando no es "bueno/malo"
git bisect terms: cuando no es "bueno/malo"bisect no es solo para buscar regresiones. Sirve para localizar cualquier cambio de estado monótono en el historial. Los ejemplos típicos:
- ¿En qué commit se arregló un fallo? (aquí "bueno" y "malo" están invertidos)
- ¿En qué commit el arranque pasó de tardar 200 ms a 900 ms?
- ¿En qué commit el paquete pasó de 400 KB a 1,2 MB?
En esos casos, la terminología por defecto confunde. git bisect terms permite renombrarla:
git bisect start --term-old=roto --term-new=arreglado
git bisect roto v1.0.0
git bisect arreglado mainA partir de ahí, en cada paso respondes git bisect roto o git bisect arreglado, y Git busca el primer commit arreglado.
| Alias | Equivale a | Significado |
|---|---|---|
--term-old / --term-good |
good |
El estado antiguo, el de antes del cambio |
--term-new / --term-bad |
bad |
El estado nuevo, el que buscamos |
Lo que hay que entender es que a Git le dan igual las palabras: siempre busca el primer commit con el estado "nuevo", sea "roto", "arreglado", "lento" o "gordo". Consultar los términos activos en cualquier momento:
Un ejemplo de rendimiento, midiendo con el propio script:
#!/usr/bin/env bash
# /tmp/probar-tamano.sh — 0 si el bundle pesa menos de 500 KB
npm ci --silent >/dev/null 2>&1 || exit 125
npm run build --silent >/dev/null 2>&1 || exit 125
bytes=$(wc -c < dist/app.min.js)
echo "→ ${bytes} bytes"
[ "$bytes" -lt 512000 ]git bisect start --term-old=ligero --term-new=pesado
git bisect ligero v1.0.0
git bisect pesado main
git bisect run /tmp/probar-tamano.shLa última línea del script es la que decide: [ "$bytes" -lt 512000 ] devuelve 0 si se cumple y 1 si no, que es justo el contrato de bisect run.
- Qué hace que un historial sea bisecable
bisect es una herramienta que te devuelve lo que le diste. Su eficacia depende directamente de la calidad del historial, y ahí es donde el módulo 5 y este se dan la mano:
| Propiedad del historial | Efecto sobre bisect |
|---|---|
| Cada commit compila y arranca | La bisección es limpia, sin skip |
| Commits pequeños y atómicos | El culpable es un diff de 10 líneas, no de 800 |
| Un cambio lógico por commit | El diff señala la causa directamente |
| Mensajes descriptivos | Entiendes el porqué del cambio culpable al instante |
| Commits gigantes "trabajo del viernes" | Encuentras el commit... y sigues sin saber qué línea |
| Commits que no compilan | skip continuo, resultado difuso o inconcluyente |
| Ramas fusionadas con historia sucia | Ruido: se resuelve con --first-parent |
Dicho de otro modo: el rebase interactivo de la lección 05-02 no es cosmética. Cuando consolidas los fixup!, divides un commit gigante y te aseguras de que cada uno deja el proyecto en un estado funcional, lo que estás construyendo es un historial bisecable. La factura de no hacerlo se paga el día en que hay que encontrar una regresión de hace dos meses.
Y con --exec (lección 05-02) puedes comprobar por adelantado que toda una rama es bisecable antes de integrarla:
Git aplica cada commit y ejecuta la comprobación en cada uno. Si alguno falla, te detiene ahí.
Todo esto —commits atómicos, historial que compila, qué consolidar antes de integrar— es el tema de la lección 08-02: Manteniendo un Historial Limpio.
bisectes la mejor justificación práctica que existe para esas prácticas: convierte una virtud abstracta en minutos de trabajo ahorrados.
Una última nota de alcance: bisect responde a "¿qué commit cambió este comportamiento?". No responde a "¿por qué mi repositorio está en un estado extraño?" ni "¿qué está haciendo Git por debajo?". Ese diagnóstico —reflog, fsck, GIT_TRACE y compañía— es el terreno de las lecciones del módulo 9, y en particular de la 09-06: Técnicas Avanzadas de Depuración.
Errores Comunes y Consejos
Error 1: empezar con el árbol de trabajo sucio. bisect hace checkout en cada paso y fallará o arrastrará tus cambios entre commits. Confirma o haz git stash antes.
Error 2: olvidar git bisect reset. Te quedas en detached HEAD sobre un commit de junio. Todo commit que hagas ahí quedará huérfano.
Error 3: invertir good y bad al empezar. El orden del atajo es git bisect start <MALO> <BUENO>. Si lo inviertes, Git buscará en el sentido contrario y no encontrará nada coherente.
Error 4: marcar bad un commit que está roto por otro motivo. Usa skip. Mentirle a bisect produce un culpable equivocado con toda la apariencia de ser correcto.
Error 5: olvidar el exit 125 en el script. Sin él, los commits que no compilan cuentan como malos y el resultado es basura.
Error 6: un script no ejecutable o con la ruta mal. Códigos 126/127: todo sale "malo" y el culpable es el primer commit del rango. Prueba el script a mano antes.
Error 7: dejar el script dentro del repositorio. En los commits antiguos puede no existir. Cópialo a /tmp y ejecútalo desde fuera.
Error 8: una prueba no determinista. Si falla una de cada cinco veces, bisect te dará una respuesta distinta cada vez. Asegura el determinismo antes de automatizar.
Consejo 1: elige el "bueno" más reciente que puedas. Empezar en v1.0.0 en lugar de en el primer commit del proyecto ahorra pasos. Las etiquetas (lección 05-05) son perfectas para esto.
Consejo 2: guarda siempre git bisect log. Una sesión larga es trabajo valioso. Un fichero de texto te permite retomarla o corregir un marcado erróneo.
Consejo 3: primero --first-parent, después dentro de la rama. Rápido para localizar la fusión, preciso para localizar el commit.
Consejo 4: bisect run con npm test es a menudo suficiente. Si ya tienes pruebas, no escribas nada nuevo.
Consejo 5: cuando encuentres el culpable, no lo revientes sin más. Míralo, entiéndelo y decide: git revert si está publicado (lección 05-06), o una corrección hacia delante si el cambio en sí era bueno y solo faltaba un detalle —como en el caso de Bruno, donde la delegación de eventos era correcta y solo faltaba closest().
Ejercicios
Ejercicio 1: bisección manual
Crea un repositorio con 15 commits donde el fichero valor.txt contiene 10, y a partir del commit número 9 pasa a contener 99 (el "fallo"). Después:
- Inicia una bisección con
maincomo malo y el primer commit como bueno. - En cada paso, comprueba
cat valor.txty marcagoodobad. - Comprueba que Git identifica exactamente el commit 9.
- ¿Cuántos pasos has necesitado? ¿Coincide con log₂(14)?
Ejercicio 2: bisección automática con run
Con el mismo repositorio del ejercicio 1:
- Escribe un script en
/tmpque devuelva 0 sivalor.txtcontiene10y 1 si no. - Prueba el script a mano en un commit bueno y en uno malo antes de usarlo.
- Lanza
git bisect runy comprueba que encuentra el mismo commit. - Añade al repositorio un commit intermedio en el que
valor.txtno exista, y haz que el script devuelva125en ese caso. Comprueba que la bisección sigue funcionando.
Ejercicio 3: términos personalizados
Crea un repositorio de 10 commits donde estado.txt contiene roto y a partir del commit 7 pasa a contener arreglado. Usando --term-old y --term-new, encuentra el primer commit arreglado. Escribe también el bisect run correspondiente.
Soluciones
Solución 1:
mkdir /tmp/practica-bisect && cd /tmp/practica-bisect
git init -b main
for i in $(seq 1 15); do
if [ "$i" -lt 9 ]; then echo "10" > valor.txt; else echo "99" > valor.txt; fi
echo "linea $i" >> registro.txt
git add .
git commit -q -m "Commit numero $i"
done
git log --oneline | tail -1 # el primer commit (bueno)Cuatro pruebas para 14 candidatos. log₂(14) ≈ 3,8, que redondeado hacia arriba son 4. Exacto.
Solución 2:
cat > /tmp/probar-valor.sh <<'FIN'
#!/usr/bin/env bash
# 0 = bueno, 1 = malo, 125 = no evaluable
[ -f valor.txt ] || { echo "→ sin valor.txt: no evaluable"; exit 125; }
valor=$(cat valor.txt)
echo "→ valor = $valor"
[ "$valor" = "10" ]
FIN
chmod +x /tmp/probar-valor.shComprobación previa, que nunca hay que saltarse:
cd /tmp/practica-bisect
git switch --detach $(git log --oneline | tail -1 | cut -d' ' -f1)
/tmp/probar-valor.sh; echo "código: $?"git bisect start main $(git log --oneline | tail -1 | cut -d' ' -f1)
git bisect run /tmp/probar-valor.shrunning '/tmp/probar-valor.sh'
→ valor = 10
Bisecting: 3 revisions left to test after this (roughly 2 steps)
running '/tmp/probar-valor.sh'
→ valor = 99
...
7b2f5a9... is the first bad commit
Commit numero 9
bisect run successEl commit no evaluable:
git switch -c con-hueco main
git rm -q valor.txt && git commit -q -m "Quitar temporalmente valor.txt"
echo "99" > valor.txt && git add . && git commit -q -m "Restaurar valor.txt"
git bisect start con-hueco $(git log --oneline main | tail -1 | cut -d' ' -f1)
git bisect run /tmp/probar-valor.shCuando la bisección llega al commit sin valor.txt, el script devuelve 125 y Git lo salta:
running '/tmp/probar-valor.sh' → sin valor.txt: no evaluable Bisecting: 1 revision left to test after this (roughly 1 step)
El culpable sigue siendo el commit 9: el 125 ha evitado que un commit inevaluable falseara el resultado.
Solución 3:
mkdir /tmp/practica-bisect-terms && cd /tmp/practica-bisect-terms
git init -b main
for i in $(seq 1 10); do
if [ "$i" -lt 7 ]; then echo "roto" > estado.txt; else echo "arreglado" > estado.txt; fi
echo "linea $i" >> registro.txt
git add . && git commit -q -m "Commit numero $i"
done
primero=$(git log --oneline | tail -1 | cut -d' ' -f1)git bisect start --term-old=roto --term-new=arreglado
git bisect roto "$primero"
git bisect arreglado maincat estado.txt # roto
git bisect roto
cat estado.txt # arreglado
git bisect arreglado
cat estado.txt # arreglado
git bisect arregladoCon automatización:
git bisect reset
cat > /tmp/probar-estado.sh <<'FIN'
#!/usr/bin/env bash
[ -f estado.txt ] || exit 125
grep -q '^roto$' estado.txt # 0 = estado antiguo (roto), 1 = nuevo
FIN
chmod +x /tmp/probar-estado.sh
git bisect start --term-old=roto --term-new=arreglado
git bisect roto "$primero"
git bisect arreglado main
git bisect run /tmp/probar-estado.sh
git bisect resetLo importante: aunque los términos se llamen roto y arreglado, el script sigue devolviendo 0 para el estado antiguo y 1 para el nuevo. bisect no interpreta las palabras; solo busca la frontera.
Conclusión
git bisect convierte una búsqueda desesperada en un procedimiento mecánico y acotado. Lo esencial:
- Es una búsqueda binaria sobre el historial: cada prueba descarta la mitad de los candidatos, así que hacen falta log₂(n) pruebas. 214 commits son 8 pruebas; un millón, veinte.
- La sesión manual es
start→bad→good→ bucle de pruebas →reset. Elresetfinal no es opcional: sin él te quedas en detached HEAD. git bisect skipes la respuesta honesta a un commit que no se puede evaluar. Mentir marcandogoodobadproduce un culpable falso.git bisect logyreplaysalvan sesiones largas y permiten corregir un marcado erróneo sin empezar de cero.git bisect run <comando>automatiza todo el proceso. El contrato son los códigos de salida: 0 bueno, 1–124 malo, 125 saltar, 128+ abortar. El125es lo que separa una automatización fiable de una que inventa respuestas.- Prueba siempre el script a mano en un commit bueno y en uno malo antes de lanzar
bisect run, y sácalo del árbol de trabajo (/tmp) para que exista en todos los commits. --first-parentlimita la búsqueda a la línea principal: menos ruido y menosskip. La táctica de dos fases —primero la fusión, después dentro de la rama— combina velocidad y precisión.git bisect termsgeneraliza la herramienta a cualquier cambio de estado monótono: cuándo se arregló algo, cuándo empezó a ir lento, cuándo engordó el paquete.- Y la conclusión de fondo: un historial de commits pequeños que compilan es un historial bisecable. La lección 08-02 lo desarrolla;
bisectes la razón por la que importa.
Carla ya tiene el commit culpable y el diff exacto. Pero al mirar app.js para arreglarlo se encuentra otra cosa: veinte líneas más abajo hay una función de normalización de texto que nadie sabe para qué está, con un replace de caracteres raros que parece arbitrario. Nadie del equipo recuerda haberla escrito. Borrarla parece tentador... y probablemente rompería algo.
Antes de tocar código que no entiendes, hay que saber quién escribió cada línea, cuándo y en qué commit, para poder leer el mensaje que explica el porqué. Esa es la herramienta de la lección 06-03: Git Blame.
Dominando Git: De Principiante a Avanzado
Módulo 1: Introducción a Git
- ¿Qué es Git?
- Instalando Git
- Terminología Básica de Git
- El Modelo de Datos de Git
- Configurando Git
- Configuración Inicial
Módulo 2: Operaciones Básicas de Git
- Creando un Repositorio
- Clonando un Repositorio
- Flujo de Trabajo Básico de Git
- Preparando y Confirmando Cambios
- Inspeccionando Cambios con git diff
- Visualizando el Historial de Confirmaciones
Módulo 3: Ramas y Fusión
- Entendiendo las Ramas
- Creando y Cambiando Ramas
- Fusionando Ramas
- Estrategias de Fusión
- Resolviendo Conflictos de Fusión
- Gestión de Ramas
Módulo 4: Trabajando con Repositorios Remotos
- Entendiendo los Repositorios Remotos
- Agregando un Repositorio Remoto
- Autenticación con Repositorios Remotos
- Obteniendo y Extrayendo Cambios
- Enviando Cambios
- Rastreando Ramas
Módulo 5: Operaciones Avanzadas de Git
- Rebase
- Rebase Interactivo
- Cherry-Picking de Confirmaciones
- Guardando Cambios Temporales
- Etiquetando Confirmaciones
- Revirtiendo Confirmaciones
Módulo 6: Herramientas y Técnicas de Git
- Usando Git Hooks
- Git Bisect
- Git Blame
- Git Log y Alias
- Submódulos de Git
- Múltiples Copias de Trabajo con git worktree
Módulo 7: Estrategias de Colaboración y Flujo de Trabajo
- Forks y Pull Requests
- Revisiones de Código con Git
- Flujo de Trabajo Git Flow
- GitHub Flow
- Trunk Based Development
- Integración Continua con Git
Módulo 8: Mejores Prácticas y Consejos de Git
- Escribiendo Buenos Mensajes de Confirmación
- Manteniendo un Historial Limpio
- Ignorando Archivos con .gitignore
- Atributos de Fichero con .gitattributes
- Mejores Prácticas de Seguridad
- Consejos de Rendimiento
Módulo 9: Solución de Problemas y Depuración
- Problemas Comunes de Git
- Deshaciendo Cambios
- Resolviendo Divergencias con el Remoto
- Recuperando Confirmaciones Perdidas
- Tratando con Repositorios Corruptos
- Técnicas Avanzadas de Depuración
