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

  1. Las tres dimensiones del crecimiento
  2. Medir antes de optimizar (y qué medir en cada caso)
  3. Clones parciales: --filter=blob:none y --filter=tree:0
  4. Clon parcial frente a clon superficial
  5. Sparse checkout y el índice disperso
  6. Aceleradores del lado del cliente
  7. Monorepo frente a multirrepo
  8. Qué hace falta para que un monorepo funcione
  9. Cuándo NO hace falta nada de esto
  10. Una receta por síntoma

  1. 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"]

  1. 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

# Tamaño y composición del repositorio (lección 08-06)
git count-objects -vH
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 -10

Tres 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

GIT_TRACE2_PERF_BRIEF=1 GIT_TRACE2_PERF=/dev/stdout git status 2>&1 | head -20
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 MiB

Ninguna de las tres dimensiones. Nada de esta lección le aplica.

  1. Clones parciales: --filter=blob:none y --filter=tree:0

El 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

git clone --filter=blob:none https://git.ejemplo.es/proyecto-grande.git

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 --all

Qué 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ón

Verlo en marcha:

git blame app.js
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:

git rev-list --objects --all --missing=print | grep '^?' | wc -l

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

git clone --filter=tree:0 https://git.ejemplo.es/proyecto-grande.git

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:none

blob: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 %
--filter=blob:none ~500 MB ~8 % (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.

  1. 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:

git clone --depth=1 https://git.ejemplo.es/proyecto.git
cd proyecto
git describe --tags
fatal: No names found, cannot describe anything.

O peor, en una comparación de propuesta:

git diff origin/main...HEAD
fatal: no merge base

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órico

Historial 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=1 es mínimo y suficiente.
  • Un contenedor de despliegue donde solo quieres los ficheros de una versión concreta. Aunque para eso, git archive suele ser aún mejor:
git archive --format=tar.gz --remote=https://git.ejemplo.es/proyecto.git v1.4.0 > proyecto.tar.gz

Eso no crea repositorio: descarga solo los ficheros de esa etiqueta.

  1. 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 status recorre 300.000 entradas.
  • El editor indexa 300.000 ficheros.
  • Cada git checkout entre 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 disable

Despué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/tareas

Esa 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
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í:

git config index.sparse true
git sparse-checkout init --cone --sparse-index

Y se comprueba:

git ls-files --sparse | grep '/$' | head
test-tool read-cache --table | wc -l   # si tienes las herramientas de prueba

El 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:

git status
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.

  1. 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 true

Sin é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:

git log -- servicios/tareas/app.js
git log --follow ruta/al/fichero

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

git config core.fsmonitor true
git config core.untrackedCache true

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

git maintenance start

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 start

Ajustes 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 true

feature.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.

  1. 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 : 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:

  1. 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.

  2. 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.

  3. 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).

  1. 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.

  1. 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 start

Esto conviene ponerlo en un guion de incorporación en el propio repositorio, porque nadie va a recordar seis comandos.

  1. 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.

  1. 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.

  1. 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.

  1. 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.

  1. 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 status responde en menos de un segundo y git clone tarda 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.

  1. 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:

git rev-list --objects --all --missing=print | grep -c '^?'

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:

git config --global fetch.writeCommitGraph true
git config --global core.commitGraph true

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:

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

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 autenticacion tarda seis semanas en llegar a todos los consumidores.
  • Hay cuatro versiones distintas de componentes-ui en 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 graficos y utilidades se 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.git

Con --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/trabajo

El 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:

git lfs migrate info --everything --above=5Mb

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.git

El 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; done

Qué le diría a Ana sobre las rarezas:

  1. "No vas a ver la mayoría de los ficheros." git status te 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.js funciona perfectamente.

  2. "Si necesitas otra área, añádela." git sparse-checkout add servicios/notificaciones. No hace falta clonar de nuevo.

  3. "Algunas operaciones irán a la red." git log -p sobre historia antigua, git blame de un fichero con muchas versiones, o git checkout de una etiqueta vieja descargarán blobs. La primera vez es lenta; después queda cacheado.

  4. "Antes de irte sin conexión, trae lo que vayas a necesitar." Un clon parcial necesita el servidor para lo que no tiene.

  5. "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-index o buscar en la web del repositorio son la salida.

  6. "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"
done

Beneficio 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
done

Los 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):

  1. CODEOWNERS por directorio — recupera la propiedad que el multirrepo daba gratis.
  2. CI selectiva con grafo de dependencias — sin ella, cada propuesta ejecuta la suite entera.
  3. Sistema de compilación con caché por hash de entradas — o los tiempos de compilación se disparan.
  4. 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 log y blame lentos), muchos ficheros (git status lento) 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 con git rev-list --count --all, git ls-files | wc -l y git count-objects -vH, y se afina con GIT_TRACE2_PERF.
  • Los clones parciales son la técnica más útil y menos conocida: --filter=blob:none descarga el grafo completo y omite el contenido histórico, pidiéndolo bajo demanda; --filter=tree:0 omite también los árboles. Conservan log, bisect, describe y merge-base.
  • Frente al clon superficial --depth, el parcial es casi siempre mejor: --depth produce 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 --cone limita 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 clientecommit-graph --changed-paths, fsmonitor, untrackedCache, feature.manyFiles y git maintenance— atacan partes distintas del coste y se combinan bien. El commit-graph es 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 con merge -s ours + read-tree --prefix.
  • Y lo más importante: la mayoría de los repositorios no necesitan nada de esto. gestor-tareas tiene nueve ficheros, 412 commits y 2 MB. Aplicarle sparse-checkout serí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 status responde 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

Módulo 2: Operaciones Básicas de Git

Módulo 3: Ramas y Fusión

Módulo 4: Trabajando con Repositorios Remotos

Módulo 5: Operaciones Avanzadas de Git

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

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

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

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

Módulo 10: Git en el Mundo Real

© Copyright 2026. Todos los derechos reservados