Aquesta lliçó salda tres deutes del curs. A la lliçó 06-05 vam dir que si el problema no és codi compartit sinó binaris pesants, la resposta no són els submòduls sinó Git LFS. A la 08-04 va aparèixer a .gitattributes una línia críptica —*.psd filter=lfs diff=lfs merge=lfs -text— i vam prometre explicar-la. I a la 08-06, en analitzar per què els binaris grans enverinen un repositori, vam dir que la solució correcta era aquesta.

Aquí la tens, i arriba en bon moment: l'equip de gestor-tasques incorporarà recursos gràfics, un vídeo de demostració i els fitxers de disseny originals de components-ui. La Carla ha afegit un mockup-panel.psd de 84 MB i ja l'ha modificat tres vegades. El repositori, que pesava 4 MB, ara pesa 250 MB, i el Bruno triga quatre minuts a clonar el que abans trigava dos segons.

Entendrem per què passa això —no per què «els binaris són dolents», sinó què fa exactament Git amb ells—, com ho resol LFS, i sobretot quin preu té, perquè LFS no és gratis ni indolor i cal adoptar-lo sabent on fa mal.

Contingut

  1. Per què Git pateix amb els binaris grans
  2. L'aritmètica del problema a gestor-tasques
  3. Què és Git LFS: la idea del punter
  4. Instal·lació i primer seguiment
  5. Com es reflecteix a .gitattributes
  6. El punter per dins
  7. Clonar, portar i no portar objectes
  8. Ordres d'inspecció i manteniment
  9. Migrar un repositori que ja té binaris a l'historial
  10. Limitacions i avisos que cal conèixer abans
  11. Alternatives a LFS
  12. Quan fer servir LFS i quan no

  1. Per què Git pateix amb els binaris grans

Cal tornar al model de dades de la lliçó 01-04, perquè el problema no és una limitació arbitrària: és una conseqüència directa de com funciona Git.

Cada versió és un blob complet

Git no desa diferències. Desa el contingut complet de cada versió de cada fitxer, com un objecte blob identificat pel hash SHA-1 del seu contingut. Quan modifiques un fitxer i confirmes, no es desa «el que ha canviat»: es desa un blob nou amb el fitxer sencer.

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 -.->|"tots continuen<br/>existint"| B2
    B2 -.-> B3

Amb fitxers de text això no importa, i aquí hi ha la clau: en empaquetar el repositori, Git calcula deltes entre objectes semblants. Un fitxer app.js de 40 KB amb cent versions es comprimeix a una fracció minúscula, perquè cada versió es desa com «l'anterior més aquests canvis».

Per què la delta no funciona amb binaris

Tres raons que s'acumulen:

Raó Què implica
Ja estan comprimits Un .psd, un .png, un .mp4 o un .zip porten compressió pròpia. Tornar a comprimir no guanya gairebé res, i zlib gasta temps per no res.
Un canvi petit altera tot el fitxer Canviar un píxel en un format comprimit reescriu el flux complet. Els dos fitxers no s'assemblen byte a byte encara que s'assemblin visualment.
Git compara bytes, no semàntica Git no sap que dos .psd són la mateixa imatge amb una capa diferent. Només veu dues seqüències de bytes sense res en comú.

El resultat: la delta entre dues versions d'un binari és tan gran com el mateix binari. Es pot comprovar (lliçó 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

Les columnes són: hash, tipus, mida real, mida dins del paquet i desplaçament. Els tres blobs del .psd ocupen gairebé 84 MB cadascun dins del paquet: la compressió no ha aconseguit res. Compara-ho amb el blob de text de l'última fila, que passa de 41 KB a 8 KB.

I l'historial és per sempre

Això és el que converteix un problema molest en un de greu. Com vam veure a la lliçó 08-05 amb els secrets i a la 08-06 amb la mida: esborrar el fitxer no elimina els objectes. Un git rm mockup-panel.psd crea un commit nou on el fitxer no hi és, però els tres blobs de 84 MB continuen a la base de dades, abastables des dels commits antics, i tothom qui cloni se'ls baixarà.

Treure'ls de debò exigeix reescriure l'historial amb git filter-repo, amb totes les conseqüències que vam estudiar: canvien tots els hashos des del punt reescrit, i tothom ha de recuperar el repositori.

El cost se'l menja tothom

I aquest és l'argument decisiu. El repositori de Git és distribuït: cada clon té l'historial complet. Que la Carla afegeixi 250 MB d'historial significa que:

  • El Bruno, l'Ana i el Diego es baixen 250 MB en clonar.
  • Cada treball de CI que cloni es baixa 250 MB (i n'hi ha molts al dia).
  • Cada worktree i cada clon nou ocupa aquest espai al disc.
  • git gc triga més, git clone triga més, i el servidor serveix més trànsit.

Un fitxer gran no és un problema de qui l'afegeix. És un impost sobre tot l'equip, cobrat per sempre.

  1. L'aritmètica del problema a gestor-tasques

Posem números concrets al cas de la Carla, per tenir la intuïció de l'ordre de magnitud:

Element Mida Versions Ocupació a l'historial
app.js, index.html, estils.css, README.md ~60 KB ~400 commits ~2 MB (amb deltes)
mockup-panel.psd 84 MB 3 ~250 MB
demo.mp4 (previst) 120 MB 2 ~240 MB
Recursos gràfics de components-ui ~15 MB ~20 ~300 MB

El codi —el que de debò és el projecte— ocupa el 0,25 % del repositori. La resta són binaris que gairebé ningú no necessita a la seva còpia de treball la majoria dels dies.

Aquí hi ha l'observació que justifica LFS: la majoria de la gent, la majoria del temps, només necessita l'última versió d'aquests fitxers, o cap. L'Ana, que treballa en la lògica d'app.js, no necessita les tres versions del .psd; ni tan sols necessita l'última. El servidor de CI que executa les proves, tampoc.

Git, per disseny, ho dona tot a tothom. LFS trenca aquest «tot a tothom» només per als fitxers que decideixis.

  1. Què és Git LFS: la idea del punter

Git LFS (Large File Storage) és una extensió de Git —no forma part de Git, s'instal·la a part— construïda sobre un mecanisme que Git sí que porta de sèrie: els filtres de .gitattributes que vam veure a la lliçó 08-04.

La idea completa cap en una frase:

En lloc del fitxer gran, al repositori es versiona un petit fitxer de text que diu on és el contingut. El contingut real viu en un magatzem a part i es baixa només quan cal.

flowchart TD
    subgraph disc["La teva còpia de treball"]
        A["mockup-panel.psd<br/>84 MB, fitxer real"]
    end
    subgraph repo["Repositori Git"]
        B["mockup-panel.psd<br/>130 bytes: punter de text<br/>oid sha256:4d7a...<br/>size 88080384"]
    end
    subgraph magatzem["Magatzem LFS del servidor"]
        C["4d7a...<br/>contingut real, 84 MB"]
    end

    A -->|"git add: filtre clean"| B
    B -->|"git checkout: filtre smudge"| A
    A -->|"git push: puja el contingut"| C
    C -->|"git checkout / lfs pull"| A

Dos mecanismes independents treballant alhora:

El filtre clean s'executa quan el contingut entra a Git (git add). Rep el fitxer de 84 MB, en calcula el hash SHA-256, desa el contingut a la memòria cau local d'LFS i retorna a Git un text de tres línies. Git versiona aquest text, que és el que acaba al blob.

El filtre smudge s'executa quan el contingut surt de Git (git checkout). Rep el punter, busca el contingut pel seu hash (a la memòria cau local o baixant-lo del servidor) i escriu el fitxer real al disc.

El resultat és que treballes exactament igual que sempre. Obres el .psd, l'edites, git add, git commit, git push. Els filtres són invisibles. El que canvia és el que es desa a l'historial: 130 bytes de text en lloc de 84 MB de binari.

Per què això ho arregla

Efecte Motiu
L'historial de Git es manté petit Cada versió són 130 bytes, i a sobre són text que comprimeix bé
El clon és ràpid Es baixen els punters; el contingut, només el de la revisió que extreus
No es baixen les versions antigues El servidor LFS només envia els objectes que demanis
Es pot evitar baixar fins i tot els actuals La CI que no necessita el .psd es pot saltar el smudge
L'emmagatzematge es pot gestionar a part Amb les seves pròpies polítiques de retenció i de cost

  1. Instal·lació i primer seguiment

Instal·lar

LFS és un binari a part, i cada persona de l'equip l'ha d'instal·lar:

# Ana, Ubuntu
sudo apt install git-lfs

# Bruno, macOS
brew install git-lfs

# Carla, Windows 11: ve inclòs a l'instal·lador oficial de Git,
# o bé:
winget install GitHub.GitLFS

I després, una vegada per màquina i per usuari:

git lfs install
Updated Git hooks.
Git LFS initialized.

Aquesta ordre fa dues coses concretes, i convé saber-ho perquè explica molts comportaments:

1. Registra els filtres a la configuració 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

És exactament el mecanisme de filtres de la lliçó 08-04. filter.lfs.required true significa que si LFS no està instal·lat, Git falla en lloc de deixar punters escampats pel disc. És el que vols.

2. Instal·la hooks al repositori (pre-push, post-checkout, post-commit, post-merge) que s'encarreguen de pujar i baixar els objectes en el moment adequat. Són hooks (lliçó 06-01), amb les mateixes propietats de sempre: viuen a .git/hooks/ i no es distribueixen. Per això qui cloni el repositori necessita haver executat git lfs install a la seva màquina.

Declarar quins fitxers se segueixen

cd ~/gestor-tasques

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/**"

Les cometes són importants: sense elles, l'intèrpret d'ordres expandiria *.psd als fitxers existents i es registrarien rutes concretes en lloc del patró.

Veure el que hi ha declarat:

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

I deixar de seguir un patró:

git lfs untrack "*.sketch"

Compte: untrack treu la regla per al futur. Els fitxers ja convertits en punters a l'historial continuen sent punters; per revertir-ho cal reescriure l'historial (apartat 9).

  1. Com es reflecteix a .gitattributes

Aquí es tanca la promesa de la lliçó 08-04. git lfs track no desa res en un fitxer propi: escriu a .gitattributes, el mateix fitxer que ja coneixes.

cat .gitattributes
# ============================================================
# Finals de línia (resol 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

Desglossem els quatre atributs, que és exactament el que va quedar pendent:

Atribut Què fa
filter=lfs Aplica els filtres clean i smudge registrats com a filter.lfs.*. És el nucli del mecanisme: converteix contingut en punter en entrar, i punter en contingut en sortir.
diff=lfs Fa servir el controlador de diferències d'LFS en lloc del genèric. Així git diff mostra alguna cosa útil sobre el fitxer (mida, identificador de l'objecte) en comptes del punter en cru.
merge=lfs Fa servir el controlador de fusió d'LFS. Com veurem, no fusiona res: obliga a triar un costat.
-text Desactiva l'atribut text: diu a Git que no normalitzi els finals de línia (lliçó 08-04). Sense això, un * text=auto com el de dalt podria corrompre el contingut abans que el filtre el vegi.

El -text és el més fàcil de passar per alt i el que causa el mal més difícil de diagnosticar. Per això git lfs track el posa sempre.

Conseqüència pràctica molt important: .gitattributes és un fitxer versionat. Quan la Carla el confirma i l'envia, tot l'equip hereta la configuració d'LFS automàticament. No cal dir a ningú quins fitxers són d'LFS: és al repositori. Això és el que fa que LFS sigui utilitzable en equip, i és un bon exemple del principi de la lliçó anterior: la configuració compartida viu al repositori.

El que no viatja al repositori és la instal·lació d'LFS ni els seus hooks. Per això el CONTRIBUTING.md (lliçó 10-01) ho ha de dir a la primera línia.

L'ordre importa

Un detall subtil: .gitattributes s'aplica en el moment del git add. Si afegeixes el .psd abans de declarar el patró, es versiona com a binari normal i ja és a l'historial. Per això la seqüència correcta és sempre:

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

# 2. Confirmar .gitattributes ABANS d'afegir els binaris
git add .gitattributes
git commit -m "chore: GT-251 configura Git LFS per als fitxers de disseny"

# 3. Ara sí, afegir els fitxers
git add recursos/mockup-panel.psd
git commit -m "feat: GT-251 afegeix el mockup del panell de tasques"

Confirmar .gitattributes en un commit separat i anterior no és cosmètica: garanteix que qui faci checkout de qualsevol punt de l'historial tingui la configuració correcta abans de trobar-se els fitxers.

  1. El punter per dins

Mirem exactament què es desa. Al disc, el fitxer és normal:

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

Però el que Git té versionat és una altra cosa:

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

Cent trenta bytes. Això és tot el que hi ha a l'historial de Git per cada versió del .psd. Tres línies:

Línia Significat
version Versió del format de punter d'LFS
oid sha256:... L'identificador del contingut: el hash SHA-256 del fitxer real
size La mida en bytes del contingut real

Fixa't en l'elegància conceptual: LFS aplica exactament el mateix principi que Git —adreçament per contingut (lliçó 01-04)— però un nivell més avall. El contingut s'identifica pel seu hash, és immutable, i el mateix contingut no es desa mai dues vegades. LFS fa servir SHA-256 mentre que Git (encara) fa servir SHA-1, una cosa sobre la qual tornarem a la lliçó 10-06.

Per veure-ho des de més avall, amb lampisteria (lliçó 09-06):

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

Cent trenta bytes a la base de dades d'objectes. Davant de 88.080.384. Aquesta és tota la lliçó.

Què mostra git diff

Gràcies a l'atribut 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 dins del que és possible: diu que el contingut ha canviat i que la mida és la mateixa. No diu què ha canviat a la imatge, perquè això Git no ho pot saber.

I aquí convé recordar el truc de la lliçó 08-04: es pot definir un controlador de diferències textualitzador per a certs binaris, de manera que git diff mostri metadades llegibles:

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

No és una diferència visual, però saber que la imatge ha passat de 1200×800 a 2400×1600 ja és informació. Els dos mecanismes són compatibles: LFS gestiona l'emmagatzematge, textconv millora la visualització.

  1. Clonar, portar i no portar objectes

Aquí hi ha la major part del valor pràctic d'LFS, i també les seves raresses.

Un clon normal

git clone https://git.exemple.cat/gestor-tasques.git
Cloning into 'gestor-tasques'...
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.

Dues fases clarament separades:

  1. Receiving objects: l'historial de Git. 2,14 MB. Inclou els quatre-cents commits, tot el codi i tots els punters de totes les versions.
  2. Filtering content: LFS baixant el contingut real dels fitxers de la revisió extreta. 196 MB, dos fitxers.

La diferència amb l'escenari sense LFS: sense ell haurien estat 250 MB a la primera fase (totes les versions de tot) més el temps de resoldre deltes inútils. Amb LFS són 2 MB d'historial més només el contingut actual.

Clonar sense baixar el contingut

Per a qui no necessita els binaris —la CI que executa les proves, algú que només tocarà app.js— hi ha una variable d'entorn:

GIT_LFS_SKIP_SMUDGE=1 git clone https://git.exemple.cat/gestor-tasques.git

Això desactiva el filtre smudge, així que els fitxers LFS queden al disc com a punters de text:

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

És una situació perfectament vàlida i molt útil, però cal saber-la reconèixer, perquè és l'origen de la incidència de suport més habitual amb LFS: algú obre el .psd amb el seu editor d'imatges i rep un error incomprensible, sense entendre que el que té és un fitxer de text de 130 bytes.

Per deixar-ho permanent en una màquina o repositori concret:

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

O de manera més neta i moderna, per clonar sense contingut:

git clone --no-checkout https://git.exemple.cat/gestor-tasques.git
cd gestor-tasques
git lfs install --local --skip-smudge
git checkout main

Portar el contingut quan el necessitis

# Baixar el contingut dels fitxers LFS de la revisió actual
git lfs pull

# Només el que coincideixi amb un patró
git lfs pull --include="recursos/mockup-*.psd"

# Excloure vídeos, que són el més pesant
git lfs pull --exclude="*.mp4"

I la distinció entre fetch i pull, que és la mateixa que a Git (lliçó 04-04):

# fetch: baixa a la memòria cau local d'LFS, sense escriure a la còpia de treball
git lfs fetch

# fetch --all: TOTES les versions de TOTS els fitxers LFS de l'historial
git lfs fetch --all

# fetch d'una referència concreta
git lfs fetch origin v1.3.0

# checkout: escriu a la còpia de treball el que ja és a la memòria cau
git lfs checkout

# pull = fetch + checkout
git lfs pull

Compte amb git lfs fetch --all: baixa el contingut de totes les versions de tots els fitxers LFS de tota la història. És exactament el que evitaves en adoptar LFS. Només té sentit en dos casos: fer una còpia de seguretat completa, o preparar una migració a un altre servidor.

Filtres permanents de baixada

Si a la teva màquina no vols mai els vídeos, es configura una vegada:

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

I per a la CI, on això importa molt més perquè s'executa desenes de vegades al dia:

name: Proves

on: [push, pull_request]

jobs:
  provar:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          lfs: false          # no baixar contingut LFS

      - name: Instal·lar dependències
        run: npm ci

      - name: Proves
        run: npm test

  # Només el treball que genera la documentació necessita els recursos
  documentacio:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          lfs: true

      - name: Generar la documentació amb imatges
        run: npm run docs

Aquest lfs: false és l'optimització amb millor retorn de tot l'apartat. Si tens cinquanta execucions de CI al dia i cadascuna s'estalvia 196 MB de baixada, són gairebé 10 GB diaris de trànsit i uns quants minuts d'espera acumulats.

  1. Ordres d'inspecció i manteniment

git lfs ls-files: què és a LFS

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

La columna del mig és la més útil:

Marca Significat
* El contingut real és al disc (smudge aplicat)
- Només hi ha un punter; el contingut no està baixat

Variants útils:

# Amb mida
git lfs ls-files --size

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

# Només els noms, per encadenar amb altres ordres
git lfs ls-files --name-only

# Tots els objectes LFS de tota la història
git lfs ls-files --all

git lfs status: el git status d'LFS

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

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

    recursos/mockup-panel.psd (84 MB)

Objects to be committed:

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

Objects not staged for commit:

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

Aquesta última línia revela un problema real: (File: ...) en lloc de (LFS: ...) significa que aquest fitxer no està sent gestionat per LFS, probablement perquè es va afegir abans de declarar el patró. És el primer lloc on mirar quan el repositori creix sense explicació.

git lfs env: diagnòstic

git lfs env

Mostra la versió, l'URL del punt final d'LFS, la ruta de la memòria cau local i la configuració de filtres. És l'equivalent per a LFS del git config --list --show-origin de la lliçó 09-06, i la primera ordre que cal executar quan alguna cosa no funciona.

git lfs prune: alliberar espai local

La memòria cau local d'LFS (.git/lfs/objects/) va acumulant contingut de revisions antigues:

du -sh .git/lfs/objects/
1.4G	.git/lfs/objects/
# Veure què esborraria, sense esborrar res
git lfs prune --dry-run --verbose

# Esborrar els objectes locals que ja no calen
git lfs prune
prune: 47 local object(s), 12 retained, done.
prune: Deleting objects: 100% (35/35), done.

prune és conservador per disseny: només esborra objectes que estiguin confirmats i enviats al servidor, i conserva els de les revisions recents. Es controla amb:

# Quants dies de referències recents es conserven (per defecte 7)
git config lfs.pruneoffsetdays 14

# Verificar contra el servidor que l'objecte hi és abans d'esborrar-lo en local
git config lfs.pruneverifyremotealways true

Aquest pruneverifyremotealways mereix activar-se: comprova contra el servidor que cada objecte està fora de perill abans d'eliminar-lo del teu disc. Costa una mica de temps i evita l'únic escenari en què prune podria fer mal.

git lfs migrate info: auditar abans d'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

Aquesta ordre no modifica res: analitza l'historial i diu quines extensions ocupen quant. És el punt de partida obligatori de qualsevol migració, i també una bona revisió periòdica.

  1. Migrar un repositori que ja té binaris a l'historial

Aquest és el cas de gestor-tasques: els .psd ja hi són a dins. Adoptar LFS ara fa que els fitxers futurs siguin punters, però els 250 MB de l'historial continuen allà i tothom se'ls continua baixant.

Per treure'ls cal reescriure l'historial. LFS porta la seva pròpia eina.

L'advertència, primer

git lfs migrate import reescriu l'historial. Canvien els hashos de tots els commits afectats i de tots els seus descendents. És la regla d'or de la lliçó 05-01 en la seva versió més contundent, i exigeix exactament la mateixa coordinació que la neteja de secrets de la lliçó 08-05.

Conseqüències concretes:

  • Totes les branques i etiquetes que continguin commits reescrits apuntaran a objectes nous.
  • Qualsevol persona amb un clon tindrà una divergència completa (lliçó 09-03).
  • Les propostes obertes quedaran basades en commits que ja no són a main.
  • Les signatures de commits (08-05) s'invaliden, perquè l'objecte signat ja no és el mateix.

El procediment complet

Pas 0: acordar-ho amb l'equip i triar el moment.

No és un pas tècnic i és el més important. Cal una finestra en què ningú no tingui feina sense enviar. L'Ana avisa per escrit, es fixa l'hora i es demana a tothom que enviï i tanqui les seves propostes.

# Cada persona comprova que no té res pendent
git status
git log --branches --not --remotes --oneline   # commits locals sense enviar
git stash list

Aquest git log --branches --not --remotes és la comprovació exacta: llista els commits que existeixen en alguna branca local i en cap remot. Si surt buit, no hi ha res a perdre.

Pas 1: còpia de seguretat.

# Un clon mirall complet, desat a part
git clone --mirror https://git.exemple.cat/gestor-tasques.git ~/copies/gestor-tasques-abans.git

# I un paquet autocontingut, per si de cas (lliçó 09-05)
git bundle create ~/copies/gestor-tasques-$(date +%F).bundle --all

Pas 2: auditar.

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

Serveix per decidir quins patrons migrar. Migra per extensió, no per fitxer concret: és més robust i captura també els fitxers que es van reanomenar pel camí.

Pas 3: assajar en una còpia.

git clone https://git.exemple.cat/gestor-tasques.git /tmp/assaig-migracio
cd /tmp/assaig-migracio

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.

Fixa't que les etiquetes també es reescriuen. v1.0.0 ja no apunta al mateix commit. És la conseqüència de la immutabilitat del model de dades: canviar qualsevol cosa a l'historial canvia tot el que en penja.

Pas 4: verificar el resultat de l'assaig.

# Quant s'ha reduït?
git count-objects -vH

# Què hi ha ara a LFS?
git lfs ls-files --all | head -20

# El contingut de l'última revisió és correcte?
git lfs pull
ls -lh recursos/

# El codi continua sent idèntic? Comparar l'arbre amb l'original
git rev-parse HEAD^{tree}
git -C ~/gestor-tasques rev-parse HEAD^{tree}

Aquesta última comprovació és la més tranquil·litzadora i gairebé ningú no la fa. El hash de l'arbre de HEAD resumeix tot el contingut de la revisió. Si coincideix amb el del repositori original... no coincidirà, precisament perquè els .psd ara són punters. Però sí que ha de coincidir el dels subdirectoris que no contenen fitxers migrats:

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

Si aquests dos hashos són iguals, la migració no ha tocat el codi. És una verificació criptogràfica, no una impressió.

Pas 5: executar-ho de debò i publicar.

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

# Pujar el contingut LFS al magatzem del servidor ABANS que les referències
git lfs push --all origin

# I ara sí, les referències reescrites
git push --force-with-lease --all origin
git push --force-with-lease --tags origin

L'ordre importa: primer el contingut LFS, després les referències. Si publiques les referències abans, hi haurà un interval en què algú pot clonar un repositori els punters del qual apunten a objectes que encara no existeixen al magatzem.

--force-with-lease en lloc de --force, sempre (lliçó 09-03). I si el servidor té main com a branca protegida (07-06), caldrà desprotegir-la temporalment i tornar-la a protegir just després. Apunta-ho al guió, perquè s'oblida.

Pas 6: que tothom recuperi.

La instrucció per a l'equip, idèntica a la de la neteja de secrets:

# El més simple, segur i recomanable: clonar de nou
cd ~
mv gestor-tasques gestor-tasques-antic
git clone https://git.exemple.cat/gestor-tasques.git
cd gestor-tasques
git lfs install
git lfs pull

I per a qui tingués feina sense enviar en una branca local:

# Al clon nou, portar la branca del clon antic
git remote add antic ~/gestor-tasques-antic
git fetch antic GT-260-la-meva-branca

# Reubicar només els meus commits sobre el nou main (lliçó 09-03)
git switch -c GT-260-la-meva-branca
git rebase --onto main antic/main antic/GT-260-la-meva-branca

Aquest rebase --onto és exactament la tècnica de la lliçó 09-03: agafa els commits que hi ha entre el main antic i la branca antiga, i els replanta sobre el main nou.

Pas 7: netejar el servidor.

Reescriure l'historial no allibera espai al servidor per si sol: els objectes antics continuen allà mentre alguna referència els abasti. Cal demanar al servidor que executi la seva recollida d'escombraries, i en moltes plataformes gestionades això és una petició al suport. Esbrina-ho abans de començar, perquè si no, el repositori continuarà pesant el mateix del costat del servidor i la migració semblarà haver fallat.

L'operació inversa

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

Converteix punters de tornada en fitxers normals. Útil si decideixes abandonar LFS. Requereix tenir baixats els objectes, i reescriu l'historial exactament igual.

  1. Limitacions i avisos que cal conèixer abans

LFS resol un problema real, però n'introdueix d'altres. Aquests són els que cal conèixer abans d'adoptar-lo, no després.

  1. Cal un servidor LFS

LFS no funciona sense servidor. El magatzem és un servei a part que parla un protocol propi sobre HTTPS. La majoria de plataformes l'ofereixen; algunes instal·lacions autoallotjades requereixen configurar-lo explícitament.

Conseqüència immediata: un repositori amb LFS deixa de ser autocontingut. Si clones un repositori amb LFS i el servidor d'LFS no està disponible, tens els punters però no el contingut. Això xoca de front amb la naturalesa distribuïda de Git (lliçó 01-01), i és l'argument filosòfic més seriós contra LFS: has introduït un punt centralitzat en un sistema distribuït.

Per a una còpia de seguretat realment completa calen dues coses:

git bundle create copia.bundle --all   # l'historial de Git
git lfs fetch --all                    # tot el contingut LFS
# i copiar també .git/lfs/objects/

  1. Les quotes costen diners

L'emmagatzematge LFS i la seva amplada de banda solen tenir quotes de pagament a les plataformes. I hi ha un parany que sorprèn molta gent: l'amplada de banda es consumeix també en baixar. Una CI que clona amb lfs: true cinquanta vegades al dia pot esgotar una quota mensual en pocs dies. D'aquí la importància del lfs: false de l'apartat 7.

A més, els objectes LFS de la història no s'esborren sols. Si migres un fitxer de 84 MB i després l'elimines, l'objecte continua ocupant quota. Purgar el magatzem és una operació específica de cada plataforma, no una ordre de Git.

  1. Les plataformes no es comporten igual

Hi ha diferències reals entre implementacions: com es gestionen els objectes als forks, si el bloqueig de fitxers està disponible, com s'apliquen les quotes, com es purguen els objectes orfes, i què passa en transferir un repositori entre comptes. No donis per fet que el que funciona en una plataforma funciona igual en una altra, sobretot en migrar de proveïdor: moure un repositori amb LFS d'un servidor a un altre requereix moure els objectes explícitament.

  1. Els fitxers LFS no es fusionen

Aquest és el més important en el dia a dia. Quan dues branques modifiquen el mateix fitxer LFS, hi ha conflicte sempre, i no es resol fusionant: cal triar una versió guanyadora.

git merge GT-252-redisseny-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

I la resolució és la mateixa que per a qualsevol binari (lliçó 03-05):

# Quedar-se amb la versió de la meva branca
git checkout --ours recursos/mockup-panel.psd
git add recursos/mockup-panel.psd

# O amb la de l'altra branca
git checkout --theirs recursos/mockup-panel.psd
git add recursos/mockup-panel.psd

# O, si de debò cal combinar el contingut:
# obrir les dues versions a l'aplicació de disseny i refer la feina a mà
git show :2:recursos/mockup-panel.psd > /tmp/meva.psd
git show :3:recursos/mockup-panel.psd > /tmp/seva.psd

Aquests :2: i :3: són les etapes de l'índex que vam veure a la lliçó 03-05 i a la 09-06: 1 és la base comuna, 2 «el nostre», 3 «el seu». Extreure-les permet obrir-les a l'aplicació corresponent i decidir amb la informació al davant.

La conseqüència organitzativa és que amb binaris no es pot treballar en paral·lel. La solució real no és tècnica sinó de coordinació: que només una persona toqui cada fitxer de disseny alhora. LFS ofereix un mecanisme per formalitzar-ho, el bloqueig de fitxers:

# Declarar que un patró és de bloqueig obligatori (a .gitattributes)
git lfs track "*.psd" --lockable

# Bloquejar abans d'editar
git lfs lock recursos/mockup-panel.psd

# Veure què hi ha bloquejat i per qui
git lfs locks

# Alliberar en acabar
git lfs unlock recursos/mockup-panel.psd

Amb --lockable, LFS marca aquests fitxers com a només lectura al disc fins que els bloquegis. És un mecanisme d'exclusió mútua, com als sistemes centralitzats (lliçó 01-01), i és la prova que per a certs fluxos aquell model tenia sentit. Requereix suport del servidor.

  1. Tot l'equip l'ha d'instal·lar

Si algú clona sense LFS instal·lat i filter.lfs.required no està actiu, obtindrà punters de text en lloc de fitxers, i —el pitjor cas— podria confirmar un fitxer real a sobre d'un punter, trencant la coherència. Amb git lfs install ben fet això no passa, però és la raó per la qual la instal·lació ha de ser el primer del CONTRIBUTING.md.

  1. Algunes operacions es tornen més lentes o més estranyes

  • git checkout entre branques amb fitxers LFS diferents implica baixades.
  • git bisect (06-02) sobre un historial amb LFS pot baixar contingut a cada pas.
  • Els worktree (06-06) comparteixen la memòria cau d'LFS, cosa que és bona, però cadascun necessita el seu checkout.
  • El smudge fitxer a fitxer pot ser lent; git lfs pull és més eficient perquè baixa en lot.

  1. Alternatives a LFS

LFS no és l'única resposta, i de vegades no és la millor.

Opció Com funciona A favor En contra Quan triar-la
Git LFS Punter versionat + magatzem a part, via filtres de .gitattributes Integrat al flux normal; suportat per gairebé totes les plataformes; versiona el binari de debò Requereix servidor; quotes de pagament; no fusiona; trenca l'autonomia del clon Binaris que canvien i que formen part del producte: dissenys, recursos, models
git annex Substitueix fitxers per enllaços simbòlics a un magatzem de contingut; admet molts backends (discos, S3, un altre ordinador) Molt flexible; funciona sense servidor dedicat; pot repartir còpies entre diversos magatzems; excel·lent per a arxius enormes Corba d'aprenentatge molt més gran; menys suport de plataformes; model mental propi Arxius científics, col·leccions grans, entorns amb emmagatzematge heterogeni o sense connexió
No versionar el binari El fitxer viu fora (emmagatzematge d'objectes, unitat compartida, gestor de recursos) i el repositori desa només una referència (URL + hash) Sense cost ni complexitat a Git; sense quotes d'LFS; cada sistema fa el que li toca Cal construir la coordinació; el binari no queda versionat amb el codi; risc que la referència es trenqui Recursos grans que canvien poc: vídeos de màrqueting, conjunts de dades, dependències precompilades
Repensar el flux Versionar la font i generar el binari; o desar l'original allà on es va crear i versionar només l'exportació lleugera Elimina el problema d'arrel; sol millorar el procés No sempre és possible Quan el binari és derivat d'una cosa més petita

La quarta mereix èmfasi

Abans d'instal·lar res, fes-te tres preguntes:

  1. Aquest fitxer és generat? Un .zip de distribució, un .png exportat des d'un .svg, un executable compilat. Si es pot regenerar, no ha d'estar al repositori (lliçó 08-03): va al .gitignore i el produeix la CI com a artefacte.

  2. Necessito el seu historial? Un vídeo de demostració que se substitueix sencer cada sis mesos no guanya res versionat. Un .psd sobre el qual s'itera cada dia i del qual de vegades cal tornar a la versió de la setmana passada, sí.

  3. Puc versionar la font en comptes del resultat? Un .svg és text: es versiona, es diferencia i es fusiona perfectament. Un .png de 4 MB exportat d'aquest .svg no aporta res. Canviar el format de treball resol el problema millor que qualsevol eina.

Aplicat a gestor-tasques:

Fitxer Decisió Motiu
mockup-panel.psd (84 MB, canvia sovint) LFS S'hi itera, cal l'historial i és font, no derivat
demo.mp4 (120 MB, es refà sencer) Fora del repositori No cal el seu historial; un URL i un hash són prou
logo.svg Git normal És text: es diferencia i es fusiona
logo-512.png (generat del .svg) .gitignore + generar a la CI És derivat
Recursos de components-ui LFS al submòdul Cada repositori decideix el que és seu (lliçó 06-05)

  1. Quan fer servir LFS i quan no

Fes-lo servir quan es compleixin totes

  • Tens fitxers binaris de més d'uns quants MB.
  • Canvien amb certa freqüència, generant moltes versions.
  • Necessites el seu historial: cal poder tornar a una versió anterior.
  • No són derivats de res més petit que puguis versionar al seu lloc.
  • Tens un servidor LFS disponible i pressupost per a la seva quota.
  • Tot l'equip el pot instal·lar.

No el facis servir quan

  • Els fitxers són petits (menys d'1 MB): el punter i la complexitat no compensen.
  • Són generats: van al .gitignore.
  • No necessites l'historial: desa'ls fora amb una referència.
  • Són text, encara que siguin grans: Git els gestiona bé amb deltes i els fusiona.
  • No tens servidor LFS i no en tindràs.
  • El repositori ha de funcionar completament sense connexió des d'un sol clon.

La regla de decisió

flowchart TD
    A["Tinc un fitxer gran"] --> B{"És text?"}
    B -->|sí| C["Git normal.<br/>Les deltes funcionen bé"]
    B -->|no| D{"És generat?"}
    D -->|sí| E[".gitignore + artefacte de CI"]
    D -->|no| F{"Necessito<br/>el seu historial?"}
    F -->|no| G["Fora del repositori:<br/>URL + hash de verificació"]
    F -->|sí| H{"Puc versionar<br/>la font al seu lloc?"}
    H -->|sí| I["Versiona la font.<br/>Genera la resta"]
    H -->|no| J{"Tinc servidor<br/>LFS i quota?"}
    J -->|sí| K["Git LFS"]
    J -->|no| L["git annex o<br/>magatzem extern"]

La resposta més freqüent en un projecte com gestor-tasques és «no ho necessites». Un repositori de codi pur amb un grapat d'icones i un logotip no té cap problema que LFS resolgui. La necessitat apareix quan hi entren fitxers de disseny, vídeo, àudio, models 3D, conjunts de dades o recursos de videojoc. En un repositori de 50.000 fitxers amb recursos gràfics, LFS és imprescindible; en un de quatre fitxers, és complexitat sense benefici.

Errors Habituals i Consells

Error 1: adoptar LFS després d'haver ficat els binaris. Declarar el patró només afecta el futur. Els objectes que ja són a l'historial hi continuen i es continuen baixant. Cal git lfs migrate import i una reescriptura coordinada. Configura LFS el dia que creïs el repositori, no quan pesi 2 GB.

Error 2: oblidar el -text. Sense ell, un * text=auto pot normalitzar els finals de línia en un binari i corrompre'l. git lfs track el posa; si edites .gitattributes a mà, no el treguis.

Error 3: confirmar els binaris abans que .gitattributes. Els filtres s'apliquen en el moment de l'add. .gitattributes primer, en el seu propi commit, i els binaris després.

Error 4: no adonar-se que es tenen punters. Si un fitxer d'imatge pesa 130 bytes i comença per version https://git-lfs..., és un punter. Executa git lfs pull. Comprovació ràpida:

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

Error 5: deixar la CI baixant LFS sense necessitar-ho. És la via més ràpida d'esgotar la quota d'amplada de banda. lfs: false tret dels treballs que realment necessiten els fitxers.

Error 6: git lfs fetch --all per costum. Baixa tot l'historial de contingut, que és justament el que LFS existeix per evitar. Només per a còpies de seguretat o migracions.

Error 7: creure que un clon amb LFS és una còpia de seguretat completa. No ho és: et falten els objectes LFS que no hagis baixat. Una còpia real necessita el bundle i el contingut LFS.

Error 8: dues persones editant el mateix .psd alhora. Acabarà en conflicte irresoluble i algú perdrà feina. Fes servir --lockable i git lfs lock, o simplement acordeu qui toca què.

Consell 1: audita abans de decidir. git lfs migrate info --everything --above=1Mb et diu exactament què està inflant el repositori, sense tocar res.

Consell 2: posa un límit de mida a la CI. Un guardià automàtic evita el problema d'arrel (lliçó 07-06):

- name: Rebutjar fitxers grans fora d'LFS
  run: |
    LIMIT=$((5 * 1024 * 1024))
    GRANS=$(git diff --name-only --diff-filter=ACM origin/main...HEAD | while read -r f; do
      [ -f "$f" ] || continue
      # Els punters LFS són diminuts, així que mai no disparen l'avís
      MIDA=$(wc -c < "$f")
      [ "$MIDA" -gt "$LIMIT" ] && echo "$f ($MIDA bytes)"
    done)
    if [ -n "$GRANS" ]; then
      echo "Fitxers de més de 5 MB fora d'LFS:"
      echo "$GRANS"
      echo "Declara el patró amb 'git lfs track' o treu-los del repositori."
      exit 1
    fi

Consell 3: documenta LFS al CONTRIBUTING.md. Tres línies al principi: instal·la git-lfs, executa git lfs install, i si veus fitxers de 130 bytes, executa git lfs pull. Estalvia la meitat de les preguntes de suport.

Consell 4: prova la migració en un clon d'usar i llençar. Sempre. I verifica comparant el hash de l'arbre dels directoris de codi abans i després: si coincideixen, el codi és intacte.

Consell 5: git lfs prune de tant en tant. La memòria cau local creix sense límit. Amb lfs.pruneverifyremotealways = true és una operació segura.

Exercicis

Exercici 1: diagnosticar un repositori inflat

El Diego clona gestor-tasques i obté això:

$ 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 ag.  1 09:14 mockup-panel.psd
-rw-r--r-- 1 diego diego  84M ag.  1 09:14 mockup-llistat.psd
-rw-r--r-- 1 diego diego 118M ag.  1 09:14 demo.mp4

Respon:

A. Per què mockup-panel.psd pesa 130 bytes i els altres dos no? B. Per què el repositori ocupa 512 MB si suposadament fan servir LFS? C. Quines ordres executaries per confirmar el teu diagnòstic? D. Quina és la solució completa?

Exercici 2: configurar LFS des de zero, en l'ordre correcte

L'equip afegirà a gestor-tasques:

  • recursos/dissenys/*.psd — fitxers de disseny, ~80 MB, canvien setmanalment, s'hi itera
  • recursos/video/demo.mp4 — 120 MB, es refà sencer cada sis mesos
  • recursos/icones/*.svg — text, uns pocs KB cadascun
  • recursos/icones/generats/*.png — exportats automàticament dels SVG
  • dist/gestor-tasques.zip — paquet de distribució, generat a cada versió

Escriu la seqüència completa d'ordres i fitxers, decidint per a cada tipus si va a LFS, a Git normal, al .gitignore o fora del repositori. Justifica cada decisió i respecta l'ordre correcte dels commits.

Exercici 3: migració amb obstacles

gestor-tasques porta dos anys amb .psd i .mp4 versionats directament. El repositori pesa 3,2 GB. Es decideix migrar a LFS.

Restriccions:

  • Hi ha tres propostes obertes del Diego.
  • main està protegida i exigeix revisió.
  • Hi ha etiquetes v1.0.0 a v1.7.0, i v1.6.0 és el que és en producció.
  • El Bruno té una branca local amb cinc commits sense enviar.
  • Els commits van signats (08-05).

Escriu el pla complet, en ordre, indicant què passa amb cada restricció i quines comprovacions faries. Assenyala en quin punt exacte el procés és irreversible.

Solucions

Solució 1

A. mockup-panel.psd és un punter LFS; els altres dos són fitxers reals versionats directament a Git.

Només el primer va ser declarat a .gitattributes abans d'afegir-lo. Els altres dos, o bé es van afegir abans de configurar LFS, o bé el seu patró no es va declarar mai.

B. Perquè els 512 MB són els .psd i .mp4 que NO són a LFS, amb totes les seves versions històriques. Adoptar LFS a mitges no redueix res: el repositori continua arrossegant tot el que s'hi va ficar abans o fora dels patrons.

C. Diagnòstic pas a pas:

# 1. Què és realment a LFS?
git lfs ls-files
# Només hi apareixerà mockup-panel.psd

# 2. Quins patrons estan declarats?
git lfs track
cat .gitattributes
# Probablement només "recursos/mockup-panel.psd" o "*.psd" afegit tard

# 3. Què ocupa l'historial? (lliçó 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 per extensió, que és la definitiva
git lfs migrate info --everything --above=1Mb

El pas 4 donarà una cosa com:

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

Aquest 9/12 és la pista clau: de dotze fitxers .psd de l'historial, nou són fora d'LFS.

D. Solució completa, en dues parts:

Part 1: arreglar la configuració per al futur.

git lfs track "*.psd"
git lfs track "*.mp4"
git add .gitattributes
git commit -m "chore: GT-262 declara tots els binaris a LFS"

Part 2: netejar l'historial (reescriptura coordinada, apartat 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

I avisar l'equip que ha de clonar de nou. Sense la part 2 el repositori continuarà pesant 512 MB per sempre, perquè els objectes antics no desapareixen en canviar la configuració.

Solució 2

Decisions, amb el seu motiu:

Fitxer Decisió Per què
recursos/dissenys/*.psd LFS Binari gran, canvia sovint, cal l'historial, no és derivat
recursos/video/demo.mp4 Fora del repositori No cal el seu historial (es refà sencer). Referència al README.md amb URL i hash
recursos/icones/*.svg Git normal És text: es diferencia, es fusiona i comprimeix bé. LFS seria una nosa
recursos/icones/generats/*.png .gitignore Derivats dels SVG. Es generen a la CI
dist/gestor-tasques.zip .gitignore Artefacte de construcció (lliçó 08-03). Es publica com a artefacte de la versió

Sobre el .mp4: si l'equip prefereix versionar-lo, LFS és una opció defensable. Però com que no cal el seu historial, desar-lo fora evita consumir quota amb dues còpies de 120 MB.

Seqüència completa, en l'ordre correcte:

cd ~/gestor-tasques

# --- Pas 1: instal·lació (una vegada per màquina, cada persona) ---
git lfs install

# --- Pas 2: declarar els patrons LFS ---
git lfs track "recursos/dissenys/*.psd"

# --- Pas 3: .gitignore per al que és derivat ---
cat >> .gitignore <<'EOF'

# Recursos generats (GT-263)
recursos/icones/generats/
dist/
EOF

# --- Pas 4: verificar el que s'ha declarat ABANS de confirmar ---
git lfs track
cat .gitattributes

# --- Pas 5: confirmar la CONFIGURACIÓ primer, sola ---
git add .gitattributes .gitignore
git commit -m "chore: GT-263 configura Git LFS per als fitxers de disseny

Els .psd de recursos/dissenys/ passen a gestionar-se amb Git LFS: a
l'historial només hi queden punters de text i el contingut viu al
magatzem LFS del servidor.

Els PNG generats i el paquet de distribució s'ignoren: són derivats
i els produeix la canonada d'integració.

El vídeo de demostració no es versiona; el seu URL i el seu hash
queden documentats al README.

Requisit: executar 'git lfs install' abans de clonar."

Confirmar la configuració sola i abans és el que garanteix que els filtres estiguin actius quan arribin els binaris.

# --- Pas 6: ara sí, els fitxers ---
git add recursos/dissenys/panel.psd recursos/dissenys/llistat.psd
git add recursos/icones/*.svg

# --- Pas 7: verificar ABANS de confirmar ---
git lfs status
Objects to be committed:

    recursos/dissenys/panel.psd (LFS: 4d7a9f2)
    recursos/dissenys/llistat.psd (LFS: 9e2f7a4)

Aquesta comprovació és la clau de l'exercici. Si digués (File: ...) en lloc de (LFS: ...), el filtre no s'hauria aplicat i caldria desfer l'add, revisar .gitattributes i tornar a començar. Verificar aquí costa cinc segons; descobrir-ho després costa una reescriptura d'historial.

git commit -m "feat: GT-263 afegeix els dissenys del panell i del llistat"

# --- Pas 8: confirmar que a l'historial hi ha punters, no binaris ---
git show HEAD:recursos/dissenys/panel.psd
git cat-file -s HEAD:recursos/dissenys/panel.psd    # ha de donar ~130

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

I al CONTRIBUTING.md:

## Abans de clonar

Aquest repositori fa servir Git LFS per als fitxers de disseny.

1. Instal·la git-lfs: `apt install git-lfs` / `brew install git-lfs`
2. Executa una vegada: `git lfs install`
3. Clona normalment.

Si un `.psd` pesa 130 bytes i comença per `version https://git-lfs...`,
tens un punter. Executa `git lfs pull`.

El vídeo de demostració no és al repositori: baixa'l de l'URL indicada
al README i verifica'n el hash.

Solució 3

Pla complet, per fases.

Fase 0 — Preparació (reversible).

# Auditar: quant es recupera i de què?
git lfs migrate info --everything --above=1Mb

Amb aquesta dada es decideix si val la pena. Reduir de 3,2 GB a ~150 MB, sí; a 2,8 GB, no.

Restricció — les tres propostes del Diego: cal tancar-les abans. S'integren o s'abandonen. Una proposta basada en commits que deixaran d'existir queda inservible, i demanar a un col·laborador extern que refaci tres branques sobre un historial reescrit és una mala experiència. Aquesta és la restricció que marca el calendari.

Restricció — la branca local del Bruno: que l'enviï al servidor abans de la migració. Així es reescriurà juntament amb tota la resta i no l'haurà de reubicar a mà. Si no pot, haurà de fer el rebase --onto de l'apartat 9.

# Cada persona comprova que no li queda res
git log --branches --not --remotes --oneline
git stash list
git status

Fase 1 — Còpies de seguretat (reversible).

git clone --mirror https://git.exemple.cat/gestor-tasques.git ~/copies/abans.git
git bundle create ~/copies/abans-$(date +%F).bundle --all

# Verificar que la còpia serveix de debò
git bundle verify ~/copies/abans-$(date +%F).bundle

I, si en algun moment calguessin els binaris originals, baixar el contingut abans de perdre'l de vista.

Fase 2 — Assaig (reversible).

git clone https://git.exemple.cat/gestor-tasques.git /tmp/assaig
cd /tmp/assaig
git lfs migrate import --everything --include="*.psd,*.mp4"

# Quant s'ha reduït?
git count-objects -vH

# Verificació criptogràfica: el CODI no ha canviat
git rev-parse HEAD:src
git -C ~/gestor-tasques rev-parse HEAD:src   # han de coincidir

# v1.6.0 continua tenint el mateix contingut de codi?
git rev-parse v1.6.0^{tree}
git cat-file -p v1.6.0^{tree}

# Anotar la correspondència entre etiquetes velles i noves
git for-each-ref --format='%(refname:short) %(objectname:short)' refs/tags

Aquesta taula de correspondències cal desar-la i publicar-la: si algú té apuntat que en producció hi ha el commit 3f2a1b9, necessitarà saber quin és el seu equivalent nou.

Restricció — les signatures: en reescriure els commits, les signatures s'invaliden i es perden. L'objecte commit canvia, així que la signatura sobre l'objecte anterior deixa de ser vàlida. Cal assumir-ho i documentar-ho: els commits anteriors a la migració quedaran sense signatura verificable. És un argument seriós per fer la migració com abans millor o no fer-la mai, i una raó més per configurar LFS el primer dia. Els commits posteriors se signaran amb normalitat.

Restricció — v1.6.0 en producció: l'etiqueta canviarà de hash. Cal comprovar què apunta a aquell commit al sistema de desplegament (lliçó 10-05) i actualitzar-ho. I verificar que la nova v1.6.0 produeix el mateix artefacte que la vella: mateix codi, hash de commit diferent.

Fase 3 — Finestra de migració (PUNT IRREVERSIBLE).

El punt de no retorn és el git push --force-with-lease. Tot l'anterior és reversible. A partir d'aquí, el servidor té l'historial nou i qualsevol que cloni o porti canvis obté la versió reescrita. Tornar enrere requereix restaurar des de la còpia mirall i repetir l'anunci.

# 1. Anunciar l'inici i demanar que ningú no enviï res
# 2. Desprotegir main temporalment al servidor  <-- restricció de branca protegida
# 3. Migrar
cd ~/gestor-tasques
git fetch --all --prune
git lfs migrate import --everything --include="*.psd,*.mp4"

# 4. PRIMER el contingut LFS
git lfs push --all origin

# 5. Verificar que el magatzem el té abans de tocar les referències
git lfs ls-files --all | wc -l

# 6. ARA les referències  <-- IRREVERSIBLE a partir d'aquí
git push --force-with-lease --all origin
git push --force-with-lease --tags origin

# 7. Tornar a protegir main

La branca protegida exigeix desprotegir-la i tornar-la a protegir. Apunta-ho al guió: és el pas que més s'oblida, i deixar main desprotegida un cap de setmana és un risc innecessari.

Fase 4 — Recuperació de l'equip.

Missatge a l'equip:

L'historial de gestor-tasques s'ha reescrit per migrar els binaris a LFS.
El repositori ha passat de 3,2 GB a ~150 MB.

QUÈ HAS DE FER:
1. git lfs install   (si encara no ho has fet)
2. Reanomena el teu clon actual i clona de nou.
3. git lfs pull

NO facis 'git pull' sobre el teu clon vell: produirà una divergència total.

Les etiquetes han canviat de hash. Correspondències: <enllaç>
Els commits anteriors a avui han perdut la signatura; els nous se signen igual.
Si tens feina local sense enviar, avisa'm abans de tocar res.

Fase 5 — Tancament.

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

I al servidor: demanar la recollida d'escombraries perquè alliberi l'espai dels objectes antics. Sense aquest pas, el repositori del servidor continuarà pesant 3,2 GB i la migració semblarà no haver servit de res.

Per últim, posar el guardià a la CI (consell 2) perquè això no torni a passar, i documentar LFS al CONTRIBUTING.md.

Conclusió

Git és excel·lent per al text i pèssim per als binaris grans, i ara saps exactament per què: no és una limitació arbitrària, és una conseqüència del model de dades. Cada versió és un blob complet, les deltes no funcionen sobre contingut ja comprimit, els objectes són immutables i tothom es baixa l'historial sencer.

  • Git LFS substitueix el fitxer per un punter de text d'uns 130 bytesversion, oid sha256:..., size— que sí que es versiona bé, i desa el contingut real en un magatzem a part. Funciona mitjançant els filtres clean i smudge de .gitattributes, el mateix mecanisme de la lliçó 08-04.
  • La configuració viu a .gitattributes com a filter=lfs diff=lfs merge=lfs -text: el filter fa la substitució, diff i merge registren els controladors d'LFS, i -text evita que la normalització de finals de línia corrompi el binari. En estar versionada, tot l'equip l'hereta; el que no s'hereta és la instal·lació d'LFS.
  • L'ordre importa: git lfs track, confirmar .gitattributes sol, i després afegir els binaris. Verificar sempre amb git lfs status que diu (LFS: ...) i no (File: ...).
  • En clonar es baixen els punters i només el contingut de la revisió extreta. GIT_LFS_SKIP_SMUDGE=1 i lfs: false a la CI eviten baixades innecessàries; git lfs pull les porta quan calen. I git lfs fetch --all baixa tot l'historial de contingut: fes-lo servir només per a còpies de seguretat.
  • git lfs migrate import reescriu l'historial, amb tota la força de la regla d'or de la lliçó 05-01: canvien els hashos, canvien les etiquetes, s'invaliden les signatures, i tothom ha de clonar de nou. Assaja en una còpia, verifica amb el hash de l'arbre que el codi no s'ha tocat, publica primer el contingut LFS i després les referències, i no oblidis demanar la recollida d'escombraries del servidor.
  • Les limitacions són reals i cal conèixer-les abans: cal servidor, les quotes costen diners (també l'amplada de banda de baixada), les plataformes no es comporten igual, el clon deixa de ser autònom, i els fitxers LFS no es fusionen: cal triar versió guanyadora, o coordinar-se amb --lockable i git lfs lock.
  • I hi ha alternatives: git annex per a escenaris més flexibles, desar el binari fora amb una referència quan no necessites el seu historial, i sobretot repensar el flux: si el fitxer és generat, va al .gitignore; si pots versionar la font en lloc del resultat, fes-ho.

La idea que resumeix la lliçó:

LFS no fa que Git gestioni bé els binaris. Fa que Git deixi de gestionar-los, versionant al seu lloc una referència. És una solució excel·lent quan el binari és part real del producte i necessites el seu historial, i complexitat innecessària en qualsevol altre cas.

Per a gestor-tasques, amb els seus quatre fitxers de text, LFS era irrellevant fins ahir. Amb l'arribada dels fitxers de disseny ha passat a ser necessari, i ha arribat just a temps: configurar-lo avui costa dos commits; fer-ho d'aquí a un any hauria costat una reescriptura d'historial i una tarda de coordinació.

El que ve

LFS resol una de les tres maneres en què un repositori es torna ingovernable: els fitxers molt grans. En queden les altres dues, i són independents.

Un repositori pot tenir moltíssim historial —vint anys i centenars de milers de commits— encara que cada fitxer sigui diminut: clonar-lo triga una eternitat i git log s'arrossega. I pot tenir moltíssims fitxers —centenars de milers a la còpia de treball— encara que l'historial sigui curt: git status triga mig minut perquè ha de recórrer l'arbre sencer.

Cadascuna té el seu propi remei, i cap no serveix per a les altres dues. Els clons parcials amb --filter=blob:none ataquen el volum de contingut baixat. El sparse-checkout amb índex dispers ataca el nombre de fitxers al disc. El commit-graph ataca el cost de recórrer l'historial. I per damunt de tot hi ha la decisió que condiciona la resta: monorepo o multirepo, la comparativa que la lliçó 06-05 va deixar oberta i que el cas 3 de la 10-01 va deixar pendent d'explicar.

La lliçó 10-04: Escalant Git per a Projectes Grans tanca totes dues promeses, i comença per l'única regla que importa en optimització: mesurar abans de tocar res.

Dominant Git: De Principiant a Avançat

Mòdul 1: Introducció a Git

Mòdul 2: Operacions Bàsiques de Git

Mòdul 3: Branques i Fusió

Mòdul 4: Treballant amb Repositoris Remots

Mòdul 5: Operacions Avançades de Git

Mòdul 6: Eines i Tècniques de Git

Mòdul 7: Estratègies de Col·laboració i Flux de Treball

Mòdul 8: Bones Pràctiques i Consells de Git

Mòdul 9: Resolució de Problemes i Depuració

Mòdul 10: Git al Món Real

© Copyright 2026. Tots els drets reservats