Automatize Seu Fluxo de Trabalho: Um Guia Prático para Hooks do Lado do Cliente no Git

Use hooks do lado do cliente no Git para verificações locais rápidas, configuração compartilhada, regras de mensagens de commit e automação pós-merge mais segura.

Automatize Seu Fluxo de Trabalho: Um Guia Prático para Hooks do Lado do Cliente no Git

Os hooks do lado do cliente no Git são pequenos scripts que são executados na sua máquina quando o Git atinge certos pontos em um fluxo de trabalho. Um hook pre-commit é executado antes de um commit ser criado. Um hook commit-msg é executado depois que você escreve a mensagem, mas antes que o Git a aceite. Um hook post-merge é executado após um merge ser concluído. Quando bem utilizados, os hooks capturam erros chatos cedo: formatação esquecida, arquivos gerados quebrados, instalações de dependências ausentes ou mensagens de commit que não correspondem à convenção da sua equipe.

A limitação importante é que os hooks do lado do cliente são locais. Eles não viajam automaticamente com o repositório quando alguém o clona. Isso os torna ótimos para feedback rápido e conveniência local, mas fracos como a única camada de aplicação de uma regra da equipe. Se uma verificação realmente protege o branch principal, coloque-a também no CI ou em uma regra do lado do servidor.

Todo repositório tem um diretório de hooks em .git/hooks:

ls .git/hooks

Um novo repositório geralmente contém arquivos de exemplo como pre-commit.sample. Um hook de exemplo não faz nada até que você crie um arquivo executável sem o sufixo .sample:

cp .git/hooks/pre-commit.sample .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit

Os hooks podem ser scripts shell, scripts Python, scripts Ruby, scripts Node ou qualquer outra coisa que sua máquina possa executar. A primeira linha deve apontar para o interpretador:

#!/usr/bin/env bash

Para a maioria das equipes, o melhor padrão de longo prazo não é editar manualmente .git/hooks em cada laptop. Armazene os scripts de hook no repositório e configure o Git para usar esse diretório:

git config core.hooksPath .githooks
mkdir -p .githooks

Agora, um hook em .githooks/pre-commit pode ser commitado e revisado como código normal do projeto. Cada desenvolvedor ainda precisa da configuração core.hooksPath, mas a configuração pode ser adicionada a um script de inicialização ou documentada na integração.

Um Hook Pre-Commit Útil

Um bom hook pre-commit deve ser rápido e focado. Se levar dois minutos em cada commit, as pessoas vão ignorá-lo com git commit --no-verify, e o hook se tornará ruído. Guarde as suítes de teste completas para o CI, a menos que o projeto seja pequeno o suficiente para que elas realmente sejam rápidas.

Aqui está um hook shell prático que verifica apenas arquivos staged. Essa distinção é importante. Você pode ter trabalho inacabado em sua árvore de trabalho que não deseja testar ainda. O commit deve ser julgado pelo que está staged.

Crie .githooks/pre-commit:

#!/usr/bin/env bash
set -u

changed_files=$(git diff --cached --name-only --diff-filter=ACMR)

if [ -z "$changed_files" ]; then
  exit 0
fi

if git diff --cached --check; then
  :
else
  echo "Corrija erros de espaço em branco antes de commitar."
  exit 1
fi

secret_matches=$(git diff --cached --name-only --diff-filter=ACMR | xargs grep -nE 'AKIA[0-9A-Z]{16}|BEGIN RSA PRIVATE KEY' 2>/dev/null || true)
if [ -n "$secret_matches" ]; then
  echo "Possível segredo encontrado em arquivos staged:"
  echo "$secret_matches"
  exit 1
fi

python_files=$(printf '%s\n' "$changed_files" | grep '\.py$' || true)
if [ -n "$python_files" ]; then
  printf '%s\n' "$python_files" | while IFS= read -r file; do
    [ -f "$file" ] || continue
    python3 -m py_compile "$file" || exit 1
  done
fi

exit 0

Este hook faz três coisas modestas: permite que o Git detecte erros de espaço em branco, verifica arquivos staged para alguns padrões de segredo óbvios e compila arquivos Python alterados. Não é um substituto para um scanner de segredos real ou uma suíte de testes. É um alarme rápido.

Um erro comum é usar grep em nomes de arquivos em vez de conteúdos de arquivos. Este padrão quebrado apenas verifica se o caminho contém TODO, não se o arquivo o contém:

git diff --cached --name-only | grep TODO

Se você quiser bloquear comentários TODO, inspecione o diff staged:

if git diff --cached -U0 | grep -E '^\+.*TODO:'; then
  echo "Comentários TODO staged encontrados."
  exit 1
fi

Mesmo assim, tenha cuidado. Algumas equipes usam comentários TODO de forma responsável. Bloquear todos os TODOs pode ser mais irritante do que útil.

Hooks de Mensagem de Commit

Um hook commit-msg recebe o caminho para o arquivo de mensagem de commit temporário como seu primeiro argumento. Isso o torna útil para regras como "todo commit deve começar com um ID de ticket" ou "use Commits Convencionais".

Um pequeno exemplo:

#!/usr/bin/env bash
set -u

message_file="$1"
first_line=$(head -n 1 "$message_file")

if printf '%s' "$first_line" | grep -Eq '^(feat|fix|docs|test|refactor|chore)(\(.+\))?: .+'; then
  exit 0
fi

echo "A mensagem de commit deve ser parecida com: fix(api): handle empty token"
exit 1

Isso é útil quando as notas de lançamento ou changelogs são gerados a partir de commits. É menos útil quando sua equipe faz merges squash e reescreve títulos de PR de qualquer maneira. Corresponda o hook ao fluxo de trabalho que você realmente usa.

Hooks Post-Merge

Um hook post-merge é melhor para limpeza local após sua árvore de trabalho mudar. O exemplo clássico é atualizar dependências após um arquivo de bloqueio mudar.

#!/usr/bin/env bash
set -u

previous_head="HEAD@{1}"

if git diff --name-only "$previous_head" HEAD | grep -Eq '(^package-lock\.json$|^pnpm-lock\.yaml$|^yarn\.lock$)' ; then
  if command -v npm >/dev/null 2>&1 && [ -f package-lock.json ]; then
    echo "Arquivo de bloqueio alterado; executando npm install."
    npm install
  fi
fi

if git diff --name-only "$previous_head" HEAD | grep -q '^\.gitmodules$'; then
  echo "Configuração de submódulo alterada; sincronizando submódulos."
  git submodule sync --recursive
  git submodule update --init --recursive
fi

Este hook não deve fazer alterações surpreendentes. Se ele instalar dependências, imprima o que está fazendo. Se a instalação falhar, diga ao desenvolvedor como se recuperar. Um hook que silenciosamente altera a árvore de trabalho é difícil de confiar.

Compartilhando Hooks Sem Fazer Bagunça

Existem três maneiras comuns de compartilhar hooks.

A mais simples é core.hooksPath, onde o repositório contém .githooks/ e a configuração define o Git para usá-lo. Isso é transparente e não requer outro gerenciador de pacotes.

Projetos JavaScript geralmente usam Husky porque ele se integra com fluxos de instalação npm, pnpm ou yarn. Isso pode ser uma boa opção quando todos os contribuidores já usam a cadeia de ferramentas Node.

Muitas equipes de linguagens mistas usam o framework pre-commit. Ele instala e executa hooks definidos em .pre-commit-config.yaml, com versões fixadas para ferramentas como formatadores, linters e verificações de arquivos. Ele adiciona outra ferramenta, mas resolve o problema "como instalamos os mesmos hooks em todos os lugares?" melhor do que uma página wiki.

O que eu evito é copiar scripts grandes para .git/hooks manualmente. Ninguém os revisa, ninguém sabe qual versão está instalada e a depuração se torna uma arqueologia pessoal.

Depurando Hooks

Quando um hook não é executado, verifique estes em ordem:

git config --get core.hooksPath
ls -l .git/hooks .githooks 2>/dev/null

Se core.hooksPath estiver definido, o Git ignora .git/hooks e usa o diretório configurado. Se o arquivo de hook não for executável no macOS ou Linux, o Git não o executará:

chmod +x .githooks/pre-commit

Quando um hook é executado, mas falha misteriosamente, adicione rastreamento temporário:

set -x
pwd
env | sort

Os hooks são executados a partir da raiz do repositório no uso normal do Git, mas clientes GUI e IDEs podem expor diferenças de caminho ou ambiente. Use command -v toolname dentro do hook antes de assumir que um linter ou gerenciador de pacotes está disponível.

Lembre-se também do interruptor de bypass:

git commit --no-verify

Isso não é uma falha de segurança por si só; é como o Git funciona. É outra razão pela qual a aplicação séria pertence ao CI ou às regras de branch protegido.

Uma Política de Hook Sensata

Use hooks para verificações que são rápidas, determinísticas e fáceis de explicar. Formatando arquivos staged, capturando erros de espaço em branco, validando mensagens de commit e lembrando os desenvolvedores de instalar dependências são bons candidatos. Evite hooks que exigem acesso à rede, levam muito tempo ou dependem de estado local frágil.

Se um hook bloquear um commit, sua mensagem deve dizer exatamente o que falhou e como corrigi-lo. "Hook falhou" não é suficiente. Um desenvolvedor no meio de um merge ou hotfix de produção precisa de um próximo comando claro.

Os hooks do lado do cliente no Git funcionam melhor quando parecem uma barreira de proteção útil, em vez de uma burocracia local. Mantenha-os pequenos, mantenha-os versionados e mantenha a autoridade final no CI.

Mantenha os Hooks Amigáveis Durante Emergências

Os hooks devem ajudar durante o trabalho normal sem prender alguém durante uma correção urgente. Isso significa que todo hook de bloqueio precisa de uma mensagem de falha clara e uma saída de emergência realista. O Git já fornece --no-verify para hooks de commit e push, mas sua equipe ainda deve decidir quando o bypass é aceitável. Um hotfix de produção é diferente de pular a formatação porque um desenvolvedor está com pressa.

Uma boa mensagem de hook diz o que falhou, onde falhou e o que executar em seguida:

echo "ESLint falhou em arquivos JavaScript staged."
echo "Execute: npm run lint -- --fix"
exit 1

Uma mensagem ruim diz apenas falhou ou despeja páginas de saída de ferramenta sem contexto. As pessoas aprendem a ignorar esse tipo de hook.

Se o hook modificar arquivos, tenha cuidado extra. Os formatadores podem ser úteis em pre-commit, mas também podem criar confusão quando alteram partes não staged de um arquivo. Muitas equipes preferem verificar a formatação no hook e deixar o desenvolvedor executar o formatador manualmente. Outros usam ferramentas que formatam apenas hunks staged. Escolha um comportamento e documente-o no repositório, não em um tópico de chat que desaparece.

Para equipes, revise as alterações de hook como código de aplicação. Um hook pode atrasar cada commit, vazar detalhes do ambiente para logs ou quebrar contribuidores no Windows se assumir comportamento apenas Bash. Se seu projeto tiver contribuidores Windows, teste hooks no Git Bash ou use um executor de hook multiplataforma. Se seu projeto tiver contêineres ou shells de desenvolvimento, considere executar hooks dentro do mesmo ambiente que o aplicativo para que todos usem as mesmas versões de ferramentas.

Os melhores hooks são quase invisíveis quando tudo está bem e muito específicos quando algo está errado. Esse é o padrão a ser almejado.

Versionar Hooks Como Código de Produto

Um script de hook se torna parte da experiência do desenvolvedor. Se ele quebrar, todos os contribuidores sentem. Mantenha os scripts pequenos, nomeie funções auxiliares claramente e evite truques inteligentes de shell quando um comando direto resolveria. Se um hook crescer além de uma tela ou duas, mova a lógica real para um script de projeto testado e deixe o hook chamar esse script.

Por exemplo, em vez de incorporar uma rotina de lint longa em .githooks/pre-commit, chame:

./scripts/check-staged-files.sh

Esse script pode ser executado por desenvolvedores, hooks e CI. Também significa que um desenvolvedor pode reproduzir a falha sem fingir fazer um commit. A reprodutibilidade é a diferença entre um hook útil e um obstáculo local misterioso.

Fixe versões de ferramentas onde puder. Um hook que chama qualquer black, eslint ou prettier que estiver primeiro no PATH pode se comportar de forma diferente entre máquinas. Dependências locais do projeto, arquivos de bloqueio, contêineres ou gerenciadores de versão tornam a saída do hook mais previsível.

Finalmente, mantenha os hooks com escopo no repositório. Hooks globais parecem convenientes, mas muitas vezes surpreendem você meses depois, quando um repositório não relacionado começa a falhar por causa de uma regra pessoal antiga. Use hooks globais apenas para preferências verdadeiramente pessoais, não para política de equipe.

Uma última regra prática: nunca deixe os hooks serem o único lugar onde um comando existe. Se o hook verifica arquivos Python staged, mantenha esse comando em um script ou executor de tarefas também. Os desenvolvedores devem ser capazes de executar a mesma verificação de propósito, antes que o Git os interrompa.

Para projetos de código aberto, assuma que os contribuidores podem não ter sua cadeia de ferramentas completa instalada ainda. Um hook que falha com uma mensagem de configuração amigável está bem. Um hook que lança um stack trace de um binário local ausente parece quebrado. Verifique os pré-requisitos antes de executar comandos mais pesados e aponte as pessoas para o comando de configuração usado pelo projeto.

Pense também em commits parciais. Muitos desenvolvedores experientes fazem stage de apenas parte de um arquivo. Hooks que formatam o arquivo inteiro podem acidentalmente puxar trabalho não staged para o commit. Se sua equipe usa commits parciais com frequência, prefira verificações que leem o diff staged ou ferramentas projetadas para conteúdo staged.

Se um hook continua sendo ignorado, trate isso como feedback. Ou a verificação é muito lenta, a mensagem de falha não é clara ou a regra pertence ao CI em vez do caminho de commit local. Corrija o atrito em vez de culpar os desenvolvedores por usar o bypass que o Git fornece.