Módulo 10: Laboratorio — worktrees, submódulos, LFS y repositorios grandes
Unidad: GIT-12
Nivel: avanzado, opcional.
Público: alumnado de DAW, DAM, ASIR y grados de informática que ya trabaja con ramas, remotos y conflictos.
Itinerario: laboratorio avanzado, posterior a ramas, remotos y recuperación.
Prerrequisitos: switch, branch, commit, fetch, diferencias entre repositorio local y remoto.
Objetivo observable: trabajar en varias ramas a la vez con worktrees, incorporar otro repositorio como submódulo entendiendo su anclaje a un commit, versionar binarios grandes con Git LFS y preparar una copia eficiente de un repositorio grande con sparse-checkout y partial clone.
Fuente técnica: documentación oficial de git worktree, git submodule, Git LFS, git sparse-checkout, partial clone y git maintenance.
Duración orientativa: 4 horas.
Idea clave
Las cuatro partes de este laboratorio responden a cuatro problemas distintos que suelen confundirse entre sí:
| Problema | Herramienta |
|---|---|
| Necesito dos ramas abiertas a la vez, sin cambiar de una a otra | git worktree |
| Necesito otro repositorio dentro del mío, con su propia historia | Submódulos |
| Necesito versionar binarios grandes sin inflar el repositorio | Git LFS |
| El repositorio ya es enorme y quiero trabajar con una parte | sparse-checkout + partial clone |
Ninguna sustituye a otra. El error habitual es aplicar la solución de escala a un problema de organización, o al revés.
Parte A. Dos ramas abiertas a la vez con git worktree
Un worktree permite trabajar simultáneamente en varias ramas del mismo repositorio local, en carpetas distintas. Comparte la base de datos de objetos e historial, pero cada worktree tiene su propio directorio de trabajo, HEAD e índice.
Partimos de un repositorio local vacío:
mkdir tienda
cd tienda
git init -b main
printf "# Tienda\n" > README.md
git add README.md
git commit -m "docs: añade README inicial"
git worktree add -b hotfix/login ../tienda-hotfix main
git worktree listSalida esperada aproximada:
Preparing worktree (new branch 'hotfix/login')
HEAD is now at c10d7a7 docs: añade README inicial
/ruta/tienda c10d7a7 [main]
/ruta/tienda-hotfix c10d7a7 [hotfix/login]Qué ha cambiado:
- En
tienda/sigue activamain. - Git ha creado la carpeta hermana
tienda-hotfix/con la ramahotfix/login. - Ambas carpetas comparten los commits y objetos del repositorio, pero no los archivos modificados ni el área de preparación.
Ahora se puede trabajar en paralelo:
cd ../tienda-hotfix
printf "Corrección temporal del acceso.\n" > HOTFIX.md
git add HOTFIX.md
git commit -m "fix: corrige acceso de usuarios"
cd ../tienda
printf "Catálogo inicial.\n" > catalogo.txt
git add catalogo.txt
git commit -m "feat: añade catálogo inicial"
git log --oneline --all --decorate --graph* 99ec4e3 (hotfix/login) fix: corrige acceso de usuarios
| * b8a42a6 (HEAD -> main) feat: añade catálogo inicial
|/
* c10d7a7 docs: añade README inicialDos commits en dos ramas distintas sin haber cambiado de rama ni una vez. Ese es todo el propósito: no interrumpir lo que tienes a medias para atender una urgencia.
Error frecuente: abrir la misma rama dos veces
git worktree add ../tienda-main mainPreparing worktree (checking out 'main')
fatal: 'main' is already used by worktree at '/ruta/tienda'Git lo rechaza. Es una protección: evita que dos directorios de trabajo modifiquen simultáneamente la misma rama.
Retirada segura
Antes de retirar un worktree, revisa su estado:
cd ../tienda-hotfix
git status --shortSi está limpio:
cd ../tienda
git worktree remove ../tienda-hotfix
git worktree listCon cambios sin confirmar, Git se niega:
fatal: '../tienda-tmp' contains modified or untracked files, use --force to delete itNo uses --force como atajo: primero confirma o guarda el trabajo con git stash. Si alguien ha borrado la carpeta manualmente, quedan metadatos pendientes:
git worktree prune --dry-run
git worktree pruneParte B. Submódulos: un repositorio dentro de otro
Un submódulo es un repositorio Git completo anidado dentro de otro. El proyecto contenedor no guarda los archivos del submódulo: guarda la dirección del repositorio y el commit exacto en el que debe estar.
Esa última parte —el commit exacto— es la fuente de casi todas las confusiones y también de todo el valor: tu proyecto no depende de «la última versión» de la biblioteca, sino de una versión concreta y reproducible.
Antes de empezar: permitir rutas locales
Para practicar en tu equipo usaremos repositorios en carpetas locales. Git bloquea eso por defecto desde 2022, por una vulnerabilidad de seguridad:
git submodule add ../utilidades libs/utilidadesfatal: transport 'file' not allowedPara el laboratorio, habilítalo explícitamente:
git config --global protocol.file.allow alwaysEs una configuración solo para esta práctica local. Con repositorios reales por HTTPS o SSH no hace falta, y conviene revertirla al terminar con git config --global --unset protocol.file.allow.
La biblioteca compartida
git init -b main utilidades
cd utilidades
printf "def formatear_fecha(f):\n return f.strftime('%d/%m/%Y')\n" > fechas.py
git add fechas.py
git commit -m "feat: formatea fechas"
git log --oneline -152ded1a feat: formatea fechasAñadir el submódulo
cd ..
git init -b main app
cd app
printf "# App\n" > README.md
git add README.md && git commit -m "docs: crea README"
git submodule add ../utilidades libs/utilidades
cat .gitmodules
git status --shortCloning into '/ruta/app/libs/utilidades'...
done.
[submodule "libs/utilidades"]
path = libs/utilidades
url = ../utilidades
A .gitmodules
A libs/utilidades.gitmodules es un archivo normal, versionado como cualquier otro: es lo que permite que quien clone el proyecto sepa de dónde sacar el submódulo.
Lo que se guarda es un puntero
Confirma y mira qué ha quedado registrado:
git commit -m "chore: añade utilidades como submódulo"
git ls-tree HEAD libs/160000 commit 52ded1a95964128407411b27fa38f0f467bed9eb libs/utilidadesEse 160000 es el modo especial de Git para un gitlink. Compáralo con el 100644 de un archivo normal: aquí no hay contenido, hay una referencia a un commit de otro repositorio. El proyecto pesa lo mismo tenga la biblioteca un archivo o diez mil.
Clonar un proyecto con submódulos
Este es el tropiezo número uno. Clona el proyecto y mira:
cd ..
git clone app app-clon
ls app-clon/libs/utilidades/La carpeta está vacía. Git ha clonado el puntero, no el contenido:
cd app-clon
git submodule status-52ded1a95964128407411b27fa38f0f467bed9eb libs/utilidadesEl - inicial significa «no inicializado». Se resuelve así:
git submodule update --init
ls libs/utilidades/
git submodule statusSubmodule path 'libs/utilidades': checked out '52ded1a95964128407411b27fa38f0f467bed9eb'
fechas.py
52ded1a95964128407411b27fa38f0f467bed9eb libs/utilidades (heads/main)El - ha desaparecido. Para ahorrarse el paso, se clona directamente con:
git clone --recurse-submodules app app-clon2El anclaje: el submódulo no se actualiza solo
Actualiza la biblioteca desde su propio repositorio:
cd ../utilidades
printf "\ndef formatear_hora(h):\n return h.strftime('%H:%M')\n" >> fechas.py
git add fechas.py && git commit -m "feat: formatea horas"Y vuelve al proyecto:
cd ../app
git submodule status
git status --short 52ded1a95964128407411b27fa38f0f467bed9eb libs/utilidades (heads/main)git status no muestra nada: el proyecto sigue apuntando al commit antiguo y eso es lo correcto. Un submódulo no es una suscripción a la última versión; es un anclaje. Si tu proyecto compilaba ayer, seguirá compilando hoy aunque la biblioteca cambie.
Actualizar a propósito
Cuando decides subir de versión:
git submodule update --remote libs/utilidades
git status --short
git diff --submodule=log 52ded1a..8143e92 main -> origin/main
Submodule path 'libs/utilidades': checked out '8143e92...'
M libs/utilidades
Submodule libs/utilidades 52ded1a..8143e92:
> feat: formatea horasAhora sí aparece como modificado, y --submodule=log muestra qué commits entran con la actualización. Es un cambio del proyecto como cualquier otro, y se confirma igual:
git add libs/utilidades
git commit -m "chore: actualiza utilidades a la última versión"Lo que se confirma no es el código de la biblioteca: es el puntero nuevo.
La trampa del HEAD separado
cd libs/utilidades
git status -sb## HEAD (no branch)Un submódulo actualizado queda en HEAD separado, porque el proyecto lo fija a un commit, no a una rama. Los commits que hicieras aquí no pertenecerían a ninguna rama y se perderían de vista con facilidad (módulo 6, reflog).
Si vas a trabajar dentro del submódulo, sitúate primero en una rama:
git switch main
git status -sb## main...origin/main [behind 1]Y recuerda que hay dos repositorios: hay que confirmar y publicar en el submódulo y después actualizar el puntero en el proyecto contenedor.
Retirar un submódulo
Son tres pasos, y el tercero se olvida siempre:
git submodule deinit -f libs/utilidades
git rm libs/utilidades
rm -rf .git/modules/libs/utilidadesCleared directory 'libs/utilidades'
Submodule 'libs/utilidades' (../utilidades) unregistered for path 'libs/utilidades'
M .gitmodules
D libs/utilidadesdeinit vacía la carpeta, git rm elimina la entrada y actualiza .gitmodules, y el rm -rf final limpia el repositorio interno que Git conserva cacheado en .git/modules/.
Cuándo no usar submódulos
Son una herramienta legítima, pero exigen disciplina de todo el equipo. Antes de elegirlos, comprueba si tu ecosistema ya resuelve el problema mejor: pip, npm, Maven o Composer gestionan dependencias con versionado, resolución de conflictos y publicación. Un submódulo es preferible cuando el código es tuyo, cambia a la vez que el proyecto y no quieres publicarlo como paquete.
Parte C. Binarios grandes con Git LFS
El problema
Git guarda una copia completa de cada versión de cada archivo. Con texto lo comprime muy bien, porque almacena diferencias con eficacia. Con binarios —imágenes, vídeo, PSD, modelos 3D, datasets— no puede: cada versión de un archivo de 50 MB añade otros 50 MB al repositorio, para siempre y en el clon de todo el mundo.
Diez versiones de ese archivo son 500 MB que se descargan aunque solo necesites la última. Y como el historial es inmutable, borrar el archivo hoy no reduce el repositorio.
Cómo funciona
Git LFS (Large File Storage) es una extensión que sustituye el archivo por un puntero de texto de pocos bytes. El contenido real vive en un almacén aparte y se descarga solo cuando hace falta.
No es parte de Git: es un programa que hay que instalar (git-lfs), y el servidor tiene que admitirlo. GitHub, GitLab y Bitbucket lo hacen, con cuotas de almacenamiento y tráfico propias.
Configurar
Una vez por equipo:
git lfs installGit LFS initialized.Eso registra en tu configuración los filtros que hacen la sustitución:
git config --global --list | grep lfsfilter.lfs.clean=git-lfs clean -- %f
filter.lfs.smudge=git-lfs smudge -- %f
filter.lfs.process=git-lfs filter-process
filter.lfs.required=trueEn el repositorio se declara qué patrones gestiona LFS:
git lfs track "*.psd"
git lfs track "*.mp4"
cat .gitattributesTracking "*.psd"
Tracking "*.mp4"
*.psd filter=lfs diff=lfs merge=lfs -text
*.mp4 filter=lfs diff=lfs merge=lfs -text.gitattributes hay que confirmarlo: es lo que hace que la regla se aplique a todo el equipo.
git add .gitattributes
git commit -m "chore: gestiona binarios con LFS"Qué se guarda realmente
A partir de ahí se trabaja con normalidad. Añade un binario de 2 MB:
head -c 2000000 /dev/urandom > cartel.psd
git add cartel.psd
git commit -m "feat: añade cartel"Y mira qué ha entrado en el historial:
git cat-file -p HEAD:cartel.psd
git cat-file -s HEAD:cartel.psdversion https://git-lfs.github.com/spec/v1
oid sha256:3d7c9a29cba36e8ce206f3d0fb835782388256ff7955018fca26813b3be6c361
size 2000000
132132 bytes en el historial para un archivo de 2 MB. Eso es todo el mecanismo: el commit guarda el puntero, con el hash del contenido y su tamaño. El contenido real queda en un almacén local aparte:
find .git/lfs -type f
git lfs ls-files -s.git/lfs/objects/3d/7c/3d7c9a29cba36e8ce206f3d0fb835782388256ff7955018fca26813b3be6c361
3d7c9a29cb * cartel.psd (2.0 MB)Qué ahorra LFS de verdad
Aquí conviene ser preciso, porque se explica mal a menudo. Con cuatro versiones del mismo archivo de 2 MB, medido en local:
| Sin LFS | Con LFS | |
|---|---|---|
Objetos de Git (.git/objects) |
7,8 MB | 72 KB |
Almacén LFS local (.git/lfs) |
— | 7,6 MB |
| Total en tu disco | 7,8 MB | 7,8 MB |
En tu equipo ocupa lo mismo: las cuatro versiones están, solo que guardadas en otro sitio. LFS no comprime ni deduplica por arte de magia.
Lo que cambia es lo que se transfiere. Al clonar, LFS descarga solo las versiones que necesita el estado que sacas:
| Clon del repositorio | .git descargado |
|---|---|
| Sin LFS, 4 versiones del binario | 7,8 MB |
| Con LFS | 2,1 MB |
Quien clona se lleva el historial completo y una sola versión del binario, no las cuatro. En un proyecto real con años de versiones de vídeo o PSD, esa es la diferencia entre un clon de minutos y uno de horas.
Publicar
git push funciona igual, con un paso extra visible:
git push -u origin mainUploading LFS objects: 100% (4/4), 0 B | 0 B/s, done.
* [new branch] main -> mainEn el servidor, los binarios también viven aparte: en el remoto de esta práctica, 72 KB de objetos Git frente a 7,6 MB de almacén LFS.
Al clonar sin git-lfs
Este es el síntoma que hay que saber reconocer. Quien clone sin la extensión instalada obtiene los punteros en lugar del contenido:
cat cartel.psd
ls -lh cartel.psdversion https://git-lfs.github.com/spec/v1
oid sha256:1465629e2dff6c5466892eaa335e8ac91ab26a7ae52c11aae78358ea9bb2d321
size 2000000
132BUn archivo .psd de 132 bytes que Photoshop no abre. No es corrupción: falta git-lfs. Se resuelve instalándolo y ejecutando:
git lfs pullEl archivo recupera sus 2 MB.
El punto crítico: solo cuenta desde que se activa
LFS no migra hacia atrás. Los binarios que ya estaban en el historial siguen ahí con todo su peso. Para saber cuánto pesan:
git lfs migrate info --everything*.psd 8.0 MB 4/4 files 100%Convertirlos requiere git lfs migrate import, que reescribe el historial completo: cambian todos los hashes y el equipo entero tiene que volver a clonar. Es la misma categoría de operación que el push --force que el curso evita.
De ahí la única regla que importa: configura LFS antes del primer binario grande, no después.
Parte D. Preparar una copia ligera de un repositorio grande
Escenario: solo necesitas backend/ de un monorepo y quieres descargar el mínimo contenido posible.
git clone --filter=blob:none --no-checkout <URL-del-repositorio> copia
cd copia
git sparse-checkout init --cone
git sparse-checkout set backend
git switch main--filter=blob:nonesolicita un clon parcial: Git aplaza la descarga del contenido de los archivos hasta que los necesite.sparse-checkoutlimita el directorio de trabajo abackend/y los archivos sueltos de la raíz.- Son cosas distintas y complementarias: uno reduce lo que se descarga, el otro lo que se ve.
Al practicar en local: con una ruta de carpeta normal Git avisa
warning: --filter is ignored in local clones; use file:// insteady no filtra nada. Usafile:///ruta/al/repo.gitpara que el filtrado funcione de verdad. Con un remoto real por HTTPS o SSH no aplica.
Medir la diferencia
Sobre un monorepo de práctica con cuatro carpetas (backend, frontend, infra, docs) de tres archivos cada una:
| Momento | Tamaño de .git |
|---|---|
| Clon completo | 592 KB |
| Clon parcial, sin checkout | 120 KB |
Tras sparse-checkout set backend |
260 KB |
Tras sparse-checkout add frontend |
388 KB |
Se ve el mecanismo entero: el clon parcial trae casi solo el historial, y el contenido de los archivos se descarga bajo demanda conforme pides carpetas. En un monorepo real la diferencia se cuenta en gigabytes.
Inspección y ajuste
git sparse-checkout list
git count-objects -vH
git statusPara incorporar otra zona del monorepo:
git sparse-checkout add frontendPara volver a mostrar todo el árbol:
git sparse-checkout disablePara repositorios grandes usados a diario, se puede activar el mantenimiento en segundo plano:
git maintenance startActúa sobre el repositorio local. No modifica el remoto ni elimina ramas o commits de otras personas.
Errores frecuentes y señales
- Confundir worktree con clon: un worktree no es un segundo repositorio independiente; comparte objetos y referencias con el principal.
- Borrar una carpeta de worktree con el explorador: deja metadatos pendientes. Ejecuta
git worktree prune --dry-runantes de limpiar. - Usar
--forcepara retirar un worktree con cambios: puede eliminar archivos no confirmados. Revisagit statusy confirma o usagit stashantes. - Clonar un proyecto con submódulos y encontrar carpetas vacías: falta
git submodule update --init, o clonar con--recurse-submodules. - Esperar que un submódulo se actualice solo: está anclado a un commit a propósito. Se sube de versión con
git submodule update --remotey se confirma el puntero nuevo. - Hacer commits dentro de un submódulo en HEAD separado: sitúate antes en una rama, y recuerda que hay que publicar en los dos repositorios.
- Activar LFS después de haber subido binarios grandes: el historial ya pesa; migrar exige reescribirlo y que todo el equipo vuelva a clonar.
- Clonar un repositorio con LFS sin tener
git-lfsinstalado: obtendrás archivos de puntero de tres líneas en lugar del contenido. - Esperar que
sparse-checkoutreduzca el tamaño de.git: reduce lo que se ve en el directorio de trabajo; para ahorrar descarga hay que combinarlo con partial clone. - Atribuir lentitud local al historial sin medir: empieza con
git count-objects -vH, el tamaño de la copia de trabajo y los tiempos de clonación. No ejecutes limpiezas agresivas sin diagnóstico.
Reto autocorregible
En un repositorio de práctica:
- Crea un worktree para la rama
docs/api. - Haz un commit distinto en
mainy otro endocs/api, sin cambiar de rama dentro de ninguna carpeta. - Comprueba con
git worktree listque ambas ramas están activas en rutas distintas. - Crea un segundo repositorio
componentescon un commit e incorpóralo al primero como submódulo enlibs/componentes. Demuestra congit ls-treeque lo que se guarda es un gitlink160000. - Clona el proyecto en otra carpeta sin
--recurse-submodulesy comprueba que la carpeta del submódulo está vacía. Inicialízala después. - Añade un commit en
componentesy demuestra que el proyecto contenedor no cambia. Actualiza el puntero a propósito y confirma el cambio. - Simula un monorepo con las carpetas
backend,frontendeinfra. Configurasparse-checkoutpara mostrar primero solobackendy añade despuésinfra. - Retira el worktree de documentación únicamente cuando
git statusno muestre cambios.
Criterios de revisión:
- Cada worktree tiene una rama diferente y no se ha usado
--force. - Los commits se conservan y aparecen en
git log --all. - Se muestra la salida de
git ls-treecon el modo160000y se explica qué significa. - Se evidencia con
git submodule statusel estado no inicializado (-) y el inicializado. - Se explica por qué un commit en el submódulo no altera el proyecto contenedor hasta actualizar el puntero.
git sparse-checkout listmuestra exactamente los directorios solicitados.- El alumnado distingue los cuatro problemas de la tabla inicial y no confunde worktree con clon ni sparse-checkout con partial clone.
Estado editorial: listo.
Registro de revisión técnica
Revisado el 4 de agosto de 2026 con Git 2.47.1 y git-lfs 3.7.1. Las cuatro partes se han reproducido desde carpetas vacías, con identidad de práctica y configuración aislada.
Parte A verificada sin cambios de fondo: git worktree add -b crea la carpeta hermana y git worktree list muestra ambas rutas con su rama; se han hecho commits en las dos sin cambiar de rama. Confirmados los dos mensajes de protección: fatal: 'main' is already used by worktree at … al intentar abrir la misma rama dos veces, y fatal: '…' contains modified or untracked files, use --force to delete it al retirar un worktree sucio. Se ha promovido la estructura de encabezados a ## para que las cuatro partes aparezcan al mismo nivel en el índice lateral.
Parte B, nueva. Se ha comprobado que:
git submodule addcon una ruta local falla confatal: transport 'file' not allowedmientras no se activeprotocol.file.allow. Se documenta en el laboratorio porque bloquearía la práctica de todo el alumnado; Git lo restringe desde 2022 por seguridad.git ls-tree HEAD libs/devuelve160000 commit <hash>: el proyecto guarda un gitlink, no contenido.- Un
git clonenormal deja la carpeta del submódulo vacía ygit submodule statusantepone-al hash;git submodule update --initla puebla y el-desaparece.--recurse-submoduleshace ambas cosas en un paso. - Tras un commit nuevo en la biblioteca, el proyecto contenedor no registra ningún cambio: sigue anclado al commit antiguo.
git submodule update --remotelo mueve,git statuslo marca comoMygit diff --submodule=loglista los commits que entran. - El submódulo actualizado queda en
## HEAD (no branch). git submodule deinit -f+git rmretiran el submódulo y actualizan.gitmodules, pero el repositorio interno permanece en.git/modules/, que hay que borrar aparte.
Parte C, reproducida con git-lfs 3.7.1. Se ha comprobado que:
git lfs installrespondeGit LFS initialized.y registra los cuatro ajustesfilter.lfs.*en la configuración.git lfs trackimprimeTracking "*.psd"y escribe la línea*.psd filter=lfs diff=lfs merge=lfs -texten.gitattributes.- Un archivo de 2 000 000 bytes deja en el historial un objeto de 132 bytes:
git cat-file -p HEAD:cartel.psddevuelve el puntero con suoid sha256y susize, ygit cat-file -sconfirma el tamaño. El contenido real queda en.git/lfs/objects/<2>/<2>/<oid>. - Medición del ahorro, con cuatro versiones del binario de 2 MB. En local ocupa lo mismo con LFS y sin él (7,8 MB en ambos casos): lo que cambia es el reparto, 72 KB de objetos Git frente a 7,6 MB de almacén LFS. La diferencia real aparece al clonar: 2,1 MB con LFS frente a 7,8 MB sin él, porque el clon se lleva una sola versión del binario y no las cuatro. La tabla del laboratorio recoge ambas mediciones, y se ha redactado explícitamente para no repetir la simplificación habitual de que «LFS reduce el repositorio».
git pusha un remoto local informaUploading LFS objects: 100% (4/4); en el remoto quedan 72 KB de objetos Git y 7,6 MB de almacén LFS.- Un clon sin el contenido de LFS deja en el directorio de trabajo un
cartel.psdde 132 bytes cuyocatmuestra el puntero — el síntoma que ve quien no tienegit-lfsinstalado —, ygit lfs pulllo restituye a 2 MB. git lfs migrate info --everythingsobre el repositorio sin LFS informa*.psd 8.0 MB 4/4 files 100%, que es el diagnóstico previo a decidir una migración.
Parte D verificada, con una corrección: en un clon desde una ruta local Git responde warning: --filter is ignored in local clones; use file:// instead y el clon parcial no surte efecto, de modo que la práctica no demostraba nada. Se ha añadido el aviso y el uso de file://. Con él, y sobre un monorepo de práctica de cuatro carpetas, se han medido estos tamaños de .git: 592 KB en clon completo, 120 KB en clon parcial sin checkout, 260 KB tras sparse-checkout set backend y 388 KB tras añadir frontend — lo que evidencia la descarga bajo demanda. También se ha comprobado que sparse-checkout add y disable muestran y ocultan las carpetas esperadas.
Los hashes de las salidas son los de esta ejecución y serán distintos en cada repositorio.