Hooks do Git: o que correr antes de commitar, e o que não
Um hook do Git é um executável que o Git corre num momento da vida dele: antes de gravar um commit, depois de mudar de ramo, antes de enviar para o servidor. Vivem em .git/hooks — e é aí que começa o problema, porque a pasta .git não vai no repositório. Um hook escrito por si fica só na sua máquina.
1. Como é que um hook passa a ser de toda a gente
Desde a versão 2.9 o Git aceita que lhe indiquem outra pasta, e é nisso que assenta o husky:
git config core.hooksPath
# .husky/_É o que a linha "prepare": "husky" do package.json faz: quando alguém corre npm install, o husky aponta o Git para uma pasta que está versionada. A partir daí os hooks viajam com o repositório, como qualquer outro ficheiro.
2. Os três que interessam
pre-commit— antes de o commit ser criado. É onde quase toda a gente põe o lint.commit-msg— recebe o caminho do ficheiro com a mensagem, e serve para a validar.pre-push— antes de enviar. O último sítio barato para correr alguma coisa demorada.
3. Este site tem um hook, e é só este
# .husky/commit-msg
npx --no -- commitlint --edit "$1"O $1 é o ficheiro onde o Git acabou de pôr a mensagem. O commitlint lê-o e recusa o commit se não seguir os Conventional Commits.
Não é preciosismo de forma. O número da versão deste site é calculado a partir das mensagens: um feat: sobe a versão menor, um fix: a de correção, um BREAKING CHANGE a maior. Uma mensagem fora do formato dá uma versão errada — ou versão nenhuma.
4. O que não está lá, e porquê
Quase todos os guias mandam pôr o lint e os testes no pre-commit. Aqui não estão, e a ausência é uma decisão.
Um hook que demora quarenta segundos é um hook que vai ser contornado. O git commit --no-verify existe, toda a gente o descobre ao terceiro dia mau, e a partir daí ele deixa de proteger seja o que for — fica só a irritar quem trabalha.
O sítio onde o lint e os testes têm mesmo de correr é aquele que ninguém pode saltar: a integração contínua. Neste repositório, o workflow corre npm run lint, npm run typecheck e npm test a cada envio, e outra vez antes de construir a imagem — de propósito, porque uma etiqueta de versão pode apontar para um commit que nunca passou pelo ramo principal.
A regra que daí sai é simples: o hook é para o que é rápido e diz respeito ao commit; o CI é para o que é lento e diz respeito ao código.
5. Quando o pre-commit vale mesmo a pena
Vale quando é instantâneo e só toca no que está em staging. É exatamente para isso que existe o lint-staged:
{
"lint-staged": {
"*.{ts,tsx}": "eslint --fix",
"*.{json,css,md}": "prettier --write"
}
}# .husky/pre-commit
npx lint-stagedA diferença está no alcance. Correr o ESLint no projeto inteiro demora; correr nos três ficheiros que vão neste commit é imediato. Formatar, sim. Correr a bateria de testes, não.
6. Três coisas que apanham toda a gente
Os hooks não correm no CI. Nem no servidor. São uma conveniência para quem escreve, e nunca uma garantia para o projeto. Se a regra tem mesmo de ser cumprida, tem de estar nos dois sítios.
Quem clona tem de correr
npm install. Sem isso opreparenão corre, ocore.hooksPathnão é apontado, e os hooks não existem naquela cópia — sem aviso nenhum. O commit passa, e a mensagem sai do formato.O husky 9 já não quer o cabeçalho antigo. As duas linhas que toda a gente copiou dos guias — o
#!/usr/bin/env she o. "$(dirname -- "$0")/_/husky.sh"— estão obsoletas e vão falhar na versão 10. Num husky 9, o hook é só o comando.
O resumo
Antes de acrescentar um hook, duas perguntas. Isto corre em menos de um segundo? Se não, vai ser contornado. Isto é sobre o commit — a mensagem, o formato, o que está em staging — ou é sobre o código estar correto? Se for a segunda, o lugar é o CI.