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