Todos os artigos
176 artigos · atualizado semanalmente Veja nossas Ferramentas
Todos os artigos
Dicas

Mensagem de commit: padrão, exemplos e Conventional Commits

Como escrever boas mensagens de commit: tipos do Conventional Commits (feat, fix, chore), título de até 50 caracteres, corpo e exemplos prontos para copiar.

Mensagem de commit: padrão, exemplos e Conventional Commits
COVER · Dicas

Você abre o git log de um projeto que existe há dois anos e encontra isso: "fix", "ajustes", "wip", "tentando de novo", "agora vai". Não tem como saber o que foi feito, nem por quê. Quando precisar entender por que uma função mudou três meses atrás, você vai ler o diff de um commit por vez e adivinhar a intenção de quem não está mais no time.

Este post é o padrão que eu uso para mensagem de commit: o formato do título e do corpo, os tipos do Conventional Commits (os "commits semânticos", como muita gente chama no Brasil), exemplos prontos e como travar o padrão com commitlint. Se você ainda está nos fundamentos (branches, staging, merge), comece pelo Git para iniciantes.

Por que a mensagem de commit importa

Commit não é botão de salvar. É documentação com data, presa exatamente à mudança que ela explica. Três situações em que uma boa mensagem economiza horas:

  1. Entender uma regressão. O git bisect encontra o commit que introduziu o bug pelo resultado dos testes (você marca cada versão como good ou bad), não pela mensagem. Mas, quando ele para e diz "o culpado é a1b2c3", a mensagem decide se você entende o que aconteceu em dez segundos ou em meia hora. "fix" não diz nada; fix(auth): evita loop infinito quando o token expira no fuso UTC-3 já dá o contexto antes de abrir o diff.
  2. Gerar changelog e versão automaticamente. Ferramentas como semantic-release e conventional-changelog leem o histórico para escrever as notas de versão e decidir o próximo número de versão. Sem estrutura, não tem automação.
  3. Review assíncrono e git blame. Quem revisa o PR em outro fuso, ou quem chega ao código daqui a um ano pelo git blame, lê a mensagem antes de ler o código.

A anatomia de uma boa mensagem: título, linha em branco, corpo

A própria documentação do git commit recomenda começar a mensagem com uma única linha curta, de no máximo 50 caracteres, resumindo a mudança, seguida de uma linha em branco e de uma descrição mais completa. O guia clássico de Tim Pope, de 2008, acrescenta a outra metade da regra: quebrar o corpo em cerca de 72 colunas, porque o git log não quebra linhas e, num terminal de 80 colunas, sobram 72 depois do recuo de 4 de cada lado.

A regra que eu sigo, unificada:

  • Título com até 50 caracteres como alvo, e 72 como limite duro. Acima de 72, o git log --oneline vira uma linha que transborda e o GitHub corta o título com reticências (comportamento observado, não documentado oficialmente).
  • Linha em branco entre título e corpo. O Git trata tudo até a primeira linha em branco como título; sem ela, o git log --oneline, o git rebase -i e o GitHub mostram título e corpo grudados.
  • Corpo quebrado em 72 colunas, explicando o porquê e não o como (o como está no diff).
  • Rodapé com referências: Refs: #1847, Closes #123 (o GitHub fecha a issue quando o commit chega na branch padrão) ou BREAKING CHANGE:.
fix(billing): arredonda total da fatura em centavos

A conta em ponto flutuante gerava totais como 59.699999999999996 (três
itens de 19.9), e a conciliação com o banco rejeitava a fatura. Passamos
a somar em centavos (inteiros) e converter só na exibição.

Não usei toFixed(2) porque ele devolve string e arredonda 1.005 para
"1.00", já que 1.005 não tem representação exata em binário.

Closes #1847

Regra prática para decidir se precisa de corpo: se no review alguém perguntaria "por que você fez assim?", a resposta vai no corpo. O corpo não aparece no git log --oneline, mas aparece no git show, no git log completo e na página do commit no GitHub.

Conventional Commits: o padrão de commit mais adotado

Conventional Commits é uma especificação (versão 1.0.0) que dá estrutura mínima ao título:

<tipo>(<escopo opcional>): <descrição>

<corpo opcional>

<rodapé(s) opcional(is)>

Ela é muito adotada em projetos JavaScript e em times de produto, mas não é universal: o kernel Linux e o próprio Git usam outras convenções. Vale pela automação que habilita.

Quais tipos a especificação define (e de onde vêm os outros)

Um detalhe que quase todo tutorial erra: a especificação só define dois tipos, e os dois têm efeito no versionamento semântico:

  • feat: nova funcionalidade, corresponde a uma versão MINOR.
  • fix: correção de bug, corresponde a uma versão PATCH.

A própria especificação diz que outros tipos são permitidos e cita os recomendados pelo @commitlint/config-conventional, baseado na convenção do Angular. Esses não têm efeito implícito no SemVer. A lista completa que o commitlint aceita, conferida na versão 21.2.4 (novembro de 2026):

Tipo Quando usar
feat Nova funcionalidade
fix Correção de bug
docs Só documentação
style Formatação, espaços, ponto e vírgula, sem mudar lógica
refactor Mudança interna sem alterar comportamento
perf Melhoria de desempenho
test Adição ou correção de testes
build Sistema de build e dependências (npm, Vite, Dockerfile)
ci Configuração de CI (GitHub Actions, pipelines)
chore Manutenção que não entra em nenhum dos anteriores
revert Reverte um commit anterior

Repare que CI tem tipo próprio: atualizar o workflow do GitHub Actions é ci, não chore. Exemplos:

feat(pagamentos): adiciona suporte a Pix recorrente
fix(auth): corrige refresh do token quando a sessão expira
build(deps): atualiza Node.js de 22 para 24 LTS
ci: roda os testes em paralelo no GitHub Actions
refactor(usuario): extrai validação de CPF para um service
revert: desfaz "feat(pagamentos): adiciona suporte a Pix recorrente"

Breaking changes e a versão major

Mudança que quebra compatibilidade é sinalizada com ! antes dos dois-pontos ou com um rodapé BREAKING CHANGE: (em maiúsculas, é o único trecho que a especificação exige assim; BREAKING-CHANGE é sinônimo). Ela pode aparecer em commit de qualquer tipo e corresponde a uma versão MAJOR.

feat(api)!: remove o endpoint /v1/users

BREAKING CHANGE: /v1/users foi removido. Migre para /v2/users conforme
docs/migracao-v2.md.

É isso que o semantic-release usa para subir a versão major automaticamente. Eu uso os dois sinais juntos: o preset padrão do semantic-release (angular) documenta a detecção pelo rodapé BREAKING CHANGE, e o ! deixa a quebra visível no git log --oneline. Nas regras padrão dele, feat gera minor, fix e perf geram patch.

Escopo: quando vale

O escopo em feat(escopo): é opcional. Vale em monorepo ou em backend com domínios separados (checkout, estoque, auth), porque economiza leitura. Em projeto pequeno, em que tudo é um módulo só, é ruído.

Commit em português ou inglês? E qual forma verbal?

O Git recomenda, nas suas diretrizes para contribuidores, descrever a mudança no modo imperativo ("make xyzzy do frotz"), como se você desse uma ordem ao código. Em inglês isso é natural: add email validation, fix race condition. E combina com as mensagens que o próprio Git gera, como Merge branch 'feature/x' e Revert "...".

Em português, a tradução não é direta. "Adicionar validação" é infinitivo, não imperativo; o imperativo seria "adicione", que quase ninguém usa. Os times brasileiros se dividem entre o infinitivo ("adicionar validação") e a terceira pessoa do presente ("adiciona validação"). A tradução oficial da especificação para pt-br usa majoritariamente a terceira pessoa: "adiciona tradução", "corrige pequenos erros", "remove suporte para Node 6".

Minha escolha é a terceira pessoa do presente, pelo mesmo motivo do imperativo em inglês: a mensagem completa a frase "este commit...". "Este commit adiciona validação de e-mail" lê bem; "este commit adicionar" não. Se o seu time já usa infinitivo, não troque por gosto: consistência vale mais que a forma. O que evitar em qualquer caso:

  • adicionei validação de email: passado, narra o que você fez em vez do que o commit faz.
  • adicionando validação de email: gerúndio.
  • validação de email adicionada: passiva, esconde o verbo no fim.

Sobre o idioma: se o time, as issues e a documentação são em português, escreva em português; ninguém ganha nada com um inglês travado. Se o projeto é open source ou o time é internacional, inglês. O que não pode é misturar os dois no mesmo histórico.

Antipadrões que estragam o histórico

  • Commit diário: segunda, segunda 2, trabalho do dia. Histórico de mudanças não é ponto eletrônico; conta o que mudou, não quando você trabalhou.
  • Commit gigante: cinquenta arquivos em cinco assuntos. Se você não consegue resumir o commit em 50 caracteres, ele provavelmente é grande demais; quebre em partes que façam sentido sozinhas.
  • WIP permanente: wip serve para a sua branch de vida curta, não para o histórico da main. Antes do PR, junte e renomeie com git rebase -i (o passo a passo está em git merge vs rebase) ou deixe o squash merge do PR resolver.
  • Mensagem mentirosa: fix typo que refatora meio módulo. O bisect ainda acha o commit certo, mas quem lê a mensagem é levado a ignorar justamente o commit que importa.

Como travar o padrão com commitlint e Husky

O commitlint valida a mensagem contra uma configuração. O ponto que muito tutorial erra: ele roda no hook commit-msg, que recebe o arquivo com a mensagem. O guia oficial diz com todas as letras que o hook pre-commit não é suportado (ele roda antes de a mensagem existir). E instalar os pacotes não liga nada sozinho; são três passos. Rodei a sequência abaixo num repositório de teste, com Node 24, commitlint 21.2.4 e Husky 9.1.7:

npm install -D @commitlint/cli @commitlint/config-conventional husky
npx husky init
echo "npx --no -- commitlint --edit \$1" > .husky/commit-msg

E a configuração. Use a extensão .mjs: o guia mostra commitlint.config.js com export default, mas o npm init atual cria o package.json com "type": "commonjs", e nesse caso o arquivo .js quebra com SyntaxError: Unexpected token 'export'.

// commitlint.config.mjs
export default {
  extends: ['@commitlint/config-conventional'],
  rules: { 'header-max-length': [2, 'always', 72] },
};

A regra extra existe porque o limite padrão do config-conventional para o título é de 100 caracteres, folgado demais. Dois cuidados: o husky init cria também um .husky/pre-commit que roda npm test; ajuste ou apague se o projeto não tiver testes. E a configuração convencional reprova assunto começando com maiúscula ("sentence-case") e terminando em ponto. No teste, as mensagens se comportaram assim:

ajustes                          -> reprovada: type-empty, subject-empty
Chore: Configurar commitlint.    -> reprovada: type-case, type-enum, subject-case, subject-full-stop
ci: roda commitlint no pipeline  -> aprovada
feat(api)!: remove endpoint ...  -> aprovada (com rodapé BREAKING CHANGE)

Para CI, o guia oficial valida os commits do PR com npx commitlint --from <sha da base> --to <sha do head> --verbose. Também vale conhecer o Commitizen, uma CLI que pergunta tipo, escopo e descrição e monta a mensagem para você, útil enquanto o time ainda está decorando o formato.

Perguntas frequentes

Preciso seguir Conventional Commits em projeto pessoal?

Não. O benefício direto (changelog e versão automáticos) só aparece com a ferramenta configurada. Mas a parte barata vale sempre: título curto e descritivo, linha em branco, corpo com o porquê. Você mesmo, daqui a três meses, vai agradecer fix(parser): aceita datas no formato DD/MM em vez de fix.

Qual o tamanho ideal de uma mensagem de commit?

Título com até 50 caracteres como alvo e 72 como teto; corpo quebrado em 72 colunas, com o tamanho que a explicação pedir. A maioria dos commits não precisa de corpo; os que mudam comportamento, corrigem bug sutil ou tomam uma decisão não óbvia precisam.

Como corrigir a mensagem do último commit?

git commit --amend -m "nova mensagem" reescreve o último commit. Se ele ainda não foi enviado ao remoto, pode usar sem medo. Se já foi, você está reescrevendo histórico publicado e vai precisar de push forçado; numa branch compartilhada, prefira deixar como está. Mensagens de commits mais antigos se corrigem com git rebase -i e reword, e os outros jeitos de voltar atrás estão em como desfazer alterações no Git.

Como adotar o padrão no time sem gerar atrito?

Comece pelo CI: valide os commits do PR com commitlint e mostre o erro na revisão, sem bloquear o commit local. Quando o time estiver acostumado, ligue o hook commit-msg com Husky. Introduza a convenção na revisão primeiro e automatize depois; bloquear o commit de todo mundo no primeiro dia é o caminho mais curto para alguém rodar git commit --no-verify para sempre. Se o time também está definindo o fluxo de branches, veja estratégias de branching no Git.

Escreva para quem vai ler o histórico daqui a um ano

Título curto no presente, linha em branco, corpo com o porquê, tipo do Conventional Commits na frente quando o projeto automatiza versão. Lembre que a especificação só define feat e fix; o resto é convenção do commitlint, e BREAKING CHANGE é o que sobe a major. Trave o padrão no hook certo (commit-msg), comece pelo CI e escolha uma forma verbal para o time inteiro. Com o histórico organizado, gerar o changelog e documentar o projeto fica quase de graça, como mostro em documentar projetos no GitHub.

O conteúdo editorial termina aqui. Quando quero conferir se a mensagem do commit bate com o que mudou num arquivo fora do repositório, como uma config de produção antes e depois, colo as duas versões no Comparador de Texto do Quick Tools e vejo as diferenças lado a lado.

RD
Autor
Rafael Duarte
Desenvolvedor backend com passagem por fintech e SaaS B2B — trabalhou em times que escalaram APIs de zero a milhões de requisições. Carrega cicatrizes de produção suficientes para ter opiniões fortes sobre ferramentas, padrões e decisões de arquitetura. Não é acadêmico: leu a RFC do UUID quando precisou escolher entre v4 e v7 para uma tabela de alta escrita.
Ver perfil