La lliçó anterior va deixar gestor-tasques amb un historial llegible, bisecable i reversible. Però un historial impecable pot contenir brossa, i el de gestor-tasques en conté: el Bruno va pujar sense adonar-se'n la carpeta node_modules sencera —14.000 fitxers i 180 MB—, la Carla ha anat sembrant Thumbs.db pels directoris d'imatges, el macOS del Bruno deixa un .DS_Store a cada carpeta que obre al Finder, i en algun commit de fa mesos hi ha un fitxer config.js amb la contrasenya de la base de dades de proves.

Aquesta lliçó tracta de la primera línia de defensa contra tot això: .gitignore, el mecanisme amb què se li diu a Git quins fitxers no ha ni de proposar-se versionar. El vam esmentar de passada a la lliçó 02-04, en veure git status, i vam prometre desenvolupar-lo aquí.

És un fitxer enganyosament simple. La seva sintaxi té cantonades que confonen tothom —la negació, la barra inicial, el parany dels directoris— i, sobretot, amaga una regla que és la causa del 90 % de les preguntes sobre Git als fòrums: .gitignore no serveix per deixar de seguir un fitxer que ja està versionat.

Contingut

  1. Per què importa allò que no es versiona
  2. Què no ha d'entrar mai: taula per categories
  3. Com funciona: precedència i àmbit
  4. Sintaxi completa dels patrons
  5. La negació amb ! i el seu parany
  6. Els tres nivells: projecte, local i global
  7. La regla que confon tothom: només afecta allò sense seguiment
  8. Treure del seguiment amb git rm --cached
  9. Depurar amb git check-ignore -v
  10. Forçar amb git add -f
  11. Plantilles per llenguatge
  12. El patró per a secrets: .env i .env.exemple

  1. Per què importa allò que no es versiona

Versionar el que no toca té quatre costos, i els quatre són més grans del que sembla:

Cost Conseqüència concreta
Mida permanent Els 180 MB de node_modules queden a l'historial per sempre, encara que després s'esborrin. Cada clon se'ls baixa (lliçó 08-06).
Conflictes absurds Fitxers generats (compilats, bloquejos, .idea/workspace.xml) canvien a cada màquina i provoquen conflictes a cada fusió, sense cap significat.
Soroll a les revisions Una PR de 30 línies útils amb 4.000 línies d'artefactes és irrevisable (lliçó 07-02).
Risc de seguretat Un secret al repositori es replica a cada clon i no s'esborra esborrant el fitxer. És el que veurem a la lliçó 08-05.

Hi ha un principi que ordena totes les decisions:

Es versiona la font, no el resultat. Si un fitxer es pot regenerar a partir d'altres fitxers del repositori, no es versiona. Si conté informació pròpia d'una màquina o d'una persona, no es versiona. Si és un secret, no es versiona mai.

Amb dues excepcions importants que convé conèixer, perquè són les que confonen:

  • Els fitxers de bloqueig de dependències (package-lock.json, yarn.lock, Cargo.lock en aplicacions) sí que es versionen. No són un resultat: són la font de la reproductibilitat. Sense ells, dues persones instal·len versions diferents.
  • La configuració compartida del projecte (.editorconfig, .gitattributes, la part del .vscode/ que defineix tasques i extensions recomanades) sí que es versiona. És font, no resultat.

  1. Què no ha d'entrar mai: taula per categories

Categoria Exemples Per què no
Dependències node_modules/, vendor/, bower_components/, venv/, .venv/, target/ (Rust) Es regeneren amb npm ci, composer install, pip install -r. Milers de fitxers, desenes o centenars de MB, i contingut diferent segons el sistema operatiu.
Artefactes de compilació dist/, build/, out/, *.o, *.class, *.pyc, __pycache__/, *.min.js generats Són la sortida del codi que sí que està versionat. Canvien a cada compilació i generen conflictes garantits.
Fitxers del sistema operatiu .DS_Store, ._*, .Spotlight-V100 (macOS); Thumbs.db, Desktop.ini, $RECYCLE.BIN/ (Windows); .directory (Linux) No tenen res a veure amb el projecte: són metadades de l'explorador de fitxers. El Bruno i la Carla embruten el repositori sense voler.
Configuració de l'editor i de l'IDE .idea/, .vscode/settings.json personal, *.swp, *.swo, .project, .classpath, *.sublime-workspace Reflecteixen preferències personals i rutes absolutes d'una màquina. Provoquen conflictes entre qui fa servir un editor i qui en fa servir un altre.
Secrets i credencials .env, .env.local, config/secrets.yml, *.pem, *.key, id_rsa, fitxers de credencials de serveis Mai. Un secret a l'historial és un secret compromès, encara que l'esborris després (lliçó 08-05).
Registres i temporals *.log, logs/, tmp/, *.tmp, *.bak, *.swp, npm-debug.log* Efímers per definició. Ningú els consultarà d'aquí a un any.
Bases de dades locals i bolcats *.sqlite, *.db, dump.sql, dades-locals/ Grans, binaris i amb dades que poden ser personals.
Fitxers grans i binaris generats Vídeos de prova, imatges exportades, *.zip, *.tar.gz Cada versió es desa sencera (lliçó 08-06). Si són binaris necessaris, la solució és Git LFS, lliçó 10-03.
Cobertura i resultats de proves coverage/, .nyc_output/, junit.xml, .pytest_cache/ Sortida d'una execució concreta.
Memòries cau d'eines .cache/, .parcel-cache/, .eslintcache, .next/, .nuxt/ Reconstruïbles i específiques de la màquina.

El .gitignore que l'equip de gestor-tasques va acabar adoptant:

# --- Dependències ---------------------------------------------------
node_modules/

# --- Artefactes de compilació ---------------------------------------
dist/
build/
*.min.js
*.min.css

# --- Secrets (veure 08-05) ------------------------------------------
.env
.env.*
!.env.exemple
*.pem
*.key

# --- Registres i temporals ------------------------------------------
*.log
npm-debug.log*
tmp/
*.tmp
*.bak

# --- Cobertura i memòries cau ---------------------------------------
coverage/
.eslintcache
.cache/

# --- Sistema operatiu -----------------------------------------------
# macOS (Bruno)
.DS_Store
._*
.Spotlight-V100
.Trashes

# Windows (Carla)
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
Desktop.ini
$RECYCLE.BIN/

# Linux (Ana)
.directory
*~

# --- Editors --------------------------------------------------------
.idea/
*.swp
*.swo
.vscode/*
!.vscode/extensions.json
!.vscode/tasks.json

Fixa't en els dos usos de ! (negació) al final de blocs: !.env.exemple i !.vscode/extensions.json. Els expliquem a l'apartat 5, juntament amb el parany que amaguen.

  1. Com funciona: precedència i àmbit

Tres regles governen el comportament:

Regla 1: .gitignore s'aplica per directori i cap avall. Un .gitignore afecta el seu propi directori i tots els seus subdirectoris. En pots tenir diversos en un mateix repositori.

gestor-tasques/
├── .gitignore            ← regles globals del projecte
├── app.js
├── imatges/
│   ├── .gitignore        ← regles només per a imatges/ i els seus fills
│   └── originals/
└── proves/
    └── .gitignore        ← regles només per a proves/

Regla 2: guanya el patró més específic. Git avalua, en aquest ordre de prioritat creixent:

  1. core.excludesFile (global de l'usuari)
  2. .git/info/exclude (local del repositori)
  3. .gitignore dels directoris, de l'arrel cap avall (el més profund guanya)
  4. La línia d'ordres (git add -f)

Regla 3: dins d'un mateix fitxer, guanya l'última línia que coincideixi. Per això les negacions van després del patró que anul·len:

*.log        # ignora tots els .log
!errors.log  # ...llevat d'aquest  (correcte: va després)
!errors.log  # aquesta negació no serveix de res...
*.log        # ...perquè aquesta línia posterior ho torna a ignorar tot

El .gitignore es versiona. És part del projecte i ha de ser al primer commit. Si no hi és, cada persona de l'equip ha de descobrir pel seu compte què no ha de pujar, i algú fallarà.

  1. Sintaxi completa dels patrons

Git fa servir una sintaxi derivada dels globs de shell, amb algunes particularitats pròpies.

Patró Què fa Exemple del que hi encaixa
nom Qualsevol fitxer o directori anomenat així, a qualsevol profunditat app.log, src/app.log, a/b/c/app.log
nom/ Només si és un directori logs/ sí; un fitxer anomenat logs no
/nom Només a l'arrel del directori del .gitignore /build encaixa amb build a l'arrel, no amb src/build
* Qualsevol seqüència de caràcters, sense creuar / *.log encaixa amb error.log, no amb logs/error.log (encara que el nom solt sí)
? Exactament un caràcter imatge?.png encaixa amb imatge1.png, no amb imatge10.png
[abc] Un d'aquests caràcters imatge[123].png encaixa amb imatge2.png
[0-9] Un caràcter del rang registre[0-9].txt encaixa amb registre7.txt
**/ Qualsevol nombre de directoris (inclòs cap) **/temporal encaixa amb temporal, a/temporal, a/b/temporal
/** Tot el que hi ha a dins logs/** encaixa amb tot el contingut de logs/
a/**/b b sota a, a qualsevol profunditat a/b, a/x/b, a/x/y/b
!patro Des-ignora allò que encaixaria abans (amb límits: apartat 5)
# text Comentari (línia sencera)
\#literal Un fitxer que comença per # Encaixa amb el fitxer anomenat #literal
nom\ Espai final literal (els espais finals s'ignoren llevat que vagin escapats)

Els dos matisos que cal interioritzar

La barra inicial ancora a l'arrel. És la diferència més important i la més oblidada:

build/       # ignora QUALSEVOL directori build, a qualsevol profunditat
/build/      # ignora NOMÉS el build de l'arrel del projecte

Si el teu projecte té build/ a l'arrel i també src/components/build/ que sí que vols versionar, necessites la barra inicial.

Una barra al mig del patró també ancora. Aquesta és la regla que sorprèn: si el patró conté una / en qualsevol posició que no sigui la final, s'interpreta com a relatiu al directori del .gitignore, no com a "a qualsevol profunditat".

doc/notes.txt    # només el doc/notes.txt de l'arrel
notes.txt        # qualsevol notes.txt, a qualsevol profunditat
**/doc/notes.txt # qualsevol doc/notes.txt, a qualsevol profunditat

Exemples comentats

# Tots els .log, siguin on siguin
*.log

# Però no els de la carpeta d'exemples (veure apartat 5)
!exemples/*.log

# Només el directori build de l'arrel
/build/

# Qualsevol directori anomenat tmp, a qualsevol profunditat
tmp/

# El fitxer config.local.js de l'arrel, no altres config.local.js
/config.local.js

# Qualsevol fitxer el nom del qual comenci per "esborrany"
esborrany*

# Fitxers numerats del 0 al 9
bolcat[0-9].sql

# Tot el contingut de dades/, però conservant la carpeta (veure més avall)
dades/*
!dades/.gitkeep

# Un fitxer que es diu literalment "!important"
\!important

El truc de .gitkeep

Git no versiona directoris, només fitxers: un directori buit simplement no existeix per a Git. Si la teva aplicació necessita que existeixi dades/ en arrencar, el patró habitual és:

dades/*
!dades/.gitkeep

I crees un fitxer buit dades/.gitkeep. El nom no té res d'especial —Git no el coneix—; és només una convenció per dir "aquest fitxer existeix perquè el directori existeixi".

Fixa't que fem servir dades/* i no dades/. La raó és el parany de l'apartat següent.

  1. La negació amb ! i el seu parany

La regla que més temps fa perdre a tot .gitignore:

Si un directori està ignorat, Git no hi entra. Per tant, no es pot des-ignorar un fitxer que sigui dins d'un directori ignorat.

És una optimització deliberada: Git no recorre milers de fitxers de node_modules/ només per si alguna regla posterior en rescata algun. I produeix aquesta fallada, que sembla un error de Git i no ho és:

# NO FUNCIONA
dades/
!dades/important.csv

dades/important.csv continua ignorat. Git ha descartat dades/ sencer i mai no ha arribat a avaluar la segona línia.

La manera correcta és ignorar el contingut, no el directori:

# SÍ QUE FUNCIONA
dades/*
!dades/important.csv

Amb dades/*, Git sí que entra a dades/, ignora cadascun dels seus fills individualment, i la negació en pot rescatar un.

Quan hi ha diversos nivells

Si el fitxer que vols rescatar és més profund, cal des-ignorar cada nivell intermedi:

# Ignorar-ho tot llevat de config/produccio/ajustos.json
*
!*/
!config/produccio/ajustos.json

Traduït: ignora-ho tot (*), però no ignoris els directoris (!*/, necessari perquè Git hi pugui entrar), i rescata explícitament aquell fitxer.

Aquest patró de llista blanca —"ignorar-ho tot i rescatar el que vull"— és agressiu però molt útil en repositoris on allò versionable és una minoria (per exemple, un repositori de configuració):

# Llista blanca estricta
*
!*/
!*.yml
!*.md
!.gitignore

El cas del .gitignore de gestor-tasques

Tornem a les dues negacions que vam deixar pendents:

.env
.env.*
!.env.exemple

Funciona perquè .env.* ignora fitxers, no un directori, i la negació posterior en rescata un. Correcte.

.vscode/*
!.vscode/extensions.json
!.vscode/tasks.json

Fixa't en .vscode/* i no .vscode/. Si haguéssim escrit .vscode/, les dues negacions no farien res. Aquest és exactament l'error que comet tothom la primera vegada.

  1. Els tres nivells: projecte, local i global

Hi ha tres llocs on declarar exclusions, i triar malament és la causa d'una discussió recurrent a l'equip: la Carla va proposar afegir .idea/ al .gitignore del projecte i l'Ana s'hi va oposar perquè ningú més fa servir aquell IDE. Tots dos tenien part de raó, i el problema era de nivell.

Nivell Fitxer Es versiona? A qui afecta? Per a què
Projecte .gitignore al repositori A tot l'equip Allò que és del projecte: node_modules/, dist/, .env, coverage/
Local .git/info/exclude No Només a tu, només en aquell repositori Fitxers temporals teus en aquell projecte: notes-ana.md, proves-locals/
Global Ruta indicada per core.excludesFile No A tu, a tots els teus repositoris Allò que és de la teva màquina o la teva eina: .DS_Store, .idea/, *.swp

El criteri

Aquest fitxer apareixeria a la màquina de qualsevol que treballés en aquest projecte? Si sí, va al .gitignore del projecte. Si apareix només pel teu sistema operatiu o el teu editor, va al teu global. Si és un caprici teu en aquest repositori concret, va a .git/info/exclude.

Estrictament, .DS_Store i .idea/ van al global. A la pràctica, gairebé tots els projectes els posen també al .gitignore del projecte, com a xarxa de seguretat per a qui no tingui el global configurat. És defensiu i és raonable; simplement cal saber que és una duplicació deliberada.

Configurar el global

# 1. Crear el fitxer
cat > ~/.gitignore_global <<'FI'
# Sistema operatiu
.DS_Store
._*
Thumbs.db
Desktop.ini
.directory

# Editors
.idea/
*.swp
*.swo
*~
.vscode/

# Eines personals
.env.personal
notes-privades.md
FI

# 2. Dir-li-ho a Git
git config --global core.excludesFile ~/.gitignore_global

# 3. Comprovar
git config --get core.excludesFile

A macOS i Linux, Git ja fa servir ~/.config/git/ignore sense necessitat de configurar res si el fitxer existeix. És la ubicació per defecte segons l'estàndard XDG:

mkdir -p ~/.config/git
# i escriure les regles a ~/.config/git/ignore

.git/info/exclude

És un .gitignore normal que viu dins de .git/. Com tot el que hi ha allà (lliçó 01-04), no es versiona ni es clona:

cat >> .git/info/exclude <<'FI'
# Les meves coses en aquest repositori, que ningú més necessita ignorar
notes-ana.md
proves-locals/
bolcat-avui.sql
FI

Avantatge respecte a modificar el .gitignore del projecte: no embrutes el fitxer compartit amb les teves manies, i no les has de justificar en una revisió. Desavantatge: es perd si esborres i tornes a clonar el repositori.

  1. La regla que confon tothom: només afecta allò sense seguiment

Aquí hi ha la font del 90 % de les preguntes sobre .gitignore:

.gitignore només afecta els fitxers SENSE SEGUIMENT. Si un fitxer ja és a l'índex, Git continuarà registrant-ne els canvis per molts patrons que escriguis.

Recorda els tres estats de la lliçó 01-03 i el cicle de la lliçó 02-03. .gitignore actua exclusivament sobre la transició de sense seguiment a preparat: li diu a Git "ni me'l proposis a git status, ni l'agafis amb git add .". Sobre un fitxer que ja va creuar aquella frontera, no té cap efecte.

L'escenari típic, que li va passar al Bruno:

# Dilluns: el Bruno puja node_modules sense adonar-se'n
git add .
git commit -m "chore: primer commit"
git push
# Dimarts: l'Ana ho veu i afegeix el .gitignore
echo "node_modules/" >> .gitignore
git add .gitignore && git commit -m "chore: ignora node_modules"

git status
On branch main
Changes not staged for commit:
	modified:   node_modules/marked/package.json
	modified:   node_modules/.package-lock.json
	...

Continua sent-hi. El .gitignore no ha fet res, perquè aquells fitxers ja tenen seguiment.

flowchart LR
    A["Sense seguiment"] -->|"git add"| B["Preparat"]
    B -->|"git commit"| C["Confirmat / amb seguiment"]
    A -.->|".gitignore actua AQUÍ<br/>i només aquí"| A
    C -->|"git rm --cached"| A

La fletxa de tornada —git rm --cached— és l'única manera de retornar un fitxer a l'estat on .gitignore el pot protegir.

  1. Treure del seguiment amb git rm --cached

git rm --cached treu el fitxer de l'índex però el deixa al teu disc. A partir d'aquell moment passa a estar sense seguiment, el .gitignore comença a aplicar-s'hi i desapareix de git status.

# Un fitxer
git rm --cached config.js

# Un directori sencer (recursiu)
git rm -r --cached node_modules/

# Comprovar abans de tocar res (--dry-run no modifica res)
git rm -r --cached --dry-run node_modules/

El flux complet, tal com el va fer l'Ana:

# 1. Assegurar-se que el patró és al .gitignore
grep -q "^node_modules/" .gitignore || echo "node_modules/" >> .gitignore

# 2. Treure de l'índex, conservant-lo al disc
git rm -r --cached node_modules/

# 3. Verificar que git status queda net
git status --short

# 4. Confirmar
git commit -m "chore: deixa de versionar node_modules"

# 5. Avisar l'equip (veure més avall)
git push

El truc per netejar-ho tot de cop

Quan hi ha molts fitxers que ara haurien d'estar ignorats, hi ha una recepta que els treu tots alhora respectant el .gitignore actual:

# Buida l'índex i el reconstrueix aplicant el .gitignore vigent
git rm -r --cached .
git add .
git status
git commit -m "chore: aplica el .gitignore als fitxers ja versionats"

git rm -r --cached . no esborra res del disc: buida l'índex. git add . el torna a omplir, i aquesta vegada sí que respecta el .gitignore. La diferència entre tots dos estats és exactament el conjunt de fitxers que sobraven.

Comprova sempre amb git status abans de confirmar. Si el .gitignore té un patró massa ampli, aquesta recepta pot treure del seguiment fitxers que sí que volies.

Els dos advertiments importants

Advertiment 1: el fitxer s'esborra a les altres màquines. Per a Git, un git rm --cached seguit de commit és un esborrat. Quan el Bruno i la Carla facin pull, aquell fitxer desapareixerà de la seva còpia de treball. Amb node_modules/ tant se val (es regenera amb npm ci), però si treus del seguiment un config.js que la gent necessita, els l'estàs esborrant. Avisa l'equip abans de fer-ho.

Advertiment 2: no esborra res de l'historial. I aquest és crític:

git rm --cached config.js
git commit -m "chore: deixa de versionar config.js"

El fitxer desapareix de la punta, però continua a tots els commits anteriors:

git log --all --oneline -- config.js     # allà continuen
git show HEAD~5:config.js                # i el seu contingut és accessible

Si config.js contenia una contrasenya, la contrasenya continua al repositori i a tots els clons. git rm no és una eina de seguretat. El procediment correcte per a un secret ja filtrat —rotar primer la credencial, després reescriure l'historial— és el contingut de la lliçó 08-05.

  1. Depurar amb git check-ignore -v

Arriba el dia en què un fitxer s'ignora i no saps per què, o al revés. git check-ignore -v t'ho respon: et diu quin fitxer d'exclusió, quina línia i quin patró ho estan decidint.

git check-ignore -v dist/app.min.js
.gitignore:8:dist/	dist/app.min.js

Es llegeix: el patró dist/, a la línia 8 del fitxer .gitignore, és el que ignora dist/app.min.js.

Exemples de les tres respostes possibles:

# Ignorat per una regla global
git check-ignore -v .DS_Store
/home/ana/.gitignore_global:3:.DS_Store	.DS_Store
# Ignorat per una regla local no versionada
git check-ignore -v notes-ana.md
.git/info/exclude:6:notes-ana.md	notes-ana.md
# NO ignorat: no imprimeix res i retorna codi de sortida 1
git check-ignore -v app.js
echo $?
1

Opcions útils

# Diversos fitxers de cop
git check-ignore -v app.js dist/app.min.js .DS_Store node_modules/marked/index.js

# Mostrar també els que NO s'ignoren, i per què
git check-ignore -v --no-index app.js

# Comprovar TOT el que git status amaga
git status --ignored --short

git status --ignored mereix un apartat propi, perquè és la manera de descobrir sorpreses:

git status --ignored --short
!! node_modules/
!! dist/
!! .env
!! coverage/
!! app.js.bak

Cada !! és un fitxer o directori que Git està amagant. Si hi veus alguna cosa que que volies versionar, ja saps que un patró és massa ampli.

El cas especial de --no-index

Si un fitxer té seguiment, check-ignore pot resultar confús, perquè els patrons sí que hi encaixen encara que no tinguin efecte. Per preguntar "hi encaixaria algun patró, amb independència de si té seguiment?":

git check-ignore -v --no-index config.js
.gitignore:12:config.js	config.js

És a dir: hi ha un patró que el cobriria, però com que el fitxer ja és a l'índex, no fa efecte. Aquest diagnòstic —"el patró està bé, el problema és l'apartat 7"— és exactament el que necessites saber.

  1. Forçar amb git add -f

De vegades cal versionar un fitxer que un patró ampli està ignorant. git add -f (o --force) se salta les regles per a aquella operació concreta:

git add -f dist/app.min.js

Sense -f, Git avisa i no fa res:

The following paths are ignored by one of your .gitignore files:
dist/app.min.js
hint: Use -f if you really want to add them.

Compte amb el que passa després. Un cop afegit amb -f, el fitxer passa a tenir seguiment, i per la regla de l'apartat 7 el .gitignore deixa de tenir-hi efecte per sempre: cada modificació apareixerà a git status. -f no és una excepció puntual, és un canvi d'estat permanent.

Per això gairebé sempre és millor arreglar el patró que forçar:

dist/
!dist/app.min.js     # l'excepció, escrita i visible per a tothom

Així l'excepció queda documentada al repositori en lloc de viure a la memòria de qui un dia va escriure -f.

I un advertiment que enllaça amb la lliçó següent: -f és la via per on es colen els secrets. Algú té pressa, Git es queixa que .env està ignorat, i git add -f .env resol el problema dels propers cinc minuts a canvi de crear-ne un de permanent. Si Git es resisteix a afegir un fitxer, atura't a pensar per què.

  1. Plantilles per llenguatge

No cal escriure un .gitignore des de zero. Hi ha col·leccions mantingudes per la comunitat amb plantilles per llenguatge, framework, sistema operatiu i editor; la més coneguda és el repositori github/gitignore, i la majoria de plataformes ofereixen triar-ne una en crear el repositori.

Recomanacions d'ús:

  1. Parteix de la plantilla del teu llenguatge, però llegeix-la. Conté regles per a eines que potser no fas servir, i de vegades ignora coses que tu sí que vols.
  2. Combina, no concatenis a cegues. Una plantilla de Node més una de Python més tres d'editors produeixen un fitxer de 300 línies que ningú manté.
  3. Agrupa per seccions amb comentaris, com a l'exemple de l'apartat 2. Un .gitignore es llegeix moltes vegades.
  4. Les regles de sistema operatiu i editor, al teu global. Si tot l'equip té el global ben configurat, el .gitignore del projecte queda curt i significatiu.
  5. Revisa'l quan canviïn les eines. Un .gitignore amb regles per a un empaquetador que vau deixar de fer servir fa dos anys és soroll.

Una comprovació útil de tant en tant, per veure quines regles ja no serveixen de res:

# Fitxers ignorats que existeixen realment a la teva còpia de treball
git status --ignored --short | grep '^!!'

Si una regla no apareix mai aquí en cap màquina de l'equip, probablement sobra.

  1. El patró per a secrets: .env i .env.exemple

El problema real: l'aplicació necessita saber la URL de la base de dades, la clau de l'API de correu i un secret de sessió. Aquests valors no poden ser al repositori, però qui cloni el projecte ha de saber quins valors necessita i com es diuen.

La solució convencional són dos fitxers:

.env — amb els valors reals. Ignorat, mai versionat.

BASE_DADES_URL=postgres://gestor:una-clau-de-veritat@localhost:5432/tasques
API_CORREU_CLAU=el-valor-real-que-no-va-al-repositori
SESSIO_SECRET=un-altre-valor-real-generat-a-l-atzar
PORT=3000

.env.exemple — amb les claus i valors de mostra, sense res real. Versionat.

# Copia aquest fitxer a .env i omple els valors reals.
# El .env MAI no es versiona (veure .gitignore).

# Cadena de connexió a PostgreSQL
BASE_DADES_URL=postgres://usuari:contrasenya@localhost:5432/tasques

# Clau del servei d'enviament de correu.
# Demana-la a l'Ana o genera'n una de proves al plafó del servei.
API_CORREU_CLAU=posa-aqui-la-teva-clau

# Secret de sessió. Genera'n un amb: openssl rand -hex 32
SESSIO_SECRET=canvia-aixo-per-un-valor-aleatori

# Port local del servidor de desenvolupament
PORT=3000

I al .gitignore, amb la negació que ja coneixem:

.env
.env.*
!.env.exemple

Per què funciona bé:

  • El Diego clona el seu fork, copia .env.exemple a .env, omple els seus valors i arrenca. La documentació de què cal és al repositori, actualitzada, perquè forma part del codi.
  • Quan algú afegeix una variable nova, la revisió de la PR detecta que falta a .env.exemple. És un bon candidat a comprovació automàtica al CI.
  • Els valors de mostra són evidentment falsos (posa-aqui-la-teva-clau), així que ningú els confon amb reals ni els intenta fer servir.

Els tres errors que arruïnen el patró:

  1. Posar valors reals a .env.exemple "perquè sigui més còmode". Llavors el fitxer versionat és el que té el secret, i no has guanyat res.
  2. Escriure .env* sense la negació. El .env.exemple també queda ignorat, ningú el puja, i el patró sencer deixa d'existir.
  3. Afegir el .gitignore massa tard. Si .env ja es va confirmar alguna vegada, git rm --cached el treu de la punta però no de l'historial (apartat 8). El secret continua sent-hi.

Aquest tercer cas —el secret ja filtrat— té un procediment propi, amb un ordre de passos que importa molt: primer es rota la credencial, després es reescriu l'historial. És el nucli de la lliçó 08-05.

Errors Habituals i Consells

Error 1: creure que .gitignore deixa de seguir un fitxer ja versionat. És el malentès número u. Només actua sobre fitxers sense seguiment; per a la resta, git rm --cached.

Error 2: dades/ quan volies dades/*. Si el directori està ignorat, Git no hi entra i cap negació posterior funciona. Ignora el contingut, no el directori.

Error 3: posar la negació abans del patró. Guanya l'última línia que coincideix. !errors.log seguit de *.log no serveix de res.

Error 4: oblidar la barra inicial. build/ ignora qualsevol build del projecte; /build/ només el de l'arrel. Sol importar més del que sembla.

Error 5: fer servir git add -f com a solució habitual. Converteix el fitxer en seguit per sempre i l'excepció queda sense documentar. Arregla el patró i escriu la negació.

Error 6: .env* sense !.env.exemple. Trenca el patró de secrets sencer.

Error 7: confiar en git rm --cached per eliminar un secret. Només el treu de la punta. L'historial i tots els clons el conserven. Lliçó 08-05.

Error 8: omplir el .gitignore del projecte amb regles personals. .idea/ i *.swp són de la teva màquina; van a core.excludesFile. El .gitignore del projecte hauria de contenir només allò que li passaria a qualsevol.

Consell 1: crea el .gitignore al primer commit. Abans d'instal·lar dependències. És infinitament més barat que netejar-ho després.

Consell 2: git status --ignored de tant en tant. És l'única manera de veure què està amagant Git, i de detectar patrons massa amplis.

Consell 3: git check-ignore -v així que dubtis. Respon en un segon allò que si no es converteix en mitja hora de prova i error.

Consell 4: configura el teu core.excludesFile una vegada a la vida. T'estalvia embrutar tots els repositoris on treballis.

Consell 5: comenta el .gitignore per seccions. I explica els patrons estranys. D'aquí a un any ningú recordarà per què !config/produccio/ajustos.json hi és.

Consell 6: valida .env.exemple al CI. Una comprovació que compari les claus de .env.exemple amb les que el codi llegeix de process.env evita que es desincronitzin.

Exercicis

Exercici 1: el parany de la negació

En un repositori de proves, crea aquesta estructura:

dades/
├── public.csv
├── privat.csv
└── copies/
    └── privat-2026.csv
  1. Escriu un .gitignore amb dades/ i !dades/public.csv. Comprova amb git status i amb git check-ignore -v que public.csv continua ignorat, i explica per què.
  2. Corregeix-ho perquè public.csv es versioni i tota la resta continuï ignorada.
  3. Ara aconsegueix que a més es versioni dades/copies/privat-2026.csv, sense versionar privat.csv.
  4. Verifica cada pas amb git check-ignore -v.

Exercici 2: netejar un repositori ja contaminat

Simula el desastre del Bruno:

  1. Crea un repositori, un app.js, un directori node_modules/ amb tres fitxers, un .env amb una contrasenya fictícia i un .DS_Store.
  2. Confirma-ho tot sense .gitignore (l'error de partida).
  3. Afegeix ara un .gitignore correcte, amb el patró de secrets de l'apartat 12.
  4. Comprova que git status continua mostrant els fitxers que haurien d'estar ignorats i explica per què.
  5. Treu-los del seguiment amb la recepta de l'apartat 8, sense esborrar-los del disc.
  6. Verifica que git status queda net, que els fitxers continuen al teu disc i que git status --ignored els mostra com a ignorats.
  7. Demostra que la contrasenya del .env continua accessible a l'historial i digues quina lliçó ho resol.

Exercici 3: els tres nivells

  1. Configura un core.excludesFile global amb les regles del teu sistema operatiu i el teu editor.
  2. En un repositori de proves, afegeix a .git/info/exclude un fitxer notes-personals.md.
  3. Afegeix al .gitignore del projecte la regla dist/.
  4. Crea els tres tipus de fitxer i comprova amb git check-ignore -v que cadascun s'ignora pel nivell correcte.
  5. Per a cadascun d'aquests fitxers, decideix raonadament a quin nivell hauria d'estar la regla: node_modules/, .idea/workspace.xml, proves-de-la-carla.js, coverage/, .DS_Store, .env.

Solucions

Solució 1:

mkdir -p /tmp/practica-ignore/dades/copies && cd /tmp/practica-ignore
git init -b main
echo "a,b" > dades/public.csv
echo "clau,valor" > dades/privat.csv
echo "hist" > dades/copies/privat-2026.csv
# 1. La versió que NO funciona
cat > .gitignore <<'FI'
dades/
!dades/public.csv
FI

git status --short
?? .gitignore

dades/public.csv no apareix: continua ignorat.

git check-ignore -v dades/public.csv
.gitignore:1:dades/	dades/public.csv

El diagnòstic és explícit: qui decideix és la línia 1, amb el patró dades/. La negació de la línia 2 no s'avalua mai, perquè Git, en ignorar el directori dades/, no hi entra. És una optimització deliberada de rendiment.

# 2. La versió correcta: ignorar el contingut, no el directori
cat > .gitignore <<'FI'
dades/*
!dades/public.csv
FI

git status --short
?? .gitignore
?? dades/public.csv
git check-ignore -v dades/public.csv   # sense sortida, codi 1: NO ignorat
git check-ignore -v dades/privat.csv
.gitignore:1:dades/*	dades/privat.csv
# 3. Rescatar també un fitxer d'un subdirectori
cat > .gitignore <<'FI'
dades/*
!dades/public.csv
!dades/copies/
dades/copies/*
!dades/copies/privat-2026.csv
FI

git status --short
?? .gitignore
?? dades/copies/privat-2026.csv
?? dades/public.csv

La clau del pas 3: cal des-ignorar el directori intermedi (!dades/copies/) perquè Git hi entri, i després tornar a ignorar-ne el contingut (dades/copies/*) per poder rescatar només allò que interessa. Cada nivell de profunditat exigeix el seu propi parell de línies.

Solució 2:

mkdir -p /tmp/practica-neteja/node_modules/marked && cd /tmp/practica-neteja
git init -b main
echo "console.log('gestor-tasques');" > app.js
echo '{"name":"marked"}' > node_modules/marked/package.json
echo "modul" > node_modules/marked/index.js
echo "{}" > node_modules/.package-lock.json
echo "BASE_DADES_URL=postgres://gestor:clau-ficticia@localhost/tasques" > .env
touch .DS_Store
# 2. L'error de partida
git add . && git commit -m "chore: primer commit"
git ls-files
.DS_Store
.env
app.js
node_modules/.package-lock.json
node_modules/marked/index.js
node_modules/marked/package.json
# 3. El .gitignore que hauria d'haver existit des del principi
cat > .gitignore <<'FI'
node_modules/
.env
.env.*
!.env.exemple
.DS_Store
FI

cat > .env.exemple <<'FI'
# Copia a .env i omple els valors reals.
BASE_DADES_URL=postgres://usuari:contrasenya@localhost:5432/tasques
FI
# 4. Encara no serveix de res
echo "canvi" >> node_modules/marked/index.js
git status --short
 M node_modules/marked/index.js
?? .env.exemple
?? .gitignore

Continua apareixent perquè ja té seguiment. .gitignore només actua a la frontera "sense seguiment → preparat", i aquests fitxers la van creuar al pas 2.

# 5. La recepta de neteja
git rm -r --cached . > /dev/null
git add .
git status --short
D  .DS_Store
D  .env
A  .env.exemple
A  .gitignore
D  node_modules/.package-lock.json
D  node_modules/marked/index.js
D  node_modules/marked/package.json

Les D són exactament els fitxers que sobraven. app.js no apareix perquè el seu contingut no ha canviat.

git commit -m "chore: aplica el .gitignore als fitxers ja versionats"
# 6. Verificació
git status --short          # buit
ls -a                       # .env, .DS_Store i node_modules/ CONTINUEN al disc
git status --ignored --short
!! .DS_Store
!! .env
!! node_modules/
# 7. La contrasenya continua a l'historial
git log --all --oneline -- .env
git show HEAD~1:.env
BASE_DADES_URL=postgres://gestor:clau-ficticia@localhost/tasques

Allà és. Qualsevol amb accés al repositori, ara o d'aquí a cinc anys, la pot recuperar amb una sola ordre, i tots els clons existents la contenen. git rm --cached protegeix el futur, no repara el passat. El procediment correcte —rotar primer la credencial i després reescriure l'historial amb git filter-repo— és el contingut de la lliçó 08-05.

Solució 3:

# 1. Global
cat > ~/.gitignore_global <<'FI'
.DS_Store
Thumbs.db
.idea/
*.swp
*~
FI
git config --global core.excludesFile ~/.gitignore_global
# 2 i 3. Local del repositori i del projecte
mkdir /tmp/practica-nivells && cd /tmp/practica-nivells && git init -b main
echo "notes-personals.md" >> .git/info/exclude
echo "dist/" > .gitignore
# 4. Un fitxer de cada tipus
mkdir dist && touch dist/app.min.js notes-personals.md .DS_Store app.js

git check-ignore -v dist/app.min.js notes-personals.md .DS_Store app.js
.gitignore:1:dist/	dist/app.min.js
.git/info/exclude:6:notes-personals.md	notes-personals.md
/home/ana/.gitignore_global:1:.DS_Store	.DS_Store

app.js no apareix: no està ignorat per cap regla. Cadascun dels altres tres s'ignora exactament pel nivell que li correspon.

5. On va cada regla:

Fitxer Nivell correcte Raó
node_modules/ Projecte És del projecte: li apareixerà a tothom qui executi npm install. Sense ell, qualsevol el pot pujar.
.idea/workspace.xml Global És del teu IDE, no del projecte. L'Ana amb Vim i el Bruno amb VS Code no el generen mai. (Molts projectes el dupliquen al .gitignore com a xarxa de seguretat; és una decisió defensiva legítima.)
proves-de-la-carla.js .git/info/exclude És de la Carla i només d'aquest repositori. No té per què aparèixer al fitxer compartit ni justificar-se en una revisió.
coverage/ Projecte El genera l'eina de proves del projecte a la màquina de qualsevol.
.DS_Store Global El genera macOS, no el projecte. Que el Bruno el tingui al seu global protegeix tothom; posar-lo també al projecte és xarxa de seguretat.
.env Projecte, sense cap dubte És la regla de seguretat més important del repositori i no pot dependre que cada persona tingui ben configurat el seu global. Va al .gitignore versionat, sempre.

L'última fila és el criteri de fons: com més gran sigui la conseqüència que la regla falti, més amunt ha d'estar i més versionada.

Conclusió

L'essencial d'aquesta lliçó:

  • Es versiona la font, no el resultat. A fora queden dependències, artefactes de compilació, fitxers del sistema operatiu, configuració personal de l'editor, registres, temporals, binaris grans i —per damunt de tot— secrets. A dins es queden els fitxers de bloqueig de dependències i la configuració compartida del projecte.
  • La sintaxi té tres regles que cal interioritzar: la barra final significa "només directoris", la barra inicial ancora a l'arrel, i qualsevol barra al mig del patró també ancora. Dins d'un fitxer, guanya l'última línia que coincideix.
  • La negació amb ! té un límit dur: no es pot des-ignorar un fitxer dins d'un directori ignorat, perquè Git ni tan sols hi entra. S'ignora el contingut (dades/*), no el directori (dades/).
  • Hi ha tres nivells: el .gitignore del projecte (versionat, per a allò que li passa a qualsevol), .git/info/exclude (local, per a les teves coses en aquell repositori) i core.excludesFile (global, per a allò que genera el teu sistema operatiu o el teu editor). Com més greu sigui la conseqüència que falti una regla, més amunt i més versionada ha d'estar.
  • La regla que confon tothom: .gitignore només afecta els fitxers sense seguiment. Sobre allò ja versionat no té cap efecte. La tornada enrere és git rm --cached, que treu de l'índex però conserva al disc, amb dos advertiments: esborra el fitxer a les altres màquines en fer pull, i no toca l'historial.
  • git check-ignore -v respon en un segon quin fitxer, quina línia i quin patró decideixen. git status --ignored mostra allò que Git t'està amagant.
  • git add -f força, però converteix el fitxer en seguit per sempre. Gairebé sempre és millor escriure l'excepció com a negació, perquè quedi documentada.
  • El patró per a secrets és .env ignorat + .env.exemple versionat amb claus i valors evidentment falsos, més la negació !.env.exemple. És la millor documentació possible de quina configuració necessita el projecte.

gestor-tasques ja sap quins fitxers no han d'entrar. Queda l'altre costat del problema: els fitxers que que entren, però que Git no hauria de tractar com si tots fossin iguals. Un PNG no és un fitxer de text i no té sentit intentar fusionar-lo. Un CHANGELOG.md sí que voldria fusionar-se d'una manera especial. I, sobretot, hi ha el problema que la Carla arrossega des del mòdul 1: cada vegada que edita un fitxer a Windows, el git diff marca totes les línies com a modificades.

Aquest és el terreny de la lliçó 08-04: Atributs de Fitxer amb .gitattributes.

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