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.
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:
- Entender uma regressão. O
git bisectencontra o commit que introduziu o bug pelo resultado dos testes (você marca cada versão comogoodoubad), 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-3já dá o contexto antes de abrir o diff. - Gerar changelog e versão automaticamente. Ferramentas como
semantic-releaseeconventional-changelogleem 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. - Review assíncrono e
git blame. Quem revisa o PR em outro fuso, ou quem chega ao código daqui a um ano pelogit 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 --onelinevira 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, ogit rebase -ie 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) ouBREAKING 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:
wipserve para a sua branch de vida curta, não para o histórico damain. Antes do PR, junte e renomeie comgit rebase -i(o passo a passo está em git merge vs rebase) ou deixe o squash merge do PR resolver. - Mensagem mentirosa:
fix typoque 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.
- 01 Base64 online seguro: ferramentas que não enviam seus dados Como saber se uma ferramenta de Base64 online processa no navegador ou no servidor, quais opções gratuitas usar para tokens e arquivos e quando evitar sites.
- 02 Como decodificar JWT em JavaScript sem biblioteca Leia header e payload de um JWT com atob e TextDecoder, sem dependências. Veja por que decodificar no navegador é seguro e por que validar lá não é.