Pre-receive hooks: validando pushes no lado do servidor
1. Introdução aos hooks server-side no Git
Hooks do Git são scripts executados automaticamente em pontos específicos do ciclo de vida de um repositório. Eles permitem que desenvolvedores e administradores implementem lógica personalizada para validar, modificar ou reagir a operações do Git. Diferentemente dos hooks client-side (como pre-commit e commit-msg), que rodam na máquina de cada desenvolvedor e podem ser ignorados ou desabilitados, os hooks server-side executam no repositório central, garantindo que as regras sejam aplicadas a todos os pushes.
Os principais hooks server-side são:
- pre-receive: executado antes de qualquer referência ser atualizada, recebendo todas as referências do push via stdin
- update: executado para cada referência individualmente, permitindo validação por branch
- post-receive: executado após todas as referências serem atualizadas, ideal para notificações e builds
O hook pre-receive é particularmente poderoso porque permite rejeitar um push inteiro antes que qualquer alteração seja registrada no repositório, funcionando como uma barreira centralizada de validação.
2. Anatomia de um pre-receive hook
O script pre-receive deve estar localizado em hooks/pre-receive dentro do diretório do repositório bare no servidor. Ele recebe via stdin linhas no formato:
<old-value> <new-value> <ref-name>
Onde:
- old-value é o SHA-1 do commit atual da referência (40 zeros se a referência será criada)
- new-value é o SHA-1 do novo commit (40 zeros se a referência será deletada)
- ref-name é o nome completo da referência (ex: refs/heads/main)
O código de saída determina o resultado:
- 0 (zero): aceita o push
- Qualquer valor não-zero: rejeita o push, e a saída padrão do script é exibida ao usuário
Exemplo mínimo em shell:
#!/bin/sh
while read oldrev newrev refname
do
echo "Push rejeitado: operações manuais não permitidas neste servidor."
exit 1
done
exit 0
3. Validando mensagens de commit e autorias
Para acessar os commits de um push, usamos git rev-list para listar os commits entre oldrev e newrev, e git cat-file ou git log para inspecionar cada commit.
Exemplo de script que exige prefixo obrigatório na mensagem de commit:
#!/bin/sh
while read oldrev newrev refname
do
commits=$(git rev-list $oldrev..$newrev 2>/dev/null)
for commit in $commits
do
message=$(git cat-file -p $commit | sed -n '4,/^$/p' | head -n -1)
if ! echo "$message" | grep -qE '^(feat|fix|chore|docs|refactor):'
then
echo "ERRO: Commit $commit não segue o padrão de mensagem."
echo "Use: feat:, fix:, chore:, docs: ou refactor:"
exit 1
fi
done
done
exit 0
Para verificar autor contra domínios permitidos:
#!/bin/sh
DOMINIOS_PERMITIDOS="empresa.com"
while read oldrev newrev refname
do
commits=$(git rev-list $oldrev..$newrev 2>/dev/null)
for commit in $commits
do
email=$(git log --format='%ae' -1 $commit)
dominio=$(echo $email | cut -d'@' -f2)
if ! echo "$DOMINIOS_PERMITIDOS" | grep -q "$dominio"
then
echo "ERRO: Commit $commit de $email não autorizado."
exit 1
fi
done
done
exit 0
4. Controle de branches e permissões por usuário
Para identificar o usuário que está fazendo o push, pode-se usar variáveis de ambiente como GL_USER (GitLab) ou extrair do SSH. O script pode então aplicar regras específicas por branch.
Exemplo que protege a branch main e permite pushes para release/* apenas para QA:
#!/bin/sh
USUARIO=${GL_USER:-$(whoami)}
while read oldrev newrev refname
do
case "$refname" in
refs/heads/main)
echo "ERRO: Push para main bloqueado. Use Pull Request."
exit 1
;;
refs/heads/release/*)
if [ "$USUARIO" != "qa-team" ] && [ "$USUARIO" != "jenkins" ]
then
echo "ERRO: Apenas QA pode fazer push para release/*"
exit 1
fi
# Verifica se é fast-forward
if ! git merge-base --is-ancestor $oldrev $newrev
then
echo "ERRO: Apenas fast-forward permitido em release/*"
exit 1
fi
;;
esac
done
exit 0
5. Validação de conteúdo e tamanho dos pushes
Para verificar arquivos específicos ou tamanho total, podemos inspecionar as árvores dos commits e usar git ls-tree e git cat-file para analisar objetos.
Exemplo que rejeita arquivos maiores que 10MB e extensões .exe:
#!/bin/sh
TAMANHO_MAXIMO=$((10 * 1024 * 1024))
EXTENSOES_BLOQUEADAS="\.exe$|\.dll$|\.pdb$"
while read oldrev newrev refname
do
commits=$(git rev-list $oldrev..$newrev 2>/dev/null)
for commit in $commits
do
tree=$(git rev-parse $commit^{tree})
git ls-tree -r -l $tree | while read mode type obj size path
do
if [ "$type" = "blob" ] && [ "$size" -gt "$TAMANHO_MAXIMO" ]
then
echo "ERRO: Arquivo $path tem $size bytes (limite: $TAMANHO_MAXIMO)"
exit 1
fi
if echo "$path" | grep -qE "$EXTENSOES_BLOQUEADAS"
then
echo "ERRO: Arquivo $path não é permitido."
exit 1
fi
done
done
done
exit 0
Para bloquear secrets, adicione verificações com regex para padrões como chaves AWS (AKIA[0-9A-Z]{16}) ou senhas.
6. Boas práticas e considerações de segurança
Para hooks complexos, considere usar linguagens mais robustas como Python ou Ruby:
#!/usr/bin/env python3
import sys
import subprocess
def get_commits(oldrev, newrev):
result = subprocess.run(
['git', 'rev-list', f'{oldrev}..{newrev}'],
capture_output=True, text=True
)
return result.stdout.strip().split('\n') if result.stdout else []
for line in sys.stdin:
oldrev, newrev, refname = line.strip().split()
commits = get_commits(oldrev, newrev)
# Lógica de validação aqui
sys.exit(0)
Práticas recomendadas:
- Logging: registre todas as rejeições em /var/log/git-hooks.log para auditoria
- Performance: evite git log com formatação complexa para pushes com centenas de commits
- Testes: crie um repositório de staging e simule pushes com git push --dry-run ou scripts de teste
- Atomicidade: ao atualizar hooks, copie o novo script para um arquivo temporário e depois renomeie
7. Integração com ferramentas de code review e CI/CD
Hooks podem consultar APIs externas para validar se um push pode ser aceito. Exemplo que verifica status de CI via API (GitLab):
#!/bin/sh
API_URL="https://gitlab.com/api/v4/projects/123/statuses"
TOKEN="seu-token"
while read oldrev newrev refname
do
# Pega o último commit do push
last_commit=$(git rev-list $oldrev..$newrev | tail -1)
status=$(curl -s -H "PRIVATE-TOKEN: $TOKEN" \
"$API_URL?ref=$refname&sha=$last_commit" | \
python3 -c "import sys,json; print(json.load(sys.stdin)[0]['status'])")
if [ "$status" != "success" ]
then
echo "ERRO: CI não passou para o commit $last_commit (status: $status)"
exit 1
fi
done
exit 0
Para integrar com Pull Requests, use a API do GitHub/GitLab para verificar se o branch tem aprovações pendentes.
8. Depuração e manutenção de hooks em produção
Para testar hooks sem impacto:
- Crie um repositório bare de staging:
git init --bare /tmp/test-hooks.git - Copie o hook para lá:
cp hooks/pre-receive /tmp/test-hooks.git/hooks/ - Simule um push local:
git push /tmp/test-hooks.git main
Para logging em produção:
#!/bin/sh
LOG_FILE="/var/log/git-hooks.log"
exec 2>>"$LOG_FILE"
while read oldrev newrev refname
do
user=${GL_USER:-$(whoami)}
echo "$(date): $user tentou push em $refname" >> "$LOG_FILE"
# validação...
if [ $? -ne 0 ]
then
echo "$(date): Push REJEITADO de $user em $refname" >> "$LOG_FILE"
exit 1
fi
done
exit 0
Para atualização atômica:
# Instalação segura
cp novo-hook /tmp/pre-receive.new
mv /tmp/pre-receive.new /caminho/repo.git/hooks/pre-receive
Referências
- Git Hooks Documentation — Documentação oficial do Git sobre hooks, incluindo server-side hooks e exemplos detalhados
- Atlassian Git Hooks Tutorial — Tutorial completo da Atlassian com exemplos práticos de pre-receive hooks e casos de uso
- GitLab Pre-receive Hooks Documentation — Guia oficial do GitLab para implementação de server hooks, incluindo variáveis de ambiente e exemplos
- GitHub Pre-receive Hooks Documentation — Documentação do GitHub Enterprise sobre pre-receive hooks, com foco em políticas de segurança
- Writing Git Hooks in Python — Artigo técnico sobre como escrever hooks Git em Python, incluindo tratamento de stdin e chamadas a APIs externas
- Git Hooks for CI/CD Integration — Tutorial da DigitalOcean sobre integração de hooks com pipelines de CI/CD e automação de deploys