# Git avanzado II: detective — bisect, blame y reflog

*Segunda entrega de la serie **[Git avanzado](/search?tag=git-avanzado)**. La anterior fue sobre **[reescribir la historia con rebase, cherry-pick y amend](/search?tag=git-avanzado)**. 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.

```bash
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:

```bash
# 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:

```bash
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):

```bash
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 skip`** si 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.

```bash
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:

```bash
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:

```bash
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.

```bash
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:**

```bash
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:**

```bash
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:**

```bash
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:

```bash
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."

```bash
# 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](/search?tag=git-avanzado)** 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.
