Abra o histórico de commits de qualquer projeto que já passou por várias mãos e você vai encontrar de tudo: "fix", "ajustes", "correção final", "correção final 2", "wip". Nenhuma dessas mensagens diz o que mudou, por que mudou, ou se quebra algo. O resultado prático: gerar um changelog vira trabalho manual, descobrir quando um bug foi introduzido vira arqueologia, e decidir se a próxima versão é 1.2.0 ou 2.0.0 vira palpite.
É esse o problema que o Conventional Commits resolve: uma convenção leve para escrever mensagens de commit de um jeito que tanto humanos quanto máquinas conseguem interpretar, permitindo automatizar changelog, versionamento semântico e até gatilhos de CI a partir do próprio histórico do Git.
Este artigo cobre a origem da especificação, para que ela serve, sua estrutura formal e exemplos práticos do dia a dia.
Como e por que surgiu
O Conventional Commits nasceu da observação de que vários projetos open source já usavam, de forma independente, convenções parecidas para prefixar mensagens de commit. O Angular Commit Message Guidelines é a referência mais citada como origem direta do formato. A ideia central do Angular era simples: prefixar cada commit com um tipo (feat, fix, docs...) para que uma ferramenta pudesse gerar o CHANGELOG.md automaticamente, sem depender de alguém escrever isso manualmente a cada release.
A especificação Conventional Commits 1.0.0 consolidou essa prática em um padrão formal, documentado e versionado, inspirado também no SemVer, com o qual se integra diretamente (mais sobre isso adiante). Hoje é mantida como projeto aberto em conventionalcommits.org, com traduções para várias línguas, incluindo português.
Para que serve
| Necessidade | Como o Conventional Commits ajuda |
|---|---|
| Gerar changelog automaticamente | Ferramentas leem o histórico e montam o CHANGELOG.md sem intervenção manual |
| Determinar a próxima versão semântica |
fix sugere PATCH, feat sugere MINOR, BREAKING CHANGE sugere MAJOR |
| Comunicar a natureza da mudança ao time | Quem revisa o PR já sabe se é bug, feature ou refatoração antes de abrir o diff |
| Disparar builds e pipelines automaticamente | CI pode decidir se publica um pacote, dispara um deploy ou pula uma etapa com base no tipo de commit |
| Facilitar contribuição em projetos abertos | Reduz a barreira para quem nunca contribuiu entender como categorizar a própria mudança |
Na prática, o ganho não é estético, é que o histórico do Git passa a ser dado estruturado, não só texto livre, e dá para automatizar em cima dele.
Estrutura da mensagem
O formato básico definido pela especificação é:
<tipo>[escopo opcional]: <descrição>
[corpo opcional]
[rodapé(s) opcional(is)]
Os tipos mais usados
| Tipo | Quando usar |
|---|---|
feat |
Nova funcionalidade (correlaciona com MINOR no SemVer) |
fix |
Correção de bug (correlaciona com PATCH no SemVer) |
docs |
Mudança apenas em documentação |
style |
Formatação, espaços, ponto e vírgula, sem mudança de lógica |
refactor |
Mudança de código que não corrige bug nem adiciona feature |
perf |
Mudança que melhora performance |
test |
Adição ou correção de testes |
build |
Mudança no sistema de build ou dependências externas |
ci |
Mudança em arquivos e scripts de CI/CD |
chore |
Tarefas de manutenção que não afetam código de produção |
A especificação define oficialmente apenas
featefixcomo obrigatórios para a relação com SemVer. Os demais tipos (baseados na convenção do Angular) são uma extensão amplamente adotada, mas o time pode ajustar a lista conforme a necessidade do projeto.
Exemplos práticos
Commit simples
feat: adiciona endpoint de exportação de relatórios em PDF
Commit com escopo
O escopo (entre parênteses) indica qual parte do código foi afetada:
fix(auth): corrige expiração prematura do token JWT
feat(checkout): adiciona suporte a pagamento via Pix
Commit com corpo explicando o contexto
fix(api): corrige timeout em requisições de upload grandes
O timeout estava fixo em 30s independente do tamanho do arquivo.
Agora o timeout escala proporcionalmente ao tamanho do payload,
com um teto máximo de 5 minutos.
Commit com breaking change
Uma mudança que quebra compatibilidade é sinalizada com ! após o tipo/escopo, ou com o rodapé BREAKING CHANGE: os dois podem ser usados juntos:
feat(api)!: remove suporte ao endpoint /v1/users
BREAKING CHANGE: o endpoint /v1/users foi descontinuado.
Use /v2/users, que retorna o campo `email` como obrigatório.
Esse commit, por conter BREAKING CHANGE, indica que a próxima versão deve ser um MAJOR no versionamento semântico (ex: 2.4.1 → 3.0.0), independente de conter também um feat.
Commit apenas de manutenção
chore: atualiza dependências de dev para as versões mais recentes
Múltiplos rodapés
Rodapés seguem o mesmo formato de trailers do Git (chave: valor), e podem referenciar issues, coautores ou revisores:
fix(payments): corrige arredondamento incorreto no cálculo de juros
Refs: #482
Reviewed-by: Ana Costa
Co-authored-by: João Silva <joao@example.com>
Relação com Versionamento Semântico (SemVer)
A especificação foi desenhada para mapear diretamente para as três posições do SemVer (MAJOR.MINOR.PATCH):
| Tipo de commit | Impacto na versão | Exemplo |
|---|---|---|
fix |
PATCH |
1.4.2 → 1.4.3
|
feat |
MINOR |
1.4.2 → 1.5.0
|
Qualquer tipo com BREAKING CHANGE ou !
|
MAJOR |
1.4.2 → 2.0.0
|
docs, style, chore, test, etc. |
Nenhum (não gera release) | — |
Essa correlação é o que permite que ferramentas de release automatizem a decisão de qual versão publicar, sem que ninguém precise decidir manualmente "isso é uma minor ou uma patch?".
Ferramentas do ecossistema
| Ferramenta | Função |
|---|---|
commitlint |
Valida no commit (via hook) se a mensagem segue o padrão |
commitizen |
CLI interativa que monta a mensagem de commit por perguntas |
semantic-release |
Automatiza versionamento e publicação de pacotes com base nos commits |
standard-version |
Gera changelog e bump de versão localmente, sem publicar |
conventional-changelog |
Biblioteca base para gerar changelogs a partir do histórico |
Exemplo de hook local com commitlint
npm install --save-dev @commitlint/cli @commitlint/config-conventional
// commitlint.config.js
module.exports = { extends: ['@commitlint/config-conventional'] };
Integrando com Husky para rodar a validação em todo commit:
npx husky add .husky/commit-msg 'npx --no -- commitlint --edit "$1"'
A partir daí, um commit fora do padrão é rejeitado antes mesmo de entrar no histórico:
$ git commit -m "ajustei umas coisas"
⧗ input: ajustei umas coisas
✖ subject may not be empty [subject-empty]
✖ type may not be empty [type-empty]
✖ found 2 problems, 0 warnings
Boas práticas
-
Descrição no imperativo, minúscula, sem ponto final — ex:
fix: corrige, nãofix: Corrigido.oufix: correção de -
Um commit, uma mudança lógica — evite misturar
featerefactorno mesmo commit só porque tocaram os mesmos arquivos -
Escopo consistente — combine com o time uma lista fixa de escopos (
auth,api,ui,db) em vez de deixar livre - Corpo explica o "porquê", não o "o quê" — o diff já mostra o que mudou; o corpo do commit deve justificar a decisão
-
BREAKING CHANGEsempre no rodapé, em maiúsculas — é o texto que as ferramentas de automação procuram para disparar um MAJOR -
Combine com
commitlintno pipeline ou pre-commit hook — depender de disciplina manual do time não escala além de poucas pessoas
Troubleshooting
| Problema | Causa provável | Solução |
|---|---|---|
commitlint rejeita commit com mensagem que parece correta |
Espaço faltando após os dois-pontos, ou tipo com maiúscula (Feat:) |
Sempre tipo: descrição, tipo em minúsculas, um espaço após :
|
| Changelog gerado não lista uma mudança importante | Commit usou tipo que não gera changelog por padrão (chore, style) |
Reclassifique como feat/fix se a mudança for relevante ao usuário final |
semantic-release não bumpou a versão MAJOR esperada |
BREAKING CHANGE escrito errado no corpo (ex: Breaking change: com minúscula) |
Use exatamente BREAKING CHANGE: maiúsculo, ou ! após o tipo/escopo |
| Time reclama que o padrão é burocrático demais | Falta de ferramenta que automatize a checagem, virando esforço manual | Adicione commitizen para gerar a mensagem por prompt interativo, reduzindo fricção |
| Merge commit "genérico" polui o changelog | Merge de branch sem squash mistura vários commits não convencionais | Prefira squash merge com uma mensagem convencional única no PR |
Conclusão
Conventional Commits não é sobre burocratizar o Git, é sobre transformar uma mensagem de commit em um dado que máquinas conseguem ler, sem tirar a legibilidade para humanos. O ganho aparece principalmente na escala: um projeto pequeno sobrevive com "fix bug", mas um projeto com múltiplos contribuidores, releases frequentes e changelog público sente a diferença entre montar isso manualmente ou automaticamente a cada versão.
Comece pequeno: adote feat: e fix: no seu próximo commit, adicione o commitlint como hook local, e deixe o time se acostumar antes de cobrar escopos e rodapés. Depois de validado no dia a dia, avalie ferramentas como semantic-release para automatizar versionamento e publicação por completo.
Top comments (1)
Great guide. Conventional Commits may seem like a small practice, but they create a strong foundation for better collaboration, automation, and maintainability. Clear commit messages make it easier to understand project history, generate changelogs, and integrate with CI/CD workflows.
The biggest value is not the exact format — it’s creating a shared language across the team. When everyone follows a consistent convention, reviewing changes and tracing decisions becomes much easier.
Simple engineering habits like this often have a compounding effect as projects and teams grow. Great resource for developers looking to improve their workflow!