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
- Por qué Git sufre con los binarios grandes
- La aritmética del problema en
gestor-tareas - Qué es Git LFS: la idea del puntero
- Instalación y primer seguimiento
- Cómo se refleja en
.gitattributes - El puntero por dentro
- Clonar, traer y no traer objetos
- Comandos de inspección y mantenimiento
- Migrar un repositorio que ya tiene binarios en el historial
- Limitaciones y avisos que hay que conocer antes
- Alternativas a LFS
- Cuándo usar LFS y cuándo no
- 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):
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
worktreey cada clon nuevo ocupa ese espacio en disco. git gctarda más,git clonetarda 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.
- La aritmética del problema en
gestor-tareas
gestor-tareasPongamos 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.
- 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 |
- 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.GitLFSY después, una vez por máquina y por usuario:
Ese comando hace dos cosas concretas, y conviene saberlo porque explica muchos comportamientos:
1. Registra los filtros en la configuración global:
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/**"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:
Listing tracked patterns
*.psd (.gitattributes)
*.mp4 (.gitattributes)
*.sketch (.gitattributes)
recursos/video/** (.gitattributes)Y dejar de seguir un patrón:
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).
- Cómo se refleja en
.gitattributes
.gitattributesAquí 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.
# ============================================================
# 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 -textDesglosemos 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.
- El puntero por dentro
Vamos a mirar exactamente qué se guarda. En el disco, el fichero es normal:
Pero lo que Git tiene versionado es otra cosa:
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):
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:
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:
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.
- 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
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:
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.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:
Esto desactiva el filtro smudge, así que los ficheros LFS quedan en el disco como punteros de texto:
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 mainTraer 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 pullCuidado 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 docsEse 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.
- Comandos de inspección y mantenimiento
git lfs ls-files: qué está en LFS
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 --allgit lfs status: el git status de LFS
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
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:
# Ver qué borraría, sin borrar nada
git lfs prune --dry-run --verbose
# Borrar los objetos locales que ya no hacen falta
git lfs pruneprune 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 trueEse 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
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.
- 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 importreescribe 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 listEse 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 --allPaso 2: auditar.
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:
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 originEl 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 pullY 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-ramaEse 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
Convierte punteros de vuelta en ficheros normales. Útil si decides abandonar LFS. Requiere tener descargados los objetos, y reescribe el historial exactamente igual.
- 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.
- 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/
- 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.
- 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.
- 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.
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.
Objects to be committed:
recursos/mockup-panel.psd (LFS: 4d7a9f2 -> 9e2f7a4)
Unmerged paths:
recursos/mockup-panel.psdY 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.psdEsos :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.psdCon --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.
- 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.
- Algunas operaciones se vuelven más lentas o más raras
git checkoutentre 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 sucheckout. - El
smudgefichero a fichero puede ser lento;git lfs pulles más eficiente porque descarga en lote.
- 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:
-
¿Es este fichero generado? Un
.zipde distribución, un.pngexportado desde un.svg, un ejecutable compilado. Si se puede regenerar, no debe estar en el repositorio (lección 08-03): va al.gitignorey lo produce la CI como artefacto. -
¿Necesito su historial? Un vídeo de demostración que se sustituye entero cada seis meses no gana nada versionado. Un
.psdsobre el que se itera a diario y del que a veces hay que volver a la versión de la semana pasada, sí. -
¿Puedo versionar la fuente en vez del resultado? Un
.svges texto: se versiona, se diferencia y se fusiona perfectamente. Un.pngde 4 MB exportado de ese.svgno 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) |
- 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:
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
fiConsejo 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 ellosrecursos/video/demo.mp4— 120 MB, se rehace entero cada seis mesesrecursos/iconos/*.svg— texto, unos pocos KB cada unorecursos/iconos/generados/*.png— exportados automáticamente de los SVGdist/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.
mainestá protegida y exige revisión.- Hay etiquetas
v1.0.0av1.7.0, yv1.6.0es 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=1MbEl paso 4 dará algo como:
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 originY 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 statusObjects 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-filesY 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).
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 statusFase 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).bundleY, 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/tagsEsa 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 mainLa 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.
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 bytes —
version,oid sha256:...,size— que sí se versiona bien, y guarda el contenido real en un almacén aparte. Funciona mediante los filtroscleanysmudgede.gitattributes, el mismo mecanismo de la lección 08-04. - La configuración vive en
.gitattributescomofilter=lfs diff=lfs merge=lfs -text: elfilterhace la sustitución,diffymergeregistran los controladores de LFS, y-textevita 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.gitattributessolo, y después añadir los binarios. Verificar siempre congit lfs statusque 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=1ylfs: falseen la CI evitan descargas innecesarias;git lfs pulllas trae cuando hacen falta. Ygit lfs fetch --alldescarga todo el historial de contenido: úsalo solo para copias de seguridad. git lfs migrate importreescribe 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
--lockableygit lfs lock. - Y hay alternativas:
git annexpara 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
- ¿Qué es Git?
- Instalando Git
- Terminología Básica de Git
- El Modelo de Datos de Git
- Configurando Git
- Configuración Inicial
Módulo 2: Operaciones Básicas de Git
- Creando un Repositorio
- Clonando un Repositorio
- Flujo de Trabajo Básico de Git
- Preparando y Confirmando Cambios
- Inspeccionando Cambios con git diff
- Visualizando el Historial de Confirmaciones
Módulo 3: Ramas y Fusión
- Entendiendo las Ramas
- Creando y Cambiando Ramas
- Fusionando Ramas
- Estrategias de Fusión
- Resolviendo Conflictos de Fusión
- Gestión de Ramas
Módulo 4: Trabajando con Repositorios Remotos
- Entendiendo los Repositorios Remotos
- Agregando un Repositorio Remoto
- Autenticación con Repositorios Remotos
- Obteniendo y Extrayendo Cambios
- Enviando Cambios
- Rastreando Ramas
Módulo 5: Operaciones Avanzadas de Git
- Rebase
- Rebase Interactivo
- Cherry-Picking de Confirmaciones
- Guardando Cambios Temporales
- Etiquetando Confirmaciones
- Revirtiendo Confirmaciones
Módulo 6: Herramientas y Técnicas de Git
- Usando Git Hooks
- Git Bisect
- Git Blame
- Git Log y Alias
- Submódulos de Git
- Múltiples Copias de Trabajo con git worktree
Módulo 7: Estrategias de Colaboración y Flujo de Trabajo
- Forks y Pull Requests
- Revisiones de Código con Git
- Flujo de Trabajo Git Flow
- GitHub Flow
- Trunk Based Development
- Integración Continua con Git
Módulo 8: Mejores Prácticas y Consejos de Git
- Escribiendo Buenos Mensajes de Confirmación
- Manteniendo un Historial Limpio
- Ignorando Archivos con .gitignore
- Atributos de Fichero con .gitattributes
- Mejores Prácticas de Seguridad
- Consejos de Rendimiento
Módulo 9: Solución de Problemas y Depuración
- Problemas Comunes de Git
- Deshaciendo Cambios
- Resolviendo Divergencias con el Remoto
- Recuperando Confirmaciones Perdidas
- Tratando con Repositorios Corruptos
- Técnicas Avanzadas de Depuración
