Pre Commit
O pre-commit é uma ferramenta de controle de qualidade de código que permite executar verificações automáticas antes de confirmar suas alterações no sistema de controle de versão (como Git). A ideia principal do pre-commit é que ele executa uma série de "hooks" (ganchos) automaticamente toda vez que você tenta fazer um commit, verificando se o código está de acordo com os padrões definidos. Caso alguma verificação falhe, o commit é bloqueado até que os problemas sejam corrigidos.
Principais características do pre-commit:
- É configurado através de um arquivo YAML
.pre-commit-config.yaml
. - Permite definir múltiplos hooks para diferentes tipos de verificação.
- Pode ser usado para verificar formatação de código, linting, testes, vulnerabilidades e muito mais.
- Funciona com diversos tipos de linguagens de programação.
- É extensível com hooks personalizados.
Após configurado, o pre-commit executa automaticamente os hooks definidos sempre que você tenta fazer um commit.
É necessário que o arquivo de configuração esteja preparado para que o pré-commit saiba o que fazer.
Instalação
Podemos instalar o pre-commit de várias formas. Na documentação oficial mostra como instalar o python pip, mas eu prefiro utilizar o gerenciador de pacote no sistema operacional.
## linux
sudo apt-get install pre-commit
## mac
brew install pre-commit
pre-commit --version
pre-commit 4.2.0
Para habilitar o pre-commit no repositório que estamos trabalhando executamos o comando install.
pre-commit install
pre-commit installed at .git/hooks/pre-commit
Esse comando configura o Git para executar automaticamente os hooks do pre-commit sempre que você tentar fazer um commit, mas como isso acontece?
-
Cria um hook Git: Ele cria (ou substitui) o arquivo .git/hooks/pre-commit no seu repositório local. Este é um script executável que o Git chamará automaticamente antes de finalizar cada commit.
-
Configura o pipeline de verificação: O script instalado é configurado para ler seu arquivo .pre-commit-config.yaml e executar todas as verificações definidas lá.
-
Estabelece o fluxo de trabalho: A partir desse momento, sempre que você executar git commit, o Git primeiro executará o hook pre-commit, que por sua vez executará todas as verificações configuradas.
-
Automatiza a verificação de qualidade: Se alguma das verificações falhar, o commit será abortado, forçando você a corrigir os problemas antes de conseguir commitar o código.
Este comando "conecta" a ferramenta pre-commit ao processo de commit do Git, garantindo que suas verificações de qualidade de código sejam executadas automaticamente, sem que você precise se lembrar de executá-las manualmente a cada commit.
Para desinstalar é só rodar o comando pre-commit uninstall
e o hook será removido do .git
Local vs Pipeline
Quando você executa pre-commit install, a instalação é apenas local e não reflete no seu repositório remoto. Isso ocorre porque:
- O comando pre-commit install modifica apenas a pasta .git/hooks/ do seu repositório local.
- Por padrão, a pasta .git/ e seu conteúdo não são versionados - eles são ignorados pelo Git.
- Cada desenvolvedor precisa executar pre-commit install em sua própria cópia local do repositório.
Podemos versionar o arquivo .pre-commit-config.yaml
ao seu repositório, mas será necessário documentar no README do projeto que os desenvolvedores precisam executar pre-commit install após clonar o repositório.
Você pode ter o arquivo .pre-commit-config.yaml
versionado no repositório e não utilizá-lo nas pipelines, somente para uso local o que te possibilita fazer seus próprios testes.
O grande problema disso é que o colaborador PODE ou NÃO seguir a documentação. Para evitar isso, coloque o pre-commit na suas pipelines e mostre ao colaborador que se ele não instalar para garantir o sucesso do commit localmente também não irá passará na pipeline.
A vantagem do uso do pre-commit em todos os processo, local e pipeline é:
Consistência
: Mantém o código consistente em todo o projetoAutomação
: Reduz o trabalho manual de verificação.Educação
: Ensina boas práticas para a equipe.Produtividade
: Identifica erros antes de enviá-los para o repositório remoto
Sobre os items acima eu vejo uma importância maior no quesito EDUCAÇÃO. Forçar o uso garante que sua equipe trabalhe de forma correta e aprenda o método certo de trabalho.
Primeiros Hooks
Como vimos, os hoosk que serem executados serão definidos dentro do arquivo .pre-commit-config.yaml
.
Temos um modelo inicial para começar com os nossos hooks.
pre-commit sample-config
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.2.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
# Salvando o samples
pre-commit sample-config > .pre-commit-config.yaml
Os hooks listados nessa configuração (trailing-whitespace, end-of-file-fixer, check-yaml, e check-added-large-files) são todos parte do repositório padrão de hooks do pre-commit https://github.com/pre-commit/pre-commit-hooks, e o pre-commit os baixará e configurará automaticamente na primeira execução.
Uma lista de alguns mais relevantes do repositório do pre-commit-hooks.
-
check-added-large-files
: Previne arquivos grandes de serem comitados- id: check-added-large-files
args: ['--maxkb=1024'] #(default=500kB) -
check-executables-have-shebangs
: Verifica se os executáveis não binários têm um shebang adequado. -
check-json
: Tenta carregar todos os arquivos json para verificar a sintaxe. -
check-yaml
: Tenta carregar todos os arquivos yaml para verificar a sintaxe. -
check-merge-conflict
: Verifique se há arquivos que contenham sequências de caracteres de conflito de mesclagem. --assume-in-merge- Permite executar o gancho quando não há nenhuma operação de mesclagem em andamento -
check-symlinks
: Verifica se há links simbólicos que não apontam para nada, mais conhecidos como atalhos. -
detect-aws-credentials
: Examina arquivos em busca de padrões que correspondam a credenciais da AWS e compara com suas próprias credenciais configuradas na AWS CLI gerando falha se encontrar correspondências. -
detect-private-key
: Detecta se tem arquivos de chaves privadas no commit. -
double-quote-string-fixer
: Substitui strings entre aspas duplas por strings entre aspas simples. -
end-of-file-fixer
: Garante que os arquivos terminem somente com uma nova linha. -
trailing-whitespace
: Remove espaços em branco à direita. Para preservar quebras de linha rígidas do Markdown...- id: trailing-whitespace
args: ['--markdown-linebreak-ext=md'] -
no-commit-to-branch
: Este hook impede commits diretos em branches específicas, geralmente as principais do seu projeto (como main ou master). Isso ajuda a:- Forçar o uso de Pull/Merge Requests para todas as alterações nas branches principais.
- Prevenir alterações acidentais em ambientes de produção.
- Garantir que todas as alterações passem pelo processo de revisão de código.
- Manter a qualidade e a integridade das branches protegidas.
- id: no-commit-to-branch
args: ['--branch', 'main', '--branch', 'develop', '--branch', 'release']Podemos inclusive configurar para permitir somente commit para branches específicas. Ótimos para uso do gitflow.
- id: no-commit-to-branch
args: ['--pattern', '^(feature|bugfix|hotfix)/']
E como ficaria então o nosso config inicial? Observe que o sample não trouxe a versao mais recente do repositório e já substituímos pela mais nova.
## Aqui temos os items globais que falaremos mais tarde, repos é um deles
#...
##
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0 # Use a versão mais recente disponível
hooks:
- id: check-added-large-files
args: ['--maxkb=1024'] # Permite arquivos até 1MB (padrão é 500kB)
- id: check-executables-have-shebangs
- id: check-json
- id: check-yaml
- id: check-merge-conflict
args: ['--assume-in-merge'] # Permite executar quando não há operação de mesclagem
- id: check-symlinks
- id: detect-aws-credentials
args: ['--credentials-file=~/.aws/credentials', '--allow-missing-credentials']
- id: detect-private-key
- id: double-quote-string-fixer
- id: end-of-file-fixer
- id: trailing-whitespace
args: ['--markdown-linebreak-ext=md'] # Preserva quebras de linha rígidas em arquivos Markdown
- id: no-commit-to-branch
args: ['--branch', 'main', '--branch', 'develop', '--branch', 'release']
E onde estão esses hooks? Referenciamos diretamente o que temos no repositório? Quem executa esses hooks?
Quando utilizamos o repositório somente estamos apontando o projeto que vamos utilizar, mas na verdade esse repositório é clonado para sua máquina localmente e irá para algum lugar que veremos adiante.
Para testar siga o fluxo normalmente para fazer um commit no seu repositório.
git add -A
git commit -m "teste pre-commit"
## E verá ele executando... mas provavelmente falhará.
Se falhar, observe que ele corrigiu algumas coisas e alguns arquivos sofreram modificações. Por isso é necessário que refaça os commandos git add
e git commit
novamente para adicionar as modificações que foram feitas.
Ambiente
O pre-commit é um orquestrador de hooks e nada mais. Quando apontamos um repositório ele já possui tudo que é necessário para executar o hook. Na verdade estamos pontando o repositório que ele irá clonar.
Quando você executa um commit, este orquestrador lê seu arquivo .pre-commit-config.yaml. Na primeira execução, ele baixa automaticamente os hooks especificados de seus repositórios remotos e cria ambientes isolados (geralmente virtualenvs) para cada hook baixado. Quando necessário, ele instala as dependências do hook nesse ambiente isolado.
Ao passar isso repo: https://github.com/pre-commit/pre-commit-hooks
o pre-commit irá clonar este repositorio para dentro de uma pasta do seu sistema.
# mac e linux
~/.cache/pre-commit
# windows
C:\Users\<seu-usuario>\AppData\Local\pre-commit
Como já utilizo o pre-commit em outros projetos tenho mais repositórios do que somente o pre-commit-hook.
~/.cache/pre-commit
❯ tree -L 1 .
.
├── README
├── db.db
├── patch1742487237-94180
├── patch1742489491-15045
├── patch1742578081-54469
├── pre-commit.log
├── repo8nab35wd
├── repo_bvmclu5
├── repolgpjw5vx
├── repomto3fe10
├── repon25rfbaz
└── repouta6jw6j
Uma dessas pastas é o clone do repo https://github.com/pre-commit/pre-commit-hooks
(repouta6jw6j).
Já vemos que na verdade o projeto usa o python para rodar os scripts de cada um dos hooks. Outros projetos podem usar outros métodos.
~/.cache/pre-commit/repouta6jw6j
❯ ls
CHANGELOG.md build py_env-python3.12 setup.cfg tests
LICENSE pre_commit_hooks py_env-python3.13 setup.py tox.ini
README.md pre_commit_hooks.egg-info requirements-dev.txt testing
E podemos ver que temos a pasta pre_commit_hooks com o script python que é executado quando passamos o id.
❯ ls pre_commit_hooks
__init__.py check_toml.py forbid_new_submodules.py
check_added_large_files.py check_vcs_permalinks.py mixed_line_ending.py
check_ast.py check_xml.py no_commit_to_branch.py
check_builtin_literals.py check_yaml.py pretty_format_json.py
check_byte_order_marker.py debug_statement_hook.py removed.py
check_case_conflict.py destroyed_symlinks.py requirements_txt_fixer.py
check_docstring_first.py detect_aws_credentials.py sort_simple_yaml.py
check_executables_have_shebangs.py detect_private_key.py string_fixer.py
check_json.py end_of_file_fixer.py tests_should_end_in_test.py
check_merge_conflict.py file_contents_sorter.py trailing_whitespace_fixer.py
check_shebang_scripts_are_executable.py fix_byte_order_marker.py util.py
check_symlinks.py fix_encoding_pragma.py
O que foi executado foi práticamente isso para cada um dos hooks
python /caminho/para/.cache/pre-commit/repoXYZ/script_do_hook.py [argumentos]
Se você não tem o python como ele executou? Na verdade o executável veio junto com o repositório.
~/.cache/pre-commit/repouta6jw6j
❯ ls py_env-python3.13/bin/python
py_env-python3.13/bin/python
Local Vs Repo
O parâmetro repo
não precisa apontar para uma url de repositório podendo ser definido como local
. Neste caso iremos passar os parâmetros que quisermos para isso.
- repo: local
hooks:
- id: custom-script
name: Custom local script
entry: ./scripts/validate.sh
language: script
- id: trivy
name: Run trivy
entry: trivy config . --exit-code 1 --severity HIGH,CRITICAL --skip-dirs "tests" --timeout 5m
language: system
pass_filenames: false
verbose: true
Parâmetros Globais
Não passamos nada de parâmetros globais, mas é possível. Esses blocos de configuração global definem valores padrão que se aplicam a todos os hooks em seu arquivo .pre-commit-config.yaml, a menos que sejam sobrescritos nas configurações específicas de cada hook. Abaixo está os valores default que não precisamos definir mas vale o conhecimento caso necessite.
### BLOCO GLOBAL ####
# Define quais tipos de hooks serão instalados por padrão quando você executa pre-commit install. Neste caso, apenas hooks do tipo pre-commit serão instalados.
default_install_hook_types:
- "pre-commit"
# Define as versões de linguagem padrão para os diferentes hooks. O objeto vazio significa que serão usadas as versões padrão do sistema.
default_language_version: {}
# Define em quais estágios do git cada hook será executado por padrão. Esta configuração inclui todos os estágios possíveis que é o padrão. Não quer dizer correrá em todos os estágio, apena no que instalamos neste caso pre-commit. Falaremos disso a seguir.
default_stages:
- "commit-msg"
- "post-checkout"
- "post-commit"
- "post-merge"
- "post-rewrite"
- "pre-commit"
- "pre-merge-commit"
- "pre-push"
- "pre-rebase"
- "prepare-commit-msg"
# Define um padrão global de arquivos que serão verificados. String vazia significa que não há filtro global adicional.
files: ''
# Define um padrão regex para excluir arquivos. ^$ é uma regex que não corresponde a nenhum arquivo (corresponde apenas a strings vazias).
exclude: ^$
#Se definido como true, o pre-commit irá parar na primeira falha de hook. Como está false, todos os hooks serão executados mesmo se algum falhar.
fail_fast: false
# Versão mínima do pre-commit necessária para executar esta configuração.
minimum_pre_commit_version: '0'
####################
repos:
#... Continua...
Geralmente vemos mais o uso do pre-commi no estágio pré commit, mas poderíamos instalar hooks para vários estágios. Na verdade o nome pre-commit confunde um pouco achando que ele somente é limitado em seu estágio.
Esses são os estágios possíveis.
pre-commit
: Antes de finalizar uma mensagem de commi`tcommit-msg
: Para validar a mensagem do commitpost-checkout
: Após um checkoutpost-commit
: Após completar um commitpost-merge
: Após completar um mergepost-rewrite
: Após comandos que reescrevem commits (como rebase)pre-merge-commit
: Antes de um merge commitpre-push
: Antes de um pushpre-rebase
: Antes de um rebaseprepare-commit-msg
: Preparando a mensagem de commit padrão
Se fossemos instalar para mais estágios seria esse exemplo:
# instalando hooks para pre-push e commit-msg
pre-commit install --hook-type pre-commit --hook-type pre-push --hook-type commit-msg
Caso tivessemos isso no .pre-commit-config.yaml somente o comando pre-commit install faria todo o trabalho.
default_install_hook_types:
- "pre-commit"
- "pre-push"
- "commit-msg"
Parâmetros Principais dos Hooks
Aqui estão os principais parâmetros disponíveis para configurar hooks no pre-commit:
id
: Identificador único do hook dentro do repositório (ou do local).name (opcional)
: Nome personalizado para o hook, que será exibido durante a execução.entry
: Ponto de entrada para o hook. Pode ser um script, módulo ou comando.language
Linguagem utilizada pelo hook. Alguns valores possíveis: python, node, ruby, rust, dotnet, perl conda, system (utiliza comandos do sistema).files
: Expressão regular que define quais arquivos devem ser analisados pelo hook.exclude
: Expressão regular que define quais arquivos devem ser ignorados pelo hook.types
,types_or
etypes_and
: Filtra os arquivos por tipo.exclude_types
: Tipos de arquivos a serem excluídos.args
: Lista de argumentos a serem passados para o hook.stages
Define em quais estágios o hook deve ser executado: commit, merge-commit, push, prepare-commit-msg, commit-msg, post-checkout, post-commit, post-merge, post-rewrite, manual (somente quando chamado manualmente).additional_dependencies
Lista de dependências adicionais necessárias para o hook.always_run
: Define se o hook deve ser executado mesmo quando não há arquivos correspondentes.pass_filenames
: Define se os nomes dos arquivos devem ser passados para o hook.fail_fast
: Se true, falha na primeira ocorrência de erro.verbose
: Se true, produz saída mais detalhada.require_serial
Se true, executa o hook de forma serial (não paralela).description
Descrição do que o hook faz.minimum_pre_commit_version
Versão mínima do pre-commit necessária para executar o hook.
Não existe um valor default para os parâmetros dos hooks. Esses parâmetros podem variar de acordo com cada hook no repositório. Quando utilizar um hook e necessário conferir a documentação. Geralmente, os hooks do pre-commit preservam os seguintes valores como default, sem especificá-los explicitamente em suas configurações:
## Os hooks normalmente especificam explicitamente apenas:
- id:
name:
entry:
language:
files:
types:
################################################
##### Valores normalmente mantidos default ######
# Sem padrão de exclusão específico
exclude: ^$
types_or: []
types_and: []
exclude_types: []
# Frequentemente deixado vazio ou com descrição mínima
description: ''
# Sem argumentos adicionais por padrão
args: []
# Apenas executado na fase pre-commit por padrão
stages: [pre-commit]
# Sem dependências adicionais
additional_dependencies: []
# Executam apenas quando arquivos relevantes são modificados
always_run: false
# Recebem os nomes dos arquivos modificados como argumentos
pass_filenames: true
# A maioria dos hooks não interrompe a execução após uma falha
fail_fast: false
# Não exibem informações detalhadas por padrão
verbose: false
# Podem ser executados em paralelo com outros hooks
require_serial: false
#################################################
Só um detalhe...
#### ISSO É VÁLIDO, MAS É REDUNDANTE.
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0 # Use a versão mais recente disponível
hooks:
- id: check-json
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0 # Use a versão mais recente disponível
hooks:
- id: check-yaml
Seria a mesma coisa que escrever isso.
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0 # Use a versão mais recente disponível
hooks:
- id: check-json
- id: check-yaml
Se quiser validar o arquivo de configuração.
# Forcei um erro para conferir.
❯ pre-commit validate-config .pre-commit-config.yaml
==> File .pre-commit-config.yaml
==> At Config()
==> At key: repos
==> At Repository(repo='https://github.com/pre-commit/pre-commit-hooks')
=====> Missing required key: hooks
Evoluindo Mais
Claro que os hooks que vamos executar dependem muito do projeto. Se estamos desenvolvendo um módulo terraform seria interessante executar um terraform fmt, terraform validate, terraform test caso tenha testes, checkov para conferência estática de segurança, etc.
Vamos falar dessa configuração para ampliar nossos horizontes. Isso poderia ser uma configuração para um projeto terraform ou módulo.
repos:
# Já sabemos...
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: check-merge-conflict
- id: end-of-file-fixer
exclude: "README.md"
- id: trailing-whitespace
# Esse repo possui vários hooks para terraform vamos utilizar
- repo: https://github.com/antonbabenko/pre-commit-terraform
rev: v1.96.1
hooks:
- id: terraform_fmt
files: ^[^/]+\.tf$
exclude: ^examples/
- id: terraform_tflint
exclude: ^examples/
args: # Vários argumentos passando para o tflint que esta no repositório
- '--args=--only=terraform_deprecated_interpolation'
- '--args=--only=terraform_deprecated_index'
- '--args=--only=terraform_unused_declarations'
- '--args=--only=terraform_comment_syntax'
- '--args=--only=terraform_documented_outputs'
- '--args=--only=terraform_documented_variables'
- '--args=--only=terraform_typed_variables'
- '--args=--only=terraform_module_pinned_source'
- '--args=--only=terraform_naming_convention'
- '--args=--only=terraform_required_version'
- '--args=--only=terraform_required_providers'
- '--args=--only=terraform_standard_module_structure'
- '--args=--only=terraform_workspace_remote'
- id: terraform_validate
exclude: ^examples/
# Porém o comando terraform test não foi implementado neste repositório e vamos resolver isso localmente
- repo: local
hooks:
- id: terraform-test
name: Run Terraform Test
entry: terraform test # O comando executado será o terraform test sem nenhum argumento
language: system
pass_filenames: false
- id: checkov # E também vamos utilizar o checkov para analise estática do código
name: Run checkov
## o entry aqui foi passado com os argumentos, poderíamos ter separado <<<<<<
entry: checkov -d . --skip-path "examples" --skip-path "tests" --quiet
language: system
pass_filenames: false
verbose: true
# Também vamos gerar a documentação desse projeto terraform
- repo: https://github.com/terraform-docs/terraform-docs
rev: "v0.19.0"
hooks:
- id: terraform-docs-go
args: ["--output-mode", "replace", "--output-file", "README.md", "."]
O último passo foi desnecessário pois o próprio repo antonbabenko/pre-commit-terraform possui esse hook.
Para que esse pre-commit funcione precisaremos ter o checkov disponível no path do sistema pois não existe o repositório que esta trazendo este binário para nós.
Outros Hooks
Se navegarmos até https://pre-commit.com/hooks.html podemos encontrar mais hooks que pode fazer parte do seu projeto.
Vários comandos que podemos colocar nos hooks podem executado localmente.
Hooks populares:
- Formatadores de código: black, prettier, autopep8
- Linters: flake8, eslint, pylint
- Verificadores de segurança: bandit, safety
- Verificadores de estilo: isort, yapf
- Verificadores de sintaxe: check-json, check-yaml
Comandos úteis
pre-commit run
: Executa os hooks manualmentepre-commit run check-yaml --all-files
: Executa este hook para todos os arquivospre-commit run --all-files
: Executa em todos os arquivospre-commit autoupdate
: Atualiza os hooks para as versões mais recentes
O pre-commit é uma ferramenta valiosa para equipes de desenvolvimento que desejam manter alta qualidade de código e padronização em seus projetos.