gestor-tareas ha recorrido un camino largo. Tiene mensajes de confirmación que explican el porqué, un historial limpio con una política de integración acordada, los ficheros correctos dentro y bien tratados, y los secretos fuera. Queda un último problema del módulo, mucho menos dramático que el anterior pero cada vez más molesto:

"Cada git status tarda tres segundos. El clon del repositorio pesa 900 MB y el código son cuatro. Y ayer, clonar en el portátil nuevo de Carla tardó ocho minutos."

Un repositorio lento no rompe nada, pero degrada silenciosamente todo lo demás: si git status tarda tres segundos, dejas de ejecutarlo; si el clon tarda ocho minutos, el CI se vuelve caro; si git log se arrastra, dejas de consultar el historial. La lentitud erosiona los hábitos que hemos construido en todo el módulo.

Esta lección enseña a medir antes que a optimizar, y luego a actuar sobre lo que la medición señale. Se centra en el repositorio de tamaño normal que se ha vuelto lento por descuido. Los repositorios verdaderamente enormes y los monorepos tienen técnicas propias —clones parciales, clones superficiales, índice disperso— y son el contenido de la lección 10-04.

Contenido

  1. Medir antes de optimizar
  2. git count-objects -vH: la foto del repositorio
  3. Encontrar los objetos más grandes del historial
  4. Empaquetado: objetos sueltos y packfiles
  5. git gc: qué hace realmente
  6. git maintenance: el sustituto moderno
  7. Acelerar git status: fsmonitor y untrackedCache
  8. feature.manyFiles y otros ajustes
  9. El coste de los ficheros binarios grandes
  10. Higiene de referencias: prune y packed-refs
  11. Otros comandos lentos y sus causas
  12. Buenas costumbres que evitan el problema

  1. Medir antes de optimizar

La regla es la misma que en cualquier optimización: la intuición sobre qué es lento es casi siempre equivocada. Antes de tocar nada, mide.

Git trae un mecanismo de trazas que dice exactamente dónde se va el tiempo:

# Traza básica: tiempos por etapa
GIT_TRACE=1 git status

# Traza de rendimiento, mucho más detallada
GIT_TRACE_PERFORMANCE=1 git status
12:04:31.882 read-cache.c:2402  performance: 0.412 s: read cache .git/index
12:04:32.741 name-hash.c:610    performance: 0.856 s: init name hash
12:04:34.102 dir.c:2419         performance: 1.361 s: directory traversal
12:04:34.180 trace.c:487        performance: 2.298 s: git command: git status

Ahí está el diagnóstico completo: 1,36 segundos recorriendo directorios y 0,41 leyendo el índice. El problema no es el historial: es la cantidad de ficheros en la copia de trabajo. Eso apunta al apartado 7, no al 5.

Una forma más estructurada, con la traza moderna en formato legible:

GIT_TRACE2_PERF_BRIEF=1 GIT_TRACE2_PERF=/dev/stdout git status 2>&1 | head -30

Y para comparar antes y después de un cambio, mide varias veces:

for i in 1 2 3; do /usr/bin/time -f "%e s" git status > /dev/null; done

Qué medir en cada síntoma

Síntoma Causa probable Apartado
git status lento Muchos ficheros en la copia de trabajo 7
git clone lento o clon enorme Objetos grandes en el historial 3, 9
git log lento Historial muy largo, o mal empaquetado 4, 5
git fetch/push lento Muchas referencias remotas obsoletas 10
Todo un poco lento tras mucha actividad Muchos objetos sueltos sin empaquetar 4, 5, 6
git checkout/switch lento Muchos ficheros, o ficheros grandes 7, 9

  1. git count-objects -vH: la foto del repositorio

Es el primer comando que hay que ejecutar. Da el estado de la base de objetos en una pantalla:

git count-objects -vH
count: 8432
size: 156.42 MiB
in-pack: 214893
packs: 7
size-pack: 743.18 MiB
prune-packable: 0
garbage: 0
size-garbage: 0 bytes

Cómo leerlo:

Campo Qué significa Cuándo preocupa
count Objetos sueltos (un fichero por objeto en .git/objects/XX/) Más de unos pocos miles: falta empaquetar
size Espacio que ocupan los sueltos Si es una fracción importante del total
in-pack Objetos dentro de packfiles Informativo
packs Número de packfiles Más de 5 o 10: conviene consolidar
size-pack Espacio de los packfiles La cifra real del repositorio
prune-packable Sueltos que ya están también en un pack (duplicados) Cualquier valor alto: gc los limpia
garbage Ficheros que Git no reconoce en .git/objects Distinto de 0: algo raro pasó

En el ejemplo: 743 MB de packfiles para un proyecto cuyo código ocupa 4 MB. Ahí está el problema, y el apartado 3 dice de dónde viene.

Comparación con el tamaño real:

# Lo que ocupa .git entero
du -sh .git

# Lo que ocupa la copia de trabajo (sin .git)
du -sh --exclude=.git .
897M	.git
4.2M	.

Una relación de 200 a 1 entre el historial y el contenido actual es una señal inequívoca de que en el historial hay algo que no debería estar.

  1. Encontrar los objetos más grandes del historial

Este es el diagnóstico clave, y merece entenderlo pieza a pieza. La receta combina dos comandos de bajo nivel que ya conoces de la lección 01-04.

#!/usr/bin/env bash
# objetos-grandes.sh — los N blobs más grandes del historial, con su ruta
#
# Cómo funciona:
#   1. rev-list --objects --all
#        Lista TODOS los objetos alcanzables desde cualquier referencia,
#        con el formato "<sha> <ruta>". La ruta solo aparece en blobs y árboles.
#   2. cat-file --batch-check
#        Recibe SHA por la entrada estándar y, para cada uno, imprime el
#        formato pedido sin volcar el contenido (que sería carísimo).
#        %(rest) devuelve lo que venía después del SHA: la ruta.
#   3. awk / sort / head
#        Se queda con los blobs, ordena por tamaño y muestra los mayores.

N=${1:-20}

git rev-list --objects --all \
  | git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize:disk) %(objectsize) %(rest)' \
  | awk '$1 == "blob" { print $3, $4, $2, $5 }' \
  | sort -rn \
  | head -n "$N" \
  | while read -r en_disco real sha ruta; do
      printf '%8s en disco  %8s real  %s  %s\n' \
        "$(numfmt --to=iec --suffix=B "$en_disco")" \
        "$(numfmt --to=iec --suffix=B "$real")" \
        "${sha:0:10}" "$ruta"
    done
chmod +x objetos-grandes.sh
./objetos-grandes.sh 10
 214MiB en disco   240MiB real  a1b2c3d4e5  demo/video-presentacion.mp4
  96MiB en disco   112MiB real  b2c3d4e5f6  diseno/mockups.psd
  84MiB en disco    84MiB real  c3d4e5f6a7  datos/volcado-pruebas.sql
  71MiB en disco    88MiB real  d4e5f6a7b8  demo/video-presentacion.mp4
  52MiB en disco    52MiB real  e5f6a7b8c9  diseno/mockups.psd
  31MiB en disco    36MiB real  f6a7b8c9d0  demo/capturas.zip
 2.1MiB en disco   8.4MiB real  a7b8c9d0e1  node_modules/.package-lock.json
 1.8MiB en disco   1.9MiB real  b8c9d0e1f2  package-lock.json

El diagnóstico salta a la vista:

  • video-presentacion.mp4 aparece dos veces, con 214 y 71 MB. Son dos versiones del mismo fichero: alguien lo actualizó y Git guarda las dos enteras.
  • mockups.psd también aparece dos veces. Lo mismo.
  • Un volcado de base de datos de 84 MB que nunca debió versionarse.
  • Y un node_modules/.package-lock.json, resto de la contaminación que limpiamos en la lección 08-03.

Los dos tamaños que imprime el script son importantes:

  • objectsize (real): el tamaño del contenido descomprimido.
  • objectsize:disk (en disco): lo que ocupa realmente en el packfile, después de comprimir y de aplicar deltas.

Que sean parecidos en los vídeos y las imágenes lo dice todo: los formatos ya comprimidos no se comprimen más ni admiten deltas útiles. Es el apartado 9.

Variantes útiles

# Solo lo alcanzable desde HEAD (lo que un clon nuevo se llevaría de la rama actual)
git rev-list --objects HEAD \
  | git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' \
  | awk '$1=="blob" {print $3, $4}' | sort -rn | head

# Sumar por extensión: ¿qué tipo de fichero pesa más?
git rev-list --objects --all \
  | git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' \
  | awk '$1=="blob" && $4 != "" {
      n = split($4, p, "."); ext = (n > 1) ? p[n] : "(sin ext)";
      suma[ext] += $3
    }
    END { for (e in suma) printf "%12d  %s\n", suma[e], e }' \
  | sort -rn | head -15
   357891234  mp4
   154329871  psd
    88129043  sql
    33021884  zip
     4102993  json
      982341  js

Esta vista por extensión es la más útil para tomar decisiones: dice qué categoría de fichero está inflando el repositorio.

Qué hacer con lo que encuentres

Localizar el problema es la mitad del trabajo; la otra mitad es el apartado 9, y anticipa la conclusión: borrar el fichero de la punta no libera nada. Sacarlo del historial exige la misma reescritura que vimos en la lección 08-05, con las mismas consecuencias para todo el equipo.

  1. Empaquetado: objetos sueltos y packfiles

Para entender gc hay que entender cómo almacena Git los objetos. Hay dos formas:

Objetos sueltos

Cada objeto es un fichero independiente en .git/objects/, comprimido con zlib, en un directorio nombrado por los dos primeros caracteres de su SHA:

ls .git/objects/a1/
b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0

Es el formato que Git usa al crear objetos nuevos: rápido de escribir, muy ineficiente de almacenar. Si modificas una línea de un fichero de 1 MB y confirmas, se crea un blob nuevo y completo de 1 MB.

Packfiles

Un packfile es un único fichero que contiene muchos objetos, con dos optimizaciones:

  1. Compresión conjunta, más eficaz que comprimir cada objeto por separado.
  2. Deltas: en lugar de guardar cada versión entera, Git guarda una versión completa y las demás como diferencias respecto a ella.
flowchart LR
    subgraph S["Objetos sueltos"]
        A1["blob v1<br/>1 MB"]
        A2["blob v2<br/>1 MB"]
        A3["blob v3<br/>1 MB"]
    end
    subgraph P["Packfile"]
        B1["blob v3 completo<br/>1 MB"]
        B2["delta v2 ← v3<br/>2 KB"]
        B3["delta v1 ← v2<br/>3 KB"]
    end
    S -->|"git gc"| P

De 3 MB a poco más de 1 MB. Con un historial de cientos de versiones de un fichero de texto, el ahorro es de uno o dos órdenes de magnitud.

Detalles que conviene conocer:

  • Git delta contra el objeto más parecido, no necesariamente contra la versión anterior. Puede encadenar deltas (con un límite de profundidad configurable).
  • Los deltas se calculan por contenido, no por historia. Dos ficheros parecidos de commits lejanos pueden servir de base uno del otro.
  • Los formatos ya comprimidos (JPG, PNG, MP4, ZIP, PSD) no admiten deltas útiles. Un cambio mínimo en la imagen cambia todo el flujo de bytes comprimido. Por eso en el apartado 3 el tamaño en disco y el real coincidían.
# Ver los packfiles y su contenido
ls -lh .git/objects/pack/

# Estadísticas de un packfile: profundidad de las cadenas de delta
git verify-pack -v .git/objects/pack/pack-*.idx | tail -5

Cuándo se crea un packfile: al hacer git gc (manual o automático), al hacer git clone o git fetch (lo que viaja por la red es siempre un pack), y al llegar a los umbrales de gc.auto.

  1. git gc: qué hace realmente

git gc (garbage collection) es el comando de mantenimiento clásico. Hace cinco cosas:

  1. Empaqueta los objetos sueltos en packfiles.
  2. Consolida varios packfiles en menos.
  3. Elimina objetos inalcanzables que hayan superado el periodo de gracia.
  4. Empaqueta las referencias en .git/packed-refs (apartado 10).
  5. Caduca las entradas antiguas del reflog (90 días por defecto para lo alcanzable, 30 para lo demás).
# El mantenimiento normal
git gc

# Ver qué haría, sin hacerlo
git gc --auto --dry-run
git count-objects -vH     # antes
git gc
git count-objects -vH     # después
# Antes                    # Después
count: 8432                count: 0
size: 156.42 MiB           size: 0 bytes
in-pack: 214893            in-pack: 223325
packs: 7                   packs: 1
size-pack: 743.18 MiB      size-pack: 698.44 MiB

Los 8.432 objetos sueltos han desaparecido, los 7 packfiles se han consolidado en 1, y el total ha bajado. Fíjate en que sigue habiendo 698 MB: gc reorganiza y comprime, pero no puede eliminar objetos que siguen siendo alcanzables desde alguna referencia. Los vídeos del apartado 3 siguen ahí porque están en commits que siguen en el historial.

gc automático

Git ya ejecuta gc --auto por su cuenta después de ciertos comandos (commit, merge, rebase, receive-pack). Solo actúa si se superan unos umbrales:

# Umbrales, con sus valores por defecto
git config --get gc.auto            # 6700 objetos sueltos
git config --get gc.autoPackLimit   # 50 packfiles

# Ajustarlos
git config --global gc.auto 256

# Desactivar el gc automático (si prefieres git maintenance, apartado 6)
git config --global gc.auto 0

--aggressive: por qué casi nunca hace falta

git gc --aggressive     # puede tardar HORAS en un repositorio grande

--aggressive descarta los deltas existentes y recalcula todo desde cero, con una ventana de búsqueda mucho mayor. Es extremadamente caro y el beneficio suele ser marginal, porque los deltas que ya había eran razonablemente buenos.

Situación ¿--aggressive?
Mantenimiento periódico No. git gc normal, o mejor git maintenance
Tras un filter-repo que reescribió todo , una vez
Tras importar desde otro sistema de control de versiones , una vez
"Por si acaso", cada semana No. Horas de CPU para nada
El repositorio va lento y no sé por qué No. Mide primero (apartado 1)

Si de verdad quieres un reempaquetado a fondo, esto es más controlable que --aggressive:

git repack -a -d -f --depth=250 --window=250
  • -a: todo en un solo pack. -d: borra los packs viejos. -f: recalcula los deltas.
  • --window: cuántos objetos considerar como base de delta (más = mejor y más lento).
  • --depth: longitud máxima de la cadena de deltas (más = más pequeño y más lento de leer).

La advertencia importante

gc puede eliminar objetos inalcanzables, y con ellos la posibilidad de recuperar commits perdidos:

# ESTO DESTRUYE la red de seguridad
git reflog expire --expire=now --all
git gc --prune=now

Después de esos dos comandos, un commit que hubieras perdido con un reset desafortunado ya no se puede recuperar. La recuperación de commits perdidos y el papel del reflog son el contenido de la lección 09-04; hasta que la hayas visto, no ejecutes --prune=now salvo que sepas exactamente qué estás haciendo (por ejemplo, tras el filter-repo de la lección 08-05, donde es precisamente lo que se busca).

  1. git maintenance: el sustituto moderno

Desde Git 2.29 existe git maintenance, pensado para sustituir al gc manual y automático. Sus ventajas:

  • Se programa en segundo plano, así que no bloquea tus comandos.
  • Tareas separadas con frecuencias distintas, en lugar de un gc monolítico.
  • Incremental: reempaqueta poco a poco en lugar de todo de golpe.
  • No caduca el reflog por sorpresa.
# Activar el mantenimiento programado para este repositorio
git maintenance start

Eso registra el repositorio y crea las tareas programadas del sistema (cron, systemd, launchd o el programador de tareas de Windows, según la plataforma). A partir de ese momento:

Tarea Frecuencia Qué hace
prefetch Cada hora Descarga objetos del remoto en segundo plano; tus fetch posteriores son casi instantáneos
commit-graph Cada hora Actualiza el grafo de commits, que acelera muchísimo log, merge-base y los cálculos de alcanzabilidad
loose-objects A diario Empaqueta objetos sueltos de forma incremental
incremental-repack A diario Consolida packfiles poco a poco
gc Desactivada La sustituyen las tareas anteriores
# Ver qué está registrado
git config --get-all maintenance.repo --global

# Ejecutar una tarea manualmente
git maintenance run --task=commit-graph
git maintenance run --task=incremental-repack

# Ejecutarlo todo ahora
git maintenance run

# Desactivarlo
git maintenance stop

# Sacar el repositorio del registro
git maintenance unregister

El commit-graph, la joya escondida

Es probablemente la mejora más rentable de esta lección. El fichero commit-graph es un índice con la información estructural de los commits —padres, fechas, números de generación— que evita tener que leer y descomprimir cada objeto de commit para recorrer el historial.

# Generarlo a mano
git commit-graph write --reachable

# Y que se mantenga solo
git config --global fetch.writeCommitGraph true

El efecto en un repositorio con historial largo:

# Sin commit-graph
time git log --oneline --graph --all > /dev/null      # 4,8 s

# Con commit-graph
time git log --oneline --graph --all > /dev/null      # 0,3 s

Acelera git log --graph, git merge-base, git branch --contains, git tag --contains, git bisect (lección 06-02) y todo lo que necesite calcular alcanzabilidad. Es gratis y no tiene contrapartidas.

La configuración recomendada

# Mantenimiento moderno en lugar de gc manual
git maintenance start
git config --global gc.auto 0            # que el gc automático no interfiera
git config --global fetch.writeCommitGraph true

  1. Acelerar git status: fsmonitor y untrackedCache

Este es el problema de Carla, y merece entenderse porque la causa no es la que la gente supone.

Por qué git status es lento

git status tiene que responder a tres preguntas:

  1. ¿Qué ficheros seguidos han cambiado? → comparar el índice con el disco.
  2. ¿Qué hay preparado? → comparar el índice con HEAD.
  3. ¿Qué ficheros sin seguimiento hay?recorrer todos los directorios de la copia de trabajo.

La tercera es la cara. Recorrer 40.000 ficheros —node_modules incluido, aunque esté ignorado, porque Git tiene que mirar para saber que lo ignora— cuesta miles de llamadas al sistema. En Linux es rápido; en Windows y macOS, notablemente más lento, y esa es la razón de que Carla lo sufra más que Ana.

core.fsmonitor (Git 2.37+)

En lugar de recorrer el árbol, Git pregunta al sistema de vigilancia de ficheros del sistema operativo qué ha cambiado desde la última vez. Desde Git 2.37 hay un monitor integrado, sin herramientas externas:

git config core.fsmonitor true

La primera ejecución arranca un demonio en segundo plano:

git fsmonitor--daemon status
fsmonitor-daemon is watching '/home/carla/proyectos/gestor-tareas'

El efecto es drástico:

# Antes
time git status      # 2,8 s

# Después (segunda ejecución en adelante)
time git status      # 0,15 s
# Parar el demonio
git fsmonitor--daemon stop

# Desactivarlo
git config --unset core.fsmonitor

core.untrackedCache

Cachea el resultado del recorrido de directorios, usando la marca de tiempo de modificación de cada directorio para saber cuáles hay que volver a mirar.

# Comprobar si tu sistema de ficheros lo soporta
git update-index --test-untracked-cache

# Activarlo
git config core.untrackedCache true

Requiere que el sistema de ficheros actualice fiablemente el mtime de los directorios. El comando de prueba lo verifica; si falla, no lo actives.

Cuándo ayuda cada uno

Ajuste Ayuda cuando No ayuda cuando Coste
core.fsmonitor Muchos ficheros en la copia de trabajo (>10.000); Windows o macOS Repositorios pequeños; sistemas de ficheros de red Un demonio en segundo plano por repositorio
core.untrackedCache Muchos directorios con ficheros sin seguimiento El sistema de ficheros no da mtime fiable de directorios Un poco más de tamaño de índice
index.version 4 Índices muy grandes (comprime los nombres de ruta) Índices pequeños Incompatible con versiones de Git muy antiguas
core.preloadIndex Sistemas con varios núcleos (activo por defecto) Ninguno
Reducir ficheros Siempre Requiere cambiar el proyecto

La última fila es la que más rinde y la que menos se aplica: si node_modules tiene 40.000 ficheros, ninguna optimización de Git va a ser tan buena como no tenerlos. Los sistemas de dependencias modernos con almacén central y enlaces reducen mucho ese número.

Un truco sencillo para medir cuánto cuesta el recorrido de ficheros sin seguimiento:

time git status                                 # 2,8 s
time git status --untracked-files=no            # 0,2 s

Si la diferencia es grande, tu problema es el recorrido de directorios y fsmonitor es la solución.

  1. feature.manyFiles y otros ajustes

Git agrupa configuraciones recomendadas en "macros" de funcionalidad:

git config feature.manyFiles true

Equivale a activar de golpe:

Ajuste Efecto
index.version 4 Formato de índice comprimido: índice más pequeño y rápido de leer
core.untrackedCache true El caché del apartado 7
index.skipHash true Omite el cálculo del hash del índice al escribirlo (Git 2.40+)

Está pensado para copias de trabajo con muchos ficheros. No toca el historial: no ayuda con un repositorio grande por su historia, solo por su número de ficheros actuales.

Otros ajustes con buena relación beneficio/coste:

# Escribir el commit-graph al hacer fetch (apartado 6)
git config --global fetch.writeCommitGraph true

# Escribir el índice de bitmaps al reempaquetar: acelera mucho clone y fetch
git config --global repack.writeBitmaps true

# Paralelizar la compresión con todos los núcleos
git config --global pack.threads 0

# Limitar la memoria que usa el empaquetado (útil en máquinas con poca RAM)
git config --global pack.windowMemory 256m
git config --global pack.packSizeLimit 2g

Y un ajuste que no deberías tocar a la ligera:

# NO: baja la compresión a cambio de velocidad de escritura
git config --global core.compression 1

Hace que el repositorio ocupe bastante más a cambio de una mejora marginal. Solo tiene sentido en casos muy concretos, y casi nunca en el tuyo.

  1. El coste de los ficheros binarios grandes

Volvamos al diagnóstico del apartado 3: 357 MB de vídeos y 154 MB de ficheros PSD. Este es el problema estructural de gestor-tareas, y hay que entender bien por qué es tan grave.

Por qué duelen tanto

1. No se comprimen. Un MP4 o un PSD ya están comprimidos. Git aplica zlib encima y el resultado es prácticamente el mismo tamaño.

2. No admiten deltas útiles. Cambiar un fotograma de un vídeo altera todo el flujo de bytes comprimido posterior. Git no encuentra similitud aprovechable y guarda cada versión completa.

3. Cada versión se guarda entera. Un vídeo de 200 MB actualizado cinco veces son 1 GB en el historial, para siempre.

4. Todo el mundo lo descarga. git clone trae el historial completo. Carla descarga los cinco vídeos aunque solo necesite el último. El CI también, en cada ejecución que no use caché.

5. No se puede fusionar. Un conflicto en un binario solo se resuelve eligiendo una versión entera (lección 08-04, atributo binary).

Comparación con un fichero de texto:

app.js (200 KB, 500 versiones) video.mp4 (200 MB, 5 versiones)
Contenido lógico 100 MB 1.000 MB
Espacio real en el pack ~3 MB (deltas) ~1.000 MB (sin deltas)
Coste del clon Despreciable Minutos

Por qué no se arregla borrándolos

git rm demo/video-presentacion.mp4
git commit -m "chore: elimina el vídeo de la demo"
git count-objects -vH      # el tamaño NO baja

Es exactamente el mismo mecanismo que vimos con los secretos en la lección 08-05: el commit nuevo no incluye el fichero, pero los commits anteriores siguen referenciando los blobs, que siguen siendo alcanzables y por tanto gc no los toca jamás.

Para recuperar el espacio de verdad hay que reescribir el historial:

# Sobre un clon --mirror, con copia de seguridad previa (lección 08-05)
git filter-repo --path demo/ --path diseno/ --path datos/volcado-pruebas.sql --invert-paths

# O por tamaño: elimina cualquier blob de más de 10 MB
git filter-repo --strip-blobs-bigger-than 10M

Con exactamente las mismas consecuencias que en la lección anterior: todos los SHA cambian, todos los clones quedan obsoletos, hay que coordinar con el equipo, y los forks (el de Diego) no se limpian solos. Reescribir el historial para ahorrar espacio es una decisión seria, no una tarea de mantenimiento. Casi siempre conviene más aprender la lección y no volver a hacerlo.

La solución correcta: Git LFS

Para binarios que hay que versionar —diseños, recursos gráficos de un juego, documentos maestros—, la respuesta es Git LFS (Large File Storage). Recuerda del apartado 13 de la lección 08-04 que se activa con un filter en .gitattributes:

*.mp4   filter=lfs diff=lfs merge=lfs -text
*.psd   filter=lfs diff=lfs merge=lfs -text

El repositorio guarda un puntero de texto de 130 bytes en lugar del fichero, y el contenido vive en un almacén aparte que solo se descarga cuando hace falta.

El mecanismo completo, el servidor de almacenamiento, los costes, git lfs migrate para convertir un historial existente y las limitaciones que hay que conocer antes de adoptarlo son el contenido de la lección 10-03: Git LFS para Ficheros Grandes.

  1. Higiene de referencias: prune y packed-refs

Las referencias —ramas, etiquetas, ramas de seguimiento— son ficheros de 41 bytes (lección 03-01). Individualmente no pesan nada; acumuladas por miles, sí importan.

Ramas remotas obsoletas

Cuando alguien borra una rama en el servidor, tu copia local de esa referencia no desaparece sola:

git branch -r | wc -l
1247

1.247 ramas remotas, de las que quizá 8 siguen existiendo. Cada fetch las procesa y cada git branch -a las lista.

# Ver qué se eliminaría, sin hacerlo
git remote prune origin --dry-run

# Limpiar
git fetch --prune

# Y que sea el comportamiento por defecto, para siempre
git config --global fetch.prune true
git config --global fetch.pruneTags false   # con las etiquetas, mejor ser conservador

Retomando lo de la lección 03-06: fetch.prune true debería estar en la configuración global de todo el mundo. Es una línea que evita una acumulación silenciosa que nadie mira nunca.

Y las ramas locales ya fusionadas:

# Ver cuáles están completamente integradas en main
git branch --merged main | grep -vE '^\*|main|develop'

# Borrarlas
git branch --merged main | grep -vE '^\*|main|develop' | xargs -r git branch -d

Cuidado con --merged si tu política de integración es squash (lección 08-02): una rama aplastada no aparece como fusionada, porque sus commits no están en main. Comprueba antes de borrar, o borra por la rama remota que la plataforma elimina al fusionar.

packed-refs

Con miles de referencias sueltas, cada una es un fichero diminuto y leerlas todas cuesta miles de operaciones de disco:

find .git/refs -type f | wc -l

git pack-refs las consolida en un único fichero .git/packed-refs:

# Empaquetar todas las referencias
git pack-refs --all

# Ver el resultado
head -5 .git/packed-refs
# pack-refs with: peeled fully-peeled sorted
a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0 refs/heads/main
b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1 refs/remotes/origin/main
c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2 refs/tags/v1.5.0
^d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3

Lo hace git gc automáticamente, así que rara vez hay que ejecutarlo a mano. Las referencias que cambian vuelven a escribirse como ficheros sueltos y se reempaquetan en el siguiente gc.

Otras referencias que se acumulan

# Etiquetas: si el equipo etiqueta cada compilación, se acumulan miles
git tag | wc -l

# Referencias de notas, stash y worktrees
ls .git/refs/

Un caso concreto que sorprende: los worktrees de la lección 06-06 dejan referencias y metadatos. Si creas y borras worktrees a menudo:

git worktree prune

  1. Otros comandos lentos y sus causas

Comando lento Causa habitual Solución
git log --graph --all Recorrer todo el historial calculando alcanzabilidad git commit-graph write --reachable (apartado 6)
git blame en un fichero grande Recorre el historial línea a línea Acotar con -L 100,200 o con un rango de commits
git clone Historial completo con binarios grandes Reducir el historial (apartado 9); o clon parcial, lección 10-04
git checkout / switch Escribir muchos ficheros en disco core.fsmonitor; menos ficheros
git fetch Muchas referencias, o falta de bitmaps fetch.prune true, repack.writeBitmaps true
git push Calcular qué falta en el servidor repack.writeBitmaps true
git grep Buscar en toda la copia de trabajo git grep --cached (busca en el índice, mucho más rápido)
git bisect Muchos pasos, cada uno con checkout completo commit-graph; bisect --first-parent (lección 08-02)
git diff en ficheros grandes Cálculo del diff -diff en .gitattributes si es binario (lección 08-04)

Un caso concreto que merece mención, porque enlaza con la lección 06-05: los submódulos ralentizan git status de forma notable, porque Git tiene que entrar en cada uno y comprobar su estado.

# No comprobar el estado interno de los submódulos
git config --global status.submoduleSummary false
git config diff.ignoreSubmodules dirty

Con componentes-ui como submódulo, esos dos ajustes se notan en cada git status.

  1. Buenas costumbres que evitan el problema

Todo lo anterior es tratamiento. Esto es prevención, que es infinitamente más barata:

1. Un .gitignore correcto desde el primer commit (lección 08-03). Casi todos los repositorios enormes lo son por algo que nunca debió entrar.

2. Nunca versionar binarios grandes sin pensarlo. Antes de añadir un fichero de más de unos pocos MB, pregúntate: ¿va a cambiar? ¿Cuántas veces? Si la respuesta es "sí, muchas", necesitas Git LFS (lección 10-03) o un almacén externo.

3. Poner un límite y comprobarlo en el hook pre-commit (lección 06-01):

#!/usr/bin/env bash
# .githooks/pre-commit — bloquea ficheros demasiado grandes
LIMITE=$((5 * 1024 * 1024))    # 5 MB
fallos=0

while IFS= read -r fichero; do
  [ -f "$fichero" ] || continue
  tam=$(wc -c < "$fichero")
  if [ "$tam" -gt "$LIMITE" ]; then
    echo "BLOQUEADO: '$fichero' ocupa $((tam / 1024 / 1024)) MB (límite: 5 MB)." >&2
    echo "  Si de verdad hace falta versionarlo, usa Git LFS." >&2
    fallos=1
  fi
done < <(git diff --cached --name-only --diff-filter=ACM)

exit $fallos

Y la misma comprobación en el CI (lección 07-06), porque --no-verify existe.

4. Commits atómicos y pequeños (lección 08-02). Un historial de commits pequeños se comprime mejor y produce deltas más eficientes que uno de commits gigantescos.

5. fetch.prune true y gc.auto razonable en la configuración global de todo el equipo.

6. git maintenance start en cada repositorio con el que trabajes a diario.

7. Medir de vez en cuando. Un git count-objects -vH trimestral detecta el problema cuando aún es fácil de arreglar.

8. No versionar salidas de herramientas. Ficheros minificados, documentación generada, capturas de pruebas fallidas. Se regeneran.

9. Cuidado con los volcados de base de datos. Además de grandes, suelen contener datos personales (lección 08-05).

Un guion de revisión trimestral

#!/usr/bin/env bash
# revision-repositorio.sh — informe de salud del repositorio

echo "=== Tamaño ==="
du -sh .git
git count-objects -vH

echo ""
echo "=== Los 10 objetos más grandes del historial ==="
git rev-list --objects --all \
  | git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' \
  | awk '$1=="blob" {printf "%10.1f MB  %s\n", $3/1048576, $4}' \
  | sort -rn | head -10

echo ""
echo "=== Referencias ==="
echo "Ramas locales:   $(git branch | wc -l)"
echo "Ramas remotas:   $(git branch -r | wc -l)"
echo "Etiquetas:       $(git tag | wc -l)"
echo "Refs sueltas:    $(find .git/refs -type f | wc -l)"

echo ""
echo "=== Ramas remotas que ya no existen en el servidor ==="
git remote prune origin --dry-run

echo ""
echo "=== Velocidad de git status ==="
/usr/bin/time -f "  con untracked:  %e s" git status > /dev/null
/usr/bin/time -f "  sin untracked:  %e s" git status --untracked-files=no > /dev/null

echo ""
echo "=== Mantenimiento ==="
echo "commit-graph: $([ -f .git/objects/info/commit-graph ] && echo 'sí' || echo 'NO — ejecuta git commit-graph write --reachable')"
echo "fsmonitor:    $(git config --get core.fsmonitor || echo 'no configurado')"
echo "fetch.prune:  $(git config --get fetch.prune || echo 'no configurado')"

Errores Comunes y Consejos

Error 1: optimizar sin medir. Ejecutar gc --aggressive porque "el repositorio va lento" cuando el problema es el recorrido de ficheros sin seguimiento gasta horas de CPU para nada. Mide primero con GIT_TRACE_PERFORMANCE=1.

Error 2: gc --aggressive como rutina. Es caro y su beneficio es marginal salvo tras una reescritura completa del historial o una importación.

Error 3: creer que git gc va a reducir un repositorio inflado por binarios. gc reorganiza y comprime, pero no puede eliminar objetos alcanzables. Los binarios siguen ahí porque siguen en el historial.

Error 4: borrar un fichero grande y esperar que el repositorio adelgace. Mismo mecanismo que con los secretos: git rm no toca el historial. Hace falta filter-repo, con todas sus consecuencias.

Error 5: git reflog expire --expire=now --all && git gc --prune=now sin saber lo que se hace. Destruye la red de seguridad que permite recuperar commits perdidos (lección 09-04).

Error 6: no activar fetch.prune. Miles de ramas remotas fantasma que ralentizan cada fetch y ensucian cada listado.

Error 7: versionar binarios grandes "porque son pocos". Cinco versiones de un vídeo de 200 MB son 1 GB permanente para todo el que clone, para siempre.

Error 8: ignorar el commit-graph. Es la optimización con mejor relación beneficio/coste de toda la lección y casi nadie la activa.

Consejo 1: git maintenance start en tus repositorios de trabajo. Sustituye al gc manual, se ejecuta en segundo plano y mantiene el commit-graph al día.

Consejo 2: core.fsmonitor true si tienes muchos ficheros, especialmente en Windows o macOS. Es la diferencia entre 2,8 s y 0,15 s por cada git status.

Consejo 3: mide el coste de los ficheros sin seguimiento con git status --untracked-files=no. Si la diferencia es grande, ya sabes dónde actuar.

Consejo 4: guarda el guion del apartado 3. Encontrar los objetos más grandes del historial es la pregunta que se hace una vez al año y siempre hay que buscar cómo se hacía.

Consejo 5: pon un límite de tamaño en el pre-commit y en el CI. Cuesta diez líneas y evita el problema entero.

Consejo 6: revisión trimestral de dos minutos. El guion del apartado 12 detecta los problemas cuando todavía son baratos de arreglar.

Ejercicios

Ejercicio 1: diagnosticar un repositorio inflado

  1. Crea un repositorio y confirma un app.js pequeño.
  2. Añade un fichero binario grande generado al azar y confírmalo:
    head -c 20000000 /dev/urandom > demo/video.bin
    
  3. Modifícalo tres veces (regenerándolo) y confirma cada versión.
  4. Ejecuta git count-objects -vH y anota size-pack.
  5. Ejecuta el guion del apartado 3 e identifica los objetos grandes.
  6. Borra el fichero con git rm, confirma, ejecuta git gc y vuelve a mirar count-objects. Explica el resultado.
  7. Compara el tamaño con el mismo experimento hecho sobre un fichero de texto de 20 MB modificado tres veces. Explica la diferencia.

Ejercicio 2: medir y acelerar git status

  1. En un repositorio de pruebas, crea 20.000 ficheros pequeños en subdirectorios:
    for i in $(seq 1 200); do
      mkdir -p "dir$i"
      for j in $(seq 1 100); do echo "contenido $i-$j" > "dir$i/f$j.txt"; done
    done
    
  2. Confírmalos y mide git status tres veces.
  3. Mide git status --untracked-files=no y calcula qué porcentaje del tiempo se va en el recorrido de directorios.
  4. Activa core.fsmonitor true, ejecuta git status dos veces (la primera arranca el demonio) y vuelve a medir.
  5. Prueba git config feature.manyFiles true y mide de nuevo.
  6. Genera el commit-graph y compara git log --graph --all antes y después.

Ejercicio 3: higiene de referencias y mantenimiento

  1. Crea un repositorio con 50 ramas locales, la mitad de ellas fusionadas en main.
  2. Cuenta las referencias sueltas con find .git/refs -type f | wc -l.
  3. Ejecuta git pack-refs --all y vuelve a contar. Examina .git/packed-refs.
  4. Borra las ramas fusionadas con git branch --merged. Explica qué pasaría si la política de integración fuera squash.
  5. Activa git maintenance start y comprueba qué tareas quedan registradas con git config --get-all maintenance.repo --global.
  6. Escribe el guion de revisión del apartado 12 en un fichero, ejecútalo e interpreta cada bloque de la salida.

Soluciones

Solución 1:

mkdir -p /tmp/practica-rend/demo && cd /tmp/practica-rend && git init -b main
echo "console.log('gestor-tareas');" > app.js
git add . && git commit -m "chore: commit inicial"
# 2 y 3. El binario, en cuatro versiones
for v in 1 2 3 4; do
  head -c 20000000 /dev/urandom > demo/video.bin
  git add . && git commit -q -m "chore: vídeo de la demo, versión $v"
done
# 4. La foto
git gc -q
git count-objects -vH
count: 0
size: 0 bytes
in-pack: 18
packs: 1
size-pack: 76.30 MiB

76 MB para un proyecto cuyo código son 30 bytes. Las cuatro versiones se guardan enteras: no hay compresión posible sobre datos aleatorios, ni deltas aprovechables.

# 5. Los objetos grandes
git rev-list --objects --all \
  | git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' \
  | awk '$1=="blob" {printf "%8.1f MB  %s  %s\n", $3/1048576, substr($2,1,10), $4}' \
  | sort -rn | head
    19.1 MB  a1b2c3d4e5  demo/video.bin
    19.1 MB  b2c3d4e5f6  demo/video.bin
    19.1 MB  c3d4e5f6a7  demo/video.bin
    19.1 MB  d4e5f6a7b8  demo/video.bin
     0.0 MB  e5f6a7b8c9  app.js

Cuatro blobs distintos con la misma ruta: las cuatro versiones, cada una completa.

# 6. Borrar no arregla nada
git rm demo/video.bin && git commit -q -m "chore: elimina el vídeo"
git gc -q --prune=now
git count-objects -vH
size-pack: 76.30 MiB

El tamaño no ha bajado ni un byte. Los cuatro blobs siguen siendo alcanzables desde los commits anteriores, que siguen en la historia de main. gc --prune=now solo elimina objetos inalcanzables, y estos no lo son. Es exactamente el mismo mecanismo que impedía borrar un secreto en la lección 08-05: los objetos de Git son inmutables y los árboles antiguos siguen apuntando a ellos.

Para recuperar el espacio haría falta:

git filter-repo --path demo/video.bin --invert-paths --force
git count-objects -vH        # ahora sí baja
# 7. La comparación con texto
mkdir /tmp/practica-texto && cd /tmp/practica-texto && git init -b main
# 20 MB de texto: unas 400.000 líneas
seq 1 400000 | sed 's/$/ linea de texto del gestor de tareas/' > datos.txt
git add . && git commit -q -m "datos v1"
for v in 2 3 4; do
  sed -i "1000s/.*/$v linea MODIFICADA/" datos.txt
  git commit -q -am "datos v$v"
done
git gc -q
git count-objects -vH
size-pack: 5.71 MiB

5,7 MB frente a 76 MB. Con 80 MB de contenido lógico en ambos casos. Dos mecanismos explican la diferencia:

  1. Compresión: el texto repetitivo se comprime a una fracción de su tamaño; los datos aleatorios (como un vídeo o una imagen ya comprimida) no se comprimen nada.
  2. Deltas: las versiones 2, 3 y 4 del texto se guardan como diferencias de unas pocas líneas respecto a la versión completa. En el binario, cada versión es un flujo de bytes completamente distinto y Git no encuentra similitud aprovechable.

Esta es la razón técnica de fondo por la que Git es excelente para texto y pésimo para binarios grandes, y por la que existe Git LFS (lección 10-03).

Solución 2:

mkdir /tmp/practica-status && cd /tmp/practica-status && git init -b main
for i in $(seq 1 200); do
  mkdir -p "dir$i"
  for j in $(seq 1 100); do echo "contenido $i-$j" > "dir$i/f$j.txt"; done
done
git add . && git commit -q -m "chore: 20.000 ficheros"
# 2. Medición base
for i in 1 2 3; do /usr/bin/time -f "%e s" git status > /dev/null; done
0.94 s
0.91 s
0.92 s
# 3. Sin recorrer los sin seguimiento
/usr/bin/time -f "%e s" git status --untracked-files=no > /dev/null
0.21 s

El recorrido de directorios cuesta 0,71 s de 0,92 s: el 77 % del tiempo. El diagnóstico es claro y apunta a fsmonitor.

# 4. fsmonitor
git config core.fsmonitor true
git status > /dev/null          # arranca el demonio; esta primera aún es lenta
for i in 1 2 3; do /usr/bin/time -f "%e s" git status > /dev/null; done
0.13 s
0.11 s
0.12 s

De 0,92 s a 0,12 s: casi ocho veces más rápido. El demonio recibe del sistema operativo las notificaciones de cambio, así que Git ya no necesita recorrer nada.

git fsmonitor--daemon status
fsmonitor-daemon is watching '/tmp/practica-status'
# 5. feature.manyFiles
git config feature.manyFiles true
git config --list | grep -E 'index.version|untrackedCache|skipHash'
index.version=4
core.untrackedcache=true
index.skiphash=true

La mejora adicional es pequeña cuando fsmonitor ya está activo, porque el cuello de botella principal ya está resuelto. Sin fsmonitor, untrackedCache por sí solo suele dar una mejora notable (del orden del 40-60 % en el recorrido).

# 6. commit-graph
for i in $(seq 1 300); do echo "$i" >> app.js; git commit -q -am "commit $i"; done

/usr/bin/time -f "%e s" git log --oneline --graph --all > /dev/null
git commit-graph write --reachable
/usr/bin/time -f "%e s" git log --oneline --graph --all > /dev/null

En un historial de 300 commits la diferencia es pequeña; con decenas de miles es de un orden de magnitud. La razón: sin commit-graph, Git tiene que leer y descomprimir cada objeto de commit para conocer sus padres y su fecha; con él, esa información está en un índice binario plano.

Solución 3:

mkdir /tmp/practica-refs && cd /tmp/practica-refs && git init -b main
echo "inicio" > app.js && git add . && git commit -q -m "chore: inicio"

# 1. 50 ramas, 25 de ellas fusionadas
for i in $(seq 1 50); do
  git switch -q -c "GT-$i" main
  echo "cambio $i" >> app.js
  git commit -q -am "feat: cambio $i"
done
git switch -q main
for i in $(seq 1 25); do git merge -q --no-ff "GT-$i" -m "Merge GT-$i" 2>/dev/null; done
# 2. Referencias sueltas
find .git/refs -type f | wc -l
51
# 3. Empaquetarlas
git pack-refs --all
find .git/refs -type f | wc -l
0
head -4 .git/packed-refs
# pack-refs with: peeled fully-peeled sorted
a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0 refs/heads/GT-1
b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1 refs/heads/GT-10
c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2 refs/heads/GT-11

51 ficheros diminutos convertidos en uno solo, ordenado. Con miles de referencias, la diferencia al leerlas es notable.

# 4. Borrar las fusionadas
git branch --merged main | grep -vE '^\*|main' | wc -l
25
git branch --merged main | grep -vE '^\*|main' | xargs -r git branch -q -d
git branch | wc -l
26

Qué pasaría con política de squash (lección 08-02): git branch --merged comprueba si el commit de la punta de la rama es alcanzable desde main. Con un squash merge, los commits originales de la rama nunca entran en main: se crea un commit nuevo con el mismo contenido pero distinto SHA y distinto linaje. Por tanto:

  • git branch --merged main no listaría ninguna de esas ramas, aunque su trabajo esté íntegramente en main.
  • git branch -d se negaría a borrarlas ("not fully merged"), obligando a -D, que borra sin comprobar nada.

Con squash, el criterio fiable no es --merged, sino la rama remota: la plataforma la elimina al fusionar la PR, y git fetch --prune limpia tu copia local. Es un motivo más para tener fetch.prune true en la configuración global.

# 5. Mantenimiento programado
git maintenance start
git config --get-all maintenance.repo --global
/tmp/practica-refs
git config --list | grep maintenance
maintenance.auto=false
maintenance.strategy=incremental
maintenance.repo=/tmp/practica-refs

maintenance.auto=false desactiva el gc automático tras cada comando: ya no hace falta, porque las tareas programadas se encargan en segundo plano.

# 6. El guion de revisión
# (guardar el guion del apartado 12 como revision-repositorio.sh)
chmod +x revision-repositorio.sh && ./revision-repositorio.sh

Interpretación de cada bloque:

Bloque Qué buscar
Tamaño Que .git no sea desproporcionado respecto al código. Relación >20:1 es sospechosa
Objetos grandes Cualquier blob de más de unos pocos MB, y sobre todo el mismo fichero repetido (versiones sucesivas de un binario)
Referencias Ramas remotas muy por encima de las reales; refs sueltas por miles
Ramas fantasma Todo lo que liste remote prune --dry-run es basura acumulada
git status Si la diferencia con --untracked-files=no es grande, activar fsmonitor
Mantenimiento commit-graph presente, fetch.prune configurado

Conclusión

Lo esencial de esta lección:

  • Mide antes de optimizar. GIT_TRACE_PERFORMANCE=1 dice exactamente dónde se va el tiempo, y git count-objects -vH da la foto del repositorio en una pantalla. La intuición sobre qué es lento casi siempre falla.
  • git rev-list --objects --all + git cat-file --batch-check es la receta para encontrar los objetos más grandes del historial. Ver el mismo fichero repetido varias veces es el síntoma inequívoco de un binario versionado.
  • Git almacena los objetos sueltos (rápido de escribir, ineficiente) y en packfiles (comprimidos conjuntamente y con deltas). git gc empaqueta, consolida y limpia lo inalcanzable, pero no puede eliminar lo que sigue estando en el historial. --aggressive casi nunca hace falta: solo tras una reescritura completa o una importación.
  • git maintenance start es el sustituto moderno del gc manual: tareas separadas, programadas y en segundo plano. Y trae consigo el commit-graph, que es la optimización con mejor relación beneficio/coste de toda la lección.
  • git status es lento por el recorrido de ficheros sin seguimiento, no por el historial. Se mide con --untracked-files=no y se arregla con core.fsmonitor (Git 2.37+) y core.untrackedCache. feature.manyFiles agrupa los ajustes para copias de trabajo con muchos ficheros.
  • Los binarios grandes son caros porque no se comprimen, no admiten deltas, se guardan enteros en cada versión y todo el mundo los descarga. Y borrarlos no libera espacio, exactamente por el mismo motivo que borrar un secreto no lo elimina: hace falta reescribir el historial, con todas sus consecuencias. La solución correcta es Git LFS, en la lección 10-03.
  • Higiene de referencias: fetch.prune true en la configuración global de todo el mundo, borrado de ramas fusionadas (con cuidado si la política es squash) y packed-refs, que gc mantiene solo.
  • Y sobre todo, prevención: .gitignore desde el primer commit, un límite de tamaño en el hook pre-commit y en el CI, commits pequeños, y una revisión trimestral de dos minutos.

Para lo que va más allá —monorepos, clones parciales con --filter=blob:none, clones superficiales, índice disperso— está la lección 10-04.

El módulo, en una idea

El módulo 7 terminó diciendo que a gestor-tareas le faltaban hábitos. Ahora los tiene todos.

Mensajes que explican el porqué y de los que se deducen la versión y el changelog (08-01). Un historial legible, bisecable y reversible, con una política de integración escrita y acordada (08-02). Solo lo que debe estar dentro (08-03), tratado como corresponde, con los finales de línea de Carla resueltos de una vez (08-04). Los secretos fuera, y un procedimiento escrito para el día en que vuelva a pasar (08-05). Y un repositorio rápido, medido y mantenido (08-06).

Ana, Bruno, Carla y Diego tienen ahora herramienta, proceso y hábitos. Han hecho todo bien.

Y eso es exactamente lo que no ocurre en la realidad.

Porque alguien va a hacer git reset --hard sobre tres días de trabajo sin confirmar. Alguien va a confirmar en la rama equivocada, o con el usuario equivocado, y no se dará cuenta hasta después de publicar. Alguien va a hacer pull sobre una rama que ha divergido y se va a encontrar con un enredo que no sabe deshacer. Alguien va a borrar una rama que sí importaba y va a descubrir, con pánico, que git log ya no la encuentra. Y algún día, un .git/index corrupto va a hacer que Git se niegue a hacer absolutamente nada.

Todo eso tiene solución, y casi siempre una más sencilla de lo que parece en el momento de pánico. Aprender a salir de los líos —deshacer cambios, resolver divergencias con el remoto, recuperar commits perdidos con el reflog, reparar un repositorio roto y diagnosticar lo que no encaja— es todo el módulo 9.

Empezamos por el catálogo de los problemas que todo el mundo se encuentra tarde o temprano, en la lección 09-01: Problemas Comunes de Git.

Dominando Git: De Principiante a Avanzado

Módulo 1: Introducción a Git

Módulo 2: Operaciones Básicas de Git

Módulo 3: Ramas y Fusión

Módulo 4: Trabajando con Repositorios Remotos

Módulo 5: Operaciones Avanzadas de Git

Módulo 6: Herramientas y Técnicas de Git

Módulo 7: Estrategias de Colaboración y Flujo de Trabajo

Módulo 8: Mejores Prácticas y Consejos de Git

Módulo 9: Solución de Problemas y Depuración

Módulo 10: Git en el Mundo Real

© Copyright 2026. Todos los derechos reservados