Che cos'è un file .gitignore?
Un file .gitignore è un file di testo che dice a Git quali file tenere fuori dal controllo di versione. Ogni riga è un pattern — *.log, node_modules/, .env — e ogni percorso non tracciato che corrisponde a un pattern sparisce da git status, viene saltato da git add . e non finisce mai in un commit.
L'espressione non tracciato porta quasi tutto il peso di quella frase e, da sola, spiega più casi di «gitignore non funziona» di tutti gli errori nei pattern messi insieme. Git consulta il .gitignore solo per i file che non sta già tracciando. Dal momento in cui un file è stato committato, Git continua a tracciarlo per sempre, qualunque cosa tu aggiunga dopo al .gitignore.
Cosa fa il Gitignore Debugger
Incolla il percorso di un file e il contenuto del tuo .gitignore: questo debugger di .gitignore dà lo stesso verdetto di Git — ignorato o non ignorato — insieme alla regola esatta e al numero di riga responsabili. Poi elenca tutte le regole che corrispondono al percorso, nell'ordine in cui Git le valuta, e segnala quella che ha davvero deciso.
È quest'ultima parte che un semplice validatore di gitignore lascia fuori. Sapere che un file è ignorato non è quasi mai la parte difficile. Sapere quale delle tue quaranta regole l'ha fatto — e perché ha perso quella che ti aspettavi — sì. Funziona in entrambe le direzioni: perché Git ignora il file che vuoi committare, e perché non ignora quello di cui vuoi liberarti.
Il motore reimplementa le regole di corrispondenza dei pattern di Git: pattern sul nome base contro pattern ancorati, * che non attraversa mai una /, le tre forme di **, i suffissi / validi solo per le directory, le classi di caratteri, gli spazi finali con escape. Il suo comportamento è verificato contro l'output reale di git check-ignore, non dato per scontato.
Come si usa
- Inserisci il percorso così come appare nel tuo repository, relativo alla radice —
src/config/local.json, nonC:\projects\app\src\config\local.json. Usa le barre in avanti; i backslash di Windows vengono convertiti per te. - Incolla tutto il tuo
.gitignore. Non ridurlo alla sola regola che sospetti: di solito il vero bug è la precedenza tra le regole, e tagliare il file butta via proprio quell'informazione. - Leggi il verdetto e poi la catena di regole più sotto. È lì che una regola gitignore che non funziona smette di essere un mistero.
Per provare le regole gitignore su una directory invece che su un file, termina il percorso con una barra — build/. La distinzione conta, perché una regola che finisce con / corrisponde solo a directory.
Perché a volte le regole gitignore non funzionano
I colpevoli, più o meno in ordine di frequenza:
- Il file è già tracciato.
.gitignorenon è retroattivo. Se il file è stato committato prima che la regola esistesse, la regola viene saltata del tutto. Eseguigit rm --cached path/to/filee fai il commit: il file resta su disco e Git smette di tracciarlo. - Una regola successiva l'ha sovrascritta. Git non si ferma al primo pattern che corrisponde.
- Una directory superiore è ignorata. La regola che hai scritto per il file non viene nemmeno raggiunta.
- Il pattern è ancorato e lo credevi libero. Un pattern con una barra in qualsiasi posizione diversa dalla fine è ancorato alla directory che contiene il
.gitignore, quindisrc/*.logcorrisponde asrc/debug.loge a nulla di più profondo. Un pattern senza nessuna barra corrisponde sul nome base a qualsiasi profondità:*.logprende siadebug.logchesrc/debug.logea/b/c/debug.log. - Il pattern è malformato. Una classe di caratteri non chiusa come
[abcnon ripiega su un[letterale. Git abbandona la corrispondenza e la regola, silenziosamente, non corrisponde a nulla — indistinguibile da una regola mai scritta. Nel debugger, semplicemente non compare mai nell'elenco delle regole corrispondenti.
Precedenza delle regole gitignore: vince l'ultima corrispondenza
Questa è quella che frega tutti. All'interno dello stesso .gitignore, è l'ultimo pattern che corrisponde a un percorso a decidere il suo destino, non il primo. Questi due file contengono regole identiche e danno risultati opposti per important.log:
*.log
!important.log → important.log viene committato!important.log
*.log → important.log viene ignoratoQuindi metti prima le regole ampie e poi le eccezioni. Tra file diversi, un .gitignore in una sottodirectory vince su uno più in alto per i percorsi che stanno sotto. Git legge anche .git/info/exclude, proprio del repository e mai committato, e il tuo core.excludesFile globale. Una regola che non trovi in nessun .gitignore spesso si nasconde in uno di quei due posti: git check-ignore -v mostra il file e la riga che hanno deciso.
Le regole di negazione, e perché ! fallisce così spesso
Mettere ! davanti a un pattern reinclude qualcosa che una regola precedente aveva ignorato. Ha due limiti, e il secondo spiega quasi tutti i casi di «la negazione di gitignore non funziona».
Primo: qualcosa prima deve aver ignorato quel percorso. !important.log da solo non fa niente, perché non c'è mai stato nulla da sovrascrivere.
Secondo, e assai più sorprendente: non puoi reincludere un file che sta dentro una directory ignorata. Questo non funziona, e nessun riordino lo sistema:
build/
!build/keep.txtQuando build/ è ignorata, Git non ci entra mai. Non esamina build/keep.txt, quindi non arriva mai alla riga 2. Ignora invece il contenuto della directory, così la directory stessa resta percorribile:
build/*
!build/keep.txtPer salvare qualcosa di più profondo, anche ogni directory intermedia deve restare accessibile — build/*, poi !build/assets/, poi !build/assets/logo.svg.
Directory superiori ignorate
La scansione delle directory di Git si ferma alla prima directory ignorata che incontra, e tutto ciò che sta sotto eredita il verdetto senza essere esaminato. È per questo che una directory superiore ignorata vince su qualsiasi regola scritta per il file stesso, quanto specifica possa sembrare.
In quel caso il debugger nomina la directory che ha bloccato la scansione invece di dire soltanto che il file è ignorato, e mostra comunque le regole di negazione che hai scritto per il file — segnate come irraggiungibili, perché Git non è mai andato abbastanza in profondità da prenderle in considerazione. Sistema prima la directory e le regole più sotto ricominciano a funzionare da sole.
Errori comuni nel .gitignore
- Aggiungere la regola dopo aver committato il file. La regola è corretta e non fa niente. Vedi
git rm --cachedsopra. - Commenti a fine riga.
#apre un commento solo a inizio riga, quindi*.log # debug outputè un unico lungo pattern che non corrisponde a nulla di ciò che hai. - Backslash come separatori. In un
.gitignore,\è un carattere di escape, mai un separatore di percorso.src\configindica il nome letteralesrcconfig. Scrivi sempresrc/config. - Aspettarsi che
*attraversi le directory.*si ferma a una barra, quindisrc/*/test.jsscende di un solo livello. Usasrc/**/test.jsper qualsiasi profondità. - Dimenticare la barra finale.
logscorrisponde sia a un file chiamatologssia alla directory;logs/corrisponde solo alla directory. - Pensare che ignorare cancelli la cronologia. Una regola blocca i commit futuri. Non rimuove un segreto già pushato: cambia la credenziale.
Qualche esempio concreto
Il percorso src/logs/debug.log con queste regole non è ignorato. La negazione è l'ultima corrispondenza e nessuna directory superiore blocca la scansione, quindi viene raggiunta e vince:
*.log
!src/logs/debug.logIl percorso build/keep.txt con queste regole è ignorato, per via di build/ alla riga 1, che corrisponde alla directory superiore. La riga 2 non scatta mai:
build/
!build/keep.txtIl percorso docs/api/notes.md con questa regola non è ignorato, perché il pattern è ancorato dalla sua barra e * non attraverserà quella di api/notes.md. Quello che serviva era docs/**/*.md:
docs/*.mdIncolla uno qualsiasi di questi esempi nel tester gitignore qui sopra per vedere l'intera catena di regole. Tornando al terminale, git check-ignore -v path/to/file è l'equivalente integrato: nomina il file di origine, il numero di riga e il pattern che hanno deciso, e vale la pena conoscerlo.