Automatizza il tuo flusso di lavoro: una guida pratica agli hook lato client di Git
Usa gli hook lato client di Git per controlli locali rapidi, configurazione condivisa, regole per i messaggi di commit e automazione post-merge più sicura.
Automatizza il tuo flusso di lavoro: una guida pratica agli hook lato client di Git
Gli hook lato client di Git sono piccoli script che vengono eseguiti sulla tua macchina quando Git raggiunge determinati punti in un flusso di lavoro. Un hook pre-commit viene eseguito prima che venga creato un commit. Un hook commit-msg viene eseguito dopo che hai scritto il messaggio ma prima che Git lo accetti. Un hook post-merge viene eseguito dopo che un merge è stato completato. Se usati bene, gli hook intercettano precocemente errori noiosi: formattazione dimenticata, file generati rotti, installazioni di dipendenze mancanti o messaggi di commit che non corrispondono alla convenzione del tuo team.
Il limite importante è che gli hook lato client sono locali. Non viaggiano automaticamente con il repository quando qualcuno lo clona. Questo li rende ottimi per feedback rapidi e comodità locale, ma deboli come unico livello di applicazione per una regola di team. Se un controllo protegge veramente il ramo principale, inseriscilo anche in CI o in una regola lato server.
Ogni repository ha una directory hooks sotto .git/hooks:
ls .git/hooks
Un nuovo repository di solito contiene file di esempio come pre-commit.sample. Un hook di esempio non fa nulla finché non crei un file eseguibile senza il suffisso .sample:
cp .git/hooks/pre-commit.sample .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit
Gli hook possono essere script shell, script Python, script Ruby, script Node o qualsiasi altra cosa la tua macchina possa eseguire. La prima riga dovrebbe puntare all'interprete:
#!/usr/bin/env bash
Per la maggior parte dei team, il modello migliore a lungo termine non è modificare manualmente .git/hooks su ogni laptop. Memorizza gli script hook nel repository, quindi configura Git per usare quella directory:
git config core.hooksPath .githooks
mkdir -p .githooks
Ora un hook in .githooks/pre-commit può essere committato e revisionato come normale codice del progetto. Ogni sviluppatore ha ancora bisogno dell'impostazione core.hooksPath, ma la configurazione può essere aggiunta a uno script di bootstrap o documentata nell'onboarding.
Un Hook Pre-Commit Utile
Un buon hook pre-commit dovrebbe essere veloce e mirato. Se impiega due minuti per ogni commit, le persone lo bypasseranno con git commit --no-verify e l'hook diventerà rumore. Riserva le suite di test complete per CI a meno che il progetto non sia abbastanza piccolo da essere davvero veloci.
Ecco un hook shell pratico che controlla solo i file staged. Questa distinzione è importante. Potresti avere lavoro non finito nel tuo albero di lavoro che non vuoi ancora testare. Il commit dovrebbe essere giudicato da ciò che è staged.
Crea .githooks/pre-commit:
#!/usr/bin/env bash
set -u
changed_files=$(git diff --cached --name-only --diff-filter=ACMR)
if [ -z "$changed_files" ]; then
exit 0
fi
if git diff --cached --check; then
:
else
echo "Correggi gli errori di spaziatura prima di committare."
exit 1
fi
secret_matches=$(git diff --cached --name-only --diff-filter=ACMR | xargs grep -nE 'AKIA[0-9A-Z]{16}|BEGIN RSA PRIVATE KEY' 2>/dev/null || true)
if [ -n "$secret_matches" ]; then
echo "Possibile segreto trovato nei file staged:"
echo "$secret_matches"
exit 1
fi
python_files=$(printf '%s\n' "$changed_files" | grep '\.py$' || true)
if [ -n "$python_files" ]; then
printf '%s\n' "$python_files" | while IFS= read -r file; do
[ -f "$file" ] || continue
python3 -m py_compile "$file" || exit 1
done
fi
exit 0
Questo hook fa tre cose modeste: lascia che Git rilevi errori di spaziatura, controlla i file staged per un paio di pattern di segreti ovvi e compila i file Python modificati. Non è un sostituto per un vero scanner di segreti o una suite di test. È un rapido allarme.
Un errore comune è usare grep sui nomi dei file invece del contenuto. Questo pattern rotto controlla solo se il percorso contiene TODO, non se il file lo contiene:
git diff --cached --name-only | grep TODO
Se vuoi bloccare i commenti TODO, ispeziona il diff staged invece:
if git diff --cached -U0 | grep -E '^\+.*TODO:'; then
echo "Commenti TODO staged trovati."
exit 1
fi
Anche in questo caso, fai attenzione. Alcuni team usano i commenti TODO in modo responsabile. Bloccare ogni TODO può essere più fastidioso che utile.
Hook per i Messaggi di Commit
Un hook commit-msg riceve il percorso del file del messaggio di commit temporaneo come primo argomento. Questo lo rende utile per regole come "ogni commit deve iniziare con un ID ticket" o "usa i Conventional Commits."
Un piccolo esempio:
#!/usr/bin/env bash
set -u
message_file="$1"
first_line=$(head -n 1 "$message_file")
if printf '%s' "$first_line" | grep -Eq '^(feat|fix|docs|test|refactor|chore)(\(.+\))?: .+'; then
exit 0
fi
echo "Il messaggio di commit dovrebbe essere tipo: fix(api): handle empty token"
exit 1
Questo è utile quando le note di rilascio o i changelog sono generati dai commit. È meno utile quando il tuo team fa squash merge e riscrive comunque i titoli delle PR. Abbina l'hook al flusso di lavoro che usi effettivamente.
Hook Post-Merge
Un hook post-merge è ottimo per la pulizia locale dopo che il tuo albero di lavoro cambia. L'esempio classico è aggiornare le dipendenze dopo che un lockfile cambia.
#!/usr/bin/env bash
set -u
previous_head="HEAD@{1}"
if git diff --name-only "$previous_head" HEAD | grep -Eq '(^package-lock\.json$|^pnpm-lock\.yaml$|^yarn\.lock$)'
; then
if command -v npm >/dev/null 2>&1 && [ -f package-lock.json ]; then
echo "Lockfile cambiato; eseguo npm install."
npm install
fi
fi
if git diff --name-only "$previous_head" HEAD | grep -q '^\.gitmodules$'; then
echo "Configurazione sottomoduli cambiata; sincronizzo i sottomoduli."
git submodule sync --recursive
git submodule update --init --recursive
fi
Questo hook non dovrebbe fare modifiche sorprendenti. Se installa dipendenze, stampa cosa sta facendo. Se l'installazione fallisce, dì allo sviluppatore come recuperare. Un hook che modifica silenziosamente l'albero di lavoro è difficile da fidarsi.
Condividere gli Hook Senza Creare Caos
Ci sono tre modi comuni per condividere gli hook.
Il più semplice è core.hooksPath, dove il repository contiene .githooks/ e la configurazione imposta Git per usarlo. Questo è trasparente e non richiede un altro gestore di pacchetti.
I progetti JavaScript usano spesso Husky perché si integra con i flussi di installazione di npm, pnpm o yarn. Può essere una buona scelta quando ogni contributore usa già la toolchain Node.
Molti team con linguaggi misti usano il framework pre-commit. Installa ed esegue hook definiti in .pre-commit-config.yaml, con versioni bloccate per strumenti come formattatori, linter e controlli sui file. Aggiunge un altro strumento, ma risolve il problema "come installiamo gli stessi hook ovunque?" meglio di una pagina wiki.
Quello che evito è copiare grandi script in .git/hooks a mano. Nessuno li revisiona, nessuno sa quale versione è installata e il debug diventa archeologia personale.
Debug degli Hook
Quando un hook non viene eseguito, controlla questi in ordine:
git config --get core.hooksPath
ls -l .git/hooks .githooks 2>/dev/null
Se core.hooksPath è impostato, Git ignora .git/hooks e usa la directory configurata. Se il file hook non è eseguibile su macOS o Linux, Git non lo eseguirà:
chmod +x .githooks/pre-commit
Quando un hook viene eseguito ma fallisce misteriosamente, aggiungi tracciamento temporaneo:
set -x
pwd
env | sort
Gli hook vengono eseguiti dalla radice del repository nell'uso normale di Git, ma i client GUI e gli IDE possono esporre differenze di percorso o ambiente. Usa command -v toolname all'interno dell'hook prima di assumere che un linter o un gestore di pacchetti sia disponibile.
Ricorda anche l'interruttore di bypass:
git commit --no-verify
Questo non è di per sé un buco di sicurezza; è così che funziona Git. È un altro motivo per cui l'applicazione seria appartiene a CI o alle regole di ramo protetto.
Una Politica Sensata per gli Hook
Usa gli hook per controlli che sono veloci, deterministici e facili da spiegare. Formattare i file staged, intercettare errori di spaziatura, validare i messaggi di commit e ricordare agli sviluppatori di installare le dipendenze sono buoni candidati. Evita hook che richiedono accesso alla rete, impiegano molto tempo o dipendono da uno stato locale fragile.
Se un hook blocca un commit, il suo messaggio dovrebbe dire esattamente cosa è fallito e come risolverlo. "Hook fallito" non è sufficiente. Uno sviluppatore nel bel mezzo di un merge o di un hotfix di produzione ha bisogno di un comando chiaro per proseguire.
Gli hook lato client di Git funzionano meglio quando sembrano un guardrail utile piuttosto che una burocrazia locale. Mantienili piccoli, mantienili versionati e lascia l'autorità finale in CI.
Mantieni gli Hook Amichevoli Durante le Emergenze
Gli hook dovrebbero aiutare durante il lavoro normale senza intrappolare qualcuno durante una correzione urgente. Ciò significa che ogni hook bloccante ha bisogno di un messaggio di fallimento chiaro e di una via di fuga realistica. Git fornisce già --no-verify per gli hook di commit e push, ma il tuo team dovrebbe comunque decidere quando il bypass è accettabile. Un hotfix di produzione è diverso dal saltare la formattazione perché uno sviluppatore ha fretta.
Un buon messaggio di hook dice cosa è fallito, dove è fallito e cosa eseguire dopo:
echo "ESLint fallito sui file JavaScript staged."
echo "Esegui: npm run lint -- --fix"
exit 1
Un messaggio cattivo dice solo fallito o scarica pagine di output dello strumento senza contesto. Le persone imparano a ignorare quel tipo di hook.
Se l'hook modifica i file, fai ancora più attenzione. I formattatori possono essere utili in pre-commit, ma possono anche creare confusione quando modificano parti non staged di un file. Molti team preferiscono controllare la formattazione nell'hook e lasciare che lo sviluppatore esegua il formattatore manualmente. Altri usano strumenti che formattano solo le porzioni staged. Scegli un comportamento e documentalo nel repository, non in un thread di chat che scompare.
Per i team, revisiona le modifiche agli hook come il codice dell'applicazione. Un hook può rallentare ogni commit, far trapelare dettagli dell'ambiente nei log o rompere i contributori su Windows se assume un comportamento solo Bash. Se il tuo progetto ha contributori Windows, testa gli hook in Git Bash o usa un esecutore di hook cross-platform. Se il tuo progetto ha contenitori o shell di sviluppo, considera di eseguire gli hook nello stesso ambiente dell'app in modo che tutti usino le stesse versioni degli strumenti.
I migliori hook sono quasi invisibili quando tutto va bene e molto specifici quando qualcosa va storto. Questo è lo standard a cui puntare.
Versiona gli Hook Come il Codice del Prodotto
Uno script hook diventa parte dell'esperienza dello sviluppatore. Se si rompe, ogni contributore lo sente. Mantieni gli script piccoli, dai nomi chiari alle funzioni helper ed evita trucchi shell ingegnosi quando un comando semplice basterebbe. Se un hook cresce oltre uno schermo o due, sposta la logica reale in uno script di progetto testato e lascia che l'hook chiami quello script.
Ad esempio, invece di incorporare una lunga routine di lint in .githooks/pre-commit, chiama:
./scripts/check-staged-files.sh
Quello script può essere eseguito da sviluppatori, hook e CI. Significa anche che uno sviluppatore può riprodurre il fallimento senza fingere di fare un commit. La riproducibilità è la differenza tra un hook utile e un misterioso ostacolo locale.
Blocca le versioni degli strumenti dove puoi. Un hook che chiama qualunque black, eslint o prettier sia per primo in PATH può comportarsi diversamente su macchine diverse. Dipendenze locali al progetto, lockfile, contenitori o gestori di versioni rendono l'output dell'hook più prevedibile.
Infine, mantieni gli hook limitati al repository. Gli hook globali sembrano convenienti, ma spesso ti sorprendono mesi dopo quando un repository non correlato inizia a fallire a causa di una vecchia regola personale. Usa gli hook globali solo per preferenze veramente personali, non per politiche di team.
Un'ultima regola pratica: non lasciare mai che gli hook siano l'unico posto in cui esiste un comando. Se l'hook controlla i file Python staged, mantieni quel comando anche in uno script o in un task runner. Gli sviluppatori dovrebbero essere in grado di eseguire lo stesso controllo intenzionalmente, prima che Git li interrompa.
Per i progetti open-source, supponi che i contributori potrebbero non avere ancora la tua toolchain completa. Un hook che fallisce con un messaggio di configurazione amichevole va bene. Un hook che lancia uno stack trace da un binario locale mancante sembra rotto. Controlla i prerequisiti prima di eseguire comandi più pesanti e indirizza le persone al comando di configurazione usato dal progetto.
Pensa anche ai commit parziali. Molti sviluppatori esperti mettono in stage solo una parte di un file. Gli hook che formattano l'intero file possono accidentalmente portare lavoro non staged nel commit. Se il tuo team usa spesso commit parziali, preferisci controlli che leggono il diff staged o strumenti progettati per contenuti staged.
Se un hook viene continuamente bypassato, trattalo come feedback. O il controllo è troppo lento, il messaggio di fallimento non è chiaro o la regola appartiene a CI invece del percorso di commit locale. Risolvi l'attrito piuttosto che incolpare gli sviluppatori per aver usato il bypass che Git fornisce.