Git avanzado II: detective — bisect, blame y reflog
Segunda entrega de la serie Git avanzado. La anterior fue sobre reescribir la historia con rebase, cherry-pick y amend. Tiempo de lectura estimado: 5 minutos.
La parte menos glamurosa pero más salvavidas de git: los comandos para investigar. No los escribes a diario, pero el día que los necesitas, te ahorran horas. Tres: git bisect para cazar el commit que introdujo un bug, git blame para saber quién escribió qué línea y cuándo, y git reflog para recuperar lo que parecía perdido para siempre.
git bisect: búsqueda binaria de bugs
El escenario: ayer funcionaba, hoy no. Entre ayer y hoy hay treinta commits. ¿Cuál de ellos lo rompió?
git bisect automatiza la búsqueda binaria por tu historial: tú le das un commit "bueno" (pasado) y uno "malo" (actual), y git va saltando a puntos intermedios para que los pruebes. En vez de probar treinta, pruebas cinco.
git bisect start
git bisect bad # HEAD está roto
git bisect good v1.2.0 # v1.2.0 estaba bien
Git calcula el commit del medio y hace checkout. Tú compruebas si ese commit está roto o no:
# Pruebo que el bug está aquí o no
git bisect bad # si el bug sigue en este commit
git bisect good # si aquí aún funcionaba
Git recalcula, salta al siguiente punto del medio, y repite. Al final te dice: "el primer commit malo fue abc123". Cuando termines:
git bisect reset
Vuelves a donde estabas antes de empezar.
La versión automática es aún mejor. Si tienes un comando o script que te dice si el bug está o no (exit code 0 = bueno, distinto de 0 = malo):
git bisect start HEAD v1.2.0
git bisect run ./test-del-bug.sh
Te vas a tomar un café. Cuando vuelves, git te dice en qué commit exacto se rompió, con su autor, fecha y diff.
Bisect bien hecho es magia: un bug que tardarías dos horas en localizar leyendo código lo encuentras en diez minutos. El truco es tener una forma automatizable de probar "está roto o no". Un test, un curl, un script que compila y ejecuta. Cuanto más específico el test, mejor el diagnóstico.
Consejos:
- Elige un "good" viejo de verdad. Si el bug entró hace 200 commits, buscar entre los últimos 20 no lo va a encontrar.
- Excluye cambios irrelevantes con
git bisect skipsi un commit intermedio no compila o no se puede probar. Git lo ignora y sigue. - Anota el hash y el resumen antes de hacer reset, que luego se te olvida.
git blame: quién escribió esta línea
git blame te dice, para cada línea de un fichero, cuál fue el último commit que la modificó y quién era el autor.
git blame src/handler.go
Output típico:
abc1234 (Alicia 2025-11-03 14:22:11 +0100 42) func handleLogin(w http.ResponseWriter, r *http.Request) {
def5678 (Bruno 2026-01-15 10:05:33 +0100 43) email := r.FormValue("email")
9012ghi (Alicia 2025-11-03 14:22:11 +0100 44) if email == "" {
Cada línea te muestra: hash del commit, autor, fecha, número de línea, contenido.
Variantes útiles:
git blame -L 40,60 src/handler.go # solo líneas 40 a 60
git blame -L :handleLogin src/handler.go # solo la función handleLogin
git blame -w src/handler.go # ignora cambios de whitespace
git blame -M src/handler.go # detecta movimientos dentro del fichero
git blame -C src/handler.go # detecta copias desde otros ficheros
-w es imprescindible cuando alguien reformateó el fichero con un linter y ahora blame te señala a esa persona en cada línea. Con -w, git ignora ese commit de formateo y te muestra el autor original.
-M y -C son más sofisticados: rastrean cuándo una línea vino de otro lado (otra función, otro fichero). En código refactorizado mucho, estos flags cambian totalmente el resultado.
Importante: git blame no es para culpar a nadie. El nombre es histórico (y muy americano). En la práctica es "¿de dónde viene esta línea?" para entender, no para señalar. En los repos que uso, el primer uso de blame suele ser "¿en qué commit se introdujo este comportamiento?" para leer ese commit y su contexto.
Para ver la historia completa de una línea, no solo el último cambio:
git log -L 42,42:src/handler.go
Te muestra cada vez que la línea 42 cambió, con el diff completo. Súper útil para líneas que se han tocado muchas veces.
git reflog: la máquina del tiempo
Esto es el seguro de vida de git. reflog es un registro de cada movimiento que ha hecho HEAD: cada cambio de rama, cada commit, cada reset, cada merge, todo. Está solo en tu repo local (no se pushea) y guarda unos 30-90 días de historial por defecto.
git reflog
Output típico:
abc1234 HEAD@{0}: commit: Añadir validación
def5678 HEAD@{1}: reset: moving to HEAD~1
9012ghi HEAD@{2}: commit: WIP
345jklm HEAD@{3}: checkout: moving from main to feature
Cada línea es "dónde estuvo HEAD en un momento". HEAD@{N} significa "dónde estaba HEAD hace N movimientos".
Cómo te salva el reflog:
Hiciste git reset --hard y borraste trabajo:
git reflog # busca el hash de antes del reset
git reset --hard abc1234 # (o HEAD@{1}) vuelves
Hiciste un rebase interactivo y te lo cargaste todo:
git reflog | head -20 # encuentra el commit previo al rebase
git reset --hard HEAD@{5} # vuelves al estado pre-rebase
Borraste una rama sin querer:
git reflog # encuentra el último commit de la rama
git branch recuperada abc1234 # creas una rama nueva en ese commit
Esta última es oro: incluso cuando git branch -D mi-rama parece borrar, el commit al que apuntaba sigue accesible por reflog durante un tiempo. Si lo pillas rápido, lo recuperas.
Una variante del reflog es por rama:
git reflog main
Te muestra la historia de movimientos de la rama main específicamente. Útil cuando un reset afectó a una rama concreta.
Limitaciones:
- Es local. Si clonas el repo en otra máquina, el reflog de origen no viene contigo.
- Tiene caducidad. Los commits inalcanzables (sin referencia) acaban siendo eliminados por
git gc. Si algo pasó hace tres meses y no te diste cuenta, quizá ya no esté. - No es un backup. Úsalo como red de seguridad para errores recientes, no como política de backup.
Un caso real que resuelven estos tres comandos
"Un test que ayer pasaba hoy falla. No sé qué he tocado."
# 1. ¿Qué ha cambiado entre ayer y hoy?
git log --oneline --since="1 day ago"
# 2. Si hay muchos commits, bisect:
git bisect start HEAD @{yesterday}
git bisect run ./test.sh
# → git identifica el commit culpable
# 3. Miro el commit con blame para entender:
git show abc1234
git blame -L 42,60 src/fichero-modificado.go
# 4. Si resuelvo con reset y me pasé, reflog me devuelve:
git reflog
git reset --hard HEAD@{3}
Cuatro comandos, diez minutos, problema diagnosticado. Sin bisect y blame, la misma investigación son dos horas de leer diffs.
Errores típicos en modo detective
Saltarse bisect por pereza. "Voy a leer los commits a ver si veo algo". Cuando hay más de cinco commits, bisect es siempre más rápido.
Culpar de verdad con blame. El objetivo es entender cómo llegó allí el código, no recriminar. Si el commit que introdujo algo fue hace cinco años, las circunstancias eran otras.
Asumir que el reflog es eterno. Si algo importante se ha perdido, recupéralo ya. Cada operación nueva en el repo hace correr el tiempo del reflog.
Lo que viene
Con bisect, blame y reflog puedes investigar prácticamente cualquier cosa que haya pasado en un repo. Para cerrar la serie avanzada, en la última entrega toca el equipamiento para proyectos grandes: git worktree para tener varias ramas a la vez sin reclonar, git submodule para gestionar dependencias como subrepos, y git sparse-checkout para trabajar con monorepos sin cargar todo el peso.