Esta lección salda tres deudas del curso. En la lección 06-05 dijimos que si el problema no es código compartido sino binarios pesados, la respuesta no son los submódulos sino Git LFS. En la 08-04 apareció en .gitattributes una línea críptica —*.psd filter=lfs diff=lfs merge=lfs -text— y prometimos explicarla. Y en la 08-06, al analizar por qué los binarios grandes envenenan un repositorio, dijimos que la solución correcta era esta.

Aquí está, y llega en buen momento: el equipo de gestor-tareas va a incorporar recursos gráficos, un vídeo de demostración y los ficheros de diseño originales de componentes-ui. Carla ha añadido un mockup-panel.psd de 84 MB y ya lo ha modificado tres veces. El repositorio, que pesaba 4 MB, pesa ahora 250 MB, y Bruno tarda cuatro minutos en clonar lo que antes tardaba dos segundos.

Vamos a entender por qué ocurre eso —no por qué "los binarios son malos", sino qué hace exactamente Git con ellos—, cómo lo resuelve LFS, y sobre todo qué precio tiene, porque LFS no es gratis ni indoloro y hay que adoptarlo sabiendo dónde duele.

Contenido

  1. Por qué Git sufre con los binarios grandes
  2. La aritmética del problema en gestor-tareas
  3. Qué es Git LFS: la idea del puntero
  4. Instalación y primer seguimiento
  5. Cómo se refleja en .gitattributes
  6. El puntero por dentro
  7. Clonar, traer y no traer objetos
  8. Comandos de inspección y mantenimiento
  9. Migrar un repositorio que ya tiene binarios en el historial
  10. Limitaciones y avisos que hay que conocer antes
  11. Alternativas a LFS
  12. Cuándo usar LFS y cuándo no

  1. Por qué Git sufre con los binarios grandes

Hay que volver al modelo de datos de la lección 01-04, porque el problema no es una limitación arbitraria: es una consecuencia directa de cómo funciona Git.

Cada versión es un blob completo

Git no guarda diferencias. Guarda el contenido completo de cada versión de cada fichero, como un objeto blob identificado por el hash SHA-1 de su contenido. Cuando modificas un fichero y confirmas, no se guarda "lo que cambió": se guarda un blob nuevo con el fichero entero.

flowchart TD
    C1["commit 1"] --> T1["tree"] --> B1["blob<br/>mockup.psd v1<br/>84 MB"]
    C2["commit 2"] --> T2["tree"] --> B2["blob<br/>mockup.psd v2<br/>84 MB"]
    C3["commit 3"] --> T3["tree"] --> B3["blob<br/>mockup.psd v3<br/>84 MB"]
    B1 -.->|"todos siguen<br/>existiendo"| B2
    B2 -.-> B3

Con ficheros de texto esto no importa, y aquí está la clave: al empaquetar el repositorio, Git calcula deltas entre objetos parecidos. Un fichero app.js de 40 KB con cien versiones se comprime a una fracción minúscula, porque cada versión se guarda como "la anterior más estos cambios".

Por qué el delta no funciona con binarios

Tres razones que se acumulan:

Razón Qué implica
Ya están comprimidos Un .psd, un .png, un .mp4 o un .zip llevan compresión propia. Volver a comprimir no gana casi nada, y zlib gasta tiempo para nada.
Un cambio pequeño altera todo el fichero Cambiar un píxel en un formato comprimido reescribe el flujo completo. Los dos ficheros no se parecen byte a byte aunque se parezcan visualmente.
Git compara bytes, no semántica Git no sabe que dos .psd son la misma imagen con una capa distinta. Solo ve dos secuencias de bytes sin nada en común.

El resultado: el delta entre dos versiones de un binario es tan grande como el propio binario. Se puede comprobar (lección 08-06):

git verify-pack -v .git/objects/pack/pack-*.idx | sort -k3 -rn | head -5
9c4e2f1a8b3d blob   88080384 88052193 12
7a3f9d2e1c5b blob   88080384 88048871 88052205
2e8b1f4a7c9d blob   88080384 88051002 176101076
4f1a9c3e7b2d blob      41273     8104 264152078

Las columnas son: hash, tipo, tamaño real, tamaño en el paquete y desplazamiento. Los tres blobs del .psd ocupan casi 84 MB cada uno dentro del paquete: la compresión no ha logrado nada. Compáralo con el blob de texto de la última fila, que pasa de 41 KB a 8 KB.

Y el historial es para siempre

Esto es lo que convierte un problema molesto en uno grave. Como vimos en la lección 08-05 con los secretos y en la 08-06 con el tamaño: borrar el fichero no elimina los objetos. Un git rm mockup-panel.psd crea un commit nuevo donde el fichero no está, pero los tres blobs de 84 MB siguen en la base de datos, alcanzables desde los commits antiguos, y todo el que clone se los descargará.

Sacarlos de verdad exige reescribir el historial con git filter-repo, con todas las consecuencias que estudiamos: cambian todos los hashes desde el punto reescrito, y todo el mundo tiene que recuperar el repositorio.

El coste se lo come todo el mundo

Y este es el argumento decisivo. El repositorio de Git es distribuido: cada clon tiene el historial completo. Que Carla añada 250 MB de historial significa que:

  • Bruno, Ana y Diego descargan 250 MB al clonar.
  • Cada trabajo de CI que clone descarga 250 MB (y hay muchos al día).
  • Cada worktree y cada clon nuevo ocupa ese espacio en disco.
  • git gc tarda más, git clone tarda más, y el servidor sirve más tráfico.

Un fichero grande no es un problema de quien lo añade. Es un impuesto sobre todo el equipo, cobrado para siempre.

  1. La aritmética del problema en gestor-tareas

Pongamos números concretos al caso de Carla, para tener la intuición del orden de magnitud:

Elemento Tamaño Versiones Ocupación en el historial
app.js, index.html, estilos.css, README.md ~60 KB ~400 commits ~2 MB (con deltas)
mockup-panel.psd 84 MB 3 ~250 MB
demo.mp4 (previsto) 120 MB 2 ~240 MB
Recursos gráficos de componentes-ui ~15 MB ~20 ~300 MB

El código —lo que de verdad es el proyecto— ocupa el 0,25 % del repositorio. El resto son binarios que casi nadie necesita en su copia de trabajo la mayoría de los días.

Ahí está la observación que justifica LFS: la mayoría de la gente, la mayoría del tiempo, solo necesita la última versión de esos ficheros, o ninguna. Ana, que trabaja en la lógica de app.js, no necesita las tres versiones del .psd; ni siquiera necesita la última. El servidor de CI que ejecuta las pruebas tampoco.

Git, por diseño, se lo da todo a todos. LFS rompe ese "todo a todos" solo para los ficheros que decidas.

  1. Qué es Git LFS: la idea del puntero

Git LFS (Large File Storage) es una extensión de Git —no forma parte de Git, se instala aparte— construida sobre un mecanismo que Git sí trae de serie: los filtros de .gitattributes que vimos en la lección 08-04.

La idea completa cabe en una frase:

En lugar del fichero grande, en el repositorio se versiona un pequeño fichero de texto que dice dónde está el contenido. El contenido real vive en un almacén aparte y se descarga solo cuando hace falta.

flowchart TD
    subgraph disco["Tu copia de trabajo"]
        A["mockup-panel.psd<br/>84 MB, fichero real"]
    end
    subgraph repo["Repositorio Git"]
        B["mockup-panel.psd<br/>130 bytes: puntero de texto<br/>oid sha256:4d7a...<br/>size 88080384"]
    end
    subgraph almacen["Almacén LFS del servidor"]
        C["4d7a...<br/>contenido real, 84 MB"]
    end

    A -->|"git add: filtro clean"| B
    B -->|"git checkout: filtro smudge"| A
    A -->|"git push: sube el contenido"| C
    C -->|"git checkout / lfs pull"| A

Dos mecanismos independientes trabajando a la vez:

El filtro clean se ejecuta cuando el contenido entra en Git (git add). Recibe el fichero de 84 MB, calcula su hash SHA-256, guarda el contenido en la caché local de LFS y devuelve a Git un texto de tres líneas. Git versiona ese texto, que es lo que acaba en el blob.

El filtro smudge se ejecuta cuando el contenido sale de Git (git checkout). Recibe el puntero, busca el contenido por su hash (en la caché local o descargándolo del servidor) y escribe el fichero real en el disco.

El resultado es que trabajas exactamente igual que siempre. Abres el .psd, lo editas, git add, git commit, git push. Los filtros son invisibles. Lo que cambia es lo que se guarda en el historial: 130 bytes de texto en lugar de 84 MB de binario.

Por qué esto lo arregla

Efecto Motivo
El historial de Git se mantiene pequeño Cada versión son 130 bytes, y encima son texto que comprime bien
El clon es rápido Se descargan los punteros; el contenido, solo el de la revisión que extraes
No se descargan las versiones antiguas El servidor LFS solo envía los objetos que pidas
Se puede evitar descargar incluso los actuales La CI que no necesita el .psd puede saltarse el smudge
El almacenamiento se puede gestionar aparte Con sus propias políticas de retención y de coste

  1. Instalación y primer seguimiento

Instalar

LFS es un binario aparte, y cada persona del equipo debe instalarlo:

# Ana, Ubuntu
sudo apt install git-lfs

# Bruno, macOS
brew install git-lfs

# Carla, Windows 11: viene incluido en el instalador oficial de Git,
# o bien:
winget install GitHub.GitLFS

Y después, una vez por máquina y por usuario:

git lfs install
Updated Git hooks.
Git LFS initialized.

Ese comando hace dos cosas concretas, y conviene saberlo porque explica muchos comportamientos:

1. Registra los filtros en la configuración global:

git config --global --get-regexp '^filter\.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

Es exactamente el mecanismo de filtros de la lección 08-04. filter.lfs.required true significa que si LFS no está instalado, Git falla en lugar de dejar punteros sueltos por el disco. Es lo que quieres.

2. Instala hooks en el repositorio (pre-push, post-checkout, post-commit, post-merge) que se encargan de subir y bajar los objetos en el momento adecuado. Son hooks (lección 06-01), con las mismas propiedades de siempre: viven en .git/hooks/ y no se distribuyen. Por eso quien clone el repositorio necesita haber ejecutado git lfs install en su máquina.

Declarar qué ficheros se siguen

cd ~/gestor-tareas

git lfs track "*.psd"
git lfs track "*.mp4"
git lfs track "*.sketch"
git lfs track "recursos/video/**"
Tracking "*.psd"
Tracking "*.mp4"
Tracking "*.sketch"
Tracking "recursos/video/**"

Las comillas son importantes: sin ellas, el shell expandiría *.psd a los ficheros existentes y se registrarían rutas concretas en lugar del patrón.

Ver lo que hay declarado:

git lfs track
Listing tracked patterns
    *.psd (.gitattributes)
    *.mp4 (.gitattributes)
    *.sketch (.gitattributes)
    recursos/video/** (.gitattributes)

Y dejar de seguir un patrón:

git lfs untrack "*.sketch"

Ojo: untrack quita la regla para el futuro. Los ficheros ya convertidos en punteros en el historial siguen siendo punteros; para revertir eso hace falta reescribir el historial (apartado 9).

  1. Cómo se refleja en .gitattributes

Aquí se cierra la promesa de la lección 08-04. git lfs track no guarda nada en un fichero propio: escribe en .gitattributes, el mismo fichero que ya conoces.

cat .gitattributes
# ============================================================
# Finales de línea (resuelve GT-190)
# ============================================================
* text=auto
*.sh   text eol=lf
*.bat  text eol=crlf

# ============================================================
# Git LFS (GT-251)
# ============================================================
*.psd            filter=lfs diff=lfs merge=lfs -text
*.mp4            filter=lfs diff=lfs merge=lfs -text
*.sketch         filter=lfs diff=lfs merge=lfs -text
recursos/video/** filter=lfs diff=lfs merge=lfs -text

Desglosemos los cuatro atributos, que es exactamente lo que quedó pendiente:

Atributo Qué hace
filter=lfs Aplica los filtros clean y smudge registrados como filter.lfs.*. Es el núcleo del mecanismo: convierte contenido en puntero al entrar, y puntero en contenido al salir.
diff=lfs Usa el controlador de diferencias de LFS en lugar del genérico. Así git diff muestra algo útil sobre el fichero (tamaño, identificador del objeto) en vez del puntero crudo.
merge=lfs Usa el controlador de fusión de LFS. Como veremos, no fusiona nada: obliga a elegir un lado.
-text Desactiva el atributo text: le dice a Git que no normalice finales de línea (lección 08-04). Sin esto, un * text=auto como el de arriba podría corromper el contenido antes de que el filtro lo vea.

El -text es el más fácil de pasar por alto y el que causa el daño más difícil de diagnosticar. Por eso git lfs track lo pone siempre.

Consecuencia práctica muy importante: .gitattributes es un fichero versionado. Cuando Carla lo confirma y lo envía, todo el equipo hereda la configuración de LFS automáticamente. No hay que decirle a nadie qué ficheros son de LFS: está en el repositorio. Eso es lo que hace que LFS sea utilizable en equipo, y es un buen ejemplo del principio de la lección anterior: la configuración compartida vive en el repositorio.

Lo que no viaja en el repositorio es la instalación de LFS ni sus hooks. Por eso el CONTRIBUTING.md (lección 10-01) debe decirlo en la primera línea.

El orden importa

Un detalle sutil: .gitattributes se aplica en el momento del git add. Si añades el .psd antes de declarar el patrón, se versiona como binario normal y ya está en el historial. Por eso la secuencia correcta es siempre:

# 1. Declarar el patrón
git lfs track "*.psd"

# 2. Confirmar .gitattributes ANTES de añadir los binarios
git add .gitattributes
git commit -m "chore: GT-251 configurar Git LFS para ficheros de diseño"

# 3. Ahora sí, añadir los ficheros
git add recursos/mockup-panel.psd
git commit -m "feat: GT-251 añadir el mockup del panel de tareas"

Confirmar .gitattributes en un commit separado y anterior no es cosmética: garantiza que quien haga checkout de cualquier punto del historial tenga la configuración correcta antes de encontrarse los ficheros.

  1. El puntero por dentro

Vamos a mirar exactamente qué se guarda. En el disco, el fichero es normal:

ls -lh recursos/mockup-panel.psd
-rw-rw-r-- 1 carla carla 84M ago  1 10:22 recursos/mockup-panel.psd

Pero lo que Git tiene versionado es otra cosa:

git show HEAD:recursos/mockup-panel.psd
version https://git-lfs.github.com/spec/v1
oid sha256:4d7a9f2e1c8b3a6d5e0f7c2b9a4d1e8f3c6b0a5d2e9f4c7b1a8d3e6f0c5b2a9d
size 88080384

Ciento treinta bytes. Eso es todo lo que hay en el historial de Git por cada versión del .psd. Tres líneas:

Línea Significado
version Versión del formato de puntero de LFS
oid sha256:... El identificador del contenido: el hash SHA-256 del fichero real
size El tamaño en bytes del contenido real

Fíjate en la elegancia conceptual: LFS aplica exactamente el mismo principio que Git —direccionamiento por contenido (lección 01-04)— pero un nivel más abajo. El contenido se identifica por su hash, es inmutable, y el mismo contenido nunca se guarda dos veces. LFS usa SHA-256 mientras Git (todavía) usa SHA-1, algo sobre lo que volveremos en la lección 10-06.

Para verlo desde más abajo, con fontanería (lección 09-06):

git cat-file -s HEAD:recursos/mockup-panel.psd
130

Ciento treinta bytes en la base de datos de objetos. Frente a 88.080.384. Esa es toda la lección.

Qué muestra git diff

Gracias al atributo diff=lfs:

git diff HEAD~1 -- recursos/mockup-panel.psd
diff --git a/recursos/mockup-panel.psd b/recursos/mockup-panel.psd
index 3f8a1c9..7b2e4d6 100644
--- a/recursos/mockup-panel.psd
+++ b/recursos/mockup-panel.psd
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:4d7a9f2e1c8b3a6d5e0f7c2b9a4d1e8f3c6b0a5d2e9f4c7b1a8d3e6f0c5b2a9d
-size 88080384
+oid sha256:9e2f7a4c1b8d3e6f0a5c2b9d4e7f1a8c3b6d0e5f2a9c4b7d1e8f3a6c0b5d2e9f
+size 88080384

Un diff útil dentro de lo posible: dice que el contenido ha cambiado y que el tamaño es el mismo. No dice qué ha cambiado en la imagen, porque eso Git no puede saberlo.

Y aquí conviene recordar el truco de la lección 08-04: se puede definir un controlador de diferencias textualizador para ciertos binarios, de modo que git diff muestre metadatos legibles:

*.png diff=imagen
git config diff.imagen.textconv "identify -verbose"

No es una diferencia visual, pero saber que la imagen pasó de 1200×800 a 2400×1600 ya es información. Los dos mecanismos son compatibles: LFS gestiona el almacenamiento, textconv mejora la visualización.

  1. Clonar, traer y no traer objetos

Aquí está la mayor parte del valor práctico de LFS, y también sus rarezas.

Un clon normal

git clone https://git.ejemplo.es/gestor-tareas.git
Cloning into 'gestor-tareas'...
remote: Enumerating objects: 1284, done.
Receiving objects: 100% (1284/1284), 2.14 MiB | 8.42 MiB/s, done.
Resolving deltas: 100% (612/612), done.
Filtering content: 100% (2/2), 196 MiB | 12.3 MiB/s, done.

Dos fases claramente separadas:

  1. Receiving objects: el historial de Git. 2,14 MB. Incluye los cuatrocientos commits, todo el código y todos los punteros de todas las versiones.
  2. Filtering content: LFS descargando el contenido real de los ficheros de la revisión extraída. 196 MB, dos ficheros.

La diferencia con el escenario sin LFS: sin él habrían sido 250 MB en la primera fase (todas las versiones de todo) más el tiempo de resolver deltas inútiles. Con LFS son 2 MB de historial más solo el contenido actual.

Clonar sin descargar el contenido

Para quien no necesita los binarios —la CI que ejecuta las pruebas, alguien que solo va a tocar app.js— hay una variable de entorno:

GIT_LFS_SKIP_SMUDGE=1 git clone https://git.ejemplo.es/gestor-tareas.git

Esto desactiva el filtro smudge, así que los ficheros LFS quedan en el disco como punteros de texto:

cat recursos/mockup-panel.psd
version https://git-lfs.github.com/spec/v1
oid sha256:4d7a9f2e1c8b3a6d5e0f7c2b9a4d1e8f3c6b0a5d2e9f4c7b1a8d3e6f0c5b2a9d
size 88080384

Es una situación perfectamente válida y muy útil, pero hay que saber reconocerla, porque es el origen de la incidencia de soporte más habitual con LFS: alguien abre el .psd con su editor de imágenes y recibe un error incomprensible, sin entender que lo que tiene es un fichero de texto de 130 bytes.

Para dejarlo permanente en una máquina o repositorio concreto:

git config --global filter.lfs.smudge "git-lfs smudge --skip -- %f"
git config --global filter.lfs.process "git-lfs filter-process --skip"

O de forma más limpia y moderna, para clonar sin contenido:

git clone --no-checkout https://git.ejemplo.es/gestor-tareas.git
cd gestor-tareas
git lfs install --local --skip-smudge
git checkout main

Traer el contenido cuando lo necesites

# Descargar el contenido de los ficheros LFS de la revisión actual
git lfs pull

# Solo lo que coincida con un patrón
git lfs pull --include="recursos/mockup-*.psd"

# Excluir vídeos, que son lo más pesado
git lfs pull --exclude="*.mp4"

Y la distinción entre fetch y pull, que es la misma que en Git (lección 04-04):

# fetch: descarga a la caché local de LFS, sin escribir en la copia de trabajo
git lfs fetch

# fetch --all: TODAS las versiones de TODOS los ficheros LFS del historial
git lfs fetch --all

# fetch de una referencia concreta
git lfs fetch origin v1.3.0

# checkout: escribe en la copia de trabajo lo que ya está en la caché
git lfs checkout

# pull = fetch + checkout
git lfs pull

Cuidado con git lfs fetch --all: descarga el contenido de todas las versiones de todos los ficheros LFS de toda la historia. Es exactamente lo que estabas evitando al adoptar LFS. Solo tiene sentido en dos casos: hacer una copia de seguridad completa, o preparar una migración a otro servidor.

Filtros permanentes de descarga

Si en tu máquina nunca quieres los vídeos, se configura una vez:

git config lfs.fetchexclude "*.mp4,recursos/video/**"
git config lfs.fetchinclude "recursos/mockup-*.psd"

Y para la CI, donde esto importa mucho más porque se ejecuta decenas de veces al día:

name: Pruebas

on: [push, pull_request]

jobs:
  probar:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          lfs: false          # no descargar contenido LFS

      - name: Instalar dependencias
        run: npm ci

      - name: Pruebas
        run: npm test

  # Solo el trabajo que genera la documentación necesita los recursos
  documentacion:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          lfs: true

      - name: Generar la documentación con imágenes
        run: npm run docs

Ese lfs: false es la optimización con mejor retorno de todo el apartado. Si tienes cincuenta ejecuciones de CI al día y cada una se ahorra 196 MB de descarga, son casi 10 GB diarios de tráfico y varios minutos de espera acumulados.

  1. Comandos de inspección y mantenimiento

git lfs ls-files: qué está en LFS

git lfs ls-files
4d7a9f2e1c * recursos/mockup-panel.psd
9e2f7a4c1b * recursos/mockup-listado.psd
1c8b3a6d5e - recursos/video/demo.mp4

La columna del medio es la más útil:

Marca Significado
* El contenido real está en el disco (smudge aplicado)
- Solo hay un puntero; el contenido no está descargado

Variantes útiles:

# Con tamaño
git lfs ls-files --size

# En una revisión concreta
git lfs ls-files v1.3.0

# Solo los nombres, para encadenar con otros comandos
git lfs ls-files --name-only

# Todos los objetos LFS de toda la historia
git lfs ls-files --all

git lfs status: el git status de LFS

git lfs status
On branch GT-251-recursos-diseno

Objects to be pushed to origin/GT-251-recursos-diseno:

    recursos/mockup-panel.psd (84 MB)

Objects to be committed:

    recursos/mockup-listado.psd (LFS: 9e2f7a4)

Objects not staged for commit:

    recursos/video/demo.mp4 (File: 1c8b3a6)

Esa última línea revela un problema real: (File: ...) en lugar de (LFS: ...) significa que ese fichero no está siendo gestionado por LFS, probablemente porque se añadió antes de declarar el patrón. Es el primer sitio donde mirar cuando el repositorio crece sin explicación.

git lfs env: diagnóstico

git lfs env

Muestra la versión, la URL del punto final de LFS, la ruta de la caché local y la configuración de filtros. Es el equivalente para LFS del git config --list --show-origin de la lección 09-06, y el primer comando que hay que ejecutar cuando algo no funciona.

git lfs prune: liberar espacio local

La caché local de LFS (.git/lfs/objects/) va acumulando contenido de revisiones antiguas:

du -sh .git/lfs/objects/
1.4G	.git/lfs/objects/
# Ver qué borraría, sin borrar nada
git lfs prune --dry-run --verbose

# Borrar los objetos locales que ya no hacen falta
git lfs prune
prune: 47 local object(s), 12 retained, done.
prune: Deleting objects: 100% (35/35), done.

prune es conservador por diseño: solo borra objetos que estén confirmados y enviados al servidor, y conserva los de las revisiones recientes. Se controla con:

# Cuántos días de referencias recientes se conservan (por defecto 7)
git config lfs.pruneoffsetdays 14

# Verificar contra el servidor que el objeto está allí antes de borrarlo local
git config lfs.pruneverifyremotealways true

Ese pruneverifyremotealways merece activarse: comprueba contra el servidor que cada objeto está a salvo antes de eliminarlo de tu disco. Cuesta algo de tiempo y evita el único escenario en el que prune podría hacer daño.

git lfs migrate info: auditar antes de actuar

git lfs migrate info --everything --above=5Mb
migrate: Fetching remote refs: ..., done.
migrate: Sorting commits: ..., done.
migrate: Examining commits: 100% (412/412), done.

*.psd   250 MB    3/3 files
*.mp4   240 MB    2/2 files
*.zip    38 MB    4/4 files

Este comando no modifica nada: analiza el historial y dice qué extensiones ocupan cuánto. Es el punto de partida obligatorio de cualquier migración, y también un buen chequeo periódico.

  1. Migrar un repositorio que ya tiene binarios en el historial

Este es el caso de gestor-tareas: los .psd ya están dentro. Adoptar LFS ahora hace que los ficheros futuros sean punteros, pero los 250 MB del historial siguen ahí y todo el mundo los sigue descargando.

Para sacarlos hay que reescribir el historial. LFS trae su propia herramienta.

La advertencia, primero

git lfs migrate import reescribe el historial. Cambian los hashes de todos los commits afectados y de todos sus descendientes. Es la regla de oro de la lección 05-01 en su versión más contundente, y exige exactamente la misma coordinación que la limpieza de secretos de la lección 08-05.

Consecuencias concretas:

  • Todas las ramas y etiquetas que contengan commits reescritos apuntarán a objetos nuevos.
  • Cualquiera con un clon tendrá una divergencia completa (lección 09-03).
  • Las propuestas abiertas quedarán basadas en commits que ya no están en main.
  • Las firmas de commits (08-05) se invalidan, porque el objeto firmado ya no es el mismo.

El procedimiento completo

Paso 0: acordarlo con el equipo y elegir el momento.

No es un paso técnico y es el más importante. Se necesita una ventana en la que nadie tenga trabajo sin enviar. Ana avisa por escrito, se fija hora y se pide a todos que envíen y cierren sus propuestas.

# Cada persona comprueba que no tiene nada pendiente
git status
git log --branches --not --remotes --oneline   # commits locales sin enviar
git stash list

Ese git log --branches --not --remotes es la comprobación exacta: lista los commits que existen en alguna rama local y en ningún remoto. Si sale vacío, no hay nada que perder.

Paso 1: copia de seguridad.

# Un clon espejo completo, guardado aparte
git clone --mirror https://git.ejemplo.es/gestor-tareas.git ~/copias/gestor-tareas-antes.git

# Y un paquete autocontenido, por si acaso (lección 09-05)
git bundle create ~/copias/gestor-tareas-$(date +%F).bundle --all

Paso 2: auditar.

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

Sirve para decidir qué patrones migrar. Migra por extensión, no por fichero concreto: es más robusto y captura también los ficheros que se renombraron por el camino.

Paso 3: ensayar en una copia.

git clone https://git.ejemplo.es/gestor-tareas.git /tmp/ensayo-migracion
cd /tmp/ensayo-migracion

git lfs migrate import --everything --include="*.psd,*.mp4,*.zip"
migrate: Fetching remote refs: ..., done.
migrate: Sorting commits: ..., done.
migrate: Rewriting commits: 100% (412/412), done.
  main            8a1f6c3d4e5b -> 2c9d4f7a1b8e
  v1.0.0          3f2a1b9c7d4e -> 6b1e8a3c5d0f
  v1.1.0          7d4e9c2f1a8b -> 9f3c6b0a5d2e
migrate: Updating refs: ..., done.
migrate: checkout: ..., done.

Fíjate en que las etiquetas también se reescriben. v1.0.0 ya no apunta al mismo commit. Es la consecuencia de la inmutabilidad del modelo de datos: cambiar cualquier cosa en el historial cambia todo lo que cuelga de ello.

Paso 4: verificar el resultado del ensayo.

# ¿Cuánto se ha reducido?
git count-objects -vH

# ¿Qué hay ahora en LFS?
git lfs ls-files --all | head -20

# ¿El contenido de la última revisión es correcto?
git lfs pull
ls -lh recursos/

# ¿El código sigue siendo idéntico? Comparar el árbol con el original
git rev-parse HEAD^{tree}
git -C ~/gestor-tareas rev-parse HEAD^{tree}

Esta última comprobación es la más tranquilizadora y casi nadie la hace. El hash del árbol de HEAD resume todo el contenido de la revisión. Si coincide con el del repositorio original... no coincidirá, precisamente porque los .psd ahora son punteros. Pero sí debe coincidir el de los subdirectorios que no contienen ficheros migrados:

git rev-parse HEAD:src
git -C ~/gestor-tareas rev-parse HEAD:src

Si esos dos hashes son iguales, la migración no ha tocado el código. Es una verificación criptográfica, no una impresión.

Paso 5: ejecutarlo de verdad y publicar.

cd ~/gestor-tareas
git lfs migrate import --everything --include="*.psd,*.mp4,*.zip"

# Subir el contenido LFS al almacén del servidor ANTES que las referencias
git lfs push --all origin

# Y ahora sí, las referencias reescritas
git push --force-with-lease --all origin
git push --force-with-lease --tags origin

El orden importa: primero el contenido LFS, después las referencias. Si publicas las referencias antes, habrá un intervalo en el que alguien puede clonar un repositorio cuyos punteros apuntan a objetos que aún no existen en el almacén.

--force-with-lease en lugar de --force, siempre (lección 09-03). Y si el servidor tiene main como rama protegida (07-06), habrá que desprotegerla temporalmente y volver a protegerla justo después. Anótalo en el guion, porque se olvida.

Paso 6: que todo el mundo recupere.

La instrucción para el equipo, idéntica a la de la limpieza de secretos:

# Lo más simple, seguro y recomendable: clonar de nuevo
cd ~
mv gestor-tareas gestor-tareas-viejo
git clone https://git.ejemplo.es/gestor-tareas.git
cd gestor-tareas
git lfs install
git lfs pull

Y para quien tuviera trabajo sin enviar en una rama local:

# En el clon nuevo, traer la rama del clon viejo
git remote add viejo ~/gestor-tareas-viejo
git fetch viejo GT-260-mi-rama

# Reubicar solo mis commits sobre el nuevo main (lección 09-03)
git switch -c GT-260-mi-rama
git rebase --onto main viejo/main viejo/GT-260-mi-rama

Ese rebase --onto es exactamente la técnica de la lección 09-03: coge los commits que hay entre el main viejo y la rama vieja, y los replanta sobre el main nuevo.

Paso 7: limpiar el servidor.

Reescribir el historial no libera espacio en el servidor por sí solo: los objetos antiguos siguen ahí mientras alguna referencia los alcance. Hay que pedir al servidor que ejecute su recolección de basura, y en muchas plataformas gestionadas eso es una petición al soporte. Averígualo antes de empezar, porque si no, el repositorio seguirá pesando lo mismo del lado del servidor y la migración parecerá haber fallado.

La operación inversa

git lfs migrate export --include="*.psd" --everything

Convierte punteros de vuelta en ficheros normales. Útil si decides abandonar LFS. Requiere tener descargados los objetos, y reescribe el historial exactamente igual.

  1. Limitaciones y avisos que hay que conocer antes

LFS resuelve un problema real, pero introduce otros. Estos son los que hay que conocer antes de adoptarlo, no después.

  1. Hace falta un servidor LFS

LFS no funciona sin servidor. El almacén es un servicio aparte que habla un protocolo propio sobre HTTPS. La mayoría de plataformas lo ofrecen; algunas instalaciones autoalojadas requieren configurarlo explícitamente.

Consecuencia inmediata: un repositorio con LFS deja de ser autocontenido. Si clonas un repositorio con LFS y el servidor de LFS no está disponible, tienes los punteros pero no el contenido. Esto choca de frente con la naturaleza distribuida de Git (lección 01-01), y es el argumento filosófico más serio contra LFS: has introducido un punto centralizado en un sistema distribuido.

Para una copia de seguridad realmente completa hacen falta dos cosas:

git bundle create copia.bundle --all   # el historial de Git
git lfs fetch --all                    # todo el contenido LFS
# y copiar también .git/lfs/objects/

  1. Las cuotas cuestan dinero

El almacenamiento LFS y su ancho de banda suelen tener cuotas de pago en las plataformas. Y hay una trampa que sorprende a mucha gente: el ancho de banda se consume también al descargar. Una CI que clona con lfs: true cincuenta veces al día puede agotar una cuota mensual en pocos días. De ahí la importancia del lfs: false del apartado 7.

Además, los objetos LFS de la historia no se borran solos. Si migras un fichero de 84 MB y luego lo eliminas, el objeto sigue ocupando cuota. Purgar el almacén es una operación específica de cada plataforma, no un comando de Git.

  1. Las plataformas no se comportan igual

Hay diferencias reales entre implementaciones: cómo se gestionan los objetos en los forks, si el bloqueo de ficheros está disponible, cómo se aplican las cuotas, cómo se purgan los objetos huérfanos, y qué pasa al transferir un repositorio entre cuentas. No des por hecho que lo que funciona en una plataforma funciona igual en otra, sobre todo al migrar de proveedor: mover un repositorio con LFS de un servidor a otro requiere mover los objetos explícitamente.

  1. Los ficheros LFS no se fusionan

Este es el más importante en el día a día. Cuando dos ramas modifican el mismo fichero LFS, hay conflicto siempre, y no se resuelve fusionando: hay que elegir una versión ganadora.

git merge GT-252-rediseno-panel
Auto-merging recursos/mockup-panel.psd
CONFLICT (content): Merge conflict in recursos/mockup-panel.psd
Automatic merge failed; fix conflicts and then commit the result.
git lfs status
Objects to be committed:

    recursos/mockup-panel.psd (LFS: 4d7a9f2 -> 9e2f7a4)

Unmerged paths:
    recursos/mockup-panel.psd

Y la resolución es la misma que para cualquier binario (lección 03-05):

# Quedarse con la versión de mi rama
git checkout --ours recursos/mockup-panel.psd
git add recursos/mockup-panel.psd

# O con la de la otra rama
git checkout --theirs recursos/mockup-panel.psd
git add recursos/mockup-panel.psd

# O, si de verdad hay que combinar el contenido:
# abrir las dos versiones en la aplicación de diseño y rehacer el trabajo a mano
git show :2:recursos/mockup-panel.psd > /tmp/mia.psd
git show :3:recursos/mockup-panel.psd > /tmp/suya.psd

Esos :2: y :3: son las etapas del índice que vimos en la lección 03-05 y en la 09-06: 1 es la base común, 2 "lo nuestro", 3 "lo suyo". Extraerlas permite abrirlas en la aplicación correspondiente y decidir con la información delante.

La consecuencia organizativa es que con binarios no se puede trabajar en paralelo. La solución real no es técnica sino de coordinación: que solo una persona toque cada fichero de diseño a la vez. LFS ofrece un mecanismo para formalizarlo, el bloqueo de ficheros:

# Declarar que un patrón es de bloqueo obligatorio (en .gitattributes)
git lfs track "*.psd" --lockable

# Bloquear antes de editar
git lfs lock recursos/mockup-panel.psd

# Ver qué hay bloqueado y por quién
git lfs locks

# Liberar al terminar
git lfs unlock recursos/mockup-panel.psd

Con --lockable, LFS marca esos ficheros como solo lectura en el disco hasta que los bloquees. Es un mecanismo de exclusión mutua, como en los sistemas centralizados (lección 01-01), y es la prueba de que para ciertos flujos ese modelo tenía sentido. Requiere soporte del servidor.

  1. Todo el equipo debe instalarlo

Si alguien clona sin LFS instalado y filter.lfs.required no está activo, obtendrá punteros de texto en lugar de ficheros, y —el peor caso— podría confirmar un fichero real encima de un puntero, rompiendo la coherencia. Con git lfs install bien hecho esto no ocurre, pero es la razón por la que la instalación debe ser lo primero del CONTRIBUTING.md.

  1. Algunas operaciones se vuelven más lentas o más raras

  • git checkout entre ramas con ficheros LFS distintos implica descargas.
  • git bisect (06-02) sobre un historial con LFS puede descargar contenido en cada paso.
  • Los worktree (06-06) comparten la caché LFS, lo cual es bueno, pero cada uno necesita su checkout.
  • El smudge fichero a fichero puede ser lento; git lfs pull es más eficiente porque descarga en lote.

  1. Alternativas a LFS

LFS no es la única respuesta, y a veces no es la mejor.

Opción Cómo funciona A favor En contra Cuándo elegirla
Git LFS Puntero versionado + almacén aparte, vía filtros de .gitattributes Integrado en el flujo normal; soportado por casi todas las plataformas; versiona el binario de verdad Requiere servidor; cuotas de pago; no fusiona; rompe la autonomía del clon Binarios que cambian y que forman parte del producto: diseños, recursos, modelos
git annex Sustituye ficheros por enlaces simbólicos a un almacén de contenido; soporta muchos backends (discos, S3, otro ordenador) Muy flexible; funciona sin servidor dedicado; puede repartir copias entre varios almacenes; excelente para archivos enormes Curva de aprendizaje mucho mayor; menos soporte de plataformas; modelo mental propio Archivos científicos, colecciones grandes, entornos con almacenamiento heterogéneo o sin conexión
No versionar el binario El fichero vive fuera (almacenamiento de objetos, unidad compartida, gestor de recursos) y el repositorio guarda solo una referencia (URL + hash) Sin coste ni complejidad en Git; sin cuotas de LFS; cada sistema hace lo suyo Hay que construir la coordinación; el binario no está versionado con el código; riesgo de que la referencia se rompa Recursos grandes que cambian poco: vídeos de marketing, conjuntos de datos, dependencias precompiladas
Repensar el flujo Versionar la fuente y generar el binario; o guardar el original donde se creó y versionar solo la exportación ligera Elimina el problema de raíz; suele mejorar el proceso No siempre es posible Cuando el binario es derivado de algo más pequeño

La cuarta merece énfasis

Antes de instalar nada, hazte tres preguntas:

  1. ¿Es este fichero generado? Un .zip de distribución, un .png exportado desde un .svg, un ejecutable compilado. Si se puede regenerar, no debe estar en el repositorio (lección 08-03): va al .gitignore y lo produce la CI como artefacto.

  2. ¿Necesito su historial? Un vídeo de demostración que se sustituye entero cada seis meses no gana nada versionado. Un .psd sobre el que se itera a diario y del que a veces hay que volver a la versión de la semana pasada, sí.

  3. ¿Puedo versionar la fuente en vez del resultado? Un .svg es texto: se versiona, se diferencia y se fusiona perfectamente. Un .png de 4 MB exportado de ese .svg no aporta nada. Cambiar el formato de trabajo resuelve el problema mejor que cualquier herramienta.

Aplicado a gestor-tareas:

Fichero Decisión Motivo
mockup-panel.psd (84 MB, cambia a menudo) LFS Se itera sobre él, hace falta el historial y es fuente, no derivado
demo.mp4 (120 MB, se rehace entero) Fuera del repositorio No hace falta su historial; una URL y un hash bastan
logo.svg Git normal Es texto: se diferencia y se fusiona
logo-512.png (generado del .svg) .gitignore + generar en CI Es derivado
Recursos de componentes-ui LFS en el submódulo Cada repositorio decide lo suyo (lección 06-05)

  1. Cuándo usar LFS y cuándo no

Úsalo cuando se cumplan todas

  • Tienes ficheros binarios de más de unos pocos MB.
  • Cambian con cierta frecuencia, generando muchas versiones.
  • Necesitas su historial: hay que poder volver a una versión anterior.
  • No son derivados de nada más pequeño que puedas versionar en su lugar.
  • Tienes un servidor LFS disponible y presupuesto para su cuota.
  • Todo el equipo puede instalarlo.

No lo uses cuando

  • Los ficheros son pequeños (menos de 1 MB): el puntero y la complejidad no compensan.
  • Son generados: van al .gitignore.
  • No necesitas el historial: guárdalos fuera con una referencia.
  • Son texto, aunque sean grandes: Git los maneja bien con deltas y los fusiona.
  • No tienes servidor LFS y no vas a tenerlo.
  • El repositorio debe funcionar completamente sin conexión desde un solo clon.

La regla de decisión

flowchart TD
    A["Tengo un fichero grande"] --> B{"¿Es texto?"}
    B -->|sí| C["Git normal.<br/>Los deltas funcionan bien"]
    B -->|no| D{"¿Es generado?"}
    D -->|sí| E[".gitignore + artefacto de CI"]
    D -->|no| F{"¿Necesito<br/>su historial?"}
    F -->|no| G["Fuera del repositorio:<br/>URL + hash de verificación"]
    F -->|sí| H{"¿Puedo versionar<br/>la fuente en su lugar?"}
    H -->|sí| I["Versiona la fuente.<br/>Genera el resto"]
    H -->|no| J{"¿Tengo servidor<br/>LFS y cuota?"}
    J -->|sí| K["Git LFS"]
    J -->|no| L["git annex o<br/>almacén externo"]

La respuesta más frecuente en un proyecto como gestor-tareas es "no lo necesitas". Un repositorio de código puro con un puñado de iconos y un logo no tiene ningún problema que LFS resuelva. La necesidad aparece cuando entran ficheros de diseño, vídeo, audio, modelos 3D, conjuntos de datos o recursos de videojuego. En un repositorio de 50.000 ficheros con recursos gráficos, LFS es imprescindible; en uno de cuatro ficheros, es complejidad sin beneficio.

Errores Comunes y Consejos

Error 1: adoptar LFS después de haber metido los binarios. Declarar el patrón solo afecta al futuro. Los objetos que ya están en el historial siguen ahí y siguen descargándose. Hace falta git lfs migrate import y una reescritura coordinada. Configura LFS el día que creas el repositorio, no cuando pesa 2 GB.

Error 2: olvidar el -text. Sin él, un * text=auto puede normalizar finales de línea en un binario y corromperlo. git lfs track lo pone; si editas .gitattributes a mano, no lo quites.

Error 3: confirmar los binarios antes que .gitattributes. Los filtros se aplican en el momento del add. .gitattributes primero, en su propio commit, y los binarios después.

Error 4: no darse cuenta de que se tienen punteros. Si un fichero de imagen pesa 130 bytes y empieza por version https://git-lfs..., es un puntero. Ejecuta git lfs pull. Comprobación rápida:

file recursos/mockup-panel.psd
head -c 60 recursos/mockup-panel.psd

Error 5: dejar la CI descargando LFS sin necesitarlo. Es la vía más rápida de agotar la cuota de ancho de banda. lfs: false salvo en los trabajos que realmente necesitan los ficheros.

Error 6: git lfs fetch --all por costumbre. Descarga todo el historial de contenido, que es justo lo que LFS existe para evitar. Solo para copias de seguridad o migraciones.

Error 7: creer que un clon con LFS es una copia de seguridad completa. No lo es: te faltan los objetos LFS que no hayas descargado. Una copia real necesita el bundle y el contenido LFS.

Error 8: dos personas editando el mismo .psd a la vez. Terminará en conflicto irresoluble y alguien perderá trabajo. Usa --lockable y git lfs lock, o simplemente acordad quién toca qué.

Consejo 1: audita antes de decidir. git lfs migrate info --everything --above=1Mb te dice exactamente qué está inflando el repositorio, sin tocar nada.

Consejo 2: pon un límite de tamaño en la CI. Un guardián automático evita el problema de raíz (lección 07-06):

- name: Rechazar ficheros grandes fuera de LFS
  run: |
    LIMITE=$((5 * 1024 * 1024))
    GRANDES=$(git diff --name-only --diff-filter=ACM origin/main...HEAD | while read -r f; do
      [ -f "$f" ] || continue
      # Los punteros LFS son diminutos, así que nunca disparan el aviso
      TAM=$(wc -c < "$f")
      [ "$TAM" -gt "$LIMITE" ] && echo "$f ($TAM bytes)"
    done)
    if [ -n "$GRANDES" ]; then
      echo "Ficheros de más de 5 MB fuera de LFS:"
      echo "$GRANDES"
      echo "Declara el patrón con 'git lfs track' o sácalos del repositorio."
      exit 1
    fi

Consejo 3: documenta LFS en el CONTRIBUTING.md. Tres líneas al principio: instala git-lfs, ejecuta git lfs install, y si ves ficheros de 130 bytes, ejecuta git lfs pull. Ahorra la mitad de las preguntas de soporte.

Consejo 4: prueba la migración en un clon desechable. Siempre. Y verifica comparando el hash del árbol de los directorios de código antes y después: si coinciden, el código está intacto.

Consejo 5: git lfs prune de vez en cuando. La caché local crece sin límite. Con lfs.pruneverifyremotealways = true es una operación segura.

Ejercicios

Ejercicio 1: diagnosticar un repositorio inflado

Diego clona gestor-tareas y obtiene esto:

$ git count-objects -vH
count: 0
size: 0 bytes
in-pack: 3847
packs: 1
size-pack: 512.84 MiB

$ ls -lh recursos/
-rw-r--r-- 1 diego diego  130 ago  1 09:14 mockup-panel.psd
-rw-r--r-- 1 diego diego  84M ago  1 09:14 mockup-listado.psd
-rw-r--r-- 1 diego diego 118M ago  1 09:14 demo.mp4

Responde:

A. ¿Por qué mockup-panel.psd pesa 130 bytes y los otros dos no? B. ¿Por qué el repositorio ocupa 512 MB si supuestamente usan LFS? C. ¿Qué comandos ejecutarías para confirmar tu diagnóstico? D. ¿Cuál es la solución completa?

Ejercicio 2: configurar LFS desde cero, en el orden correcto

El equipo va a añadir a gestor-tareas:

  • recursos/disenos/*.psd — ficheros de diseño, ~80 MB, cambian semanalmente, se itera sobre ellos
  • recursos/video/demo.mp4 — 120 MB, se rehace entero cada seis meses
  • recursos/iconos/*.svg — texto, unos pocos KB cada uno
  • recursos/iconos/generados/*.png — exportados automáticamente de los SVG
  • dist/gestor-tareas.zip — paquete de distribución, generado en cada versión

Escribe la secuencia completa de comandos y ficheros, decidiendo para cada tipo si va a LFS, a Git normal, al .gitignore o fuera del repositorio. Justifica cada decisión y respeta el orden correcto de los commits.

Ejercicio 3: migración con obstáculos

gestor-tareas lleva dos años con .psd y .mp4 versionados directamente. El repositorio pesa 3,2 GB. Se decide migrar a LFS.

Restricciones:

  • Hay tres propuestas abiertas de Diego.
  • main está protegida y exige revisión.
  • Hay etiquetas v1.0.0 a v1.7.0, y v1.6.0 es lo que está en producción.
  • Bruno tiene una rama local con cinco commits sin enviar.
  • Los commits van firmados (08-05).

Escribe el plan completo, en orden, indicando qué pasa con cada restricción y qué comprobaciones harías. Señala en qué punto exacto el proceso es irreversible.

Soluciones

Solución 1

A. mockup-panel.psd es un puntero LFS; los otros dos son ficheros reales versionados directamente en Git.

Solo el primero fue declarado en .gitattributes antes de añadirlo. Los otros dos, o bien se añadieron antes de configurar LFS, o bien su patrón nunca se declaró.

B. Porque los 512 MB son los .psd y .mp4 que NO están en LFS, con todas sus versiones históricas. Adoptar LFS a medias no reduce nada: el repositorio sigue arrastrando todo lo que se metió antes o fuera de los patrones.

C. Diagnóstico paso a paso:

# 1. ¿Qué está realmente en LFS?
git lfs ls-files
# Solo aparecerá mockup-panel.psd

# 2. ¿Qué patrones están declarados?
git lfs track
cat .gitattributes
# Probablemente solo "recursos/mockup-panel.psd" o "*.psd" añadido tarde

# 3. ¿Qué ocupa el historial? (lección 08-06)
git rev-list --objects --all \
  | git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' \
  | awk '$1=="blob" {print $3, $4}' \
  | sort -rn | head -15

# 4. La vista agregada por extensión, que es la definitiva
git lfs migrate info --everything --above=1Mb

El paso 4 dará algo como:

*.mp4   289 MB    4/4 files
*.psd   198 MB    9/12 files

Ese 9/12 es la pista clave: de doce ficheros .psd del historial, nueve están fuera de LFS.

D. Solución completa, en dos partes:

Parte 1: arreglar la configuración para el futuro.

git lfs track "*.psd"
git lfs track "*.mp4"
git add .gitattributes
git commit -m "chore: GT-262 declarar todos los binarios en LFS"

Parte 2: limpiar el historial (reescritura coordinada, apartado 9).

git lfs migrate import --everything --include="*.psd,*.mp4"
git lfs push --all origin
git push --force-with-lease --all origin
git push --force-with-lease --tags origin

Y avisar al equipo de que debe clonar de nuevo. Sin la parte 2 el repositorio seguirá pesando 512 MB para siempre, porque los objetos antiguos no desaparecen al cambiar la configuración.

Solución 2

Decisiones, con su motivo:

Fichero Decisión Por qué
recursos/disenos/*.psd LFS Binario grande, cambia a menudo, se necesita el historial, no es derivado
recursos/video/demo.mp4 Fuera del repositorio No se necesita su historial (se rehace entero). Referencia en el README.md con URL y hash
recursos/iconos/*.svg Git normal Es texto: se diferencia, se fusiona y comprime bien. LFS sería un estorbo
recursos/iconos/generados/*.png .gitignore Derivados de los SVG. Se generan en la CI
dist/gestor-tareas.zip .gitignore Artefacto de construcción (lección 08-03). Se publica como artefacto de la versión

Sobre el .mp4: si el equipo prefiere versionarlo, LFS es una opción defendible. Pero al no necesitar su historial, guardarlo fuera evita consumir cuota con dos copias de 120 MB.

Secuencia completa, en el orden correcto:

cd ~/gestor-tareas

# --- Paso 1: instalación (una vez por máquina, cada persona) ---
git lfs install

# --- Paso 2: declarar los patrones LFS ---
git lfs track "recursos/disenos/*.psd"

# --- Paso 3: .gitignore para lo derivado ---
cat >> .gitignore <<'EOF'

# Recursos generados (GT-263)
recursos/iconos/generados/
dist/
EOF

# --- Paso 4: verificar lo declarado ANTES de confirmar ---
git lfs track
cat .gitattributes

# --- Paso 5: confirmar la CONFIGURACIÓN primero, sola ---
git add .gitattributes .gitignore
git commit -m "chore: GT-263 configurar Git LFS para los ficheros de diseño

Los .psd de recursos/disenos/ pasan a gestionarse con Git LFS: en el
historial solo quedan punteros de texto y el contenido vive en el
almacén LFS del servidor.

Los PNG generados y el paquete de distribución se ignoran: son
derivados y los produce la tubería de integración.

El vídeo de demostración no se versiona; su URL y su hash quedan
documentados en el README.

Requisito: ejecutar 'git lfs install' antes de clonar."

Confirmar la configuración sola y antes es lo que garantiza que los filtros estén activos cuando lleguen los binarios.

# --- Paso 6: ahora sí, los ficheros ---
git add recursos/disenos/panel.psd recursos/disenos/listado.psd
git add recursos/iconos/*.svg

# --- Paso 7: verificar ANTES de confirmar ---
git lfs status
Objects to be committed:

    recursos/disenos/panel.psd (LFS: 4d7a9f2)
    recursos/disenos/listado.psd (LFS: 9e2f7a4)

Esta comprobación es la clave del ejercicio. Si dijera (File: ...) en lugar de (LFS: ...), el filtro no se habría aplicado y habría que deshacer el add, revisar .gitattributes y volver a empezar. Verificar aquí cuesta cinco segundos; descubrirlo después cuesta una reescritura de historial.

git commit -m "feat: GT-263 añadir los diseños del panel y del listado"

# --- Paso 8: confirmar que en el historial hay punteros, no binarios ---
git show HEAD:recursos/disenos/panel.psd
git cat-file -s HEAD:recursos/disenos/panel.psd    # debe dar ~130

# --- Paso 9: publicar ---
git push origin main
git lfs ls-files

Y en el CONTRIBUTING.md:

## Antes de clonar

Este repositorio usa Git LFS para los ficheros de diseño.

1. Instala git-lfs: `apt install git-lfs` / `brew install git-lfs`
2. Ejecuta una vez: `git lfs install`
3. Clona normalmente.

Si un `.psd` pesa 130 bytes y empieza por `version https://git-lfs...`,
tienes un puntero. Ejecuta `git lfs pull`.

El vídeo de demostración no está en el repositorio: descárgalo de la URL
indicada en el README y verifica su hash.

Solución 3

Plan completo, por fases.

Fase 0 — Preparación (reversible).

# Auditar: ¿cuánto se recupera y de qué?
git lfs migrate info --everything --above=1Mb

Con este dato se decide si merece la pena. Reducir de 3,2 GB a ~150 MB, sí; a 2,8 GB, no.

Restricción — las tres propuestas de Diego: hay que cerrarlas antes. Se integran o se abandonan. Una propuesta basada en commits que van a dejar de existir queda inservible, y pedirle a un colaborador externo que rehaga tres ramas sobre un historial reescrito es una mala experiencia. Esta es la restricción que marca el calendario.

Restricción — la rama local de Bruno: que la envíe al servidor antes de la migración. Así se reescribirá junto con todo lo demás y no tendrá que reubicarla a mano. Si no puede, tendrá que hacer el rebase --onto del apartado 9.

# Cada persona comprueba que no le queda nada
git log --branches --not --remotes --oneline
git stash list
git status

Fase 1 — Copias de seguridad (reversible).

git clone --mirror https://git.ejemplo.es/gestor-tareas.git ~/copias/antes.git
git bundle create ~/copias/antes-$(date +%F).bundle --all

# Verificar que la copia sirve de verdad
git bundle verify ~/copias/antes-$(date +%F).bundle

Y, si en algún momento se necesitaran los binarios originales, descargar el contenido antes de perderlo de vista.

Fase 2 — Ensayo (reversible).

git clone https://git.ejemplo.es/gestor-tareas.git /tmp/ensayo
cd /tmp/ensayo
git lfs migrate import --everything --include="*.psd,*.mp4"

# ¿Cuánto se ha reducido?
git count-objects -vH

# Verificación criptográfica: el CÓDIGO no ha cambiado
git rev-parse HEAD:src
git -C ~/gestor-tareas rev-parse HEAD:src   # deben coincidir

# ¿v1.6.0 sigue teniendo el mismo contenido de código?
git rev-parse v1.6.0^{tree}
git cat-file -p v1.6.0^{tree}

# Anotar la correspondencia entre etiquetas viejas y nuevas
git for-each-ref --format='%(refname:short) %(objectname:short)' refs/tags

Esa tabla de correspondencias hay que guardarla y publicarla: si alguien tiene apuntado que en producción está el commit 3f2a1b9, necesitará saber cuál es su equivalente nuevo.

Restricción — las firmas: al reescribir los commits, las firmas se invalidan y se pierden. El objeto commit cambia, así que la firma sobre el objeto anterior deja de ser válida. Hay que asumirlo y documentarlo: los commits anteriores a la migración quedarán sin firma verificable. Es un argumento serio para hacer la migración cuanto antes o no hacerla nunca, y una razón más para configurar LFS el primer día. Los commits posteriores se firmarán con normalidad.

Restricción — v1.6.0 en producción: la etiqueta cambiará de hash. Hay que comprobar qué apunta a ese commit en el sistema de despliegue (lección 10-05) y actualizarlo. Y verificar que la nueva v1.6.0 produce el mismo artefacto que la vieja: mismo código, distinto hash de commit.

Fase 3 — Ventana de migración (PUNTO IRREVERSIBLE).

El punto de no retorno es el git push --force-with-lease. Todo lo anterior es reversible. A partir de ahí, el servidor tiene el historial nuevo y cualquiera que clone o traiga cambios obtiene la versión reescrita. Volver atrás requiere restaurar desde la copia espejo y repetir el anuncio.

# 1. Anunciar el inicio y pedir que nadie envíe nada
# 2. Desproteger main temporalmente en el servidor  <-- restricción de rama protegida
# 3. Migrar
cd ~/gestor-tareas
git fetch --all --prune
git lfs migrate import --everything --include="*.psd,*.mp4"

# 4. PRIMERO el contenido LFS
git lfs push --all origin

# 5. Verificar que el almacén lo tiene antes de tocar las referencias
git lfs ls-files --all | wc -l

# 6. AHORA las referencias  <-- IRREVERSIBLE a partir de aquí
git push --force-with-lease --all origin
git push --force-with-lease --tags origin

# 7. Volver a proteger main

La rama protegida exige desprotegerla y volver a protegerla. Anótalo en el guion: es el paso que más se olvida, y dejar main desprotegida un fin de semana es un riesgo innecesario.

Fase 4 — Recuperación del equipo.

Mensaje al equipo:

El historial de gestor-tareas se ha reescrito para migrar los binarios a LFS.
El repositorio ha pasado de 3,2 GB a ~150 MB.

QUÉ TIENES QUE HACER:
1. git lfs install   (si aún no lo has hecho)
2. Renombra tu clon actual y clona de nuevo.
3. git lfs pull

NO hagas 'git pull' sobre tu clon viejo: producirá una divergencia total.

Las etiquetas han cambiado de hash. Correspondencias: <enlace>
Los commits anteriores a hoy han perdido su firma; los nuevos se firman igual.
Si tienes trabajo local sin enviar, avísame antes de tocar nada.

Fase 5 — Cierre.

# En cada máquina: verificar
git count-objects -vH
git lfs ls-files | head
git log --oneline -5

Y en el servidor: pedir la recolección de basura para que libere el espacio de los objetos antiguos. Sin este paso, el repositorio del servidor seguirá pesando 3,2 GB y la migración parecerá no haber servido de nada.

Por último, poner el guardián en la CI (consejo 2) para que esto no vuelva a pasar, y documentar LFS en el CONTRIBUTING.md.

Conclusión

Git es excelente para texto y pésimo para binarios grandes, y ahora sabes exactamente por qué: no es una limitación arbitraria, es una consecuencia del modelo de datos. Cada versión es un blob completo, los deltas no funcionan sobre contenido ya comprimido, los objetos son inmutables y todo el mundo se descarga el historial entero.

  • Git LFS sustituye el fichero por un puntero de texto de unos 130 bytesversion, oid sha256:..., size— que sí se versiona bien, y guarda el contenido real en un almacén aparte. Funciona mediante los filtros clean y smudge de .gitattributes, el mismo mecanismo de la lección 08-04.
  • La configuración vive en .gitattributes como filter=lfs diff=lfs merge=lfs -text: el filter hace la sustitución, diff y merge registran los controladores de LFS, y -text evita que la normalización de finales de línea corrompa el binario. Al estar versionada, todo el equipo la hereda; lo que no se hereda es la instalación de LFS.
  • El orden importa: git lfs track, confirmar .gitattributes solo, y después añadir los binarios. Verificar siempre con git lfs status que dice (LFS: ...) y no (File: ...).
  • Al clonar se descargan los punteros y solo el contenido de la revisión extraída. GIT_LFS_SKIP_SMUDGE=1 y lfs: false en la CI evitan descargas innecesarias; git lfs pull las trae cuando hacen falta. Y git lfs fetch --all descarga todo el historial de contenido: úsalo solo para copias de seguridad.
  • git lfs migrate import reescribe el historial, con toda la fuerza de la regla de oro de la lección 05-01: cambian los hashes, cambian las etiquetas, se invalidan las firmas, y todo el mundo debe clonar de nuevo. Ensaya en una copia, verifica con el hash del árbol que el código no se ha tocado, publica primero el contenido LFS y después las referencias, y no olvides pedir la recolección de basura del servidor.
  • Las limitaciones son reales y hay que conocerlas antes: hace falta servidor, las cuotas cuestan dinero (también el ancho de banda de descarga), las plataformas no se comportan igual, el clon deja de ser autónomo, y los ficheros LFS no se fusionan: hay que elegir versión ganadora, o coordinarse con --lockable y git lfs lock.
  • Y hay alternativas: git annex para escenarios más flexibles, guardar el binario fuera con una referencia cuando no necesitas su historial, y sobre todo repensar el flujo: si el fichero es generado, va al .gitignore; si puedes versionar la fuente en lugar del resultado, hazlo.

La idea que resume la lección:

LFS no hace que Git maneje bien los binarios. Hace que Git deje de manejarlos, versionando en su lugar una referencia. Es una solución excelente cuando el binario es parte real del producto y necesitas su historial, y complejidad innecesaria en cualquier otro caso.

Para gestor-tareas, con sus cuatro ficheros de texto, LFS era irrelevante hasta ayer. Con la llegada de los ficheros de diseño ha pasado a ser necesario, y ha llegado justo a tiempo: configurarlo hoy cuesta dos commits; hacerlo dentro de un año habría costado una reescritura de historial y una tarde de coordinación.

Lo que viene

LFS resuelve una de las tres formas en que un repositorio se vuelve inmanejable: los ficheros muy grandes. Quedan las otras dos, y son independientes.

Un repositorio puede tener muchísimo historial —veinte años y cientos de miles de commits— aunque cada fichero sea diminuto: clonarlo tarda una eternidad y git log se arrastra. Y puede tener muchísimos ficheros —cientos de miles en la copia de trabajo— aunque el historial sea corto: git status tarda medio minuto porque tiene que recorrer el árbol entero.

Cada una tiene su propio remedio, y ninguno sirve para las otras dos. Los clones parciales con --filter=blob:none atacan el volumen de contenido descargado. El sparse-checkout con índice disperso ataca el número de ficheros en el disco. El commit-graph ataca el coste de recorrer el historial. Y por encima de todo está la decisión que condiciona el resto: monorepo o multirrepo, la comparativa que la lección 06-05 dejó abierta y que el caso 3 de la 10-01 dejó pendiente de explicar.

La lección 10-04: Escalando Git para Proyectos Grandes cierra ambas promesas, y empieza por la única regla que importa en optimización: medir antes de tocar nada.

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