Hooks do Git: automatizando ações locais

1. Introdução aos Hooks do Git

Hooks do Git são scripts personalizados que o Git executa automaticamente quando determinados eventos ocorrem no repositório. Eles permitem automatizar ações locais como validações, notificações, formatação de código e muito mais, sem depender de ferramentas externas.

O ciclo de vida de um hook é simples: ao realizar uma operação como git commit ou git push, o Git verifica se existe um script correspondente no diretório de hooks. Se existir e for executável, ele o executa antes (ou depois) da operação principal. Dependendo do código de saída do script, o Git pode permitir ou bloquear a operação.

É importante distinguir entre hooks locais (lado do cliente) e hooks remotos (lado do servidor). Os hooks locais residem em cada repositório do desenvolvedor e não são enviados para o servidor. Já os hooks remotos são configurados no servidor Git e afetam todos que enviam alterações. Este artigo foca exclusivamente nos hooks locais.

2. Anatomia de um Hook no Git

Cada repositório Git contém um diretório oculto chamado .git/hooks/. Dentro dele, existem arquivos de exemplo com extensão .sample que demonstram a estrutura básica. Para ativar um hook, basta criar um arquivo sem extensão com o nome do evento desejado e torná-lo executável.

A nomenclatura segue o padrão do evento: pre-commit, post-commit, pre-push, commit-msg, prepare-commit-msg, pre-rebase, post-checkout, post-merge, entre outros. Qualquer linguagem de script pode ser usada — shell script, Python, Ruby, Node.js — desde que o arquivo seja executável e tenha um shebang apropriado.

.git/
└── hooks/
    ├── pre-commit.sample
    ├── pre-commit
    ├── commit-msg
    ├── post-commit
    └── pre-push

3. Hooks do Lado do Cliente: Principais Eventos

pre-commit

Executado antes de criar o commit. Ideal para validações como lint, verificação de código, testes unitários rápidos ou prevenção de espaços em branco à direita. Se o script retornar código diferente de zero, o commit é abortado.

prepare-commit-msg

Executado após o editor de mensagens de commit ser aberto. Permite modificar automaticamente a mensagem padrão, como adicionar um template ou número de issue.

commit-msg

Executado depois que o usuário digita a mensagem de commit, mas antes do commit ser finalizado. Excelente para validar o formato da mensagem (convenções como Conventional Commits).

post-commit

Executado após o commit ser criado. Não pode abortar a operação, mas é útil para notificações, logs ou acionar CI local.

pre-push

Executado antes de enviar commits para o remoto. Ideal para rodar testes de integração ou verificar se o branch está atualizado com o remoto.

4. Hooks do Lado do Cliente: Eventos Avançados

pre-rebase

Executado antes de um git rebase. Permite verificar se há alterações não commitadas ou se o branch está em condições seguras para rebase.

post-checkout

Executado após git checkout ou git switch. Útil para configurar o ambiente: instalar dependências, recarregar configurações ou limpar arquivos temporários.

post-merge

Executado após um git merge bem-sucedido. Pode ser usado para atualizar automaticamente dependências com npm install ou composer install.

pre-auto-gc

Executado antes da coleta de lixo automática do Git. Permite cancelar a operação se o desenvolvedor estiver no meio de uma tarefa que não deve ser interrompida.

5. Criando e Gerenciando Hooks Manualmente

Vamos criar um hook pre-commit que impede commits com espaços em branco à direita.

Passo 1: Acesse o diretório de hooks do repositório:

cd .git/hooks

Passo 2: Crie o arquivo pre-commit com o seguinte conteúdo:

#!/bin/sh

# Hook pre-commit: rejeita commits com espaços em branco à direita
if git diff --cached --check | grep -q "trailing whitespace"; then
    echo "Erro: Existem espaços em branco à direita nos arquivos staged."
    echo "Execute 'git diff --cached --check' para ver os detalhes."
    exit 1
fi

Passo 3: Torne o script executável:

chmod +x pre-commit

Agora, ao tentar commitar com espaços em branco à direita, o Git exibirá a mensagem de erro e abortará a operação.

A principal limitação dessa abordagem manual é que os hooks não são versionados nem compartilhados automaticamente com a equipe. Cada desenvolvedor precisa configurar manualmente.

6. Compartilhando Hooks com a Equipe

Para versionar e compartilhar hooks, use git config core.hooksPath para apontar para um diretório dentro do repositório.

Estrutura de projeto:

meu-repositorio/
├── githooks/
│   ├── pre-commit
│   └── commit-msg
├── src/
└── README.md

Configure o caminho dos hooks (cada desenvolvedor precisa executar uma vez):

git config core.hooksPath githooks

Exemplo prático: hook commit-msg que valida mensagens no formato Conventional Commits usando Commitlint.

#!/usr/bin/env node
// githooks/commit-msg

const fs = require('fs');
const commitMsg = fs.readFileSync(process.argv[2], 'utf8').trim();

const pattern = /^(feat|fix|docs|style|refactor|test|chore)(\(.+\))?:\s.+$/;

if (!pattern.test(commitMsg)) {
    console.error('Erro: Mensagem de commit não segue o padrão Conventional Commits.');
    console.error('Exemplo: feat(api): adiciona novo endpoint de login');
    process.exit(1);
}

Essa abordagem permite versionar os hooks e compartilhá-los via git push. A desvantagem é que cada desenvolvedor precisa configurar core.hooksPath manualmente.

7. Boas Práticas e Cuidados com Hooks Locais

Performance: hooks não devem executar processos pesados como testes completos ou builds. Eles devem ser rápidos (menos de 1 segundo) para não frustrar o desenvolvedor. Testes longos devem ficar no CI.

Saída amigável: mensagens de erro claras e específicas ajudam o desenvolvedor a corrigir rapidamente. Use cores ou formatação para destacar erros.

Ignorando hooks temporariamente: use --no-verify no commit ou -n para pular hooks em situações emergenciais. Exemplo:

git commit -m "fix: correção urgente" --no-verify

Evitando efeitos colaterais: hooks não devem modificar arquivos sem aviso prévio. Se um hook alterar arquivos, o desenvolvedor pode perder alterações não commitadas.

8. Depuração e Testes de Hooks

Para testar um hook manualmente, execute o script diretamente no terminal, passando os argumentos esperados. Por exemplo, para testar commit-msg:

echo "minha mensagem" > /tmp/msg.txt
./.git/hooks/commit-msg /tmp/msg.txt

Para depuração, redirecione a saída para um arquivo de log:

#!/bin/sh
exec >> /tmp/hook-debug.log 2>&1
echo "=== pre-commit executado em $(date) ==="

Use variáveis de ambiente para simular diferentes eventos. Por exemplo, GIT_EDITOR pode ser usado para testar prepare-commit-msg.

Exemplo de hook que falha intencionalmente para diagnóstico:

#!/bin/sh
echo "Hook pre-commit falhou intencionalmente para teste"
exit 1

Para diagnosticar, execute o script manualmente e verifique a saída. Se o Git rejeitar a operação, a mensagem de erro do hook aparecerá no terminal.

Referências