La lliçó anterior va resoldre quins fitxers no han d'entrar al repositori. Aquesta tracta del problema complementari i menys conegut: els fitxers que sí 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
- Què és
.gitattributesi en què es diferencia de.gitignore - Sintaxi i precedència
- El problema dels finals de línia
text,eolitext=auto.gitattributesdavant decore.autocrlf- Renormalitzar un projecte que ja té el problema
- Marcar binaris amb
-textibinary - Diffs personalitzats:
diff=<driver> - Que
@@mostri el nom de la funció textconv: veure el text de fitxers binaris- Drivers de fusió i el cas
merge=union export-ignoreigit archive- Altres atributs:
filter,linguist-*,export-subst - Taula resum
- Què és
.gitattributes i en què es diferencia de .gitignore
.gitattributes i en què es diferencia de .gitignoreEls 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:
*.js text eol=lf
*.png binary
*.md diff=markdown merge=union
proves/ export-ignore
*.pdf -text diff=pdfEls 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 |
- 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 |
Sí | 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:
# 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 -agit 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.
- 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 diffmostra el fitxer sencer modificat sense cap canvi visible.git statusmarca 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 blameho 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.
text, eol i text=auto
text, eol i text=autoL'atribut text
| Declaració | En confirmar (add) |
En extreure (checkout) |
|---|---|---|
text |
Converteix CRLF → LF |
Converteix LF → segons core.eol (per defecte, el natiu del sistema) |
text=auto |
Converteix CRLF → LF 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=lfno vol dir "desa LF al repositori": al repositori sempre hi haLFquan el fitxer éstext.eolcontrola què s'escriu al teu disc en extreure.text=autodecideix 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=autoI 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 binaryLes dues seccions del mig són les que eviten errors reals:
*.sh text eol=lf: un script de shell ambCRLFno arrenca a Linux. L'intèrpret busca/bin/bashi troba/bin/bash\r. Amb aquesta regla, encara que la Carla l'editi a Windows, al seu disc hi tindràLFi funcionarà dins del contenidor.*.bat text eol=crlf: els.batantics de Windows necessitenCRLF. Encara que l'Ana els editi a Ubuntu, al disc de la Carla hi arribaran ambCRLF.
.gitattributes davant de core.autocrlf
.gitattributes davant de core.autocrlfA 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 |
CRLF → LF |
LF → CRLF |
Windows |
input |
CRLF → LF |
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 inputFunciona, 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 | Sí |
| 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:
.gitattributesper a la política del projecte;core.autocrlfcom a xarxa de seguretat personal en repositoris que no en tinguin. Si tens.gitattributes,core.autocrlfsobra.
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
- 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:
- Avisa amb antelació i fixa una hora.
- Que tothom fusioni o tanqui les seves branques abans d'aquella hora.
- Una persona fa la renormalització en una PR pròpia, sense res més a dins.
- Es fusiona tan bon punt està aprovada, sense deixar-la oberta dies.
- Tothom fa
pulli, si tenia branques vives, les rebasa sobre el commit de renormalització. - 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:
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.
- Marcar binaris amb
-text i binary
-text i binaryUn 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.
binary és un macroatribut: equival exactament a escriure
É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.pngQuan 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:
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:
Git desarà el contingut en UTF-8 dins del repositori (on els diffs funcionen) i l'escriurà en UTF-16LE al teu disc.
- Diffs personalitzats:
diff=<driver>
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=jsonLa 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.
- Que
@@ mostri el nom de la funció
@@ 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:
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).
# 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.
textconv: veure el text de fitxers binaris
textconv: veure el text de fitxers binarisHi 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:
# 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 trueCada 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:
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.
- Drivers de fusió i el cas
merge=union
merge=unionIgual 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:
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
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=unionmai 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":
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.
export-ignore i git archive
export-ignore i git archivegit 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 HEADL'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-ignoreComprova el resultat abans de publicar:
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:
En fer git archive, el fitxer del paquet surt amb els valors emplenats:
É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.
- Altres atributs:
filter, linguist-*, export-subst
filter, linguist-*, export-substfilter: 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 -textAquestes 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):
git config --local filter.treu-espais-finals.clean "sed 's/[[:space:]]*$//'"
git config --local filter.treu-espais-finals.smudge catCompte 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-generatedlinguist-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.
- 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-ignoreErrors 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
- Crea un repositori amb un
estils.cssde 10 línies, desat ambLF, i confirma'l sense.gitattributes. - Simula el desament des de Windows convertint el fitxer a
CRLF:sed -i 's/$/\r/' estils.css - Executa
git diff --stati comprova que les 10 línies apareixen modificades. - Comprova l'estat real amb
git ls-files --eol. - Afegeix un
.gitattributesamb*.css text eol=lf, confirma'l i demostra que el diff continua igual de trencat fins que renormalitzes. - Renormalitza correctament i verifica amb
git ls-files --eolque l'índex queda ambi/lf.
Exercici 2: diffs amb context de funció
- Crea un
app.jsamb tres funcions d'unes cinc línies cadascuna i confirma'l. - Modifica una línia dins de la segona funció i executa
git diff. Observa la línia@@. - Afegeix
*.js diff=javascriptal.gitattributesi repeteix elgit diff. Compara. - Prova
git log -L :nomDeLaFuncio:app.jsi explica què fa. - Afegeix un
*.pdf diff=pdfamb untextconv(fes servirpdftotextsi el tens, o simula'n un amb un script propi que converteixi un format inventat a text) i demostra quegit diffen mostra el contingut.
Exercici 3: merge=union i export-ignore
- Crea un repositori amb un
CHANGELOG.mdque tingui una capçalera i una entrada. - Crea dues branques i afegeix a cadascuna una entrada diferent a la mateixa posició, al principi de la llista.
- Fusiona la segona dins de la primera i comprova el conflicte.
- Avorta la fusió, afegeix
CHANGELOG.md merge=unional.gitattributes, confirma'l i repeteix la fusió. Verifica'n el resultat. - Explica per què aquesta mateixa configuració sobre
app.jsseria perillosa, amb un exemple concret de codi trencat. - Afegeix
proves/ export-ignorei.gitattributes export-ignore, i demostra ambgit archive --format=tar HEAD | tar -tque 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"Deu línies modificades sense haver canviat res visible. Exactament el problema de la Carla.
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 --statContinua igual. L'atribut s'aplicarà a partir d'ara, però el contingut del disc continua amb CRLF i Git continua veient-hi diferència.
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Í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.
Git hi ha afegit la signatura de la funció. Comprovació que l'atribut s'aplica:
# 4. Historial d'una funció
git add . && git commit -m "refactor(filtres): compara les etiquetes sense distingir majúscules"
git log -L :coincideixAmbFiltre:app.jsMostra 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.invdiff --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 2Git 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"## 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 -tNi proves/ ni .gitattributes no hi apareixen: estan versionats, però fora del paquet.
Conclusió
L'essencial d'aquesta lliçó:
.gitignoredecideix què s'ha de versionar;.gitattributesdecideix com s'ha de tractar allò versionat. Actua acheckout,commit,diff,mergeiarchive, i el seu diagnòstic ésgit check-attr -a.- El problema dels finals de línia —el de la Carla— es resol amb una convenció:
LFal repositori, el natiu al disc. Es declara ambtext,text=autoieol=lf/eol=crlf, ieolcontrola el disc, no el repositori. .gitattributesguanyacore.autocrlfi, 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 --eolmostra l'estat real abans i després. binaryés la macro de-text -diff -mergei 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 habilitagit log -L :funcio:fitxer.textconvpermet veure com a text el contingut d'un binari, només per visualitzar-lo.merge=unionconserva les línies dels dos costats i elimina els conflictes falsos delCHANGELOG.md. Mai en codi: elimina el conflicte, no el resol.export-ignorecontrola què contenen els paquets degit archive—inclosos els que generen les plataformes per a cada etiqueta—, iexport-substomple 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 atributslinguist-*, 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
- Què és Git?
- Instal·lant Git
- Terminologia Bàsica de Git
- El Model de Dades de Git
- Configurant Git
- Configuració Inicial
Mòdul 2: Operacions Bàsiques de Git
- Creant un Repositori
- Clonant un Repositori
- Flux de Treball Bàsic de Git
- Preparant i Confirmant Canvis
- Inspeccionant Canvis amb git diff
- Visualitzant l'Historial de Confirmacions
Mòdul 3: Branques i Fusió
- Entenent les Branques
- Creant i Canviant Branques
- Fusionant Branques
- Estratègies de Fusió
- Resolent Conflictes de Fusió
- Gestió de Branques
Mòdul 4: Treballant amb Repositoris Remots
- Entenent els Repositoris Remots
- Afegint un Repositori Remot
- Autenticació amb Repositoris Remots
- Obtenint i Baixant Canvis
- Enviant Canvis
- Rastrejant Branques
Mòdul 5: Operacions Avançades de Git
- Rebase
- Rebase Interactiu
- Cherry-Picking de Confirmacions
- Desant Canvis Temporals
- Etiquetant Confirmacions
- Revertint Confirmacions
Mòdul 6: Eines i Tècniques de Git
- Usant Git Hooks
- Git Bisect
- Git Blame
- Git Log i Àlies
- Submòduls de Git
- Múltiples Còpies de Treball amb git worktree
Mòdul 7: Estratègies de Col·laboració i Flux de Treball
- Forks i Pull Requests
- Revisions de Codi amb Git
- Flux de Treball Git Flow
- GitHub Flow
- Trunk Based Development
- Integració Contínua amb Git
Mòdul 8: Bones Pràctiques i Consells de Git
- Escrivint Bons Missatges de Confirmació
- Mantenint un Historial Net
- Ignorant Fitxers amb .gitignore
- Atributs de Fitxer amb .gitattributes
- Bones Pràctiques de Seguretat
- Consells de Rendiment
Mòdul 9: Resolució de Problemes i Depuració
- Problemes Habituals de Git
- Desfent Canvis
- Resolent Divergències amb el Remot
- Recuperant Confirmacions Perdudes
- Tractant amb Repositoris Corruptes
- Tècniques Avançades de Depuració
