Las cinco lecciones anteriores resolvían problemas con nombre: he perdido commits, mi rama ha divergido, el repositorio está corrupto. Esta se ocupa de la categoría que no tiene nombre y que, en la práctica, es la más frecuente:
Git está haciendo algo que no entiendo, y no sé ni por dónde empezar a mirar.
Un fichero que aparece como modificado y nadie lo ha tocado. Un .gitignore que ignora lo que no debe. Un push que tarda cuarenta segundos en un proyecto pequeño. Un hook que no se ejecuta. Una configuración que dice una cosa y un comportamiento que dice otra. Un git log que oculta commits que existen.
No son averías: son desajustes entre lo que crees que hay y lo que hay de verdad. Y para eso Git tiene una caja de herramientas que apenas hemos rozado: variables de traza que muestran lo que ocurre por debajo, comandos que dicen de dónde sale cada ajuste, y la capa de fontanería (plumbing) que consulta la base de datos sin interpretaciones.
Más importante que las herramientas es el método, porque sin él las herramientas producen ruido. Cerraremos con un método de diagnóstico en cuatro pasos y con una investigación real sobre gestor-tareas que combina bisect y blame para ir del síntoma al commit, y del commit a la línea y su porqué.
Contenido
- El método de diagnóstico, en cuatro pasos
- Variables de traza: ver lo que Git hace por debajo
- Depurar la configuración efectiva
- Depurar rutas y patrones:
check-ignore,check-attr,ls-files - Fontanería para inspeccionar el grafo
- Acotar con
git diff --statygit log - Una investigación real: de
bisectablame - Un catálogo de misterios y sus comandos
- El método de diagnóstico, en cuatro pasos
Antes de las herramientas, el procedimiento. Es lo que separa una investigación de veinte minutos de una tarde perdida.
flowchart TD
A["1. REPRODUCIR<br/>El comando mínimo que provoca el síntoma"]
B["2. AISLAR<br/>Quitar variables: repo limpio, sin config,<br/>sin hooks, otra máquina"]
C["3. CONSULTAR EL ESTADO REAL<br/>No el recordado. Fontanería y trazas"]
D["4. COMPROBAR LA HIPÓTESIS<br/>Una predicción concreta y falsable"]
A --> B --> C --> D
D -->|"No se cumple"| C
D -->|"Se cumple"| E["Arreglar la causa,<br/>no el síntoma"]
Paso 1: reproducir
Reduce el síntoma al comando mínimo que lo provoca.
Si no puedes reproducirlo a voluntad, no puedes verificar que lo has arreglado. Y muchas veces reducir el caso ya revela la causa: "solo falla con esta rama", "solo con este fichero", "solo desde el portátil de Carla".
Paso 2: aislar
Quita variables una a una:
# Sin configuración global ni de sistema
GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null git status
# Sin hooks (lección 06-01)
git -c core.hooksPath=/dev/null commit -m "prueba"
git commit --no-verify -m "prueba"
# Sin alias ni configuración del repositorio
git -c include.path= <comando>
# En un repositorio recién clonado, en /tmp
git clone <url> /tmp/prueba-limpia && cd /tmp/prueba-limpiaSi el problema desaparece sin la configuración global, ya sabes dónde mirar. Si persiste en un clon limpio, el problema está en el repositorio o en el servidor, no en tu máquina.
Paso 3: consultar el estado real, no el recordado
Este es el paso que más veces se salta y el que más veces resuelve.
| No preguntes... | Pregunta... |
|---|---|
| "Yo creo que ese fichero está ignorado" | git check-ignore -v <fichero> |
"Tengo user.email bien configurado" |
git config --list --show-origin --show-scope |
| "Ese fichero está en el índice" | git ls-files --stage <fichero> |
| "Esa rama apunta a ese commit" | git rev-parse <rama> |
| "El fichero es texto normal" | git ls-files --eol <fichero> |
| "El remoto está en ese commit" | git ls-remote origin <rama> |
| "Ese hook se está ejecutando" | GIT_TRACE=1 git commit ... |
La memoria y la documentación mienten; los comandos no.
Paso 4: comprobar la hipótesis
Formula una predicción concreta y falsable antes de tocar nada:
"Si la causa es que
.gitattributesmarca*.csscomotext eol=crlf, entoncesgit check-attr eol estilos.cssdebe decircrlf, ygit ls-files --eol estilos.cssdebe mostrarw/crlf."
Ejecuta, comprueba, y solo entonces arregla. Si la predicción falla, la hipótesis era mala: vuelve al paso 3. Cambiar cosas al azar hasta que "funciona" deja el problema latente y te impide saber qué lo causaba.
- Variables de traza: ver lo que Git hace por debajo
Git tiene un sistema de trazas que se activa con variables de entorno. Son la forma más directa de ver qué está pasando realmente.
| Variable | Qué muestra | Cuándo usarla |
|---|---|---|
GIT_TRACE=1 |
Cada subcomando que Git ejecuta, con sus argumentos | Alias raros, hooks, comandos que llaman a otros |
GIT_TRACE_SETUP=1 |
Cómo Git localiza el repositorio: .git, raíz, prefijo |
"No es un repositorio", worktrees, submódulos |
GIT_TRACE_PERFORMANCE=1 |
Tiempos por etapa | Comandos lentos (08-06) |
GIT_TRACE_PACKET=1 |
Cada paquete del protocolo con el servidor | fetch/push que fallan o van lentos |
GIT_TRACE_PACK_ACCESS=1 |
Accesos a packfiles | Rendimiento de la base de objetos |
GIT_CURL_VERBOSE=1 |
Diálogo HTTP completo | Problemas con remotos HTTPS |
GIT_SSH_COMMAND="ssh -v" |
Diálogo SSH completo | Permission denied (publickey) |
GIT_TRACE2_PERF=1 |
Trazas modernas, estructuradas | Análisis fino |
GIT_TRACE_SHALLOW=1 |
Lógica de clones superficiales | --depth (10-04) |
Todas aceptan una ruta absoluta en lugar de 1, para escribir a fichero:
GIT_TRACE: qué está ejecutando Git realmente
10:14:22.104 git.c:463 trace: built-in: git status 10:14:22.108 run-command.c:657 trace: run_command: 'gpg' '--status-fd=2' '-bsau' '[email protected]'
Ahí se ve, por ejemplo, si Git está llamando a gpg (firma de commits, lección 08-05), a un hook, o a un credential helper.
Es especialmente útil con alias que hacen cosas raras (lección 06-04):
10:15:03.221 git.c:750 trace: alias expansion: lg => 'log' '--graph' '--abbrev-commit' '--date=relative' 10:15:03.222 git.c:463 trace: built-in: git log --graph --abbrev-commit --date=relative
Y con hooks que no se ejecutan:
Si no aparece nada, el hook no se está lanzando. Causas habituales: no es ejecutable (chmod +x), core.hooksPath apunta a otro sitio, o el nombre del fichero tiene extensión.
GIT_TRACE_SETUP: dónde cree Git que está
10:16:41.003 trace.c:318 setup: git_dir: /home/ana/proyectos/gestor-tareas/.git 10:16:41.003 trace.c:319 setup: git_common_dir: /home/ana/proyectos/gestor-tareas/.git 10:16:41.003 trace.c:320 setup: worktree: /home/ana/proyectos/gestor-tareas 10:16:41.003 trace.c:321 setup: cwd: /home/ana/proyectos/gestor-tareas/src 10:16:41.003 trace.c:322 setup: prefix: src/
Resuelve una familia entera de misterios:
- "No es un repositorio Git" estando dentro de uno: te dice qué
.gitencuentra (o que no encuentra ninguno). - Trabajas en un repositorio que no es el que crees: típico con submódulos (lección 06-05) y worktrees (06-06), donde
git_dirygit_common_dirdifieren. - Los patrones de
.gitignoreno funcionan como esperas: elprefixexplica desde dónde se interpretan las rutas.
GIT_SSH_COMMAND y GIT_CURL_VERBOSE: problemas de red
Retomando el apartado 5.8 de la lección 09-01, aquí está la versión completa.
debug1: Reading configuration data /home/ana/.ssh/config debug1: /home/ana/.ssh/config line 4: Applying options for git.ejemplo.es debug1: Connecting to git.ejemplo.es [192.0.2.10] port 22. debug1: Offering public key: /home/ana/.ssh/id_rsa RSA SHA256:xxxx agent debug1: Authentications that can continue: publickey debug1: Offering public key: /home/ana/.ssh/id_ed25519 ED25519 SHA256:yyyy agent debug1: Server accepts key: /home/ana/.ssh/id_ed25519 ED25519 SHA256:yyyy agent debug1: Authentication succeeded (publickey).
Cada línea es diagnóstico:
| Línea | Qué te dice |
|---|---|
Reading configuration data ~/.ssh/config |
Qué fichero de configuración se aplica |
Applying options for <host> |
Qué bloque Host ha coincidido |
Connecting to <ip> port 22 |
A dónde va realmente (¿el host correcto?) |
Offering public key: <ruta> |
Qué claves ofrece y en qué orden |
Server accepts key |
Cuál aceptó |
Authentications that can continue: publickey |
Rechazó la anterior y sigue probando |
El caso clásico: tienes varias claves, SSH ofrece la equivocada primero y el servidor corta tras N intentos. La solución es un bloque en ~/.ssh/config:
IdentitiesOnly yes es la clave: fuerza a ofrecer solo esa.
Para HTTPS:
* Connected to git.ejemplo.es (192.0.2.10) port 443 > GET /equipo/gestor-tareas.git/info/refs?service=git-upload-pack HTTP/2 > User-Agent: git/2.45.0 < HTTP/2 401 < www-authenticate: Basic realm="Git" * Issue another request to this URL > Authorization: Basic YW5hOnh4eA== < HTTP/2 200
Diagnostica proxies corporativos, certificados, redirecciones y credenciales. Un 401 persistente apunta al credential helper (lección 04-03):
git config --get credential.helper
git credential-cache exit # vaciar credenciales cacheadas
printf 'protocol=https\nhost=git.ejemplo.es\n\n' | git credential fillCuidado:
GIT_CURL_VERBOSEpuede mostrar cabecerasAuthorization. No pegues su salida en un ticket público sin revisarla (lección 08-05).
GIT_TRACE_PACKET: el protocolo, paquete a paquete
El nivel más profundo. Muestra el diálogo exacto con el servidor.
packet: git< version 2 packet: git< agent=git/2.45.0 packet: git< ls-refs=unborn packet: git< fetch=shallow wait-for-done packet: git< object-format=sha1 packet: git> command=ls-refs packet: git> peel packet: git> ref-prefix refs/heads/ packet: git< b52c9d1e4a7f3c8b6d2e9a5f1c7b3d8e4a6f2c9b refs/heads/main packet: git< 7d3a8f4a2c6e9b1d5f3a8c6e2b9d4f7a1c5e8b3d refs/heads/GT-241
Para qué sirve de verdad:
- Ver qué referencias anuncia el servidor y qué versión del protocolo se negocia.
- Diagnosticar un
fetchlento: si el servidor anuncia 40.000 referencias, ahí está el problema (lección 08-06, higiene de referencias). - Entender un
pushrechazado por un hook de servidor (lección 07-06): el mensaje del hook viaja en estos paquetes. - Comprobar si se usa el protocolo v2, mucho más eficiente:
Trazas para todos los comandos, temporalmente
# En la sesión actual
export GIT_TRACE=1
export GIT_TRACE_SETUP=1
# ...reproducir el problema...
unset GIT_TRACE GIT_TRACE_SETUPY un guion para capturar todo de golpe cuando hay que enviar un informe:
#!/usr/bin/env bash
# capturar-traza.sh <comando git...>
LOG=/tmp/git-traza-$(date +%s).log
GIT_TRACE="$LOG" \
GIT_TRACE_SETUP="$LOG" \
GIT_TRACE_PERFORMANCE="$LOG" \
GIT_TRACE_PACKET="$LOG" \
"$@"
echo "Traza en: $LOG"
echo "REVISA el fichero antes de compartirlo: puede contener credenciales."
- Depurar la configuración efectiva
Retomando la lección 01-05. Git lee la configuración de varios sitios y el último gana. Cuando algo se comporta de forma inesperada, la configuración es sospechosa número uno.
system /etc/gitconfig core.autocrlf=false global /home/ana/.gitconfig user.name=Ana Ferrer global /home/ana/.gitconfig [email protected] global /home/ana/.gitconfig pull.rebase=true local .git/config remote.origin.url=git.ejemplo.es:equipo/gestor-tareas.git local .git/config [email protected] local .git/config core.autocrlf=input
Dos columnas nuevas frente al --list de siempre:
--show-scope: en qué nivel está (system,global,local,worktree,command).--show-origin: el fichero exacto, incluidos los que vienen porinclude.path.
En el ejemplo, user.email aparece dos veces: el local gana. Ahí está la explicación del "he configurado bien el correo y los commits salen con otro" (lección 09-01, apartado 5.3).
El valor efectivo de un ajuste
# Qué valor gana
git config --get user.email
# TODOS los valores definidos, en orden de precedencia (el último gana)
git config --get-all user.email
# Con su origen
git config --get-all --show-origin user.email
# Todo lo que empiece por un prefijo
git config --get-regexp '^remote\.'
git config --get-regexp '^alias\.'
git config --get-regexp '^advice\.'Ese último merece atención: si alguien copió una configuración que silencia los advice.*, estás perdiendo las sugerencias que resuelven la mitad de los problemas del módulo (lección 09-01, apartado 6).
Precedencia y anulaciones
| Nivel | Fichero | Prioridad |
|---|---|---|
system |
/etc/gitconfig |
Más baja |
global |
~/.gitconfig o ~/.config/git/config |
Media |
local |
.git/config |
Alta |
worktree |
.git/config.worktree |
Más alta (si extensions.worktreeConfig) |
-c en la línea de comandos |
— | Máxima |
Variables GIT_* |
Entorno | Depende del ajuste |
# Probar un valor sin cambiar nada, solo para este comando
git -c core.autocrlf=false status
git -c diff.noprefix=false diff
# Ver qué pasa SIN configuración de usuario
GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null git statusLa última es la prueba definitiva de "¿es cosa de mi configuración?".
Configuración condicional, la fuente de sorpresas
# ~/.gitconfig
[includeIf "gitdir:~/proyectos/trabajo/"]
path = ~/.gitconfig-trabajo
[includeIf "gitdir:~/proyectos/personal/"]
path = ~/.gitconfig-personalEs una funcionalidad excelente para separar identidades, pero produce el desconcierto de "aquí funciona y allí no". --show-origin la desenreda en un segundo, porque nombra el fichero incluido.
Y ojo con la barra final: gitdir:~/proyectos/trabajo/ (con /) coincide con el directorio y sus descendientes; sin ella, el comportamiento cambia.
- Depurar rutas y patrones:
check-ignore, check-attr, ls-files
check-ignore, check-attr, ls-filesAquí viven los misterios más frecuentes del día a día.
git check-ignore -v: por qué se ignora (o no) un fichero
Retomando la lección 08-03:
Lectura: fichero de reglas : línea : patrón que decide, y el fichero afectado. No hay ambigüedad posible.
# Varios ficheros a la vez
git check-ignore -v app.js datos/volcado.sql node_modules/x/y.js
# Todo lo que está siendo ignorado en el proyecto
git status --ignored --short
# Y el caso desconcertante: ¿por qué NO se ignora?
git check-ignore -v --no-index config-local.json
echo $?Si check-ignore no devuelve nada y el código de salida es 1, ninguna regla lo ignora. Y si el fichero aparece igualmente en git status pese a estar ignorado, la causa es casi siempre la misma:
Está en el índice. .gitignore solo afecta a ficheros sin seguimiento; uno que ya tiene seguimiento se sigue vigilando aunque el patrón coincida. La solución es la de la lección 08-03:
Otras causas menos evidentes, que check-ignore -v revela al nombrar el fichero de reglas:
| Fichero que puede estar decidiendo | Dónde vive |
|---|---|
.gitignore del repositorio |
En cualquier directorio del proyecto |
.git/info/exclude |
Local, no versionado |
core.excludesFile |
Global, típicamente ~/.gitignore_global |
Un .gitignore de un directorio padre |
Uno más específico gana |
Una regla de negación !patrón |
Reactiva algo ignorado antes |
Y la trampa clásica de los directorios:
Si un directorio está ignorado, Git ni siquiera entra en él, así que una regla de negación para un fichero de dentro no funciona:
git check-attr: qué atributos se aplican
Retomando la lección 08-04:
# Un atributo concreto
git check-attr eol text diff -- estilos.css index.html app.js
# De dónde sale cada regla
git check-attr -a --source=HEAD estilos.css
# Para todos los ficheros seguidos
git ls-files | git check-attr --stdin -a | grep -v unspecifiedEsto explica de golpe una familia de misterios:
| Síntoma | Comando | Causa habitual |
|---|---|---|
| El diff de un fichero sale como binario | git check-attr diff -- <f> |
-diff o binary en .gitattributes |
| Un fichero no se fusiona nunca | git check-attr merge -- <f> |
merge=binary o merge=ours |
| Los finales de línea cambian solos | git check-attr text eol -- <f> |
text, eol=crlf, o core.autocrlf |
| Un fichero se transforma al confirmar | git check-attr filter -- <f> |
Un filter (LFS, clean/smudge) |
git blame da resultados raros |
— | .git-blame-ignore-revs (06-03) |
git ls-files: qué hay en el índice de verdad
git status interpreta; git ls-files muestra. Es la ventana directa al índice.
| Campo | Significado |
|---|---|
100644 |
Fichero normal. 100755 = ejecutable. 120000 = enlace simbólico. 160000 = submódulo |
| Hash | El blob que hay en el índice |
0 |
La etapa. 0 = normal. 1/2/3 = conflicto (lección 03-05) |
Ese último campo es oro durante un conflicto:
Tres etapas: base común, la nuestra y la suya. Exactamente lo que vimos en la lección 03-05, ahora visible en crudo.
Los modos que resuelven misterios:
# ¿Por qué Git dice que este script ha cambiado si solo le di permisos?
git ls-files --stage build.shÍndice 100644, disco ejecutable: ese es el cambio. Se corrige con:
Y el modo --eol, que resuelve el misterio de los finales de línea:
| Columna | Significado |
|---|---|
i/ |
Cómo está en el índice (lo que se guarda en el repositorio) |
w/ |
Cómo está en el directorio de trabajo (en disco) |
attr/ |
Qué atributo se le aplica |
La primera línea es el caso sano de Carla en Windows: LF en el repositorio, CRLF en su disco (lección 08-04). La tercera es un problema: i/crlf significa que se han guardado CRLF dentro del repositorio, que es justo lo que no se quiere.
Otros modos útiles:
git ls-files --others # sin seguimiento
git ls-files --others --exclude-standard # sin seguimiento y no ignorados
git ls-files --ignored --exclude-standard # ignorados
git ls-files --deleted # borrados del disco pero aún en el índice
git ls-files --modified # modificados
git ls-files --unmerged # en conflicto--others --exclude-standard es exactamente la lista de "ficheros nuevos" que muestra git status, sin el resto de la salida. Y --deleted explica el "borré el fichero y Git sigue quejándose".
- Fontanería para inspeccionar el grafo
Cuando la pregunta es sobre la estructura del repositorio, la respuesta está en la capa de fontanería que vimos en la lección 01-04.
git rev-parse: traducir cualquier cosa a un hash
git rev-parse HEAD
git rev-parse main
git rev-parse HEAD~3
git rev-parse GT-241@{2}
git rev-parse v1.5.0^{commit} # la etiqueta anotada apunta a un tag; esto da el commitY sus modos informativos, que resuelven preguntas de entorno:
git rev-parse --show-toplevel # raíz del proyecto
git rev-parse --git-dir # dónde está el .git
git rev-parse --git-common-dir # el .git compartido (worktrees, 06-06)
git rev-parse --abbrev-ref HEAD # nombre de la rama actual
git rev-parse --is-inside-work-tree # true/false
git rev-parse --is-bare-repository # true/false
git rev-parse --symbolic-full-name @{u} # el upstream completo/home/ana/proyectos/gestor-tareas /home/ana/proyectos/gestor-tareas/.git GT-241 true refs/remotes/origin/GT-241
Son la base de cualquier guion que tenga que trabajar con repositorios de forma robusta.
git rev-list: contar y listar commits
# Cuántos commits hay
git rev-list --count HEAD
# Cuántos por delante y por detrás (lección 09-03)
git rev-list --left-right --count main...origin/main
# Commits que tocaron un fichero
git rev-list HEAD -- app.js | head
# Todos los objetos alcanzables (lección 08-06)
git rev-list --objects --all | wc -l
# Los commits raíz (¿hay más de uno? → historias no relacionadas)
git rev-list --max-parents=0 HEAD
# Solo fusiones, o solo lo que no son fusiones
git rev-list --merges HEAD | head
git rev-list --no-merges HEAD | headEse --max-parents=0 es el diagnóstico exacto de las unrelated histories de la lección 09-03: si devuelve dos hashes, hay dos raíces.
git cat-file --batch-check: inspección masiva
# Comprobar si existe (sin volcar contenido)
git cat-file -e 4f8a2e6 && echo "existe" || echo "no existe"
# Un informe de todos los objetos por tipo
git rev-list --objects --all \
| git cat-file --batch-check='%(objecttype)' \
| sort | uniq -cEs la misma técnica del análisis de tamaños de la lección 08-06, aplicada aquí a entender la composición del repositorio.
git verify-pack: qué hay dentro de un packfile
b52c9d1e4a7f3c8b6d2e9a5f1c7b3d8e4a6f2c9b commit 241 168 12 7d3a8f4a2c6e9b1d5f3a8c6e2b9d4f7a1c5e8b3d tree 118 94 180 4f8a2e6c9b1d5e3a7f2c8b6d9e4a1f5c3b7d2e9a blob 8241 2104 274 6f2b9d4a8c1e5f3b7d9a2c6e4f8b1d3a5c7e9f2b blob 412 87 2378 1 4f8a2e6c9b1d5e3a
Columnas: hash, tipo, tamaño real, tamaño comprimido, posición en el pack y —en la última línea— profundidad de delta y objeto base. Es la vista de bajo nivel de lo que explicaba la lección 08-06: la última entrada se guarda como diferencia de otra.
git for-each-ref: todas las referencias con sus datos
git for-each-ref --sort=-committerdate --format='%(refname:short) %(objectname:short) %(committerdate:relative) %(authorname)' refs/headsGT-241 9c4e7b2 hace 2 horas Ana Ferrer GT-238 7d3a8f4 hace 3 días Bruno Salas main b52c9d1 hace 4 días Carla Vidal
# Ramas sin upstream: existen solo en esta máquina (lección 09-05)
git for-each-ref --format='%(refname:short) %(upstream)' refs/heads | awk '$2==""{print $1}'
# Referencias que apuntan a objetos inexistentes (lección 09-05)
git for-each-ref --format='%(refname) %(objectname)' | while read -r r s; do
git cat-file -e "$s" 2>/dev/null || echo "ROTA: $r"
donegit ls-remote: qué hay en el servidor, sin descargar nada
Consulta el servidor directamente, sin fetch y sin tocar tu repositorio. Resuelve preguntas como "¿existe esa rama en el servidor?" o "¿mi origin/main está al día?":
[ "$(git ls-remote origin main | cut -f1)" = "$(git rev-parse origin/main)" ] \
&& echo "al día" || echo "mi copia de origin/main está anticuada"
- Acotar con
git diff --stat y git log
git diff --stat y git logAntes de una investigación fina, conviene acotar. Estas son las herramientas de grano grueso.
app.js | 84 ++++++++++++++++----- estilos.css | 12 ++-- index.html | 6 +- README.md | 31 ++++++++ 4 files changed, 108 insertions(+), 25 deletions(-)
# Solo los nombres, para ver el alcance de un vistazo
git diff --name-only v1.4.0 v1.5.0
# Con el tipo de cambio: Añadido, Modificado, Borrado, Renombrado
git diff --name-status v1.4.0 v1.5.0
# Resumen de renombrados y cambios de modo
git diff --summary v1.4.0 v1.5.0
# Comparar solo el efecto de una rama (lección 07-02)
git diff --stat main...GT-241Y git log en modo investigación (lección 06-04):
# Commits que tocaron una función concreta
git log -L :calcularPendientes:app.js
# Commits que añadieron o quitaron una cadena (pickaxe, lección 02-06)
git log -S "localStorage" --oneline
# Commits cuyo diff coincide con una expresión regular
git log -G "gestor\.tareas\.v[0-9]" --oneline
# Quién y cuándo, en un rango de fechas
git log --since="2026-07-01" --until="2026-07-31" --format='%h %an %ad %s' --date=short
# Solo lo que entró por la primera línea de padres (lección 08-02)
git log --first-parent --oneline main-L :función:fichero es especialmente potente y poco conocida: sigue una función concreta a lo largo del historial, incluso cuando se mueve dentro del fichero.
- Una investigación real: de
bisect a blame
bisect a blameVamos a juntarlo todo en un caso completo sobre gestor-tareas.
El síntoma. Carla informa: "El contador de tareas pendientes muestra un número de más cuando hay tareas ocultas por el filtro. En la versión 1.4 funcionaba."
Paso 1: reproducir
Lo mínimo que provoca el fallo, y escrito como una prueba automatizable:
cat > /tmp/prueba-contador.sh <<'EOF'
#!/usr/bin/env bash
# Sale 0 si el contador es correcto, 1 si no.
node -e '
const fs = require("fs");
const src = fs.readFileSync("app.js", "utf8");
const ctx = { localStorage: { getItem: () => null, setItem: () => {} }, document: null };
// ... arranque mínimo de la aplicación ...
const tareas = [
{ texto: "a", completada: false, oculta: false },
{ texto: "b", completada: true, oculta: false },
{ texto: "c", completada: false, oculta: true }
];
const esperado = 2;
const obtenido = calcularPendientes(tareas);
process.exit(obtenido === esperado ? 0 : 1);
' 2>/dev/null
EOF
chmod +x /tmp/prueba-contador.shUna prueba automatizable es lo que convierte bisect de tedioso en instantáneo.
Paso 2: acotar el rango
34 commits entre las dos versiones. Revisarlos a mano son horas; bisect son cinco pasos.
Paso 3: git bisect run
Retomando la lección 06-02:
git bisect start
git bisect bad v1.5.0
git bisect good v1.4.0
git bisect run /tmp/prueba-contador.shBisecting: 16 revisions left to test after this (roughly 4 steps) [7d3a8f4a2c6e9b1d5f3a8c6e2b9d4f7a1c5e8b3d] GT-238 extraer la creación del elemento running /tmp/prueba-contador.sh Bisecting: 8 revisions left to test after this (roughly 3 steps) ... 4f8a2e6c9b1d5e3a7f2c8b6d9e4a1f5c3b7d2e9a is the first bad commit commit 4f8a2e6c9b1d5e3a7f2c8b6d9e4a1f5c3b7d2e9a Author: Bruno Salas <[email protected]> Date: Wed Jul 15 11:23:04 2026 +0200 GT-231 añadir el filtro de tareas ocultas app.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-)
Del síntoma al commit en cinco pasos automáticos. Ahora sabemos cuándo se rompió.
Paso 4: entender el commit
function calcularPendientes(tareas) {
- return tareas.filter(t => !t.completada).length;
+ return tareas.filter(t => !t.completada || t.oculta).length;
}Ahí está: el || t.oculta cuenta también las ocultas. Pero saber qué línea falla no es saber por qué está ahí, y arreglarla sin entenderlo puede romper lo que ese commit venía a resolver.
Paso 5: git blame para el porqué
Retomando la lección 06-03:
4f8a2e6c (Bruno Salas 2026-07-15 11:23:04 +0200 12) function calcularPendientes(tareas) {
4f8a2e6c (Bruno Salas 2026-07-15 11:23:04 +0200 13) return tareas.filter(t => !t.completada || t.oculta).length;
4f8a2e6c (Bruno Salas 2026-07-15 11:23:04 +0200 14) }
b52c9d1e (Ana Ferrer 2026-06-28 09:14:22 +0200 15)
b52c9d1e (Ana Ferrer 2026-06-28 09:14:22 +0200 16) function renderizarTareas() {# La historia completa de esa función, aunque se haya movido
git log -L :calcularPendientes:app.js --format='%h %an %ad %s' --date=shortcommit 4f8a2e6 Bruno Salas 2026-07-15 GT-231 añadir el filtro de tareas ocultas commit 8a1f6c3 Ana Ferrer 2026-05-02 GT-198 extraer el cálculo a su propia función commit 2f8c6e1 Ana Ferrer 2026-04-11 GT-142 mostrar el contador de pendientes
GT-231 añadir el filtro de tareas ocultas Las tareas archivadas dejan de aparecer en el listado. Se añade la marca `oculta` y se filtra en renderizarTareas(). El contador debe seguir incluyéndolas porque la especificación de GT-231 dice que "pendientes" cuenta todas las tareas no completadas, estén visibles o no.
Aquí está el hallazgo, y es lo que cambia todo el desenlace. El comportamiento no es un error: es una decisión deliberada y documentada de GT-231. Lo que hay es una contradicción entre dos especificaciones: GT-231 dice que cuenten todas las no completadas; Carla espera que cuente solo las visibles.
Si hubiéramos "arreglado" la línea nada más verla, habríamos roto GT-231 y el ciclo habría vuelto a empezar dentro de dos semanas.
# ¿Quién más depende de esto? (pickaxe, lección 02-06)
git log -S "calcularPendientes" --oneline
git grep -n "calcularPendientes" -- '*.js'La conclusión de la investigación no es un parche, sino una pregunta para el equipo: ¿qué significa "pendientes"? Y la respuesta, sea cual sea, se documenta en el commit que la implemente.
El recorrido, en una tabla
| Paso | Pregunta | Herramienta | Lección |
|---|---|---|---|
| 1 | ¿Cuál es el síntoma exacto? | Un guion de prueba reproducible | — |
| 2 | ¿En qué rango buscar? | git diff --stat, git rev-list --count |
02-05 |
| 3 | ¿Qué commit lo introdujo? | git bisect run |
06-02 |
| 4 | ¿Qué cambió ese commit? | git show |
02-06 |
| 5 | ¿Por qué está esa línea? | git blame, git log -L, %B |
06-03, 08-01 |
| 6 | ¿Quién depende de ello? | git log -S, git grep |
02-06 |
bisect te lleva del síntoma al commit. blame y el mensaje del commit te llevan del commit a la intención. El primero sin el segundo produce parches que rompen otra cosa. Y aquí se ve, retrospectivamente, por qué la lección 08-01 insistía tanto en explicar el porqué en los mensajes: ese párrafo de Bruno ha ahorrado un error.
- Un catálogo de misterios y sus comandos
La tabla de consulta rápida del "Git hace algo raro".
| Misterio | Comando de diagnóstico | Causa habitual |
|---|---|---|
| Un fichero sale modificado y no lo he tocado | git ls-files --eol <f>, git check-attr -a <f> |
Finales de línea, o filter (08-04) |
Un fichero ignorado aparece en status |
git ls-files --error-unmatch <f> |
Ya tenía seguimiento (08-03) |
Un fichero no se ignora aunque está en .gitignore |
git check-ignore -v <f> |
Regla en otro fichero, o negación mal puesta |
| Un fichero se ignora y no debería | git check-ignore -v <f> |
Un .gitignore de un directorio padre, o el global |
| Un script pierde el permiso de ejecución | git ls-files --stage <f> |
Modo 100644 en el índice; update-index --chmod=+x |
| Un hook no se ejecuta | GIT_TRACE=1 git commit, ls -l .git/hooks/ |
No es ejecutable, o core.hooksPath (06-01) |
| El autor de los commits no es el que espero | git config --list --show-origin | grep user |
Un user.email local pisando el global |
| "No es un repositorio Git" estando dentro | GIT_TRACE_SETUP=1 git status |
Worktree, submódulo, o .git perdido |
push/fetch fallan por autenticación |
GIT_SSH_COMMAND="ssh -v" git fetch |
Clave equivocada ofrecida primero (04-03) |
push/fetch van muy lentos |
GIT_TRACE_PACKET=1, git ls-remote | wc -l |
Miles de referencias (08-06) |
git status tarda segundos |
GIT_TRACE_PERFORMANCE=1 git status |
Recorrido de ficheros sin seguimiento (08-06) |
git log no muestra un commit que existe |
git log --all, git reflog, git cat-file -t |
Está en otra rama, o es inalcanzable (09-04) |
| Un merge dice "Already up to date" sin serlo | git merge-base <a> <b>, git log --graph --all |
Fusión revertida (05-06) |
| Dos ramas no se pueden fusionar | git rev-list --max-parents=0 HEAD |
Historias no relacionadas (09-03) |
| Un submódulo aparece siempre como modificado | git diff --submodule, git ls-files --stage |
Commit del submódulo distinto (06-05) |
| El diff sale como binario | git check-attr diff -- <f> |
-diff o binary en .gitattributes |
| Un alias hace algo inesperado | GIT_TRACE=1 git <alias> |
Expansión del alias (06-04) |
| Git se comporta distinto en dos carpetas | git config --list --show-origin --show-scope |
includeIf condicional |
| El remoto está en un commit que no esperaba | git ls-remote origin <rama> |
Alguien publicó, o forzó (09-03) |
Y un guion de diagnóstico general, para cuando no sabes ni por dónde empezar:
#!/usr/bin/env bash
# diagnostico-git.sh — foto completa del estado real del repositorio
set -u
echo "=== ENTORNO ==="
git --version
git rev-parse --show-toplevel
git rev-parse --git-dir
echo "rama: $(git rev-parse --abbrev-ref HEAD)"
echo "upstream: $(git rev-parse --symbolic-full-name '@{u}' 2>/dev/null || echo 'ninguno')"
echo; echo "=== ESTADO ==="
git status -sb | head -20
echo; echo "=== DIVERGENCIA ==="
git rev-list --left-right --count HEAD...@{u} 2>/dev/null || echo "sin upstream"
echo; echo "=== CONFIGURACIÓN CLAVE ==="
git config --list --show-scope 2>/dev/null \
| grep -E '(user\.|core\.(autocrlf|eol|hooksPath|excludesFile|fsmonitor)|pull\.|push\.|merge\.|diff\.)'
echo; echo "=== HOOKS ACTIVOS ==="
RUTA=$(git config --get core.hooksPath || echo "$(git rev-parse --git-dir)/hooks")
find "$RUTA" -maxdepth 1 -type f -perm -u+x ! -name '*.sample' 2>/dev/null
echo; echo "=== REFERENCIAS ==="
echo "ramas locales: $(git branch | wc -l) | remotas: $(git branch -r | wc -l) | etiquetas: $(git tag | wc -l)"
echo "sin publicar: $(git for-each-ref --format='%(refname:short) %(upstream)' refs/heads | awk '$2==""{print $1}' | tr '\n' ' ')"
echo; echo "=== INTEGRIDAD ==="
git fsck --connectivity-only --no-progress 2>&1 | grep -vE '^(dangling|notice|Checking)' || echo "sin errores"
echo; echo "=== ÚLTIMOS MOVIMIENTOS ==="
git reflog --date=relative -8Guárdalo. El día que te haga falta, te ahorrará quince minutos de comandos sueltos.
Errores Comunes y Consejos
Error 1: cambiar cosas al azar hasta que funcione. Deja el problema latente y no aprendes nada. Formula una hipótesis falsable y compruébala.
Error 2: fiarse de la memoria en lugar de consultar el estado real. "Yo creo que ese fichero está ignorado" es distinto de git check-ignore -v.
Error 3: no aislar. Antes de investigar a fondo, comprueba si el problema persiste sin configuración global, sin hooks y en un clon limpio. Ese descarte cuesta un minuto.
Error 4: activar GIT_TRACE_PACKET de entrada. Es el nivel más profundo y produce mucho ruido. Empieza por GIT_TRACE y GIT_TRACE_SETUP.
Error 5: pegar la salida de GIT_CURL_VERBOSE en un ticket público. Puede contener cabeceras Authorization (08-05). Revísala antes.
Error 6: usar git config --list sin --show-origin --show-scope. Sin esas opciones no sabes qué valor gana ni de dónde sale, que es justo lo que investigas.
Error 7: arreglar la línea que bisect señala sin leer el mensaje del commit. Puedes romper la funcionalidad que ese commit venía a implementar, como habría pasado en el apartado 7.
Error 8: intentar bisect sin una prueba automatizable. Con git bisect run y un guion, treinta commits son cinco pasos automáticos; a mano, media hora de aburrimiento y errores.
Error 9: confundir git status con la realidad. status interpreta; ls-files --stage, check-attr y rev-parse muestran.
Consejo 1: el método antes que las herramientas. Reproducir, aislar, consultar el estado real, comprobar la hipótesis.
Consejo 2: git config --list --show-origin --show-scope como reflejo. Resuelve una fracción sorprendente de los "Git hace algo raro".
Consejo 3: git check-ignore -v y git check-attr -a. Dos comandos que dan la respuesta exacta a dos de las preguntas más frecuentes del día a día.
Consejo 4: git ls-files --eol en cualquier problema de finales de línea. La tabla i/ y w/ lo dice todo de un vistazo.
Consejo 5: guarda el guion de diagnóstico del apartado 8. Es la foto completa en treinta segundos.
Consejo 6: escribe la prueba antes de bisecar. El esfuerzo se recupera en el primer bisect run, y la prueba se queda en el proyecto.
Ejercicios
Ejercicio 1: el misterio del fichero siempre modificado
- Crea un repositorio con
app.js,estilos.csseindex.html, y confírmalos. - Añade un
.gitattributescon*.css text eol=crlfy confírmalo. - Ejecuta
git statusy observa el resultado. Después ejecutagit ls-files --eolygit check-attr -a estilos.css. - Explica exactamente qué está pasando usando las columnas
i/yw/. - Resuélvelo con
git add --renormalize .(lección 08-04) y comprueba conls-files --eolque las columnas cuadran. - Repite el ejercicio con un fichero al que
.gitattributesmarque-diffy comprueba qué cambia engit diff.
Ejercicio 2: configuración, ignorados y trazas
- Crea un repositorio y configura
user.emaildistinto en el nivel local y en el global. - Usa
git config --list --show-origin --show-scopepara determinar cuál gana, y confírmalo haciendo un commit y mirando%ae. - Crea un
.gitignorecon*.log, un.git/info/excludecon!importante.logy uncore.excludesFileglobal contemporal*. Crea los tres ficheros correspondientes y usagit check-ignore -vcon cada uno para determinar qué regla decide. - Crea un hook
pre-commitque imprima algo, hazlo no ejecutable, e intenta confirmar. UsaGIT_TRACE=1para demostrar que no se lanza. - Hazlo ejecutable y repite, comprobando en la traza que ahora sí aparece.
- Ejecuta
GIT_TRACE_SETUP=1 git statusdesde un subdirectorio y explica cada línea de la salida.
Ejercicio 3: la investigación completa
- Crea un repositorio con un
app.jsque contenga una funcióncalcularPendientescorrecta, y haz veinte commits que toquen otras partes del proyecto. - En un commit intermedio, introduce el fallo del apartado 7 (añadir
|| t.oculta) con un mensaje de commit que explique el porqué. - Haz diez commits más encima.
- Escribe un guion de prueba que salga con
0si la función es correcta y con1si no. - Encuentra el commit culpable con
git bisect runy anota cuántos pasos ha necesitado. - Usa
git show,git blame -Lygit log -L :calcularPendientes:app.jspara reconstruir la historia completa de esa función. - Lee el mensaje completo del commit culpable con
git log -1 --format=%By explica por qué la respuesta correcta no es "cambiar la línea".
Soluciones
Solución 1:
rm -rf /tmp/p9-06 && mkdir /tmp/p9-06 && cd /tmp/p9-06 && git init -q -b main
git config user.name "Carla Vidal"; git config user.email "[email protected]"
printf 'console.log(1);\n' > app.js
printf 'body { margin: 0; }\n' > estilos.css
printf '<h1>Gestor</h1>\n' > index.html
git add . && git commit -q -m "chore: estructura inicial"
# 2. El .gitattributes
printf '*.css text eol=crlf\n' > .gitattributes
git add .gitattributes && git commit -q -m "chore: forzar CRLF en los CSS"Sorprendentemente, nada. El atributo se aplica al escribir en disco, y el fichero en disco aún tiene LF. Fuerza la actualización:
Ahí está el misterio típico: un fichero modificado que nadie ha tocado.
i/lf w/crlf attr/text eol=crlf estilos.css i/lf w/lf attr/ app.js i/lf w/lf attr/ index.html i/lf w/lf attr/ .gitattributes
Lectura de las columnas:
i/lf: en el índice hay LF, que es lo que se guardó en el commit original.w/crlf: en el disco hay CRLF, porque el atributoeol=crlflo convierte al extraerlo.- Git compara índice y disco byte a byte, ve una diferencia en cada final de línea, y lo marca como modificado.
No es un error: es el atributo funcionando, sobre un fichero que se guardó antes de que el atributo existiera.
# 5. La solución
git add --renormalize .
git status --short
git commit -q -m "chore: renormalizar los finales de línea"
git ls-files --eol estilos.cssLimpio. --renormalize reescribe el índice aplicando los atributos actuales: ahora el índice guarda LF con conocimiento del atributo, y la comparación cuadra. Es exactamente el procedimiento de la lección 08-04.
# 6. Con -diff
printf '*.css text eol=crlf\nindex.html -diff\n' > .gitattributes
git add .gitattributes && git commit -q -m "chore: index.html sin diff"
printf '<h1>Gestor de tareas</h1>\n' > index.html
git diff index.html
git check-attr diff -- index.htmldiff --git a/index.html b/index.html index 8a1f6c3..4f8a2e6 100644 Binary files a/index.html and b/index.html differ
-diff hace que Git trate el fichero como binario a efectos de diff. Es útil para minificados y generados, y desconcertante si no sabes que está puesto: git check-attr diff lo resuelve en un segundo.
Solución 2:
rm -rf /tmp/p9-06b && mkdir /tmp/p9-06b && cd /tmp/p9-06b && git init -q -b main
git config --global user.email "[email protected]" 2>/dev/null
git config user.name "Ana Ferrer"
git config user.email "[email protected]" # local, distinto
# 2. Quién gana
git config --list --show-origin --show-scope | grep user.emailglobal /home/ana/.gitconfig [email protected] local .git/config [email protected]
git config --get user.email
echo "x" > f.txt && git add . && git commit -q -m "prueba"
git log -1 --format='%ae'El local gana, porque está más cerca del repositorio. Es exactamente el mecanismo del apartado 3, y la causa habitual del "he configurado bien el correo".
# 3. Las tres capas de ignorados
printf '*.log\n' > .gitignore
printf '!importante.log\n' > .git/info/exclude
printf 'temporal*\n' > /tmp/gitignore-global
git config core.excludesFile /tmp/gitignore-global
touch salida.log importante.log temporal-1.txt normal.txt
for f in salida.log importante.log temporal-1.txt normal.txt; do
printf '%-18s ' "$f"
git check-ignore -v "$f" || echo "(no ignorado)"
donesalida.log .gitignore:1:*.log salida.log importante.log .git/info/exclude:1:!importante.log importante.log temporal-1.txt /tmp/gitignore-global:1:temporal* temporal-1.txt normal.txt (no ignorado)
Cada uno decidido por un fichero distinto, y check-ignore -v lo nombra con su número de línea. Fíjate en importante.log: la regla que coincide es la negación, y por eso el fichero no está ignorado:
Cuando check-ignore -v devuelve un patrón que empieza por !, significa "lo ha rescatado esta regla".
# 4. El hook que no se ejecuta
cat > .git/hooks/pre-commit <<'EOF'
#!/usr/bin/env bash
echo "HOOK EJECUTADO"
EOF
# sin chmod +x
echo "y" >> f.txt
GIT_TRACE=1 git commit -q -am "prueba hook" 2>&1 | grep -ci "pre-commit"
git log -1 --format=%sCero menciones al hook en la traza, y el commit se hizo. El hook no se lanzó.
# 5. Con permiso de ejecución
chmod +x .git/hooks/pre-commit
echo "z" >> f.txt
GIT_TRACE=1 git commit -am "prueba hook 2" 2>&1 | grep -i "pre-commit"Ahí está: run_command lo lanza y el echo aparece. GIT_TRACE=1 es el diagnóstico definitivo para "mi hook no se ejecuta" (lección 06-01).
# 6. GIT_TRACE_SETUP desde un subdirectorio
mkdir -p src/componentes && cd src/componentes
GIT_TRACE_SETUP=1 git status 2>&1 | head -613:04:22.101 trace.c:318 setup: git_dir: /tmp/p9-06b/.git 13:04:22.101 trace.c:319 setup: git_common_dir: /tmp/p9-06b/.git 13:04:22.101 trace.c:320 setup: worktree: /tmp/p9-06b 13:04:22.101 trace.c:321 setup: cwd: /tmp/p9-06b/src/componentes 13:04:22.101 trace.c:322 setup: prefix: src/componentes/
| Línea | Qué dice |
|---|---|
git_dir |
Dónde está el .git que se está usando |
git_common_dir |
El .git compartido: distinto en un worktree (06-06) |
worktree |
La raíz del proyecto |
cwd |
Desde dónde has lanzado el comando |
prefix |
La ruta relativa que Git antepone a los argumentos |
Ese prefix explica por qué git add . desde un subdirectorio solo añade ese subdirectorio, y por qué los patrones se interpretan como se interpretan.
Solución 3:
rm -rf /tmp/p9-06c && mkdir /tmp/p9-06c && cd /tmp/p9-06c && git init -q -b main
git config user.name "Ana Ferrer"; git config user.email "[email protected]"
cat > app.js <<'EOF'
function calcularPendientes(tareas) {
return tareas.filter(t => !t.completada).length;
}
module.exports = { calcularPendientes };
EOF
git add . && git commit -q -m "GT-142 mostrar el contador de pendientes"
# 1. Veinte commits de relleno
for i in $(seq 1 20); do echo "// cambio $i" >> otros.js; git add .; git commit -q -m "chore: cambio $i"; done# 2. El commit culpable, con un mensaje que explica el porqué
sed -i 's/!t.completada)/!t.completada || t.oculta)/' app.js
git commit -q -am "GT-231 añadir el filtro de tareas ocultas
Las tareas archivadas dejan de aparecer en el listado. Se añade la
marca \`oculta\` y se filtra en renderizarTareas().
El contador debe seguir incluyéndolas porque la especificación de
GT-231 dice que \"pendientes\" cuenta todas las tareas no completadas,
estén visibles o no."
CULPABLE=$(git rev-parse --short HEAD)
# 3. Diez commits más
for i in $(seq 21 30); do echo "// cambio $i" >> otros.js; git add .; git commit -q -m "chore: cambio $i"; done
git rev-list --count HEAD# 4. La prueba
cat > /tmp/prueba-contador.sh <<'EOF'
#!/usr/bin/env bash
node -e '
const { calcularPendientes } = require(process.cwd() + "/app.js");
const tareas = [
{ completada: false, oculta: false },
{ completada: true, oculta: false },
{ completada: false, oculta: true }
];
process.exit(calcularPendientes(tareas) === 2 ? 0 : 1);
' 2>/dev/null
EOF
chmod +x /tmp/prueba-contador.sh
/tmp/prueba-contador.sh; echo "estado actual: $?"La prueba falla en HEAD: reproducido.
# 5. Bisect
git bisect start
git bisect bad HEAD
git bisect good HEAD~31
git bisect run /tmp/prueba-contador.sh 2>&1 | tail -12Bisecting: 15 revisions left to test after this (roughly 4 steps)
running '/tmp/prueba-contador.sh'
Bisecting: 7 revisions left to test after this (roughly 3 steps)
running '/tmp/prueba-contador.sh'
Bisecting: 3 revisions left to test after this (roughly 2 steps)
running '/tmp/prueba-contador.sh'
Bisecting: 1 revision left to test after this (roughly 1 step)
running '/tmp/prueba-contador.sh'
4f8a2e6c9b1d5e3a7f2c8b6d9e4a1f5c3b7d2e9a is the first bad commit
GT-231 añadir el filtro de tareas ocultas
bisect found first bad commitCinco pasos automáticos para 31 commits. El logaritmo en base 2 de 31 es algo menos de 5: exactamente lo que predecía la lección 06-02.
function calcularPendientes(tareas) {
- return tareas.filter(t => !t.completada).length;
+ return tareas.filter(t => !t.completada || t.oculta).length;
}4f8a2e6c (Ana Ferrer 2026-07-31 13:12:04 +0200 1) function calcularPendientes(tareas) {
4f8a2e6c (Ana Ferrer 2026-07-31 13:12:04 +0200 2) return tareas.filter(t => !t.completada || t.oculta).length;
8a1f6c3d (Ana Ferrer 2026-07-31 13:11:58 +0200 3) }git log -L :calcularPendientes:app.js --format='%h %ad %s' --date=short | grep -E '^commit|^[0-9a-f]{7} '4f8a2e6 2026-07-31 GT-231 añadir el filtro de tareas ocultas 8a1f6c3 2026-07-31 GT-142 mostrar el contador de pendientes
Dos commits en toda la vida de esa función. -L :función:fichero la sigue aunque se mueva de sitio dentro del fichero, cosa que blame -L 1,3 no haría.
GT-231 añadir el filtro de tareas ocultas Las tareas archivadas dejan de aparecer en el listado. Se añade la marca `oculta` y se filtra en renderizarTareas(). El contador debe seguir incluyéndolas porque la especificación de GT-231 dice que "pendientes" cuenta todas las tareas no completadas, estén visibles o no.
Por qué la respuesta correcta no es "cambiar la línea":
El comportamiento es intencionado y está justificado por escrito. No hay un error de programación: hay dos especificaciones que se contradicen. GT-231 dice que el contador incluya las ocultas; lo que Carla espera es lo contrario.
Si se quita el || t.oculta sin más:
- Se rompe
GT-231, que alguien pidió y alguien validó. - Nadie sabrá por qué, porque el commit que lo deshaga probablemente diga "corregir el contador".
- En dos semanas, quien pidió
GT-231abrirá un ticket idéntico en sentido contrario, y el ciclo empezará otra vez.
La salida correcta es llevar la contradicción a quien pueda resolverla, y que la decisión —sea cual sea— quede escrita en el mensaje del commit que la implemente (lección 08-01), con una referencia a los dos tickets.
El mensaje de Bruno ha ahorrado un error. Ese párrafo de tres líneas es la diferencia entre una corrección y un bucle.
Conclusión
Esta lección era sobre entender por qué Git está haciendo lo que hace.
- El método va antes que las herramientas: reproducir con el comando mínimo, aislar quitando variables (configuración global, hooks, un clon limpio), consultar el estado real y no el recordado, y comprobar una hipótesis concreta antes de cambiar nada.
- Las variables de traza enseñan lo que ocurre por debajo:
GIT_TRACEpara los subcomandos, alias y hooks;GIT_TRACE_SETUPpara saber qué repositorio cree Git que está usando;GIT_SSH_COMMAND="ssh -v"yGIT_CURL_VERBOSEpara autenticación y red;GIT_TRACE_PACKETpara el protocolo. Empieza por las suaves y revisa la salida antes de compartirla. git config --list --show-origin --show-scoperesuelve una fracción sorprendente de los misterios, porque dice qué valor gana y de qué fichero sale, incluidos losincludeIfcondicionales.git check-ignore -vygit check-attr -aresponden con precisión a las dos preguntas más frecuentes del día a día: por qué se ignora (o no) un fichero, y qué atributos se le aplican.git ls-fileses la ventana al índice:--stagepara modos y etapas de conflicto,--eolpara los finales de línea con sus columnasi/yw/,--others --exclude-standardpara lo realmente nuevo.- La fontanería contesta sobre el grafo sin interpretaciones:
rev-parsepara traducir a hashes y conocer el entorno,rev-listpara contar y listar,cat-file --batch-checkpara inspección masiva,verify-packpara el interior de un packfile,for-each-refpara todas las referencias yls-remotepara el servidor sin descargar nada. - Y la investigación completa:
bisectlleva del síntoma al commit;blamey el mensaje del commit llevan del commit a la intención. Saltarse el segundo paso produce parches que rompen otra cosa.
El módulo, en una idea
El módulo 8 terminó anunciando desastres. Este los ha resuelto todos, y la lección de fondo es una sola:
En Git, casi nada se pierde de verdad. Lo que se pierde es la calma.
Los commits son objetos inmutables que sobreviven aunque ninguna referencia los apunte (09-01). reset mueve una rama, no destruye historia (09-02). Una divergencia es una decisión pendiente, no una avería (09-03). El reflog recuerda por dónde has pasado, y con él vuelve el reset --hard, la rama borrada, el rebase fallido y el stash eliminado (09-04). Un repositorio dañado es un problema de logística, porque el clon de cualquier compañero es una copia casi completa (09-05). Y cuando nada de eso encaja, hay trazas, fontanería y un método (09-06).
Lo verdaderamente frágil es lo que nunca llegó a ser un objeto: el cambio sin confirmar, el fichero sin seguimiento. Por eso el consejo más rentable de todo el módulo no es un comando, sino un hábito: confirma pronto, marca con git branch antes de lo arriesgado, y publica tus ramas.
Lo que viene
Ana, Bruno, Carla y Diego dominan Git en gestor-tareas. Saben construir el historial, manipularlo con criterio, colaborar con un proceso, mantener buenos hábitos y salir de los líos.
Pero gestor-tareas son cuatro ficheros y un equipo de cuatro personas. El mundo real es más grande y más raro.
Hay proyectos con cincuenta mil ficheros y veinte años de historia, donde un git status sin optimizar tarda medio minuto. Hay repositorios que guardan vídeos, modelos 3D y ficheros de diseño de cientos de megabytes, y que necesitan un sistema distinto para almacenarlos. Hay organizaciones donde Git no lo usa una persona en una terminal, sino cien tuberías de despliegue que clonan, etiquetan y publican sin intervención humana. Hay integraciones con editores, con sistemas de tickets, con plataformas de revisión y con herramientas de análisis que cambian por completo la experiencia diaria. Y hay un Git que sigue evolucionando: SHA-256, referencias empaquetadas en un nuevo formato, clones parciales, índices dispersos.
En el módulo 10: Git en el Mundo Real veremos estudios de caso de cómo usan Git proyectos y organizaciones reales, la integración con otras herramientas del día a día, Git LFS para ficheros grandes, cómo escalar Git en repositorios enormes y monorepos, el papel de Git en DevOps como pieza central de la entrega continua, y hacia dónde va Git en los próximos años.
Empezamos mirando cómo lo hacen otros, en la lección 10-01: Estudios de Caso.
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
