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 statustarda 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
- Medir antes de optimizar
git count-objects -vH: la foto del repositorio- Encontrar los objetos más grandes del historial
- Empaquetado: objetos sueltos y packfiles
git gc: qué hace realmentegit maintenance: el sustituto moderno- Acelerar
git status:fsmonitoryuntrackedCache feature.manyFilesy otros ajustes- El coste de los ficheros binarios grandes
- Higiene de referencias:
pruneypacked-refs - Otros comandos lentos y sus causas
- Buenas costumbres que evitan el problema
- 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 status12: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:
Y para comparar antes y después de un cambio, mide varias veces:
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 |
git count-objects -vH: la foto del repositorio
git count-objects -vH: la foto del repositorioEs el primer comando que hay que ejecutar. Da el estado de la base de objetos en una pantalla:
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 .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.
- 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"
done214MiB 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.mp4aparece dos veces, con 214 y 71 MB. Son dos versiones del mismo fichero: alguien lo actualizó y Git guarda las dos enteras.mockups.psdtambié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 -15Esta 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.
- 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:
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:
- Compresión conjunta, más eficaz que comprimir cada objeto por separado.
- 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 -5Cuá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.
git gc: qué hace realmente
git gc: qué hace realmentegit gc (garbage collection) es el comando de mantenimiento clásico. Hace cinco cosas:
- Empaqueta los objetos sueltos en packfiles.
- Consolida varios packfiles en menos.
- Elimina objetos inalcanzables que hayan superado el periodo de gracia.
- Empaqueta las referencias en
.git/packed-refs(apartado 10). - Caduca las entradas antiguas del reflog (90 días por defecto para lo alcanzable, 30 para lo demá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
--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 |
Sí, una vez |
| Tras importar desde otro sistema de control de versiones | Sí, 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:
-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:
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).
git maintenance: el sustituto moderno
git maintenance: el sustituto modernoDesde 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
gcmonolítico. - Incremental: reempaqueta poco a poco en lugar de todo de golpe.
- No caduca el reflog por sorpresa.
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 unregisterEl 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 trueEl 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 sAcelera 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
- Acelerar
git status: fsmonitor y untrackedCache
git status: fsmonitor y untrackedCacheEste 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:
- ¿Qué ficheros seguidos han cambiado? → comparar el índice con el disco.
- ¿Qué hay preparado? → comparar el índice con
HEAD. - ¿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:
La primera ejecución arranca un demonio en segundo plano:
El efecto es drástico:
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 trueRequiere 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:
Si la diferencia es grande, tu problema es el recorrido de directorios y fsmonitor es la solución.
feature.manyFiles y otros ajustes
feature.manyFiles y otros ajustesGit agrupa configuraciones recomendadas en "macros" de funcionalidad:
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 2gY un ajuste que no deberías tocar a la ligera:
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.
- 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 bajaEs 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 10MCon 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 sí 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:
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.
- Higiene de referencias:
prune y packed-refs
prune y packed-refsLas 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:
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 conservadorRetomando 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 -dCuidado con
--mergedsi 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 enmain. 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:
git pack-refs las consolida en un único fichero .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:
- 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 dirtyCon componentes-ui como submódulo, esos dos ajustes se notan en cada git status.
- 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 $fallosY 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
- Crea un repositorio y confirma un
app.jspequeño. - Añade un fichero binario grande generado al azar y confírmalo:
head -c 20000000 /dev/urandom > demo/video.bin - Modifícalo tres veces (regenerándolo) y confirma cada versión.
- Ejecuta
git count-objects -vHy anotasize-pack. - Ejecuta el guion del apartado 3 e identifica los objetos grandes.
- Borra el fichero con
git rm, confirma, ejecutagit gcy vuelve a mirarcount-objects. Explica el resultado. - 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
- 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 - Confírmalos y mide
git statustres veces. - Mide
git status --untracked-files=noy calcula qué porcentaje del tiempo se va en el recorrido de directorios. - Activa
core.fsmonitor true, ejecutagit statusdos veces (la primera arranca el demonio) y vuelve a medir. - Prueba
git config feature.manyFiles truey mide de nuevo. - Genera el
commit-graphy comparagit log --graph --allantes y después.
Ejercicio 3: higiene de referencias y mantenimiento
- Crea un repositorio con 50 ramas locales, la mitad de ellas fusionadas en
main. - Cuenta las referencias sueltas con
find .git/refs -type f | wc -l. - Ejecuta
git pack-refs --ally vuelve a contar. Examina.git/packed-refs. - Borra las ramas fusionadas con
git branch --merged. Explica qué pasaría si la política de integración fuera squash. - Activa
git maintenance starty comprueba qué tareas quedan registradas congit config --get-all maintenance.repo --global. - 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"
done76 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.jsCuatro 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 -vHEl 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:
# 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 -vH5,7 MB frente a 76 MB. Con 80 MB de contenido lógico en ambos casos. Dos mecanismos explican la diferencia:
- 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.
- 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"# 3. Sin recorrer los sin seguimiento
/usr/bin/time -f "%e s" git status --untracked-files=no > /dev/nullEl 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; doneDe 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.
# 5. feature.manyFiles
git config feature.manyFiles true
git config --list | grep -E 'index.version|untrackedCache|skipHash'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/nullEn 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# 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.
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 mainno listaría ninguna de esas ramas, aunque su trabajo esté íntegramente enmain.git branch -dse 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.
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.shInterpretació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=1dice exactamente dónde se va el tiempo, ygit count-objects -vHda 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-checkes 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 gcempaqueta, consolida y limpia lo inalcanzable, pero no puede eliminar lo que sigue estando en el historial.--aggressivecasi nunca hace falta: solo tras una reescritura completa o una importación. git maintenance startes el sustituto moderno delgcmanual: tareas separadas, programadas y en segundo plano. Y trae consigo elcommit-graph, que es la optimización con mejor relación beneficio/coste de toda la lección.git statuses lento por el recorrido de ficheros sin seguimiento, no por el historial. Se mide con--untracked-files=noy se arregla concore.fsmonitor(Git 2.37+) ycore.untrackedCache.feature.manyFilesagrupa 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 trueen la configuración global de todo el mundo, borrado de ramas fusionadas (con cuidado si la política es squash) ypacked-refs, quegcmantiene solo. - Y sobre todo, prevención:
.gitignoredesde el primer commit, un límite de tamaño en el hookpre-commity 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
- ¿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
