La lección anterior resolvió una de las tres formas en que un repositorio se vuelve inmanejable: los ficheros muy grandes. Quedan dos, y son independientes entre sí. Un repositorio puede tener muchísimo historial aunque cada fichero sea diminuto, y puede tener muchísimos ficheros aunque el historial sea corto. Cada problema tiene su síntoma y su remedio, y aplicar el remedio equivocado no arregla nada.
Esta lección cierra dos promesas del curso. La lección 06-05 dejó abierta la comparativa entre submódulos, subtree, paquetes y monorepo, aplazando el monorepo hasta aquí. La 08-06 enseñó a medir y a optimizar un repositorio normal que se ha vuelto lento, y remitió aquí los repositorios verdaderamente enormes. Y el caso 3 de la 10-01 describió el monorepo empresarial explicando el porqué y dejando el cómo para esta lección.
Empecemos por dejar clara una cosa, porque es la más importante de todas: casi nada de lo que hay aquí lo necesita gestor-tareas, ni lo necesitas tú probablemente. Este es material para el día en que te toque un repositorio de cincuenta mil ficheros, y para que sepas reconocer cuándo ese día ha llegado. Aplicar estas técnicas a un repositorio sano añade complejidad, rarezas y una superficie nueva de errores, a cambio de nada.
Contenido
- Las tres dimensiones del crecimiento
- Medir antes de optimizar (y qué medir en cada caso)
- Clones parciales:
--filter=blob:noney--filter=tree:0 - Clon parcial frente a clon superficial
- Sparse checkout y el índice disperso
- Aceleradores del lado del cliente
- Monorepo frente a multirrepo
- Qué hace falta para que un monorepo funcione
- Cuándo NO hace falta nada de esto
- Una receta por síntoma
- Las tres dimensiones del crecimiento
El error más común al enfrentarse a un repositorio lento es tratarlo como un problema. No lo es. Son tres, se manifiestan de forma distinta y se arreglan con herramientas distintas.
| Dimensión | Qué significa | Síntomas típicos | Qué NO lo arregla | Remedio |
|---|---|---|---|---|
| Mucho historial | Cientos de miles o millones de commits; muchos años de vida | git clone tardísimo; git log --graph lento; git blame lento; git branch --contains eterno |
Borrar ficheros; LFS | commit-graph; clon parcial --filter=tree:0; clon superficial para casos de usar y tirar |
| Muchos ficheros | Decenas o cientos de miles de ficheros en la copia de trabajo | git status tarda segundos o minutos; git checkout lento; el editor indexa eternamente |
Reducir el historial; gc |
sparse-checkout --cone + índice disperso; fsmonitor; untrackedCache |
| Ficheros muy grandes | Binarios de decenas o cientos de MB, con muchas versiones | Repositorio de gigabytes; clon lentísimo aunque haya pocos commits; gc costoso |
sparse-checkout; commit-graph |
Git LFS (10-03); sacar el binario del repositorio; clon parcial --filter=blob:none |
Las tres son independientes. Un repositorio puede sufrir una, dos o las tres a la vez, y hay que diagnosticar cada una por separado.
Un ejemplo que aclara la independencia: un repositorio con veinte años de historia y sesenta ficheros de texto sufre la dimensión 1 y ninguna otra. git clone tarda diez minutos y git status es instantáneo. Aplicarle sparse-checkout no serviría absolutamente de nada.
flowchart TD
P["Mi repositorio va lento"] --> Q1{"¿Qué comando<br/>es lento?"}
Q1 -->|"git status<br/>git checkout"| D2["Dimensión 2:<br/>muchos ficheros"]
Q1 -->|"git log<br/>git blame<br/>git branch --contains"| D1["Dimensión 1:<br/>mucho historial"]
Q1 -->|"git clone<br/>git gc"| Q2{"¿El repositorio<br/>pesa mucho?"}
Q2 -->|"sí, GB"| D3["Dimensión 3:<br/>ficheros grandes"]
Q2 -->|"no, pero hay<br/>muchos commits"| D1
D1 --> R1["commit-graph<br/>--filter=tree:0"]
D2 --> R2["sparse-checkout --cone<br/>fsmonitor"]
D3 --> R3["Git LFS (10-03)<br/>--filter=blob:none"]
- Medir antes de optimizar (y qué medir en cada caso)
La regla de la lección 08-06 sigue en vigor y aquí importa más: la intuición sobre qué es lento es casi siempre equivocada. Antes de aplicar ninguna de estas técnicas, mide.
La foto general
count: 0 size: 0 bytes in-pack: 4128394 packs: 3 size-pack: 8.42 GiB prune-packable: 0 garbage: 0 size-garbage: 0 bytes
Las tres mediciones que separan las dimensiones
# Dimensión 1: ¿cuánto historial hay?
git rev-list --count --all
git rev-list --count HEAD
# Dimensión 2: ¿cuántos ficheros hay en la copia de trabajo?
git ls-files | wc -l
# Dimensión 3: ¿cuánto ocupan los objetos más grandes?
git lfs migrate info --everything --above=1Mb 2>/dev/null || \
git rev-list --objects --all \
| git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' \
| awk '$1=="blob" {print $3, $4}' | sort -rn | head -10Tres números que orientan la decisión, con órdenes de magnitud aproximados:
| Medición | Sin problema | Empieza a doler | Necesita técnicas de esta lección |
|---|---|---|---|
Commits (rev-list --count --all) |
< 20.000 | 50.000 – 300.000 | > 500.000 |
Ficheros (ls-files | wc -l) |
< 10.000 | 20.000 – 80.000 | > 100.000 |
Tamaño (size-pack) |
< 500 MB | 1 – 3 GB | > 5 GB |
Son referencias, no umbrales exactos: dependen mucho del hardware, del sistema de ficheros y del sistema operativo. Un repositorio de 30.000 ficheros va fino en un portátil moderno con Linux y puede ser penoso en Windows con un antivirus revisando cada acceso.
Dónde se va el tiempo exactamente
d0 | main | region_enter | r1 | 0.001 | index:do_read_index d0 | main | region_leave | r1 | 1.842 | index:do_read_index d0 | main | region_enter | r1 | 1.843 | dir:untracked d0 | main | region_leave | r1 | 9.104 | dir:untracked d0 | main | region_leave | r1 | 11.203 | status
Lectura del diagnóstico: 1,8 segundos leyendo el índice y 9,1 segundos recorriendo directorios buscando ficheros sin seguimiento. Es un problema de dimensión 2 puro. Ni el commit-graph ni el clon parcial harían nada por él; lo que hace falta es untrackedCache, fsmonitor y, si procede, sparse-checkout.
Y gestor-tareas, para tener la referencia:
git rev-list --count --all # 412
git ls-files | wc -l # 9
git count-objects -vH # size-pack: 2.14 MiBNinguna de las tres dimensiones. Nada de esta lección le aplica.
- Clones parciales:
--filter=blob:none y --filter=tree:0
--filter=blob:none y --filter=tree:0El clon parcial es, con diferencia, la técnica más útil y menos conocida de esta lección.
La idea
Un clon normal descarga todos los objetos alcanzables: todos los commits, todos los árboles y todos los blobs de toda la historia. Un clon parcial descarga el grafo pero omite ciertos objetos, y los pide al servidor bajo demanda cuando algún comando los necesita.
Esto requiere que el servidor lo soporte —lo hacen las plataformas principales y las versiones modernas de Git— y funciona sobre una conexión persistente llamada promisor remote: el remoto "promete" tener los objetos que faltan.
flowchart LR
subgraph normal["Clon completo"]
A1["Todos los commits"]
A2["Todos los árboles"]
A3["Todos los blobs<br/>de toda la historia"]
end
subgraph parcial["Clon con --filter=blob:none"]
B1["Todos los commits"]
B2["Todos los árboles"]
B3["Solo los blobs de<br/>la revisión extraída"]
B4["El resto: bajo demanda"]
end
--filter=blob:none: sin contenido histórico
Descarga todos los commits y todos los árboles, pero ningún blob salvo los necesarios para extraer la revisión de trabajo.
Qué sigue funcionando sin descargar nada:
git log --oneline --graph --all # el grafo está completo
git log --format=%s # los mensajes están en los commits
git branch -a --contains a1b2c3d # topología pura
git tag --contains a1b2c3d
git log --name-only # los nombres están en los árboles
git rev-list --count --allQué provoca una descarga bajo demanda:
git log -p # necesita el contenido para el diff
git diff HEAD~50 # necesita blobs antiguos
git blame app.js # necesita todas las versiones del fichero
git checkout v1.0.0 # necesita los blobs de esa revisiónVerlo en marcha:
remote: Enumerating objects: 47, done.
remote: Counting objects: 100% (47/47), done.
Receiving objects: 100% (47/47), 182.41 KiB | 2.11 MiB/s, done.
4f8a2e6c (Ana Ferrer 2026-07-31 13:12:04 +0200 1) function calcularPendientes(tareas) {
...Git ha ido a buscar las 47 versiones del fichero que necesitaba para el blame, y luego ha respondido. Es más lento la primera vez y queda cacheado para las siguientes.
Los objetos que ya se han traído se quedan:
Ese --missing=print lista con un ? delante los objetos que faltan localmente. Es la forma de saber cuánto queda por descargar.
--filter=tree:0: solo commits
Aún más agresivo: omite también los árboles. Solo se descargan los objetos commit.
Con esto sigue funcionando git log --oneline, git log --graph, git rev-list y cualquier consulta puramente topológica o de mensajes. Pero casi cualquier operación sobre ficheros dispara descargas, incluido git log --name-only, porque los nombres viven en los árboles.
Es la opción para casos muy concretos: analizar el grafo de un repositorio enorme, contar commits, extraer estadísticas de autoría, generar un registro de cambios. Para trabajar a diario, --filter=blob:none es casi siempre la elección correcta.
Otros filtros
# Omitir blobs de más de 1 MB (útil si hay binarios sueltos)
git clone --filter=blob:limit=1m https://git.ejemplo.es/proyecto.git
# Aplicar el filtro a un repositorio ya clonado
git config remote.origin.promisor true
git config remote.origin.partialclonefilter blob:noneblob:limit=1m es un punto intermedio interesante: trae todo el código (que es pequeño) y deja fuera los binarios pesados. Sin necesitar LFS.
Comparación en cifras
Para un repositorio hipotético de gran tamaño, con órdenes de magnitud aproximados:
| Tipo de clon | Se descarga | Tiempo relativo | Historial completo |
|---|---|---|---|
| Completo | ~8 GB | 100 % | Sí |
--filter=blob:none |
~500 MB | ~8 % | Sí (grafo completo) |
--filter=tree:0 |
~150 MB | ~3 % | Sí (solo commits) |
--depth=1 |
~200 MB | ~4 % | No |
La fila que importa es la segunda: el 8 % de la descarga conservando el historial completo. Eso es lo que hace del clon parcial la opción por defecto razonable para repositorios grandes.
- Clon parcial frente a clon superficial
En la lección 02-02 vimos el clon superficial --depth=1. Conviene contrastarlos, porque resuelven cosas distintas y el superficial tiene trampas que el parcial no tiene.
--depth=N (superficial) |
--filter=blob:none (parcial) |
|
|---|---|---|
| Qué omite | Commits anteriores a la profundidad indicada | Blobs no necesarios ahora |
| El grafo | Truncado: los commits antiguos no existen | Completo |
git log |
Solo los N últimos commits | Todo el historial |
git blame |
Solo hasta el corte | Completo (descargando) |
git bisect |
Inutilizable | Funciona |
git describe |
Falla o da resultados raros | Funciona |
git merge-base con una rama antigua |
Falla: no hay antepasado común | Funciona |
git log v1.0.0..main |
Falla: la etiqueta no existe | Funciona |
| Fusionar / rebasar sobre base antigua | Problemático | Normal |
| Recuperar lo que falta | git fetch --unshallow (descarga completa) |
Automático y transparente |
| Soporte del servidor | Universal | Requiere servidor moderno |
| Uso típico | CI que solo compila la revisión actual | Trabajo diario en repositorios grandes |
Por qué el parcial suele ser mejor
1. No rompe la semántica de Git. Un clon superficial es un repositorio incompleto: hay commits que sencillamente no existen, y cualquier operación que necesite alcanzarlos falla. Un clon parcial es un repositorio completo con contenido diferido: todo existe, algunas cosas se descargan al pedirlas.
Este es el argumento de fondo. Con --depth, Git te mentirá sobre la historia y tú tendrás que recordar que está truncado. Con --filter, Git te dirá la verdad y descargará lo que le falte.
2. La recuperación es transparente. Si un clon superficial necesita algo antiguo, hay que ejecutar git fetch --unshallow, que descarga el repositorio entero de golpe. Un clon parcial trae solo los objetos concretos que le hacen falta, sin que tú intervengas.
3. Los errores del superficial son confusos. Este es el clásico en una tubería de CI:
O peor, en una comparación de propuesta:
Ambos fallan porque el punto de divergencia está fuera de la profundidad descargada. Es el motivo de que tantas configuraciones de CI lleven fetch-depth: 0 (descargar todo), que resuelve el problema por la vía cara. La alternativa buena:
- uses: actions/checkout@v4
with:
fetch-depth: 0
filter: blob:none # historial completo, sin contenido históricoHistorial completo para las comparaciones y el describe, sin descargar gigabytes de blobs antiguos. Es la configuración recomendada para CI en repositorios grandes.
Cuándo sigue siendo mejor el superficial
- El servidor no soporta clones parciales.
- Un trabajo de CI que solo compila la revisión actual y no consulta nada del historial:
--depth=1es mínimo y suficiente. - Un contenedor de despliegue donde solo quieres los ficheros de una versión concreta. Aunque para eso,
git archivesuele ser aún mejor:
Eso no crea repositorio: descarga solo los ficheros de esa etiqueta.
- Sparse checkout y el índice disperso
El clon parcial ataca lo que se descarga. sparse-checkout ataca lo que aparece en el disco, que es un problema distinto: la dimensión 2.
El problema
Un monorepo con 300.000 ficheros. Ana trabaja solo en servicios/tareas/, que son 400. Pero su copia de trabajo tiene los 300.000, y en consecuencia:
git statusrecorre 300.000 entradas.- El editor indexa 300.000 ficheros.
- Cada
git checkoutentre ramas comprueba 300.000 rutas. - La búsqueda de texto atraviesa código que no le interesa.
La solución
# Activar el modo disperso, en modo cono
git sparse-checkout init --cone
# Declarar qué directorios quiero en el disco
git sparse-checkout set servicios/tareas bibliotecas/componentes-ui
# Ver qué hay declarado
git sparse-checkout list
# Añadir más sin reescribir lo anterior
git sparse-checkout add servicios/notificaciones
# Volver al estado normal
git sparse-checkout disableDespués de esto, la copia de trabajo contiene solo esos directorios (más los ficheros de la raíz), y git status pasa de recorrer 300.000 entradas a recorrer unas pocas miles.
Y una forma más directa de clonar con esto ya activo:
git clone --filter=blob:none --sparse https://git.ejemplo.es/monorepo.git
cd monorepo
git sparse-checkout set servicios/tareasEsa combinación —clon parcial + sparse checkout— es el montaje estándar para trabajar en un monorepo grande: descargas poco y materializas menos.
Por qué el modo cono es el que escala
sparse-checkout tiene dos modos, y la diferencia es crucial.
| Modo original (patrones) | Modo cono (--cone) |
|
|---|---|---|
| Qué acepta | Patrones tipo .gitignore, arbitrariamente complejos |
Solo rutas de directorio |
| Cómo se decide cada fichero | Evaluando la lista de patrones fichero a fichero | Comprobando el prefijo de directorio |
| Coste | O(ficheros × patrones) | O(directorios), con búsqueda por prefijo |
| Permite índice disperso | No | Sí |
| Expresividad | Alta | Limitada a directorios completos |
El modo cono renuncia a la expresividad para ganar una propiedad estructural: como los patrones son siempre directorios enteros, Git puede razonar sobre subárboles completos en lugar de sobre ficheros sueltos. Y eso habilita lo verdaderamente importante.
El índice disperso
Recuerda de la lección 01-04 y de la 08-06 que el índice (.git/index) contiene una entrada por fichero seguido. En un monorepo de 300.000 ficheros, el índice tiene 300.000 entradas y ocupa decenas de megabytes. Leerlo y escribirlo es la primera causa de lentitud de git status, y ocurre aunque los ficheros no estén en el disco: el índice los sigue listando.
El índice disperso (sparse index) resuelve eso. En lugar de una entrada por fichero, guarda una única entrada por directorio que está fuera del cono, apuntando directamente a su objeto árbol.
Índice normal (300.000 entradas): servicios/tareas/app.js servicios/tareas/index.html ... servicios/facturacion/main.js <- fuera del cono servicios/facturacion/modelo.js <- fuera del cono ... (280.000 más, todas fuera del cono) Índice disperso (~4.000 entradas): servicios/tareas/app.js servicios/tareas/index.html ... servicios/facturacion/ (tree 8a1f6c3d) <- UNA entrada servicios/pagos/ (tree 2e9f4c7b) <- UNA entrada
Se activa así:
Y se comprueba:
git ls-files --sparse | grep '/$' | head
test-tool read-cache --table | wc -l # si tienes las herramientas de pruebaEl efecto sobre git status en un monorepo grande es de un orden de magnitud. Y el motivo es puramente estructural: una entrada de árbol representa un subárbol entero, gracias a que el hash de un árbol resume todo su contenido (lección 01-04). Si el hash del árbol de servicios/facturacion/ no ha cambiado, Git sabe con certeza matemática que nada de ese subdirectorio ha cambiado, sin mirar un solo fichero.
Este es un ejemplo precioso de por qué entender el modelo de datos importa: el índice disperso no es un truco, es una consecuencia directa del direccionamiento por contenido.
El efecto sobre git status y otros comandos
Con sparse-checkout activo, git status incluye un aviso:
On branch main You are in a sparse checkout with 3% of tracked files present. nothing to commit, working tree clean
Ese aviso es importante y hay que saber leerlo. No estás viendo el repositorio entero. Si buscas un fichero y no aparece, quizá no es que no exista: es que está fuera de tu cono.
Comandos que se comportan de forma distinta:
# Lista solo lo del cono
git ls-files
# Lista TODO lo seguido, dentro y fuera del cono
git ls-files --sparse
# Un fichero fuera del cono sigue existiendo en el historial
git show HEAD:servicios/facturacion/main.js # funciona perfectamente
# git grep busca solo en lo materializado por defecto
git grep "calcularTotal"Y la rareza que más desconcierta: si haces git checkout de una rama que borra un fichero fuera de tu cono, no verás nada, porque ese fichero nunca estuvo en tu disco. Todo es coherente, pero requiere tener el modelo claro.
- Aceleradores del lado del cliente
Estos ya aparecieron en la lección 08-06; aquí van con el matiz de la escala.
commit-graph: acelerar el recorrido del grafo
Es un fichero de índice auxiliar que guarda, precalculada, la topología del historial: para cada commit, sus padres, su fecha y su número de generación (la distancia al commit raíz).
# Generarlo
git commit-graph write --reachable --changed-paths
# Que se mantenga solo
git config fetch.writeCommitGraph true
git config core.commitGraph trueSin él, responder "¿es A antepasado de B?" obliga a leer objetos commit del disco y recorrer el grafo. Con él, el número de generación permite descartar ramas enteras del recorrido sin leer nada.
El --changed-paths merece atención especial en repositorios enormes: guarda un filtro de Bloom con las rutas modificadas en cada commit. Sirve para acelerar drásticamente:
En un monorepo, git log sobre un fichero concreto pasa de examinar millones de commits a descartarlos casi todos con una comprobación de bits. Es la diferencia entre treinta segundos y medio segundo.
Es la optimización de dimensión 1 con mejor relación coste/beneficio, y no tiene ningún inconveniente: es un fichero de caché regenerable que no altera nada del repositorio.
fsmonitor: no recorrer el disco
fsmonitor arranca un demonio que escucha las notificaciones del sistema de ficheros y mantiene la lista de lo que ha cambiado. Así git status no tiene que recorrer los directorios: pregunta al demonio.
untrackedCache guarda en el índice la última exploración de cada directorio junto con su marca de tiempo, para no volver a explorar los que no se han tocado.
Efecto conjunto en un repositorio de 100.000 ficheros, como orden de magnitud: git status puede pasar de varios segundos a fracciones de segundo. La combinación con el índice disperso es multiplicativa, porque atacan partes distintas del coste: fsmonitor reduce lo que hay que mirar en el disco, el índice disperso reduce lo que hay que leer del índice.
Nota de plataforma: fsmonitor integrado funciona en macOS y Windows de serie; en Linux depende de la versión de Git y puede requerir configuración adicional. Mídelo antes y después en tu entorno concreto.
git maintenance: que se mantenga solo
Programa tareas periódicas en el planificador del sistema: actualizar el commit-graph, empaquetar objetos sueltos de forma incremental, hacer prefetch de los remotos y limpiar referencias.
Frente al gc automático de siempre, tiene dos ventajas: se ejecuta cuando no estás trabajando en lugar de interrumpirte, e incluye tareas que gc no hace, como el prefetch (que descarga en segundo plano lo que otros publican, de modo que tu git fetch sea instantáneo).
Configuración recomendada para un repositorio grande:
git maintenance register
git config maintenance.commit-graph.enabled true
git config maintenance.prefetch.enabled true
git config maintenance.incremental-repack.enabled true
git config maintenance.loose-objects.enabled true
git config maintenance.gc.enabled false # lo sustituyen las anteriores
git maintenance startAjustes agrupados
Git ofrece dos "paquetes" de configuración que activan varias cosas a la vez:
# Repositorios con muchos ficheros
git config feature.manyFiles true
# Los ajustes experimentales recomendados de la versión
git config feature.experimental truefeature.manyFiles activa index.version=4 (formato de índice más compacto), core.untrackedCache=true y index.skipHash=true. Es el atajo razonable para la dimensión 2. Lo que hace exactamente puede cambiar entre versiones de Git, así que conviene comprobarlo con git help config.
- Monorepo frente a multirrepo
Aquí se cierra la comparativa que la lección 06-05 dejó abierta y que el caso 3 de la 10-01 dejó pendiente.
Las dos posturas
Multirrepo: cada proyecto o biblioteca tiene su repositorio. La relación entre ellos se establece con versiones publicadas (paquetes), con submódulos o con subtree.
Monorepo: un único repositorio contiene muchos proyectos, que se relacionan por rutas de directorio y se compilan juntos.
flowchart TB
subgraph multi["Multirrepo"]
M1["repo: app-web"] -->|"depende de v2.1.0"| M4["repo: componentes-ui"]
M2["repo: servicio-datos"] -->|"depende de v2.0.3"| M4
M3["repo: app-movil"] -->|"depende de v1.9.0"| M4
end
subgraph mono["Monorepo"]
R["repo único"] --- A["/app-web"]
R --- B["/servicio-datos"]
R --- C["/app-movil"]
R --- D["/bibliotecas/componentes-ui"]
end
Fíjate en las versiones del diagrama de la izquierda: tres consumidores usando tres versiones distintas de la misma biblioteca. Eso es lo normal en multirrepo, y es a la vez su mayor ventaja (autonomía) y su mayor problema (deriva).
La comparativa completa
| Criterio | Monorepo | Multirrepo |
|---|---|---|
| Cambio atómico entre proyectos | Sí: un commit cambia la biblioteca y sus 30 consumidores. Nunca hay estado incoherente | No: 31 propuestas coordinadas, versiones de transición, semanas |
| Refactorización global | Búsqueda y sustitución + un commit | Un proyecto en sí mismo |
| Versionado de dependencias internas | No existe: todos usan main |
Cada consumidor fija una versión; aparece deriva |
| Autonomía de los equipos | Menor: comparten main, CI y herramientas |
Mayor: cada equipo decide su ritmo y su publicación |
| Propiedad del código | Necesita CODEOWNERS por directorio (10-01) |
Natural: por repositorio |
| Control de acceso | Difícil de granular: quien clona ve todo | Natural: permisos por repositorio |
| Integración continua | Debe ser selectiva, calculando lo afectado | Simple: cada repositorio, lo suyo |
| Tamaño del repositorio | Crece sin límite; exige las técnicas de esta lección | Cada uno se mantiene pequeño |
| Herramientas necesarias | Sistema de compilación con grafo de dependencias, CI selectiva, escalado de Git | Gestor de paquetes, registro de artefactos |
| Descubrimiento de código | Excelente: todo es buscable | Cuesta saber qué existe y dónde |
| Incorporación de gente nueva | Un clon (grande) y ya lo tienes todo | Hay que averiguar qué repositorios clonar |
| Publicación | Continua; no hay versión global | Versiones independientes por proyecto |
git bisect entre proyectos |
Funciona: un solo historial | Imposible sin coordinación manual |
| Historial | Enorme y mezclado; hace falta filtrar por ruta | Limpio y específico por proyecto |
| Coste de empezar | Alto: no funciona sin invertir en herramientas | Bajo: es lo que sale por defecto |
Cuándo elegir cada uno
Monorepo tiene sentido cuando:
- Los proyectos cambian juntos a menudo: el síntoma inequívoco es que una propuesta necesita cambios coordinados en varios repositorios.
- Hay bibliotecas internas compartidas y la deriva de versiones ya duele.
- Todo el mundo puede ver todo el código.
- Hay capacidad de invertir en herramientas y de mantenerlas.
- Se quiere que la refactorización global sea posible.
Multirrepo tiene sentido cuando:
- Los proyectos son realmente independientes y evolucionan a ritmos distintos.
- Hay requisitos de acceso que obligan a separar.
- Los equipos son autónomos y publican por su cuenta.
- Algunos componentes se publican fuera de la organización.
- No hay capacidad de mantener herramientas propias.
- Se prefiere la solución que funciona sola.
La pregunta que decide
De todos los criterios, uno pesa más que los demás:
¿Con qué frecuencia un cambio necesita tocar varios repositorios a la vez?
Si la respuesta es "casi nunca", multirrepo, sin dudarlo. Si es "constantemente, y es nuestro mayor cuello de botella", el monorepo resuelve exactamente ese problema y merece la inversión.
Y recuerda la conclusión de la lección 06-05: los submódulos no resuelven esto. Atan una versión concreta de una biblioteca a un consumidor —dan trazabilidad— pero cada actualización sigue siendo un commit en cada consumidor. Son un multirrepo con trazabilidad, no un monorepo barato.
El camino intermedio
No es una decisión binaria ni irreversible. Los patrones intermedios más habituales:
-
Monorepo por dominio. Un repositorio por área grande (
plataforma,producto,datos), no uno por servicio ni uno para todo. Captura la mayor parte del beneficio de atomicidad sin llegar a escalas que exijan herramientas exóticas. Es el punto óptimo para la mayoría de organizaciones medianas. -
Monorepo para las bibliotecas compartidas. Se agrupan solo las bibliotecas internas, que son las que sufren la deriva de versiones, y los productos siguen separados. Es un paso reversible y con beneficio inmediato.
-
Migración gradual. Se puede fusionar un repositorio dentro de otro conservando su historial:
cd ~/monorepo
git remote add componentes-ui https://git.ejemplo.es/componentes-ui.git
git fetch componentes-ui
# Traer su historial reubicado bajo un subdirectorio
git merge -s ours --no-commit --allow-unrelated-histories componentes-ui/main
git read-tree --prefix=bibliotecas/componentes-ui/ -u componentes-ui/main
git commit -m "chore: GT-270 incorporar componentes-ui como bibliotecas/componentes-ui
Se conserva el historial completo del repositorio original."La combinación merge -s ours + read-tree --prefix es el mecanismo clásico: el merge conecta los dos historiales sin traer contenido, y el read-tree coloca el árbol del otro repositorio bajo el prefijo indicado. El resultado es que git log --follow bibliotecas/componentes-ui/boton.js sigue funcionando hacia atrás. Es también, esencialmente, lo que hace git subtree add por debajo (lección 06-05).
- Qué hace falta para que un monorepo funcione
Si la decisión es monorepo, estas piezas no son opcionales. Un monorepo sin ellas es simplemente un repositorio lento y caótico.
- Escalado de Git (todo lo anterior)
# Clon estándar para el equipo
git clone --filter=blob:none --sparse https://git.ejemplo.es/monorepo.git
cd monorepo
git sparse-checkout set servicios/tareas bibliotecas/componentes-ui
# Aceleradores
git config core.fsmonitor true
git config core.untrackedCache true
git config index.sparse true
git config feature.manyFiles true
git maintenance startEsto conviene ponerlo en un guion de incorporación en el propio repositorio, porque nadie va a recordar seis comandos.
- Propiedad del código
El fichero CODEOWNERS que vimos en la lección 10-01. Sin él, el monorepo pierde la propiedad clara que el multirrepo da gratis.
- Integración continua selectiva
Ya vimos el esqueleto en la 10-01. La versión que importa aquí es la basada en el grafo de dependencias, no en directorios:
name: CI selectiva del monorepo
on: [pull_request]
jobs:
afectados:
runs-on: ubuntu-latest
outputs:
proyectos: ${{ steps.calc.outputs.lista }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
filter: blob:none
- id: calc
run: |
# Rutas cambiadas desde el punto de divergencia
CAMBIOS=$(git diff --name-only origin/main...HEAD)
# El sistema de compilación traduce rutas -> proyectos afectados,
# incluyendo los que dependen de lo que ha cambiado
AFECTADOS=$(./herramientas/afectados.sh $CAMBIOS)
echo "lista=$AFECTADOS" >> "$GITHUB_OUTPUT"
echo "Proyectos afectados: $AFECTADOS"
probar:
needs: afectados
if: needs.afectados.outputs.proyectos != ''
runs-on: ubuntu-latest
strategy:
matrix:
proyecto: ${{ fromJson(needs.afectados.outputs.proyectos) }}
steps:
- uses: actions/checkout@v4
with:
sparse-checkout: |
${{ matrix.proyecto }}
bibliotecas
- run: ./herramientas/probar.sh ${{ matrix.proyecto }}Fíjate en el sparse-checkout dentro del trabajo de CI: cada ejecución materializa solo el proyecto que va a probar. Es la misma técnica del apartado 5, aplicada a la tubería.
Lo que hace afectados.sh es lo que distingue una CI selectiva buena de una ingenua: si cambia bibliotecas/componentes-ui, no basta con probar esa biblioteca; hay que probar todos los proyectos que dependen de ella. Eso exige un grafo de dependencias explícito, que lo proporciona el sistema de compilación.
- Sistema de compilación con caché
En un monorepo, compilar todo en cada cambio es inviable. Hace falta un sistema que entienda el grafo de dependencias y que cachee resultados por hash de las entradas: si nada de lo que entra en un objetivo ha cambiado, se reutiliza el resultado anterior.
Es, curiosamente, el mismo principio que sostiene Git: identificar por el hash del contenido y no recalcular lo que no ha cambiado.
- Convenciones y automatización de migraciones
Con muchos proyectos en un repositorio, hacen falta convenciones fuertes: estructura de directorios homogénea, nombres predecibles, y herramientas para aplicar cambios masivos ("renombrar esta función en 200 sitios") de forma automática y revisable.
- Cuándo NO hace falta nada de esto
Este apartado es tan importante como los anteriores.
gestor-tareas no necesita nada de esta lección. Ni clones parciales, ni sparse-checkout, ni fsmonitor, ni monorepo. Tiene nueve ficheros, 412 commits y pesa 2 MB. git status responde en milisegundos.
Y no es un caso excepcional: la inmensa mayoría de los repositorios del mundo están en esa situación. Un proyecto con 5.000 ficheros, 30.000 commits y 200 MB va perfectamente con Git de serie en cualquier ordenador moderno.
El coste de optimizar sin necesidad
| Técnica | Coste si no la necesitas |
|---|---|
| Clon parcial | Latencia inesperada en operaciones normales; comportamiento raro sin conexión; requiere servidor compatible |
sparse-checkout |
Ficheros que "no existen" y confunden a todo el mundo; herramientas que fallan sin explicación; grep que no encuentra cosas que sí están |
--depth |
Historial truncado, bisect inutilizable, comparaciones de propuesta que fallan |
| Monorepo | Repositorio grande sin ninguna de las ventajas, porque los proyectos no cambiaban juntos |
fsmonitor |
Un demonio más consumiendo recursos, sin beneficio medible |
La técnica más peligrosa de la lista es sparse-checkout. Un compañero nuevo que hereda una configuración dispersa sin saberlo puede pasarse horas sin entender por qué un fichero que ve en la web no está en su disco.
La regla
Si
git statusresponde en menos de un segundo ygit clonetarda menos de un minuto, no tienes ningún problema que estas técnicas resuelvan.
Mide primero (apartado 2). Aplica el remedio de la dimensión que duela. Mide después. Y si no duele nada, no hagas nada.
- Una receta por síntoma
Tabla de consulta rápida.
| Síntoma | Dimensión | Diagnóstico | Remedio |
|---|---|---|---|
git clone tarda muchísimo y el repositorio pesa GB |
3 (y/o 1) | git count-objects -vH; buscar blobs grandes |
LFS (10-03); --filter=blob:none; sacar binarios |
git status tarda segundos |
2 | GIT_TRACE2_PERF muestra dir:untracked alto |
fsmonitor, untrackedCache, feature.manyFiles; si sigue, sparse-checkout --cone --sparse-index |
git log -- ruta/fichero tarda muchísimo |
1 | Muchos commits | commit-graph write --reachable --changed-paths |
git blame tarda muchísimo |
1 | Muchas revisiones del fichero | commit-graph; blame -w -M no ayuda al rendimiento |
git checkout entre ramas es lento |
2 | Muchos ficheros que actualizar | sparse-checkout; fsmonitor |
| La CI clona 8 GB cincuenta veces al día | 1 y 3 | Configuración de la tubería | filter: blob:none + fetch-depth: 0; caché de repositorio; lfs: false (10-03) |
git gc tarda una hora |
3 | Objetos grandes incomprimibles | LFS; git maintenance incremental en vez de gc completo |
| Un cambio necesita 12 propuestas coordinadas | Organizativa | Multirrepo con acoplamiento fuerte | Considerar monorepo o monorepo por dominio |
| El repositorio va bien | Ninguna | — | No hagas nada |
Errores Comunes y Consejos
Error 1: optimizar sin medir. El error central de la lección. Aplicar sparse-checkout a un problema de historial no hace nada. Diagnostica la dimensión primero.
Error 2: usar --depth=1 en CI y luego necesitar el historial. Produce fatal: no merge base y fatal: No names found. Usa fetch-depth: 0 con filter: blob:none.
Error 3: sparse-checkout sin --cone. El modo de patrones es más lento, no permite índice disperso y es mucho más fácil de configurar mal. Usa --cone salvo que tengas una razón muy concreta.
Error 4: olvidar que sparse-checkout está activo. Documenta la configuración dispersa en el README.md y recuerda que git status avisa con "You are in a sparse checkout with N% of tracked files present".
Error 5: creer que el clon parcial funciona sin conexión. Si trabajas en un avión con un clon parcial y ejecutas git log -p sobre historia antigua, fallará: necesita ir al servidor. Antes de desconectarte, trae lo que vayas a necesitar:
Error 6: elegir monorepo por moda. Sin CI selectiva, sin CODEOWNERS y sin sistema de compilación con caché, un monorepo es peor que lo que tenías. La pregunta no es "¿qué hacen las empresas grandes?", sino "¿con qué frecuencia un cambio nuestro toca varios repositorios?".
Error 7: creer que los submódulos son un monorepo barato. No lo son: no dan atomicidad. Cada actualización de la biblioteca sigue exigiendo un commit en cada consumidor (lección 06-05).
Consejo 1: commit-graph para todo el mundo. Es la única optimización de la lección sin ningún inconveniente. Actívala aunque tu repositorio sea mediano:
Consejo 2: git maintenance start en tus repositorios grandes. Mejor que esperar al gc automático, y sin interrumpirte.
Consejo 3: mide con hyperfine o con un bucle sencillo. Antes y después, varias veces:
Consejo 4: pon un guion de incorporación en el repositorio. Si tu monorepo necesita seis comandos de configuración, ponlos en herramientas/configurar.sh y menciónalo en el README.md.
Consejo 5: la CI también puede usar sparse checkout. Los flujos modernos permiten declarar qué directorios materializar. En un monorepo, eso reduce cada trabajo de minutos a segundos.
Ejercicios
Ejercicio 1: diagnóstico por dimensiones
Para cada repositorio, di qué dimensión sufre, qué NO lo arreglaría y qué sí:
A.
$ git rev-list --count --all 1847293 $ git ls-files | wc -l 1204 $ git count-objects -vH size-pack: 1.84 GiB $ time git status real 0m0.089s $ time git log --oneline -- src/nucleo.c real 0m47.203s
B.
$ git rev-list --count --all 8420 $ git ls-files | wc -l 284917 $ git count-objects -vH size-pack: 3.21 GiB $ time git status real 0m41.887s
C.
$ git rev-list --count --all 1204 $ git ls-files | wc -l 89 $ git count-objects -vH size-pack: 6.72 GiB $ time git clone . real 8m12.443s
Ejercicio 2: configurar el puesto de trabajo en un monorepo
Ana se incorpora a un equipo con un monorepo: 340.000 ficheros, 2,1 millones de commits, 12 GB. Ella trabajará solo en servicios/tareas/ y usará bibliotecas/componentes-ui/.
Escribe la secuencia completa de comandos, desde el clon hasta un puesto operativo, explicando qué ataca cada uno y a qué dimensión. Incluye también qué le dirías sobre las rarezas con las que se va a encontrar.
Ejercicio 3: decidir monorepo o multirrepo
Una empresa de 120 personas tiene 23 repositorios: 8 servicios, 6 aplicaciones cliente, 9 bibliotecas internas.
Síntomas que reportan:
- Cambiar la interfaz de la biblioteca
autenticaciontarda seis semanas en llegar a todos los consumidores. - Hay cuatro versiones distintas de
componentes-uien producción a la vez. - Nadie sabe qué servicios usan qué bibliotecas.
- Un colaborador nuevo tarda tres días en clonar y configurar lo que necesita.
- Dos de los servicios los mantiene una empresa externa que no debe ver el resto del código.
- Las bibliotecas
graficosyutilidadesse publican públicamente como código abierto.
Decide qué recomendarías, con qué pasos y en qué orden. Señala explícitamente qué no meterías en el monorepo y por qué.
Soluciones
Solución 1
A. Dimensión 1 pura: mucho historial.
Indicios: 1,8 millones de commits pero solo 1.204 ficheros. git status responde en 89 ms (la dimensión 2 no existe). El repositorio pesa 1,84 GB, lo cual es coherente con 1,8 millones de commits de código de texto, no con binarios.
El síntoma decisivo: git log sobre un fichero tarda 47 segundos. Git tiene que recorrer millones de commits comprobando si cada uno tocó src/nucleo.c.
Qué NO lo arreglaría: sparse-checkout (solo hay 1.204 ficheros y status ya es instantáneo), LFS (no hay binarios), fsmonitor (nada que recorrer en disco).
Qué sí:
# El remedio principal: filtros de Bloom de rutas modificadas
git commit-graph write --reachable --changed-paths
git config core.commitGraph true
git config fetch.writeCommitGraph true
# Que se mantenga solo
git maintenance start
# Y para clones nuevos, sobre todo en CI
git clone --filter=blob:none https://git.ejemplo.es/proyecto.gitCon --changed-paths, ese git log -- src/nucleo.c de 47 segundos debería bajar a menos de un segundo: el filtro de Bloom descarta la inmensa mayoría de los commits sin abrir sus árboles.
B. Dimensión 2 dominante: muchos ficheros.
Indicios: 285.000 ficheros, solo 8.420 commits (el historial es corto), y git status tarda 42 segundos. Los 3,21 GB son consistentes con muchos ficheros, no necesariamente con binarios grandes.
Qué NO lo arreglaría: commit-graph (con 8.420 commits, el historial no es el problema), clon superficial (tampoco).
Qué sí, en orden de menor a mayor intrusión:
# 1. Primero lo barato y sin efectos secundarios
git config core.untrackedCache true
git config core.fsmonitor true
git config feature.manyFiles true
# 2. Medir de nuevo
for i in 1 2 3; do /usr/bin/time -f "%e s" git status >/dev/null; done
# 3. Si sigue doliendo, materializar solo lo necesario
git sparse-checkout init --cone --sparse-index
git config index.sparse true
git sparse-checkout set mi/area/de/trabajoEl orden importa: los pasos 1 y 2 no cambian lo que ves en el disco y pueden resolverlo. El paso 3 sí lo cambia y tiene coste de comprensión para todo el equipo.
Conviene además comprobar si esos 3,21 GB esconden un problema de dimensión 3:
C. Dimensión 3 pura: ficheros muy grandes.
Indicios inequívocos: 89 ficheros, 1.204 commits, 6,72 GB. La aritmética no deja lugar a dudas: 6,72 GB entre 89 ficheros son unos 75 MB por fichero de media. Son binarios.
Qué NO lo arreglaría: absolutamente nada de sparse-checkout ni commit-graph. Ni siquiera un clon superficial ayudaría mucho, porque los blobs grandes de la última revisión ya pesan.
Qué sí:
# 1. Confirmar el diagnóstico
git lfs migrate info --everything --above=1Mb
# 2. Decidir por fichero (lección 10-03):
# ¿generado? -> .gitignore
# ¿no hace falta su historial? -> fuera del repositorio
# ¿binario de producto con historial? -> LFS
# 3. Migrar, con toda la coordinación de la lección 10-03
git lfs migrate import --everything --include="*.psd,*.mp4,*.zip"
# 4. Paliativo inmediato mientras se decide la migración
git clone --filter=blob:none https://git.ejemplo.es/proyecto.gitEl paso 4 es un alivio útil: el clon parcial evita descargar todas las versiones históricas de los binarios, aunque no arregle el tamaño del repositorio del lado del servidor.
Solución 2
# ============================================================
# 1. CLON: parcial + disperso desde el principio
# Dimensiones 1 y 3 (lo que se descarga)
# ============================================================
git clone --filter=blob:none --sparse \
https://git.ejemplo.es/monorepo.git
cd monorepo--filter=blob:none evita descargar el contenido histórico de 2,1 millones de commits: de 12 GB pasamos a una fracción pequeña, conservando el grafo completo. --sparse hace que el checkout inicial materialice solo la raíz, en vez de escribir 340.000 ficheros en el disco.
# ============================================================
# 2. CONO: qué se materializa en el disco
# Dimensión 2 (lo que hay en la copia de trabajo)
# ============================================================
git sparse-checkout init --cone --sparse-index
git config index.sparse true
git sparse-checkout set servicios/tareas bibliotecas/componentes-ui
git sparse-checkout list--cone permite el índice disperso; --sparse-index e index.sparse hacen que el índice guarde una entrada por directorio fuera del cono en lugar de una por fichero. De ~340.000 entradas a unos pocos miles.
# ============================================================
# 3. ACELERADORES DEL CLIENTE
# ============================================================
git config core.fsmonitor true # dimensión 2: no recorrer el disco
git config core.untrackedCache true # dimensión 2: cachear la exploración
git config feature.manyFiles true # dimensión 2: índice v4, skipHash
git config core.commitGraph true # dimensión 1: recorrido del grafo
git config fetch.writeCommitGraph true
# ============================================================
# 4. MANTENIMIENTO AUTOMÁTICO
# ============================================================
git maintenance register
git config maintenance.prefetch.enabled true
git config maintenance.commit-graph.enabled true
git config maintenance.incremental-repack.enabled true
git maintenance start
# ============================================================
# 5. VERIFICAR
# ============================================================
git ls-files | wc -l # solo lo del cono
git ls-files --sparse | wc -l # entradas reales del índice
for i in 1 2 3; do /usr/bin/time -f "%e s" git status >/dev/null; doneQué le diría a Ana sobre las rarezas:
-
"No vas a ver la mayoría de los ficheros."
git statuste avisará: "You are in a sparse checkout with 2% of tracked files present". No están borrados: no están materializados.git show HEAD:servicios/facturacion/main.jsfunciona perfectamente. -
"Si necesitas otra área, añádela."
git sparse-checkout add servicios/notificaciones. No hace falta clonar de nuevo. -
"Algunas operaciones irán a la red."
git log -psobre historia antigua,git blamede un fichero con muchas versiones, ogit checkoutde una etiqueta vieja descargarán blobs. La primera vez es lenta; después queda cacheado. -
"Antes de irte sin conexión, trae lo que vayas a necesitar." Un clon parcial necesita el servidor para lo que no tiene.
-
"Tu editor y tus búsquedas solo ven tu cono." Eso es bueno para el rendimiento, pero si buscas una función y no aparece, quizá está fuera de tu área.
git grep --no-indexo buscar en la web del repositorio son la salida. -
"Si algo se comporta raro, mira primero
git sparse-checkout list." Es el primer sospechoso de casi cualquier rareza.
Y le pasaría un guion herramientas/configurar-puesto.sh con todo lo anterior, porque nadie recuerda catorce comandos.
Solución 3
Recomendación: monorepo parcial, en tres fases, empezando por las bibliotecas.
El síntoma dominante es inequívoco: seis semanas para propagar un cambio de biblioteca y cuatro versiones de componentes-ui en producción. Eso es exactamente el problema que el monorepo elimina de raíz, y confirma que hay acoplamiento fuerte entre las bibliotecas y sus consumidores.
Pero hay dos restricciones que impiden un monorepo total, y hay que respetarlas.
Qué NO entra en el monorepo, y por qué:
| Qué | Por qué se queda fuera |
|---|---|
| Los 2 servicios de la empresa externa | Control de acceso. Un monorepo da acceso a todo el que clona. Es la restricción más dura y no se negocia: son requisitos contractuales o de confidencialidad, no preferencias técnicas. |
Las bibliotecas graficos y utilidades |
Se publican públicamente. Un proyecto de código abierto necesita su propio repositorio: historial limpio y específico, incidencias y propuestas propias, y ningún riesgo de exponer código interno. Entran en el monorepo como dependencia externa versionada, igual que cualquier paquete de terceros. |
Fase 1: monorepo de bibliotecas internas (7 bibliotecas).
Es el paso con mejor relación beneficio/riesgo, y es reversible.
# Crear el repositorio y absorber cada biblioteca conservando su historial
mkdir plataforma && cd plataforma && git init
for BIB in autenticacion componentes-ui datos registro colas config validacion; do
git remote add "$BIB" "https://git.ejemplo.es/$BIB.git"
git fetch "$BIB"
git merge -s ours --no-commit --allow-unrelated-histories "$BIB/main"
git read-tree --prefix="bibliotecas/$BIB/" -u "$BIB/main"
git commit -m "chore: incorporar $BIB conservando su historial"
doneBeneficio inmediato: un cambio que toca autenticacion y datos a la vez pasa a ser un commit, y git bisect funciona entre bibliotecas.
Estas bibliotecas siguen publicándose como paquetes versionados para los consumidores, así que nada cambia para los 12 productos. Por eso es reversible.
Fase 2: absorber los consumidores más acoplados.
Medir primero cuáles son:
# ¿Qué propuestas de los últimos seis meses tocaron varios repositorios
# el mismo día y por el mismo ticket?
for REPO in servicio-a servicio-b app-web app-movil; do
echo "== $REPO"
git -C "$REPO" log --since=6.months --format='%ad %s' --date=short \
| grep -oE '^[0-9-]+ .*(TICKET-[0-9]+)' | head
doneLos que aparezcan repetidamente junto a cambios de biblioteca son los candidatos. Se absorben esos, no todos.
Fase 3: consolidar el resto, si la experiencia ha sido buena.
Inversión obligatoria antes de la fase 2 (sin esto, el monorepo empeora las cosas):
CODEOWNERSpor directorio — recupera la propiedad que el multirrepo daba gratis.- CI selectiva con grafo de dependencias — sin ella, cada propuesta ejecuta la suite entera.
- Sistema de compilación con caché por hash de entradas — o los tiempos de compilación se disparan.
- Guion de configuración de puesto — con
--filter=blob:none --sparse, como en la solución 2.
Qué resuelve cada síntoma reportado:
| Síntoma | Cómo queda |
|---|---|
| Seis semanas para propagar un cambio de biblioteca | Resuelto: un commit atómico actualiza biblioteca y consumidores |
Cuatro versiones de componentes-ui en producción |
Resuelto: no hay versiones internas, todos usan main |
| Nadie sabe qué usa qué | Resuelto: el grafo de dependencias es explícito y buscable |
| Tres días de configuración inicial | Resuelto: un clon parcial y disperso, con guion |
| La empresa externa no debe ver el resto | Respetado: sus 2 servicios quedan fuera |
graficos y utilidades son públicas |
Respetado: quedan fuera, como dependencias versionadas |
Resultado final: 1 monorepo (7 bibliotecas + los productos internos que se absorban), 2 repositorios privados para la empresa externa, 2 repositorios públicos. De 23 repositorios a 5, sin violar ninguna restricción.
Lo que NO haría: meterlo todo de golpe. La fase 1 es reversible y da beneficio en semanas; una migración total de 23 repositorios sin herramientas es una forma habitual de tener un repositorio de 40 GB, una CI de dos horas y un equipo furioso.
Conclusión
Escalar Git empieza por dejar de tratarlo como un solo problema.
- Son tres dimensiones independientes: mucho historial (
git logyblamelentos), muchos ficheros (git statuslento) y ficheros muy grandes (repositorio de gigabytes). Cada una tiene su síntoma, su medición y su remedio, y aplicar el remedio equivocado no arregla nada. Se diagnostican congit rev-list --count --all,git ls-files | wc -lygit count-objects -vH, y se afina conGIT_TRACE2_PERF. - Los clones parciales son la técnica más útil y menos conocida:
--filter=blob:nonedescarga el grafo completo y omite el contenido histórico, pidiéndolo bajo demanda;--filter=tree:0omite también los árboles. Conservanlog,bisect,describeymerge-base. - Frente al clon superficial
--depth, el parcial es casi siempre mejor:--depthproduce un repositorio incompleto donde faltan commits y las operaciones fallan (fatal: no merge base), mientras que el parcial produce un repositorio completo con contenido diferido. Para CI:fetch-depth: 0+filter: blob:none. sparse-checkout --conelimita lo que se materializa en el disco, y el índice disperso guarda una entrada por directorio fuera del cono en lugar de una por fichero. El modo cono renuncia a expresividad para poder razonar sobre subárboles completos, algo que solo es posible porque el hash de un árbol resume todo su contenido (lección 01-04).- Los aceleradores del cliente —
commit-graph --changed-paths,fsmonitor,untrackedCache,feature.manyFilesygit maintenance— atacan partes distintas del coste y se combinan bien. Elcommit-graphes el único sin inconvenientes: actívalo siempre. - Monorepo frente a multirrepo se decide con una sola pregunta: ¿con qué frecuencia un cambio necesita tocar varios repositorios a la vez? El monorepo compra atomicidad y refactorización global; paga con herramientas propias,
CODEOWNERS, CI selectiva y escalado de Git. Y hay caminos intermedios: monorepo por dominio, monorepo solo de bibliotecas, migración gradual conmerge -s ours+read-tree --prefix. - Y lo más importante: la mayoría de los repositorios no necesitan nada de esto.
gestor-tareastiene nueve ficheros, 412 commits y 2 MB. Aplicarlesparse-checkoutsería añadir confusión a cambio de nada.
La regla que resume la lección, y que es la misma de la 08-06 llevada a otra escala:
Mide, diagnostica la dimensión, aplica el remedio de esa dimensión, y vuelve a medir. Si
git statusresponde en menos de un segundo, no tienes ningún problema que estas técnicas resuelvan.
Lo que viene
Ya sabemos operar Git en repositorios de cualquier tamaño. Queda la última pieza del mundo real, y es la que convierte a Git en algo más que una herramienta de desarrolladores.
En la lección 07-06 trazamos una frontera: allí hablábamos de integración continua —comprobar automáticamente que lo que se integra funciona— y dejamos para más adelante cómo ese código llega a los usuarios. También en la 07-04 aplazamos la mecánica de los entornos y la promoción, y en la 05-05, al etiquetar v1.4.0, dijimos que las etiquetas anotadas serían la pieza que dispara un despliegue.
Todo eso converge aquí. En muchas organizaciones, Git ya no lo usa solo una persona en una terminal: lo usan cien tuberías que clonan, etiquetan, construyen y publican sin intervención humana. El repositorio deja de contener solo código y pasa a contener la definición del entorno, la configuración y la infraestructura. Y un git push a la rama correcta deja de ser "he guardado mi trabajo" para convertirse en "esto está en producción dentro de cuatro minutos".
La lección 10-05: Git en DevOps cierra esa promesa: Git como fuente única de verdad, los tres escalones de CI, entrega y despliegue, qué dispara cada uno, por qué el artefacto debe identificarse por el hash del commit, qué es GitOps, dónde viven los secretos en un mundo automatizado, y por qué revertir en producción casi nunca es un git revert.
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
