Risoluzione dei problemi di operazioni Git lente: insidie comuni e soluzioni

Diagnostica i comandi Git lenti separando le cause relative a stato, clone, fetch, push, hook, filesystem, rete e dimensione del repository.

Risoluzione dei problemi di operazioni Git lente: insidie comuni e soluzioni

Git lento ha cause diverse a seconda del comando che è lento. Un git status lento è solitamente dovuto al filesystem locale o al lavoro sull'indice. Un git fetch lento è spesso dovuto alla rete, alla dimensione del remoto o alla negoziazione. Un git checkout lento può essere dovuto al numero di file, alla scansione antivirus, a problemi di sparse checkout o a file generati. Un git push lento può essere dovuto a oggetti grandi, hook, compressione o al server remoto.

Quindi la prima soluzione non è git gc. La prima soluzione è misurare l'operazione esatta.

Su macOS o Linux:

time git status
time git fetch --prune
time git checkout main

Su PowerShell:

Measure-Command { git status }

Esegui il comando due volte. La prima esecuzione potrebbe essere più lenta perché la cache del sistema operativo è fredda. Se il primo git status impiega dieci secondi e il secondo uno, potresti avere a che fare con il comportamento della cache del disco. Se entrambi sono lenti, continua a indagare.

Git ha una tracciabilità integrata che può mostrare dove va il tempo:

GIT_TRACE=1 git status
GIT_TRACE_PERFORMANCE=1 git status
GIT_TRACE_PACKET=1 GIT_TRACE=1 git fetch

GIT_TRACE_PACKET è rumoroso, ma utile quando fetch o push si bloccano durante la negoziazione del protocollo. Non incollare l'output della traccia con URL di repository privati o token in ticket pubblici.

Quando git status è lento

git status controlla l'indice e la working tree. Diventa lento quando il repository ha un numero enorme di file, la working tree si trova su un filesystem lento, i metadati dei file sono costosi da leggere o un altro programma scansiona ogni file che Git tocca.

Inizia con le basi:

git status --short
git config --show-origin --get core.fsmonitor
git config --show-origin --get core.untrackedCache
git config --show-origin --get core.preloadIndex

Per working tree di grandi dimensioni, queste impostazioni possono aiutare su molti sistemi:

git config core.untrackedCache true
git config core.preloadIndex true

Usa prima la configurazione locale in modo da poter testare per repository. Se aiuta, rendila globale in seguito.

Il monitor del filesystem integrato di Git può velocizzare lo status evitando scansioni complete su piattaforme e versioni di Git supportate:

git config core.fsmonitor true

Se lo status diventa errato o strano dopo averlo abilitato, disattivalo e aggiorna Git prima di riprovare:

git config --unset core.fsmonitor

I file non tracciati possono essere un problema nascosto. Output di build, directory di dipendenze, report generati e log locali dovrebbero di solito essere ignorati. Controlla cosa sta scansionando Git:

git status --untracked-files=all --short | head -100

Se vedi node_modules/, dist/, .venv/, target/ o directory generate simili, aggiungi i pattern corretti a .gitignore. Non ignorare i file sorgente solo per velocizzare lo status. Ignora i file che non dovrebbero davvero essere versionati.

Su Windows, la scansione antivirus in tempo reale è una ragione comune per cui Git sembra lento. Git legge molti file piccoli all'interno di .git e della working tree, e il software di sicurezza può ispezionare ogni accesso. Se la tua organizzazione lo consente, escludi gli spazi di lavoro di sviluppo attendibili dalla scansione in tempo reale. Non escludere directory in cui esegui codice non attendibile.

Evita anche di posizionare repository attivi in cartelle di sincronizzazione cloud come OneDrive, Dropbox o iCloud Drive. Gli strumenti di sincronizzazione possono bloccare i file, riscrivere i metadati e competere con le operazioni sui file di Git.

Quando Clone o Fetch è lento

Un clone lento può significare una cronologia ampia, molti blob grandi, un remoto lento o un percorso di rete con alta latenza. Misura la dimensione del repository dopo il clone:

git count-objects -vH
du -sh .git 2>/dev/null

Per lavori CI e ambienti temporanei, usa un clone superficiale quando la cronologia non è necessaria:

git clone --depth 1 <url>

Per una build di un ramo:

git clone --depth 1 --branch main <url>

I cloni superficiali non sono ideali per ogni flusso di lavoro. I comandi che necessitano di cronologia, tag, merge base o calcoli di versione potrebbero fallire o produrre risposte incomplete. In CI, questo è spesso accettabile. Su una macchina sviluppatore, può essere frustrante.

Il clone parziale è utile quando la cronologia del repository è necessaria ma i blob dei file possono essere scaricati in modo lazy:

git clone --filter=blob:none <url>

Funziona meglio con server Git moderni che supportano bene il clone parziale. Testalo con il tuo host prima di renderlo la raccomandazione ufficiale del team.

Se hai bisogno solo di una parte di un monorepo, combina lo sparse checkout con un clone normale o parziale:

git clone --filter=blob:none --sparse <url>
cd repo
git sparse-checkout set services/api shared/lib

Lo sparse checkout riduce la dimensione della working tree. Non rende magicamente ogni operazione Git economica, ma aiuta quando il numero di file è il problema principale.

Per i fetch con molti rami remoti eliminati, pota i riferimenti obsoleti:

git fetch --prune

Per renderlo predefinito:

git config --global fetch.prune true

Quando Push è lento

La velocità del push dipende da quanti nuovi dati oggetto invii, quanto è costoso il packing locale, se gli hook vengono eseguiti e quanto velocemente il remoto accetta il pack.

Controlla se hai accidentalmente committato file di grandi dimensioni:

git rev-list --objects --all | sort -k 2 | tail

Questo comando è grezzo perché non mostra le dimensioni. Per un'ispezione più approfondita, usa strumenti come git-sizer o i comandi di analisi di git filter-repo se disponibili. Il punto pratico è semplice: se un video, un dump del database, un archivio o un artefatto di build è entrato nella cronologia, ogni clone potrebbe pagarne il prezzo fino a quando la cronologia non viene riscritta o il progetto si sposta verso un pattern di archiviazione migliore.

Git LFS è la risposta usuale per asset binari di grandi dimensioni che appartengono al progetto ma non dovrebbero vivere come normali blob Git:

git lfs install
git lfs track "*.psd"
git lfs track "*.mp4"
git add .gitattributes

Git LFS aiuta di più quando viene adottato prima che i file grandi entrino nella cronologia. Migrare la cronologia esistente è possibile, ma riscrive i commit e necessita di coordinazione del team.

Fai attenzione ai vecchi consigli di aumentare http.postBuffer. Viene spesso suggerito per problemi di push, ma raramente risolve la lentezza generale in Git moderno. Se i push falliscono con errori HTTP specifici, controlla l'errore esatto, il proxy, i limiti del server e la versione di Git prima di applicare impostazioni di buffer casuali.

Manutenzione del repository: git gc, Grafici dei commit e Repack

Git memorizza gli oggetti in packfile. Nel tempo, i repository locali possono accumulare oggetti sciolti e pack inefficienti. Git esegue la manutenzione automaticamente in molti flussi di lavoro, ma la manutenzione manuale può ancora aiutare repository più vecchi o molto attivi.

Inizia con un comando di manutenzione sicuro:

git maintenance run

O il comando più vecchio:

git gc

Evita di fare di git gc --prune=now la tua prima mossa casuale. Potare immediatamente rimuove oggetti irraggiungibili che altrimenti potrebbero essere recuperabili per un po'. Può andare bene quando sai cosa stai facendo, ma non è un pulsante di velocità innocuo.

Per repository con cronologie ampie, i grafici dei commit possono migliorare le passeggiate nella cronologia utilizzate da comandi come log, merge-base e negoziazione fetch:

git commit-graph write --reachable

La manutenzione moderna di Git potrebbe gestirlo per te. Controlla la tua versione:

git --version

Mantenere Git aggiornato è una delle correzioni di prestazioni meno drammatiche. Le versioni più recenti migliorano regolarmente lo sparse checkout, il clone parziale, il monitoraggio del filesystem e il comportamento di manutenzione.

Repository grandi e Monorepo

Se un repository è lento perché è genuinamente grande, le modifiche locali hanno un limite. Hai bisogno di cambiamenti nel flusso di lavoro.

Per repository pesanti in binari, sposta gli asset grandi in Git LFS o in un archivio di artefatti. Per i file generati, smetti di committare output che possono essere ricostruiti. Per i monorepo, usa lo sparse checkout e strumenti di build che comprendono i confini del progetto. Per CI, evita cloni a profondità completa a meno che il lavoro non necessiti della cronologia completa.

Una configurazione utile per un monorepo per uno sviluppatore che lavora su un servizio potrebbe essere:

git clone --filter=blob:none --sparse <url>
cd repo
git sparse-checkout set services/billing packages/common

Una configurazione CI utile per un semplice lavoro di test potrebbe essere:

git fetch --depth 50 origin main

La profondità giusta dipende dal lavoro. Se il tuo strumento di versionamento usa tag di mesi fa, una profondità di 1 lo romperà.

Hook e Strumenti esterni

Git potrebbe non essere la parte lenta. Un hook pre-commit può eseguire formattatori, linter, test, scansioni di segreti o controlli di dipendenze. Un hook post-checkout può ricostruire file. Un helper per le credenziali può mettersi in pausa mentre cerca di sbloccare un portachiavi.

Controlla gli hook:

git config --get core.hooksPath
ls -l .git/hooks .githooks 2>/dev/null

Confronta temporaneamente con gli hook disabilitati solo se comprendi il rischio:

git commit --no-verify

Per comandi non di commit, sposta o disabilita l'hook in una copia di test del repository piuttosto che eliminare gli hook del team dal tuo checkout principale.

Se un IDE rende Git lento ma il terminale è veloce, ispeziona le integrazioni Git dell'IDE. Alcuni strumenti eseguono git status ripetutamente, scansionano file non tracciati o aggiornano lo stato del ramo in background.

Controlli di rete e remoti

Per operazioni remote, separa Git dal percorso di rete. Prova:

GIT_TRACE_PERFORMANCE=1 git ls-remote <url>
GIT_TRACE_PERFORMANCE=1 git fetch

Se git ls-remote è lento, il ritardo si verifica prima che molti dati del repository vengano trasferiti. Pensa a DNS, proxy, VPN, autenticazione SSH, disponibilità remota o richieste di credenziali. Se ls-remote è veloce ma fetch è lento, la dimensione dei dati del repository e la negoziazione sono più probabili.

Per remoti SSH, testa SSH direttamente:

ssh -T [email protected]

Usa il tuo host Git effettivo. Per remoti HTTPS, le richieste del gestore di credenziali possono essere nascoste dietro finestre GUI. Un fetch bloccato potrebbe essere in attesa di autenticazione.

Un breve albero decisionale

Se git status è lento, ispeziona file non tracciati, directory generate, antivirus, cartelle di sincronizzazione cloud, monitor del filesystem e impostazioni dell'indice.

Se clone è lento, considera clone superficiale, clone parziale, sparse checkout, Git LFS e se la cronologia del repository contiene blob grandi.

Se fetch è lento, pota i riferimenti obsoleti, aggiorna Git, ispeziona le tracce di rete e controlla se il remoto ha molti rami o tag.

Se push è lento, cerca nuovi oggetti grandi, hook lenti, controlli lato server e problemi di rete o proxy.

Se tutti i comandi Git sono lenti, controlla la salute del disco, lo spazio libero, il software di sicurezza, la versione di Git e se il repository si trova su un mount di rete.

La soluzione migliore è quella che corrisponde al collo di bottiglia misurato. Il lavoro sulle prestazioni di Git diventa disordinato quando ogni suggerimento viene applicato contemporaneamente. Cambia una cosa, misura di nuovo e mantieni la modifica solo se aiuta effettivamente.

Le correzioni a livello di team battono le modifiche personali

Se solo uno sviluppatore ha Git lento, le impostazioni locali e la salute della macchina sono buoni punti di partenza. Se tutti hanno Git lento, il repository necessita di attenzione. Le modifiche personali nasconderanno il dolore per un po', ma i nuovi sviluppatori e i lavori CI continueranno a pagare il costo.

Cerca oggetti grandi che non avrebbero mai dovuto essere committati, directory generate che appartengono a .gitignore e rami vecchi che mantengono viva una cronologia non necessaria. Prima di riscrivere la cronologia, parlane con il team. Le riscritture della cronologia influenzano ogni clone e ogni ramo aperto. Possono valerne la pena, ma necessitano di coordinazione.

Per repository con asset grandi legittimi, definisci una politica invece di fare affidamento sulla memoria. Ad esempio: codice sorgente in Git, esportazioni di design in Git LFS, artefatti di build nel repository di artefatti, dump del database in archiviazione controllata e file di lavoro locali ignorati. Metti queste regole in .gitattributes e .gitignore in modo che Git possa applicare la forma del repository.

CI merita la propria revisione. Molte pipeline clonano la cronologia completa perché era l'impostazione predefinita copiata anni fa. Se il lavoro esegue solo test unitari, potrebbe non aver bisogno di tutti i tag e tutti i rami. Se il lavoro costruisce una release, potrebbe aver bisogno dei tag ma non di ogni blob in un monorepo. Misura il tempo di clone separatamente dal tempo di build in modo che il costo del repository sia visibile.

Un semplice audit CI chiede:

Questo lavoro ha bisogno della cronologia completa?
Ha bisogno dei tag?
Ha bisogno di ogni sottomodulo?
Ha bisogno di ogni directory nel monorepo?
Recupera file LFS che non legge mai?

Rispondere onestamente a queste domande spesso fa risparmiare più tempo che ottimizzare opzioni oscure di Git.

Infine, documenta il comando di clone raccomandato per il progetto. Se i nuovi sviluppatori dovrebbero usare clone parziale e sparse checkout, dillo nel README. Se hanno bisogno di Git LFS prima del checkout, dillo anche quello. Le linee guida sulle prestazioni che vivono solo nella cronologia della shell di uno sviluppatore senior non aiutano la prossima persona.