Commitlint: validando mensagens de commit automaticamente

1. Por que padronizar mensagens de commit?

O histórico de commits de um repositório Git é o diário de bordo de um projeto. Mensagens claras e consistentes transformam esse diário em uma ferramenta poderosa de comunicação entre desenvolvedores, permitindo entender rapidamente o que foi alterado, por que e como.

Mensagens não padronizadas geram problemas graves:

# Exemplos de mensagens problemáticas
"arrumei o bug"
"commit"
"varias coisas"
"fix"
""

Essas mensagens são inúteis para revisão de código, debugging e geração automatizada de changelogs. Sem padronização, ferramentas como git log --oneline ou git shortlog perdem grande parte de seu valor.

A padronização facilita:

  • Geração automática de changelogs: ferramentas como standard-version ou semantic-release dependem de mensagens estruturadas para criar notas de release.
  • Rastreamento de mudanças: identificar rapidamente commits que introduzem novas funcionalidades, corrigem bugs ou quebram a compatibilidade.
  • Automação de versionamento semântico: mensagens padronizadas permitem determinar automaticamente se a próxima versão deve ser major, minor ou patch.

2. O que é o Commitlint?

Commitlint é uma ferramenta de linha de comando que valida mensagens de commit contra um conjunto de regras configuráveis. Ela se integra diretamente ao fluxo do Git através do hook commit-msg, que é executado imediatamente após o desenvolvedor digitar a mensagem e antes do commit ser efetivado.

Diferente de validadores manuais ou scripts caseiros, o Commitlint oferece:

  • Regras predefinidas: o pacote @commitlint/config-conventional implementa o padrão Conventional Commits, amplamente adotado.
  • Flexibilidade: permite customizar tipos, escopos, formatos e até criar regras próprias.
  • Mensagens de erro claras: quando a validação falha, o Commitlint exibe exatamente qual regra foi violada e como corrigir.

A principal diferença entre Commitlint e outros validadores é que ele opera especificamente no hook commit-msg, recebendo a mensagem completa como entrada e retornando um código de saída que o Git interpreta para aceitar ou rejeitar o commit.

3. Configuração inicial do Commitlint

A instalação é feita via npm ou yarn. Primeiro, instale os pacotes necessários:

npm install --save-dev @commitlint/cli @commitlint/config-conventional

Em seguida, crie o arquivo de configuração. O formato mais comum é commitlint.config.js:

// commitlint.config.js
module.exports = {
  extends: ['@commitlint/config-conventional']
};

Ou, alternativamente, usando JSON:

// .commitlintrc.json
{
  "extends": ["@commitlint/config-conventional"]
}

Com essa configuração mínima, o Commitlint passa a validar mensagens seguindo o padrão Conventional Commits, que exige o formato:

tipo(escopo): descrição

[corpo opcional]

[rodapé opcional]

Exemplos de mensagens válidas:

feat: adiciona autenticação por OAuth2
fix(auth): corrige validação de token expirado
docs: atualiza README com instruções de instalação
refactor!: remove suporte a API legada

4. Regras e configurações avançadas

O Commitlint organiza suas regras em uma estrutura de três elementos: [nível, aplicabilidade, valor].

  • Nível: 0 para desativar, 1 para warning, 2 para error.
  • Aplicabilidade: 'always' ou 'never'.
  • Valor: o valor a ser verificado (ex.: array de tipos permitidos).

Exemplo de configuração avançada:

// commitlint.config.js
module.exports = {
  extends: ['@commitlint/config-conventional'],
  rules: {
    'type-enum': [2, 'always', ['feat', 'fix', 'docs', 'style', 'refactor', 'test', 'chore']],
    'scope-enum': [2, 'always', ['core', 'auth', 'api', 'ui']],
    'subject-case': [2, 'always', ['sentence-case', 'lower-case']],
    'header-max-length': [2, 'always', 72],
    'body-max-line-length': [2, 'always', 100],
    'footer-empty': [1, 'never']
  }
};

Regras comuns:

  • type-enum: define os tipos permitidos (feat, fix, docs, etc.)
  • scope-enum: define escopos válidos para o projeto
  • subject-case: força um formato de capitalização no assunto
  • header-max-length: limita o tamanho da primeira linha
  • body-max-line-length: limita linhas do corpo da mensagem
  • footer-empty: exige ou proíbe rodapés

5. Integração com Git Hooks (local)

O hook commit-msg é o ponto de integração natural. Localize o diretório .git/hooks no seu repositório e crie ou edite o arquivo commit-msg:

#!/bin/sh
# .git/hooks/commit-msg

npx commitlint --edit $1

Dê permissão de execução:

chmod +x .git/hooks/commit-msg

Agora, ao tentar fazer um commit com mensagem inválida:

git commit -m "corrige bug"

O Commitlint rejeitará o commit com uma mensagem como:

⧗   input: corrige bug
✖   type must be one of [feat, fix, docs, style, refactor, test, chore] [type-enum]
✖   found 1 problems, 0 warnings
ⓘ   Get help: https://github.com/conventional-changelog/commitlint/#what-is-commitlint

A vantagem dos hooks locais é que a validação ocorre antes mesmo do commit ser registrado, evitando poluir o histórico. A desvantagem é que desenvolvedores podem pular o hook com git commit --no-verify.

6. Integração com Husky e ferramentas de CI

O Husky simplifica o gerenciamento de hooks Git, permitindo configurá-los diretamente no package.json. Instale:

npm install --save-dev husky

Ative os hooks:

npx husky install

Adicione um hook commit-msg via Husky:

npx husky add .husky/commit-msg 'npx --no -- commitlint --edit $1'

O package.json pode conter a configuração completa:

{
  "scripts": {
    "prepare": "husky install"
  },
  "husky": {
    "hooks": {
      "commit-msg": "npx --no -- commitlint --edit $1"
    }
  }
}

Para ambientes de CI, o Commitlint pode validar mensagens de pull requests ou commits em pipelines. Exemplo para GitHub Actions:

# .github/workflows/commitlint.yml
name: Commitlint
on: [push, pull_request]
jobs:
  commitlint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0
      - uses: actions/setup-node@v3
        with:
          node-version: 18
      - run: npm install
      - run: npx commitlint --from ${{ github.event.before }} --to ${{ github.sha }}

Uma pipeline completa pode incluir lint-staged para formatar código, Commitlint para validar mensagens e testes automatizados:

{
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged",
      "commit-msg": "commitlint --edit $1"
    }
  },
  "lint-staged": {
    "*.js": ["eslint --fix", "git add"]
  }
}

7. Boas práticas e resolução de problemas

Mensagens inválidas: quando o Commitlint rejeita uma mensagem, a melhor prática é corrigi-la e refazer o commit. Use git commit --amend para editar a mensagem do último commit ou git rebase -i para corrigir commits mais antigos. Evite --no-verify a menos que seja absolutamente necessário (ex.: durante rebase interativo).

Commits de merge: por padrão, o Commitlint pode rejeitar mensagens geradas automaticamente pelo Git durante merges. Para ignorar validação nesses casos, configure:

// commitlint.config.js
module.exports = {
  extends: ['@commitlint/config-conventional'],
  ignores: [(message) => message.startsWith('Merge')]
};

Versionamento da configuração: mantenha o commitlint.config.js no repositório para que todos os desenvolvedores usem as mesmas regras. Isso garante consistência mesmo em equipes grandes ou distribuídas.

Dica para times: comece com regras permissivas e vá endurecendo gradualmente. Uma transição brusca pode frustrar a equipe. Use warnings (nível: 1) inicialmente e evolua para errors (nível: 2) quando todos estiverem adaptados.

Referências