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 list

Salida 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 activa main.
  • Git ha creado la carpeta hermana tienda-hotfix/ con la rama hotfix/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 inicial

Dos 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 main
Preparing 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 --short

Si está limpio:

cd ../tienda
git worktree remove ../tienda-hotfix
git worktree list

Con cambios sin confirmar, Git se niega:

fatal: '../tienda-tmp' contains modified or untracked files, use --force to delete it

No 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 prune

Parte 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/utilidades
fatal: transport 'file' not allowed

Para el laboratorio, habilítalo explícitamente:

git config --global protocol.file.allow always

Es 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 -1
52ded1a feat: formatea fechas

Añ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 --short
Cloning 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/utilidades

Ese 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/utilidades

El - inicial significa «no inicializado». Se resuelve así:

git submodule update --init
ls libs/utilidades/
git submodule status
Submodule 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-clon2

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

Ahora 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/utilidades
Cleared directory 'libs/utilidades'
Submodule 'libs/utilidades' (../utilidades) unregistered for path 'libs/utilidades'
M  .gitmodules
D  libs/utilidades

deinit 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 install
Git LFS initialized.

Eso registra en tu configuración los filtros que hacen la sustitución:

git config --global --list | grep lfs
filter.lfs.clean=git-lfs clean -- %f
filter.lfs.smudge=git-lfs smudge -- %f
filter.lfs.process=git-lfs filter-process
filter.lfs.required=true

En el repositorio se declara qué patrones gestiona LFS:

git lfs track "*.psd"
git lfs track "*.mp4"
cat .gitattributes
Tracking "*.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.psd
version https://git-lfs.github.com/spec/v1
oid sha256:3d7c9a29cba36e8ce206f3d0fb835782388256ff7955018fca26813b3be6c361
size 2000000
132

132 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 main
Uploading LFS objects: 100% (4/4), 0 B | 0 B/s, done.
 * [new branch]      main -> main

En 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.psd
version https://git-lfs.github.com/spec/v1
oid sha256:1465629e2dff6c5466892eaa335e8ac91ab26a7ae52c11aae78358ea9bb2d321
size 2000000
132B

Un archivo .psd de 132 bytes que Photoshop no abre. No es corrupción: falta git-lfs. Se resuelve instalándolo y ejecutando:

git lfs pull

El 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:none solicita un clon parcial: Git aplaza la descarga del contenido de los archivos hasta que los necesite.
  • sparse-checkout limita el directorio de trabajo a backend/ 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:// instead y no filtra nada. Usa file:///ruta/al/repo.git para 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 status

Para incorporar otra zona del monorepo:

git sparse-checkout add frontend

Para volver a mostrar todo el árbol:

git sparse-checkout disable

Para repositorios grandes usados a diario, se puede activar el mantenimiento en segundo plano:

git maintenance start

Actú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-run antes de limpiar.
  • Usar --force para retirar un worktree con cambios: puede eliminar archivos no confirmados. Revisa git status y confirma o usa git stash antes.
  • 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 --remote y 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-lfs instalado: obtendrás archivos de puntero de tres líneas en lugar del contenido.
  • Esperar que sparse-checkout reduzca 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:

  1. Crea un worktree para la rama docs/api.
  2. Haz un commit distinto en main y otro en docs/api, sin cambiar de rama dentro de ninguna carpeta.
  3. Comprueba con git worktree list que ambas ramas están activas en rutas distintas.
  4. Crea un segundo repositorio componentes con un commit e incorpóralo al primero como submódulo en libs/componentes. Demuestra con git ls-tree que lo que se guarda es un gitlink 160000.
  5. Clona el proyecto en otra carpeta sin --recurse-submodules y comprueba que la carpeta del submódulo está vacía. Inicialízala después.
  6. Añade un commit en componentes y demuestra que el proyecto contenedor no cambia. Actualiza el puntero a propósito y confirma el cambio.
  7. Simula un monorepo con las carpetas backend, frontend e infra. Configura sparse-checkout para mostrar primero solo backend y añade después infra.
  8. Retira el worktree de documentación únicamente cuando git status no 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-tree con el modo 160000 y se explica qué significa.
  • Se evidencia con git submodule status el 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 list muestra 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 add con una ruta local falla con fatal: transport 'file' not allowed mientras no se active protocol.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/ devuelve 160000 commit <hash>: el proyecto guarda un gitlink, no contenido.
  • Un git clone normal deja la carpeta del submódulo vacía y git submodule status antepone - al hash; git submodule update --init la puebla y el - desaparece. --recurse-submodules hace 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 --remote lo mueve, git status lo marca como M y git diff --submodule=log lista los commits que entran.
  • El submódulo actualizado queda en ## HEAD (no branch).
  • git submodule deinit -f + git rm retiran 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 install responde Git LFS initialized. y registra los cuatro ajustes filter.lfs.* en la configuración.
  • git lfs track imprime Tracking "*.psd" y escribe la línea *.psd filter=lfs diff=lfs merge=lfs -text en .gitattributes.
  • Un archivo de 2 000 000 bytes deja en el historial un objeto de 132 bytes: git cat-file -p HEAD:cartel.psd devuelve el puntero con su oid sha256 y su size, y git cat-file -s confirma 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 push a un remoto local informa Uploading 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.psd de 132 bytes cuyo cat muestra el puntero — el síntoma que ve quien no tiene git-lfs instalado —, y git lfs pull lo restituye a 2 MB.
  • git lfs migrate info --everything sobre 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.