Git hooks server-side: enforce policies no repositório remoto
1. Introdução aos Hooks Server-Side
Hooks server-side são scripts executados automaticamente no servidor Git durante eventos de push. Diferentemente dos hooks client-side, que rodam na máquina do desenvolvedor e podem ser ignorados ou contornados, os hooks server-side são obrigatórios e centralizados. Eles representam a última barreira antes que alterações entrem no repositório remoto.
O ciclo de vida de um push segue esta sequência:
1. Desenvolvedor executa git push
2. Cliente envia pacotes de dados para o servidor
3. Servidor executa o hook pre-receive (se existir)
4. Para cada referência atualizada, executa o hook update
5. Se todos os hooks retornam sucesso, as referências são atualizadas
6. Servidor executa o hook post-receive
Os benefícios são claros: regras centralizadas que não podem ser burladas, segurança contra pushes acidentais e consistência em todo o repositório.
2. Tipos de Hooks Server-Side no Git
O Git oferece três hooks server-side principais:
pre-receive: Executado uma única vez antes de qualquer referência ser atualizada. Recebe todas as linhas de atualização via stdin. Ideal para validações que envolvem múltiplas referências simultaneamente.
update: Executado uma vez para cada referência sendo atualizada (branch ou tag). Recebe três argumentos: nome da referência, old-rev e new-rev. Permite validações granulares por branch ou tag.
post-receive: Executado após todas as referências serem aceitas. Não pode rejeitar o push, mas é útil para notificações, deploy automatizado ou integração com CI/CD.
Cada hook tem seu caso de uso ideal:
- pre-receive: políticas globais (ex.: bloquear pushes para branches protegidos)
- update: validações por branch (ex.: formato de mensagem de commit específico para develop)
- post-receive: ações pós-aceitação (ex.: enviar e-mail, acionar webhook)
3. Implementando um Hook pre-receive Básico
O hook pre-receive lê da entrada padrão linhas no formato:
old-rev new-rev refname
Exemplo de script que rejeita pushes para branches protegidos:
#!/bin/bash
# Caminho: /path/to/repo.git/hooks/pre-receive
PROTECTED_BRANCHES=("main" "release" "master")
while read oldrev newrev refname; do
branch=$(echo "$refname" | sed 's/refs\/heads\///')
for protected in "${PROTECTED_BRANCHES[@]}"; do
if [[ "$branch" == "$protected" ]] || [[ "$branch" == "$protected/*" ]]; then
echo "ERROR: Push para '$branch' não permitido."
echo "Crie um pull request para alterar branches protegidos."
exit 1
fi
done
done
exit 0
Mensagens de erro claras ajudam o desenvolvedor a entender o problema imediatamente. Sempre use exit 1 para rejeitar e exit 0 para aceitar.
4. Validando Mensagens de Commit com update
O hook update permite validar commits individuais. Para enforce de formato de mensagem, precisamos listar os commits entre old-rev e new-rev:
#!/bin/bash
# Caminho: /path/to/repo.git/hooks/update
refname="$1"
oldrev="$2"
newrev="$3"
# Apenas para branches
if [[ "$refname" != refs/heads/* ]]; then
exit 0
fi
# Lista commits no intervalo
commits=$(git rev-list "$oldrev".."$newrev" 2>/dev/null)
if [[ -z "$commits" ]]; then
# Possível novo branch
commits=$(git rev-list "$newrev" --not --all 2>/dev/null)
fi
for commit in $commits; do
message=$(git log --format=%B -n 1 "$commit")
# Exige formato [JIRA-123]: descrição
if ! echo "$message" | grep -qE '^\[JIRA-[0-9]+\]:'; then
echo "ERROR: Commit $commit não segue formato [JIRA-XXX]: descrição"
echo "Mensagem recebida: $message"
exit 1
fi
# Rejeita linhas com mais de 72 caracteres
while IFS= read -r line; do
if [[ ${#line} -gt 72 ]]; then
echo "ERROR: Commit $commit tem linha com ${#line} caracteres (máx 72)"
exit 1
fi
done <<< "$message"
done
exit 0
5. Enforce de Tamanho e Conteúdo de Arquivos
Para evitar binários grandes ou arquivos indesejados, use git rev-list e git diff-tree:
#!/bin/bash
# Caminho: /path/to/repo.git/hooks/pre-receive
MAX_SIZE_BYTES=10485760 # 10 MB
FORBIDDEN_EXTENSIONS=(".exe" ".dll" ".env" ".zip" ".rar")
while read oldrev newrev refname; do
# Lista todos os objetos novos ou modificados
objects=$(git rev-list "$oldrev".."$newrev" --objects 2>/dev/null)
if [[ -z "$objects" ]]; then
objects=$(git rev-list "$newrev" --not --all --objects 2>/dev/null)
fi
while read sha1 path; do
[[ -z "$sha1" ]] && continue
# Verifica tamanho
size=$(git cat-file -s "$sha1" 2>/dev/null)
if [[ "$size" -gt "$MAX_SIZE_BYTES" ]]; then
echo "ERROR: Arquivo '$path' tem $size bytes (máx $MAX_SIZE_BYTES)"
exit 1
fi
# Verifica extensões proibidas
for ext in "${FORBIDDEN_EXTENSIONS[@]}"; do
if [[ "$path" == *"$ext" ]]; then
echo "ERROR: Arquivo com extensão '$ext' não permitido: $path"
exit 1
fi
done
done <<< "$objects"
done
exit 0
6. Políticas de Tags e Branches
Validar nomenclatura de branches e tags garante padronização:
#!/bin/bash
# Caminho: /path/to/repo.git/hooks/update
refname="$1"
oldrev="$2"
newrev="$3"
# Valida branches
if [[ "$refname" == refs/heads/* ]]; then
branch=$(echo "$refname" | sed 's/refs\/heads\///')
# Apenas branches feature/*, bugfix/*, hotfix/*, develop, main
if ! echo "$branch" | grep -qE '^(feature|bugfix|hotfix)/[a-z0-9_-]+$|^(develop|main)$'; then
echo "ERROR: Nome de branch inválido: $branch"
echo "Use: feature/xxx, bugfix/xxx, hotfix/xxx, develop ou main"
exit 1
fi
fi
# Valida tags
if [[ "$refname" == refs/tags/* ]]; then
tag=$(echo "$refname" | sed 's/refs\/tags\///')
# Exige tags anotadas (não leves)
if git cat-file -t "$newrev" 2>/dev/null | grep -q "commit"; then
echo "ERROR: Tag leve não permitida. Use 'git tag -a' para tags anotadas."
exit 1
fi
# Valida semver: v1.0.0, v2.3.1-beta, v1.2.3-rc.1
if ! echo "$tag" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$'; then
echo "ERROR: Tag '$tag' não segue semver (ex.: v1.0.0, v2.3.1-beta)"
exit 1
fi
fi
exit 0
7. Boas Práticas e Considerações de Performance
Mantenha hooks idempotentes e rápidos: Evite comandos caros como git log em repositórios com milhares de commits. Use git rev-list com limites quando possível.
Logging e auditoria: Registre pushes rejeitados em arquivo de log para análise posterior:
echo "$(date): Rejeitado push de $(whoami) para $refname" >> /var/log/git-hooks.log
Versionamento dos hooks: Mantenha os hooks em um repositório Git separado e use symlinks no servidor:
ln -s /opt/git-hooks/pre-receive /path/to/repo.git/hooks/pre-receive
Teste localmente: Simule o ambiente do servidor localmente:
# Simular stdin do pre-receive
echo "0000000000000000000000000000000000000000 abc123def456... refs/heads/main" | bash hooks/pre-receive
8. Conclusão e Próximos Passos
Hooks server-side são ferramentas poderosas para manter a qualidade e segurança do repositório. Com eles, você centraliza regras que não podem ser ignoradas, garantindo consistência em toda a equipe. Os ganhos incluem redução de erros humanos, padronização automática e automação de processos pós-push.
Para aprofundar, explore hooks client-side como pre-commit (validações locais antes do commit) e as implementações específicas de plataformas como GitHub (branch protection rules) e GitLab (push rules). A combinação de hooks server-side com CI/CD cria um pipeline robusto de qualidade de código.
Referências
- Git SCM - Customizing Git - Git Hooks — Documentação oficial sobre todos os tipos de hooks, incluindo server-side
- Atlassian - Server-Side Git Hooks — Tutorial completo com exemplos práticos de implementação
- GitHub Docs - About protected branches — Como o GitHub implementa proteção de branches (alternativa aos hooks)
- GitLab Docs - Push rules — Documentação das regras de push do GitLab como alternativa server-side
- DigitalOcean - How To Use Git Hooks — Guia prático com exemplos de automação usando hooks server-side