La lliçó anterior va resoldre quins fitxers no han d'entrar al repositori. Aquesta tracta del problema complementari i menys conegut: els fitxers que que hi entren, però que Git no hauria de tractar tots igual.

I comença amb un problema molt concret que la Carla arrossega des del mòdul 1. Cada vegada que obre estils.css al seu editor de Windows 11, hi fa un canvi d'una línia i executa git diff, apareix això:

 estils.css | 486 +++++++++++++++++++++++++-------------------------
 1 fitxer modificat, 243 insercions(+), 243 supressions(-)

243 línies modificades per haver-ne canviat una. El fitxer sencer. I quan ho confirma, l'Ana i el Bruno reben una PR irrevisable, el git blame queda arruïnat (lliçó 06-03) i cada fusió genera conflictes que no signifiquen res. El culpable són els finals de línia, i la solució definitiva és .gitattributes.

Aquest fitxer és l'eina menys coneguda i més útil del repertori de Git. Permet dir-li a Git com ha de tractar cada tipus de fitxer: com normalitzar-lo en desar-lo, si és text o binari, com mostrar-ne les diferències, com fusionar-lo i si s'ha d'incloure en exportar.

Contingut

  1. Què és .gitattributes i en què es diferencia de .gitignore
  2. Sintaxi i precedència
  3. El problema dels finals de línia
  4. text, eol i text=auto
  5. .gitattributes davant de core.autocrlf
  6. Renormalitzar un projecte que ja té el problema
  7. Marcar binaris amb -text i binary
  8. Diffs personalitzats: diff=<driver>
  9. Que @@ mostri el nom de la funció
  10. textconv: veure el text de fitxers binaris
  11. Drivers de fusió i el cas merge=union
  12. export-ignore i git archive
  13. Altres atributs: filter, linguist-*, export-subst
  14. Taula resum

  1. Què és .gitattributes i en què es diferencia de .gitignore

Els dos fitxers s'assemblen —viuen al repositori, fan servir patrons de fitxer, es versionen— i fan coses completament diferents:

.gitignore .gitattributes
Respon a He de versionar aquest fitxer? Com he de tractar aquest fitxer?
Afecta Fitxers sense seguiment Fitxers versionats
Moment d'actuació git add, git status checkout, commit, diff, merge, archive
Si t'equivoques Puges brossa, o no puges alguna cosa que calia Diffs il·legibles, conflictes absurds, fitxers corromputs
Sintaxi patró patró atribut1 atribut2 ...

La forma d'una línia de .gitattributes és sempre la mateixa:

<patró>  <atribut>  <atribut>=<valor>  -<atribut>
*.js        text eol=lf
*.png       binary
*.md        diff=markdown merge=union
proves/     export-ignore
*.pdf       -text diff=pdf

Els quatre estats possibles d'un atribut:

Forma Significat
atribut Activat (valor true)
-atribut Desactivat (valor false)
atribut=valor Amb un valor concret
!atribut Sense especificar: com si no hi hagués cap regla

  1. Sintaxi i precedència

Els patrons són els mateixos que a .gitignore (lliçó 08-03): *, ?, **, la barra inicial que ancora a l'arrel, la barra final que restringeix a directoris. Tot el que hi vas aprendre val aquí.

Les diferències són en on es busca i en qui guanya:

Ubicació Es versiona? Àmbit
.gitattributes al repositori El seu directori i subdirectoris; viatja amb el clon
.git/info/attributes No Només la teva còpia d'aquell repositori
Ruta de core.attributesFile No Tots els teus repositoris
$(prefix)/etc/gitattributes No Tot el sistema

Precedència, de menor a major: sistema → global → .git/info/attributes.gitattributes de l'arrel → .gitattributes de subdirectoris més profunds. I dins d'un mateix fitxer, guanya l'última línia que coincideix, igual que a .gitignore.

La fila que importa és la primera: .gitattributes es versiona i viatja amb el repositori. Aquesta propietat, que sembla un detall administratiu, és exactament el que resol el problema de la Carla, com veurem a l'apartat 5.

Per consultar quins atributs s'apliquen realment a un fitxer:

# Tots els atributs d'un fitxer
git check-attr -a estils.css
estils.css: text: set
estils.css: eol: lf
estils.css: diff: css
# Un atribut concret, en diversos fitxers
git check-attr text -- app.js imatges/logo.png

# Tot el que està versionat de cop
git ls-files | git check-attr --stdin -a

git check-attr és a .gitattributes el que git check-ignore -v és a .gitignore: l'eina de diagnòstic. Fes-la servir sempre que alguna cosa no es comporti com esperaves.

  1. El problema dels finals de línia

Per resoldre el problema de la Carla cal entendre'n l'origen, que és històric i ridícul.

Un salt de línia es codifica de dues maneres diferents segons el sistema:

Sistema Seqüència Nom Bytes
Linux, macOS, Unix LF Line Feed 0x0A (\n)
Windows, DOS CRLF Carriage Return + Line Feed 0x0D 0x0A (\r\n)

Ve de les màquines d'escriure: el retorn de carro tornava el capçal al principi i l'avanç de línia feia baixar el paper. Unix va decidir que amb un caràcter n'hi havia prou; DOS va mantenir-ne els dos. Cinquanta anys després encara ho paguem.

Per a Git, \n i \r\n són bytes diferents, o sigui que una línia que només difereix en el final de línia és una línia diferent. D'aquí ve el 243 insercions, 243 supressions de la Carla: el seu editor va desar el fitxer sencer amb CRLF, i les 243 línies van canviar de debò, encara que visualment no en canviés cap.

flowchart LR
    A["Ana / Ubuntu<br/>desa amb LF"] --> R[("Repositori")]
    B["Bruno / macOS<br/>desa amb LF"] --> R
    C["Carla / Windows<br/>desa amb CRLF"] --> R
    R --> D["Diffs sencers<br/>Conflictes absurds<br/>blame arruïnat"]

Els símptomes típics, tots amb la mateixa causa:

  • git diff mostra el fitxer sencer modificat sense cap canvi visible.
  • git status marca com a modificats fitxers que no has tocat.
  • Conflictes de fusió a cada línia d'un fitxer que només va tocar una persona.
  • git blame ho atribueix tot a l'últim que va desar des d'un altre sistema.
  • Un script de shell versionat des de Windows falla a Linux amb bad interpreter: /bin/bash^M.

La solució conceptual és una convenció que Git implementa de manera nativa:

Al repositori, tot es desa amb LF. A la còpia de treball, cadascú té el que el seu sistema necessita.

La conversió es fa en confirmar (normalització) i en extreure (desnormalització), i la controla l'atribut text.

  1. text, eol i text=auto

L'atribut text

Declaració En confirmar (add) En extreure (checkout)
text Converteix CRLFLF Converteix LF → segons core.eol (per defecte, el natiu del sistema)
text=auto Converteix CRLFLF només si Git detecta que és text Igual, només si és text
-text No toca res No toca res
text eol=lf Converteix a LF Sempre LF, a tots els sistemes
text eol=crlf Converteix a LF Sempre CRLF, a tots els sistemes

Els dos matisos importants:

  • eol=lf no vol dir "desa LF al repositori": al repositori sempre hi ha LF quan el fitxer és text. eol controla què s'escriu al teu disc en extreure.
  • text=auto decideix per heurística: Git examina els primers 8.000 bytes i, si hi troba un byte nul, el considera binari. És una bona heurística, però no infal·lible (apartat 7).

El .gitattributes recomanat

Aquest és el punt de partida que serveix per a gairebé qualsevol projecte:

# Normalitza a LF al repositori tot el que Git detecti com a text.
# És la xarxa de seguretat general.
* text=auto

I aquesta és la versió explícita, que és la que de debò convé, perquè no depèn de cap heurística:

# ============================================================
# Regla general: normalitza el text, deixa els binaris en pau
# ============================================================
* text=auto

# ============================================================
# Codi font: LF al repositori i també al disc
# ============================================================
*.js       text eol=lf
*.mjs      text eol=lf
*.json     text eol=lf
*.css      text eol=lf
*.html     text eol=lf
*.md       text eol=lf
*.yml      text eol=lf
*.yaml     text eol=lf
*.svg      text eol=lf

# ============================================================
# Fitxers que EXIGEIXEN LF encara que siguis a Windows
# ============================================================
*.sh       text eol=lf
*.bash     text eol=lf
Dockerfile text eol=lf
Makefile   text eol=lf
.gitattributes text eol=lf
.gitignore     text eol=lf

# ============================================================
# Fitxers que EXIGEIXEN CRLF encara que siguis a Linux
# ============================================================
*.bat      text eol=crlf
*.cmd      text eol=crlf
*.ps1      text eol=crlf

# ============================================================
# Binaris: no tocar sota cap concepte
# ============================================================
*.png      binary
*.jpg      binary
*.jpeg     binary
*.gif      binary
*.webp     binary
*.ico      binary
*.pdf      binary
*.zip      binary
*.woff     binary
*.woff2    binary
*.ttf      binary

Les dues seccions del mig són les que eviten errors reals:

  • *.sh text eol=lf: un script de shell amb CRLF no arrenca a Linux. L'intèrpret busca /bin/bash i troba /bin/bash\r. Amb aquesta regla, encara que la Carla l'editi a Windows, al seu disc hi tindrà LF i funcionarà dins del contenidor.
  • *.bat text eol=crlf: els .bat antics de Windows necessiten CRLF. Encara que l'Ana els editi a Ubuntu, al disc de la Carla hi arribaran amb CRLF.

  1. .gitattributes davant de core.autocrlf

A la lliçó 01-06 vam veure core.autocrlf, que resol el mateix problema des de l'altre costat. És el moment de comparar els dos enfocaments i explicar per què en guanya un.

core.autocrlf és configuració de màquina (lliçó 01-05), amb tres valors:

Valor En confirmar En extreure Recomanat per a
true CRLFLF LFCRLF Windows
input CRLFLF Sense conversió Linux i macOS
false Res Res Per defecte; deixa el problema tal com està
# El que va configurar la Carla en el seu dia
git config --global core.autocrlf true

# El que van configurar l'Ana i el Bruno
git config --global core.autocrlf input

Funciona, i si tothom ho té ben configurat el problema desapareix. El problema és "si tothom ho té ben configurat".

core.autocrlf .gitattributes
On viu Al .gitconfig de cada màquina Al repositori
Viatja amb el clon? No
Qui l'aplica? Cada persona, si se'n recorda Tothom, automàticament
Granularitat Tot o res, per a tots els repositoris Per patró de fitxer
Pot forçar LF a Windows? No Sí, amb eol=lf
Pot forçar CRLF a Linux? No Sí, amb eol=crlf
Si algú ho té malament Contamina el repositori per a tothom Tant se val: manen els atributs
Col·laborador extern (Diego) Depèn de la seva configuració Se li aplica en clonar el fork
Prioritat Menor Major: guanya sempre

L'última fila és la clau tècnica: quan un fitxer té l'atribut text definit, core.autocrlf s'ignora completament per a aquell fitxer. Els atributs guanyen.

I les dues files anteriors són les pràctiques. Quan el Diego, el col·laborador extern, clona el seu fork de gestor-tasques, ningú li pot demanar que configuri el seu core.autocrlf abans de tocar res. Amb .gitattributes no cal: la política viatja dins del repositori i se li aplica tota sola.

La regla: .gitattributes per a la política del projecte; core.autocrlf com a xarxa de seguretat personal en repositoris que no en tinguin. Si tens .gitattributes, core.autocrlf sobra.

Hi ha, a més, un ajust útil que detecta el problema abans que entri:

# Rebutja confirmar un fitxer que barreja CRLF i LF
git config --global core.safecrlf true

# Només avisar, sense bloquejar
git config --global core.safecrlf warn

  1. Renormalitzar un projecte que ja té el problema

Afegir el .gitattributes no arregla el passat. Els fitxers ja confirmats amb CRLF continuen amb CRLF dins del repositori, i fins que algú no els toqui el problema persisteix. Cal renormalitzar, i Git té una ordre exactament per a això.

# 1. Assegura't que no hi ha canvis sense confirmar. Això no és negociable.
git status
git stash push -u   # si cal apartar alguna cosa (lliçó 05-04)

# 2. Crea o actualitza el .gitattributes i confirma'l per separat
git add .gitattributes
git commit -m "chore: defineix la política de finals de línia al .gitattributes"

# 3. Renormalitza tot el repositori
git add --renormalize .

# 4. Mira què canviarà
git status --short
git diff --cached --stat

# 5. Confirma-ho en el seu PROPI commit, aïllat
git commit -m "chore: renormalitza els finals de línia a LF

Aplica la política del .gitattributes al contingut ja versionat.
Aquest commit no canvia ni una línia de codi: només substitueix CRLF
per LF al contingut emmagatzemat. Es registra a
.git-blame-ignore-revs.

Refs: GT-190"

Què fa git add --renormalize

Reescriu el contingut de cada fitxer seguit a l'índex aplicant els filtres del .gitattributes a la versió que hi ha al repositori, sense tocar la teva còpia de treball. És la manera segura de dir "aplica la política nova a tot el que ja és a dins".

El que en resulta és exactament el cas de l'apartat 11 de la lliçó 08-02: un commit gegantí que no canvia ni una coma de comportament i que arruïna el git blame. Així que se li aplica el mateix tractament:

# Registrar-lo perquè el blame el travessi
git rev-parse HEAD >> .git-blame-ignore-revs
git add .git-blame-ignore-revs
git commit -m "chore: registra la renormalització a blame-ignore-revs"

La coordinació amb l'equip

Renormalitzar toca tots els fitxers, o sigui que qualsevol branca oberta en aquell moment entrarà en conflicte massiu en fusionar-se. La seqüència sensata:

  1. Avisa amb antelació i fixa una hora.
  2. Que tothom fusioni o tanqui les seves branques abans d'aquella hora.
  3. Una persona fa la renormalització en una PR pròpia, sense res més a dins.
  4. Es fusiona tan bon punt està aprovada, sense deixar-la oberta dies.
  5. Tothom fa pull i, si tenia branques vives, les rebasa sobre el commit de renormalització.
  6. Si algú té conflictes només de finals de línia, es resolen en bloc:
# Quedar-se amb la versió de main i renormalitzar
git checkout --ours -- .    # o --theirs, segons el cas
git add --renormalize .

Una comprovació prèvia molt útil, per veure quant mal hi ha abans de començar:

# Llista els fitxers versionats que contenen CRLF
git ls-files --eol | grep 'w/crlf'
i/lf    w/crlf  attr/text=auto     estils.css
i/crlf  w/crlf  attr/                index.html
i/lf    w/lf    attr/text=auto     app.js

Es llegeix així: i/ és el final de línia a l'índex (el repositori) i w/ el de la teva còpia de treball. La línia d'index.html amb i/crlf és la que revela el problema: hi ha CRLF dins del repositori, que és el que cal renormalitzar.

  1. Marcar binaris amb -text i binary

Un fitxer binari no s'ha de convertir mai. Si Git aplica la conversió de finals de línia a un PNG, el corromp: qualsevol byte 0x0D 0x0A dins de les dades de la imatge es converteix en 0x0A i el fitxer deixa de ser vàlid.

text=auto detecta la majoria dels binaris amb l'heurística del byte nul, però falla en casos reals: fitxers comprimits sense bytes nuls als primers 8 KB, formats de dades mixtos, fitxers de font amb capçalera de text. Declarar-los explícitament surt més barat que descobrir el problema.

*.png   binary
*.pdf   binary
*.zip   binary

binary és un macroatribut: equival exactament a escriure

*.png   -text -diff -merge

És a dir, tres coses alhora:

Atribut Efecte
-text No convertir finals de línia (evita la corrupció)
-diff git diff no intenta mostrar-ne el contingut; diu Binary files differ
-merge En un conflicte, no intenta fusionar línia a línia

L'efecte de -merge mereix una explicació, perquè canvia el que veus en un conflicte. Sense ell, Git intentaria barrejar dues versions d'un PNG i produiria un fitxer corrupte amb marcadors <<<<<<< a dins. Amb ell, el conflicte es planteja com una tria entre les dues versions senceres:

warning: Cannot merge binary files: imatges/logo.png (HEAD vs. branca-disseny)
Autofusionant imatges/logo.png
CONFLICTE (contingut): Conflicte de fusió a imatges/logo.png

I es resol triant-ne una de les dues, com vam veure a la lliçó 03-05:

git checkout --ours   imatges/logo.png   # la de la meva branca
git checkout --theirs imatges/logo.png   # la de l'altra branca
git add imatges/logo.png

Quan fer servir -text sol en lloc de binary

Quan el fitxer no és text normalitzable però sí que vols poder veure'l en un diff. El cas típic és un SVG gran o un fitxer de dades amb codificació fixa:

*.svg   -text diff

I hi ha un cas especial que convé conèixer: un fitxer de text en una codificació de dos bytes per caràcter, com UTF-16, conté bytes nuls i Git el classificarà com a binari. Es corregeix amb working-tree-encoding:

*.rc    working-tree-encoding=UTF-16LE text eol=crlf

Git desarà el contingut en UTF-8 dins del repositori (on els diffs funcionen) i l'escriurà en UTF-16LE al teu disc.

  1. Diffs personalitzats: diff=<driver>

Git sap mostrar diferències de text pla. Amb diff=<driver> se li pot donar context sobre el tipus de fitxer, i el resultat millora molt.

Els drivers integrats

Git porta de fàbrica reconeixedors de funció per a molts llenguatges. Només cal declarar-los:

*.js     diff=javascript
*.ts     diff=typescript
*.css    diff=css
*.html   diff=html
*.md     diff=markdown
*.py     diff=python
*.java   diff=java
*.rb     diff=ruby
*.php    diff=php
*.go     diff=golang
*.rs     diff=rust
*.tex    diff=tex
*.json   diff=json

La llista completa és a git help attributes. Hi ha drivers per a Ada, Bash, C/C++, C#, Dts, Elixir, Fortran, Fountain, Kotlin, MATLAB, Objective-C, Perl, Scheme i alguns més.

  1. Que @@ mostri el nom de la funció

Aquí hi ha el benefici concret. Recorda de la lliçó 02-05 que cada fragment d'un diff comença amb una línia @@:

Sense driver:

@@ -142,7 +142,7 @@
   const etiquetes = tasca.etiquetes || [];
-  if (etiquetes.includes(filtre)) {
+  if (etiquetes.some(e => e.toLowerCase() === filtre.toLowerCase())) {
     return true;
   }

Amb *.js diff=javascript:

@@ -142,7 +142,7 @@ function coincideixAmbFiltre(tasca, filtre) {
   const etiquetes = tasca.etiquetes || [];
-  if (etiquetes.includes(filtre)) {
+  if (etiquetes.some(e => e.toLowerCase() === filtre.toLowerCase())) {
     return true;
   }

Git ha afegit function coincideixAmbFiltre(tasca, filtre) al final de la línia @@. Ara saps quina funció estàs llegint sense obrir el fitxer. Multiplicat pels quaranta fragments d'una revisió, és la diferència entre revisar amb context i revisar a cegues.

Aquest context apareix automàticament a git diff, git show, git log -p i a les plataformes de revisió. I funciona també amb la cerca per funció de la lliçó 02-05:

# Tot l'historial d'una funció concreta
git log -L :coincideixAmbFiltre:app.js

Sense el driver, -L :nom: no sap trobar la funció.

Definir el teu propi reconeixedor

Si el teu llenguatge no té driver, o l'integrat no reconeix el teu estil, se'n defineix un amb una expressió regular. Són dues peces: la declaració al .gitattributes (versionada) i la definició a la configuració de Git (local, perquè .gitattributes no pot contenir ordres).

# .gitattributes
*.js   diff=jsmodern
# Definir quines línies compten com a "capçalera de funció"
git config --local diff.jsmodern.xfuncname \
  '^[[:space:]]*((export[[:space:]]+)?(async[[:space:]]+)?function[[:space:]]+[A-Za-z_$][A-Za-z0-9_$]*|const[[:space:]]+[A-Za-z_$][A-Za-z0-9_$]*[[:space:]]*=[[:space:]]*(async[[:space:]]*)?\().*$'

L'expressió reconeix function nom(, export function nom(, async function nom( i const nom = ( o const nom = async (, que cobreix l'estil modern de JavaScript.

Recorda la limitació de fons: .gitattributes declara quin driver s'ha de fer servir; la definició del driver viu a git config i no es versiona. Documenta-la al README.md o a l'script d'arrencada del repositori, al costat de core.hooksPath i blame.ignoreRevsFile de les lliçons anteriors.

  1. textconv: veure el text de fitxers binaris

Hi ha binaris que contenen text: un PDF, un .docx, un full de càlcul, un fitxer d'imatge amb metadades. Per defecte, git diff sobre aquests fitxers diu Binary files differ, que és cert i inútil.

textconv li dóna a Git una ordre que converteix el binari en text, i Git compara aquest text:

*.pdf    diff=pdf
*.docx   diff=word
*.png    diff=exif
# Definicions (locals, no versionades)
git config --local diff.pdf.textconv  "pdftotext -layout"
git config --local diff.word.textconv "pandoc --to=plain"
git config --local diff.exif.textconv "exiftool"

# Desar el resultat a la memòria cau: la conversió és cara
git config --local diff.pdf.cachetextconv true

Cada ordre rep la ruta del fitxer com a argument i ha d'escriure text a la sortida estàndard.

Ara, si el Bruno canvia el manual d'usuari:

git diff manual.pdf
diff --git a/manual.pdf b/manual.pdf
index 3a4b5c6..7d8e9f0 100644
--- a/manual.pdf
+++ b/manual.pdf
@@ -12,7 +12,7 @@
 Per crear una tasca, prem el botó "Nova tasca".
-El títol és obligatori i admet fins a 80 caràcters.
+El títol és obligatori i admet fins a 200 caràcters.
 Pots assignar etiquetes separades per comes.

El que cal entendre bé: textconv afecta només la visualització. El contingut emmagatzemat continua sent el PDF binari íntegre, i les fusions continuen sense poder fer-se. És una lent de lectura, no un canvi de format.

Requisit pràctic: l'eina externa (pdftotext, pandoc, exiftool) ha d'estar instal·lada. Si no ho està, git diff falla amb un error. Per això textconv és un bon candidat per al .git/info/attributes local de qui tingui aquestes eines, en lloc del .gitattributes compartit.

  1. Drivers de fusió i el cas merge=union

Igual que amb els diffs, es pot canviar com es fusiona un tipus de fitxer.

El problema

Cada setmana, l'Ana, el Bruno i la Carla afegeixen una línia al CHANGELOG.md, cadascú a la seva branca, sempre al principi:

## Sense publicar
- Afegeix el filtre per etiqueta (GT-134)

En fusionar, conflicte garantit, tres cops per setmana. I és un conflicte fals: les tres línies s'han de quedar, no hi ha cap decisió a prendre.

merge=union

CHANGELOG.md   merge=union

union és un driver integrat (no cal definir res) que, davant d'un conflicte, conserva les línies dels dos costats, sense marcadors. En aquest ordre: primer les de la branca base, després les de l'altre costat.

Amb això, la fusió produeix:

## Sense publicar
- Afegeix el filtre per etiqueta (GT-134)
- Desa les tasques a IndexedDB (GT-137)
- Corregeix l'ordre de les tasques vençudes (GT-141)

Sense conflicte i sense intervenció.

Quan s'ha de fer servir: fitxers que són llistes acumulatives i on l'ordre no és semàntic.

Bon candidat Mal candidat
CHANGELOG.md, AUTHORS, CONTRIBUTORS Qualsevol fitxer de codi
Fitxers de registre que es versionen package.json (dues versions de la mateixa dependència = JSON invàlid)
Llistes de regles on l'ordre és indiferent Qualsevol fitxer de configuració estructurat

Advertiment seriós: merge=union mai s'ha d'aplicar a codi. Combina les dues versions d'una funció i produeix una cosa sintàcticament trencada sense avisar de res. El silenci és el seu avantatge i el seu perill: elimina el conflicte, no el resol.

Els altres drivers integrats

Driver Comportament
merge=text El comportament normal de fusió a tres bandes (lliçó 03-03)
merge=binary No intenta fusionar; conflicte per triar una versió sencera
merge=union Conserva les línies dels dos costats, sense marcadors
-merge Equivalent a merge=binary

Un driver propi

Per a casos com "en un conflicte sobre un fitxer generat, queda't amb el meu i regenera'l":

package-lock.json   merge=bloqueig-npm
git config --local merge.bloqueig-npm.name "Regenera el bloqueig d'npm"
git config --local merge.bloqueig-npm.driver \
  'npm install --package-lock-only --silent && cp package-lock.json %A'

Git substitueix els marcadors dins de l'ordre: %O és la versió base comuna, %A la teva (i on ha de quedar el resultat), %B la de l'altre costat i %L la mida del marcador de conflicte. El driver ha de retornar 0 si ho ha resolt i diferent de 0 si cal resoldre-ho a mà.

I un advertiment: si el Diego clona el fork i no té aquesta configuració local, Git farà servir el driver per defecte. Els drivers propis es degraden silenciosament, així que no han de ser l'única línia de defensa.

  1. export-ignore i git archive

git archive empaqueta el contingut d'un commit en un .tar o .zip, sense el directori .git. És la manera estàndard de produir un paquet distribuïble:

git archive --format=zip --output=/tmp/gestor-tasques-1.5.0.zip v1.5.0
git archive --format=tar.gz --prefix=gestor-tasques/ -o /tmp/paq.tar.gz HEAD

L'atribut export-ignore marca el que no ha d'entrar en aquest paquet. És diferent de .gitignore: aquests fitxers sí que estan versionats; simplement no interessen a qui es descarrega el paquet.

# Res d'això té sentit en un paquet distribuïble
.gitattributes   export-ignore
.gitignore       export-ignore
.github/         export-ignore
.githooks/       export-ignore
proves/          export-ignore
docs/intern/     export-ignore
.editorconfig    export-ignore
CONTRIBUTING.md  export-ignore
.git-blame-ignore-revs export-ignore

Comprova el resultat abans de publicar:

git archive --format=tar HEAD | tar -t

Un ús molt pràctic: les plataformes d'allotjament generen automàticament el .zip i el .tar.gz de cada etiqueta fent servir git archive, o sigui que export-ignore controla què contenen els paquets de les teves versions sense que hagis de fer res més.

export-subst

Substitueix marcadors per informació del commit en exportar:

VERSION.txt   export-subst
Versió: $Format:%(describe:tags)$
Commit: $Format:%H$
Data:   $Format:%cI$

En fer git archive, el fitxer del paquet surt amb els valors emplenats:

Versió: v1.5.0
Commit: 9f8e7d6c5b4a3f2e1d0c9b8a7f6e5d4c3b2a1f0e
Data:   2026-08-01T10:22:14+02:00

És la manera neta que un paquet distribuït sàpiga de quin commit va sortir, i es combina bé amb les etiquetes anotades de la lliçó 05-05. Compte: els marcadors només se substitueixen dins del paquet, no a la teva còpia de treball.

  1. Altres atributs: filter, linguist-*, export-subst

filter: transformar en entrar i en sortir

L'atribut filter defineix un parell de conversions: clean en confirmar (del teu disc al repositori) i smudge en extreure (del repositori al teu disc).

flowchart LR
    W["Còpia de treball"] -->|"filtre clean<br/>(git add)"| R[("Repositori")]
    R -->|"filtre smudge<br/>(git checkout)"| W

El seu ús més important, amb diferència, és Git LFS, que emmagatzema els fitxers grans fora del repositori i hi deixa a dins un punter de text:

*.psd    filter=lfs diff=lfs merge=lfs -text
*.mp4    filter=lfs diff=lfs merge=lfs -text
*.zip    filter=lfs diff=lfs merge=lfs -text

Aquestes línies te les escriu git lfs track. El filter=lfs substitueix el fitxer per un punter en confirmar i el recupera en extreure. Git LFS és el contingut de la lliçó 10-03, on s'explica el mecanisme complet, el servidor d'emmagatzematge i les seves implicacions; aquí només interessa saber que és .gitattributes qui l'activa.

Un exemple propi, per entendre el mecanisme (encara que a la pràctica és millor no fer-lo servir):

*.md   filter=treu-espais-finals
git config --local filter.treu-espais-finals.clean "sed 's/[[:space:]]*$//'"
git config --local filter.treu-espais-finals.smudge cat

Compte amb els filtres propis. Si el filtre no és idempotent, o si algú clona sense la configuració, el contingut es desquadra: git status marca fitxers com a modificats sense que ningú els hagi tocat. És una font clàssica de confusió i per això convé reservar-los per a casos on no hi hagi alternativa.

linguist-*: com et classifica la plataforma

Les plataformes d'allotjament fan servir una biblioteca anomenada Linguist per deduir en quins llenguatges està escrit el teu projecte i què han de mostrar a la vista de diferències. S'ajusta amb aquests atributs:

# No comptar això a les estadístiques de llenguatges
vendor/*           linguist-vendored
docs/exemples/*    linguist-documentation

# Sí comptar-ho, encara que sigui en un directori que sembla de tercers
lib/propi.js       -linguist-vendored

# Corregir una detecció errònia
*.inc              linguist-language=PHP

# Col·lapsar per defecte a la vista de diferències
*.min.js           linguist-generated
package-lock.json  linguist-generated

linguist-generated és el més útil en el dia a dia: fa que els fitxers generats apareguin col·lapsats a les revisions, cosa que elimina de cop el soroll d'un package-lock.json de 8.000 línies en una PR. Enllaça directament amb el que vam dir sobre la mida de la PR a la lliçó 07-02.

Aquests atributs no afecten Git: són convencions que interpreten les plataformes. Si el teu servidor git.exemple.cat no fa servir Linguist, no fan res.

  1. Taula resum

Atribut Què fa Exemple
text Normalitza a LF al repositori *.js text
text=auto Normalitza només si detecta text * text=auto
-text No convertir mai *.png -text
eol=lf Al disc, sempre LF *.sh text eol=lf
eol=crlf Al disc, sempre CRLF *.bat text eol=crlf
binary Macro: -text -diff -merge *.pdf binary
diff=<driver> Context de funció a @@, o textconv *.css diff=css
-diff No mostrar el contingut als diffs *.zip -diff
merge=union Conserva les línies dels dos costats CHANGELOG.md merge=union
merge=<driver> Estratègia de fusió pròpia *.lock merge=bloqueig
-merge Sense fusió automàtica: triar una versió *.png -merge
filter=<f> Transforma en entrar i en sortir *.psd filter=lfs
export-ignore Excloure de git archive proves/ export-ignore
export-subst Substituir $Format:...$ en exportar VERSION.txt export-subst
working-tree-encoding Codificació diferent al disc *.rc working-tree-encoding=UTF-16LE
whitespace Què s'ha de considerar error d'espaiat *.py whitespace=tab-in-indent
linguist-generated Col·lapsar a la vista de diferències *.min.js linguist-generated
linguist-vendored Excloure de les estadístiques vendor/* linguist-vendored

El .gitattributes de gestor-tasques

# ============================================================
# Finals de línia (resol GT-190)
# ============================================================
* text=auto

*.js       text eol=lf diff=javascript
*.css      text eol=lf diff=css
*.html     text eol=lf diff=html
*.json     text eol=lf
*.md       text eol=lf diff=markdown
*.yml      text eol=lf
*.sh       text eol=lf
*.bat      text eol=crlf

# ============================================================
# Binaris
# ============================================================
*.png      binary
*.jpg      binary
*.webp     binary
*.ico      binary
*.woff2    binary
*.pdf      binary diff=pdf

# ============================================================
# Fusió especial
# ============================================================
CHANGELOG.md   merge=union

# ============================================================
# Vista de diferències a la plataforma
# ============================================================
package-lock.json  linguist-generated
*.min.js           linguist-generated
*.min.css          linguist-generated

# ============================================================
# Fora del paquet distribuïble
# ============================================================
.gitattributes         export-ignore
.gitignore             export-ignore
.git-blame-ignore-revs export-ignore
.github/               export-ignore
.githooks/             export-ignore
proves/                export-ignore
CONTRIBUTING.md        export-ignore

Errors Habituals i Consells

Error 1: crear el .gitattributes i creure que ja està. No arregla el contingut ja confirmat. Cal un git add --renormalize . en un commit a part.

Error 2: renormalitzar barrejat amb altres canvis. Produeix un commit il·legible que, a més, no es pot registrar a .git-blame-ignore-revs, perquè ignorar-lo amagaria també el canvi funcional. Aïlla'l, sempre.

Error 3: renormalitzar sense avisar l'equip. Totes les branques obertes entren en conflicte massiu. Coordina-ho, fes-ho en una PR pròpia i fusiona-la de seguida.

Error 4: confiar només en core.autocrlf. No viatja amb el repositori. N'hi ha prou que el Diego no el tingui configurat perquè el problema torni.

Error 5: aplicar merge=union a codi. Combina les dues versions d'una funció i produeix codi trencat sense avisar. Només per a llistes acumulatives.

Error 6: no marcar els binaris. text=auto encerta gairebé sempre, però quan falla corromp el fitxer. Declarar-los costa una línia.

Error 7: oblidar que les definicions de driver no es versionen. El .gitattributes diu diff=jsmodern; la definició viu a git config i no viatja. Documenta-la al README.md.

Error 8: export-ignore sobre fitxers que la gent sí que necessita. Comprova sempre el resultat amb git archive --format=tar HEAD | tar -t.

Consell 1: * text=auto al primer commit de tot projecte nou. Una línia que evita anys de problemes.

Consell 2: eol=lf explícit als scripts. *.sh text eol=lf evita el bad interpreter: /bin/bash^M que apareix la primera vegada que un script editat a Windows arriba a un contenidor.

Consell 3: git check-attr -a <fitxer> quan alguna cosa no quadri. I git ls-files --eol per veure l'estat real dels finals de línia del repositori.

Consell 4: declara els drivers de diff dels teus llenguatges. És la millora de llegibilitat més barata de les revisions: el nom de la funció a cada @@.

Consell 5: linguist-generated als fitxers de bloqueig. Col·lapsa 8.000 línies de soroll a cada PR.

Consell 6: per a binaris grans, .gitattributes no és la resposta. Marcar-los com a binary evita corrompre'ls, però continuen ocupant el mateix espai a l'historial. Això ho resolen Git LFS (lliçó 10-03) i els consells de la lliçó següent.

Exercicis

Exercici 1: reproduir i arreglar el problema de la Carla

  1. Crea un repositori amb un estils.css de 10 línies, desat amb LF, i confirma'l sense .gitattributes.
  2. Simula el desament des de Windows convertint el fitxer a CRLF:
    sed -i 's/$/\r/' estils.css
    
  3. Executa git diff --stat i comprova que les 10 línies apareixen modificades.
  4. Comprova l'estat real amb git ls-files --eol.
  5. Afegeix un .gitattributes amb *.css text eol=lf, confirma'l i demostra que el diff continua igual de trencat fins que renormalitzes.
  6. Renormalitza correctament i verifica amb git ls-files --eol que l'índex queda amb i/lf.

Exercici 2: diffs amb context de funció

  1. Crea un app.js amb tres funcions d'unes cinc línies cadascuna i confirma'l.
  2. Modifica una línia dins de la segona funció i executa git diff. Observa la línia @@.
  3. Afegeix *.js diff=javascript al .gitattributes i repeteix el git diff. Compara.
  4. Prova git log -L :nomDeLaFuncio:app.js i explica què fa.
  5. Afegeix un *.pdf diff=pdf amb un textconv (fes servir pdftotext si el tens, o simula'n un amb un script propi que converteixi un format inventat a text) i demostra que git diff en mostra el contingut.

Exercici 3: merge=union i export-ignore

  1. Crea un repositori amb un CHANGELOG.md que tingui una capçalera i una entrada.
  2. Crea dues branques i afegeix a cadascuna una entrada diferent a la mateixa posició, al principi de la llista.
  3. Fusiona la segona dins de la primera i comprova el conflicte.
  4. Avorta la fusió, afegeix CHANGELOG.md merge=union al .gitattributes, confirma'l i repeteix la fusió. Verifica'n el resultat.
  5. Explica per què aquesta mateixa configuració sobre app.js seria perillosa, amb un exemple concret de codi trencat.
  6. Afegeix proves/ export-ignore i .gitattributes export-ignore, i demostra amb git archive --format=tar HEAD | tar -t que no apareixen dins del paquet.

Solucions

Solució 1:

mkdir /tmp/practica-eol && cd /tmp/practica-eol && git init -b main
printf '.tasca {\n  color: #333;\n  padding: 8px;\n}\n.tasca.completada {\n  opacity: 0.5;\n}\n.tasca.vencuda {\n  border-left: 3px solid #c00;\n}\n' > estils.css
git add . && git commit -m "feat(css): estils base"
# 2 i 3. Simular el desament des de Windows
sed -i 's/$/\r/' estils.css
git diff --stat
 estils.css | 20 ++++++++++----------
 1 fitxer modificat, 10 insercions(+), 10 supressions(-)

Deu línies modificades sense haver canviat res visible. Exactament el problema de la Carla.

# 4. L'estat real
git ls-files --eol
i/lf    w/crlf  attr/                estils.css

i/lf: l'índex té LF. w/crlf: el disc té CRLF. attr/ buit: no s'hi està aplicant cap regla.

# 5. El .gitattributes tot sol no arregla res
echo "*.css text eol=lf" > .gitattributes
git add .gitattributes && git commit -m "chore: política de finals de línia"

git diff --stat
 estils.css | 20 ++++++++++----------

Continua igual. L'atribut s'aplicarà a partir d'ara, però el contingut del disc continua amb CRLF i Git continua veient-hi diferència.

# 6. Renormalitzar
git add --renormalize .
git status --short
M  estils.css
git commit -m "chore: renormalitza els finals de línia a LF"
git checkout -- .          # forçar la reescriptura del disc segons els atributs
git ls-files --eol
i/lf    w/lf    attr/text eol=lf    estils.css

Índex LF, disc LF, atribut aplicat. El git diff torna a estar net i, a partir d'ara, encara que la Carla desi amb CRLF, Git ho normalitzarà en confirmar.

Solució 2:

mkdir /tmp/practica-diff && cd /tmp/practica-diff && git init -b main
cat > app.js <<'FI'
function creaTasca(titol, etiquetes) {
  if (!titol) throw new Error("El títol és obligatori");
  const id = Date.now().toString(36);
  return { id, titol, etiquetes: etiquetes || [], completada: false };
}

function coincideixAmbFiltre(tasca, filtre) {
  if (!filtre) return true;
  const etiquetes = tasca.etiquetes || [];
  return etiquetes.includes(filtre);
}

function comptaPendents(tasques) {
  return tasques.filter(t => !t.completada).length;
}
FI
git add . && git commit -m "feat(app): funcions base"
# 2. Sense driver
sed -i 's/return etiquetes.includes(filtre);/return etiquetes.some(e => e.toLowerCase() === filtre.toLowerCase());/' app.js
git diff
@@ -7,7 +7,7 @@
 function coincideixAmbFiltre(tasca, filtre) {
   if (!filtre) return true;
   const etiquetes = tasca.etiquetes || [];
-  return etiquetes.includes(filtre);
+  return etiquetes.some(e => e.toLowerCase() === filtre.toLowerCase());
 }

La línia @@ està despullada. Aquí la funció es veu per casualitat, perquè el fragment és petit; amb un context de tres línies dins d'una funció de seixanta, no es veuria.

# 3. Amb driver
echo "*.js diff=javascript" > .gitattributes
git diff
@@ -7,7 +7,7 @@ function coincideixAmbFiltre(tasca, filtre) {

Git hi ha afegit la signatura de la funció. Comprovació que l'atribut s'aplica:

git check-attr -a app.js
app.js: diff: javascript
# 4. Historial d'una funció
git add . && git commit -m "refactor(filtres): compara les etiquetes sense distingir majúscules"
git log -L :coincideixAmbFiltre:app.js

Mostra només l'evolució d'aquella funció al llarg de l'historial, amb el diff de cada canvi que la va tocar. Per localitzar la funció, -L :nom: fa servir el mateix reconeixedor que declara diff=javascript; sense ell, la cerca per nom no funciona de manera fiable.

# 5. textconv amb un format inventat
cat > /tmp/mostra-inventat.sh <<'FI'
#!/usr/bin/env bash
# Converteix a text un format binari fictici: base64 dins del fitxer
base64 -d "$1" 2>/dev/null || echo "(no llegible)"
FI
chmod +x /tmp/mostra-inventat.sh

echo "*.inv diff=inventat" >> .gitattributes
git config --local diff.inventat.textconv /tmp/mostra-inventat.sh
git config --local diff.inventat.cachetextconv true

echo "Manual del gestor de tasques, versio 1" | base64 > manual.inv
git add . && git commit -m "docs: afegeix el manual"

echo "Manual del gestor de tasques, versio 2" | base64 > manual.inv
git diff manual.inv
diff --git a/manual.inv b/manual.inv
--- a/manual.inv
+++ b/manual.inv
@@ -1 +1 @@
-Manual del gestor de tasques, versio 1
+Manual del gestor de tasques, versio 2

Git compara la sortida de l'ordre, no els bytes. El contingut emmagatzemat continua sent el fitxer original íntegre.

Solució 3:

mkdir /tmp/practica-union && cd /tmp/practica-union && git init -b main
cat > CHANGELOG.md <<'FI'
# Registre de canvis

## Sense publicar
- Publica la versió inicial (GT-100)
FI
git add . && git commit -m "docs: afegeix el registre de canvis"
# 2. Dues branques, dues entrades a la mateixa posició
git switch -c GT-134
sed -i '4i - Afegeix el filtre per etiqueta (GT-134)' CHANGELOG.md
git commit -am "docs: registra GT-134"

git switch main && git switch -c GT-137
sed -i '4i - Desa les tasques a IndexedDB (GT-137)' CHANGELOG.md
git commit -am "docs: registra GT-137"
# 3. El conflicte
git switch GT-134
git merge GT-137
Autofusionant CHANGELOG.md
CONFLICTE (contingut): Conflicte de fusió a CHANGELOG.md
## Sense publicar
<<<<<<< HEAD
- Afegeix el filtre per etiqueta (GT-134)
=======
- Desa les tasques a IndexedDB (GT-137)
>>>>>>> GT-137
- Publica la versió inicial (GT-100)
# 4. Amb merge=union
git merge --abort
echo "CHANGELOG.md merge=union" > .gitattributes
git add . && git commit -m "chore: fusió per unió del registre de canvis"

git merge GT-137
cat CHANGELOG.md
# Registre de canvis

## Sense publicar
- Afegeix el filtre per etiqueta (GT-134)
- Desa les tasques a IndexedDB (GT-137)
- Publica la versió inicial (GT-100)

Sense conflicte i amb les dues entrades. (Si el merge falla perquè no troba el fitxer d'atributs al commit base, confirma primer el .gitattributes a main i rebasa les branques: els atributs es llegeixen de la còpia de treball en el moment de la fusió.)

5. Per què seria perillós en codi. Suposem que l'Ana i el Bruno modifiquen la mateixa funció:

// Branca de l'Ana
function comptaPendents(tasques) {
  return tasques.filter(t => !t.completada).length;
}

// Branca del Bruno
function comptaPendents(tasques) {
  return tasques.filter(t => !t.completada && !t.arxivada).length;
}

Amb merge=union, el resultat seria:

function comptaPendents(tasques) {
  return tasques.filter(t => !t.completada).length;
  return tasques.filter(t => !t.completada && !t.arxivada).length;
}

Sintàcticament vàlid, silenciosament incorrecte: la segona línia és codi mort i el canvi del Bruno es perd sense que ningú se n'assabenti. En altres casos el resultat ni tan sols compila, i en el pitjor —dues versions d'una condició o d'una clau— produeix codi que fa una cosa diferent de la que volia cap dels dos. I tot això sense cap conflicte que avisi.

Aquesta és la clau: merge=union elimina el conflicte, no el resol. Només és acceptable quan "quedar-se amb totes dues línies" és sempre la resposta correcta, és a dir, en llistes acumulatives on l'ordre no és semàntic.

# 6. export-ignore
mkdir proves && echo "test('ok', () => {});" > proves/app.test.js
cat >> .gitattributes <<'FI'
proves/        export-ignore
.gitattributes export-ignore
FI
git add . && git commit -m "chore: exclou les proves del paquet distribuïble"

git archive --format=tar HEAD | tar -t
CHANGELOG.md

Ni proves/ ni .gitattributes no hi apareixen: estan versionats, però fora del paquet.

Conclusió

L'essencial d'aquesta lliçó:

  • .gitignore decideix què s'ha de versionar; .gitattributes decideix com s'ha de tractar allò versionat. Actua a checkout, commit, diff, merge i archive, i el seu diagnòstic és git check-attr -a.
  • El problema dels finals de línia —el de la Carla— es resol amb una convenció: LF al repositori, el natiu al disc. Es declara amb text, text=auto i eol=lf/eol=crlf, i eol controla el disc, no el repositori.
  • .gitattributes guanya core.autocrlf i, sobretot, viatja amb el repositori. Aquesta és la raó de fons per la qual és la solució robusta: la política s'aplica a tothom, inclòs el Diego amb el seu fork, sense dependre que ningú configuri res.
  • Afegir el fitxer no arregla el passat: cal un git add --renormalize ., en un commit aïllat, coordinat amb l'equip i registrat a .git-blame-ignore-revs (lliçó 08-02). git ls-files --eol mostra l'estat real abans i després.
  • binary és la macro de -text -diff -merge i evita que Git corrompi un fitxer binari intentant convertir-ne els bytes. Declarar els binaris explícitament costa una línia i estalvia sorpreses.
  • diff=<driver> fa que la línia @@ mostri el nom de la funció, i habilita git log -L :funcio:fitxer. textconv permet veure com a text el contingut d'un binari, només per visualitzar-lo.
  • merge=union conserva les línies dels dos costats i elimina els conflictes falsos del CHANGELOG.md. Mai en codi: elimina el conflicte, no el resol.
  • export-ignore controla què contenen els paquets de git archive —inclosos els que generen les plataformes per a cada etiqueta—, i export-subst omple la versió i el commit dins del paquet.
  • filter=lfs és la porta d'entrada a Git LFS, el mecanisme complet del qual es veu a la lliçó 10-03; i els atributs linguist-*, encara que no afecten Git, col·lapsen el soroll dels fitxers generats a les revisions.

gestor-tasques ja no versiona el que no ha de versionar, i tracta cada fitxer com correspon. Queda pendent l'assumpte més greu dels que vam anunciar en tancar el mòdul 7, i el que no admet improvisació: el fitxer de configuració amb la contrasenya de la base de dades que fa mesos que és a l'historial.

Treure'l d'allà té un procediment amb un ordre de passos que importa moltíssim, i comença per una cosa que no té res a veure amb Git. És la lliçó 08-05: Bones Pràctiques de Seguretat.

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