Como escrever boas mensagens de commit no Git
Conventional Commits, modo imperativo, título vs. corpo — como transformar o histórico do Git em documentação útil em vez de um cemitério de "ajustes".
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, quando, nem por quê. Quando precisar entender por que uma função mudou três meses atrás, você vai ler código de um commit por vez e adivinhar a intenção de quem não está mais no time.
Mensagem de commit ruim é dívida técnica que você paga com juros na forma de tempo perdido em git blame.
Por que a mensagem de commit importa mais do que parece
Commit não é só um mecanismo de save. É documentação com timestamp. O histórico do Git é a linha do tempo de decisões de um projeto — e como toda documentação, é inútil quando mal escrita.
Três situações onde uma boa mensagem de commit salva horas:
Debugando uma regressão.
git bisectte leva até o commit causador. Se a mensagem for "fix", você ainda não sabe nada. Se forfix(auth): corrigir loop infinito quando token JWT expira com fuso UTC-3, você já entende o contexto antes de abrir o diff.Gerando changelogs automáticos. Ferramentas como
conventional-changelogesemantic-releaseleem o histórico para gerar notas de versão e calcular semver. Sem estrutura nas mensagens, não tem automação.Code review assíncrono. Em times distribuídos, o revisor pode estar em outro fuso. Uma mensagem bem escrita reduz perguntas no PR e acelera o merge.
Se você ainda está estruturando o básico do Git — branches, merge, como funciona o staging area — tem um artigo mais introdutório aqui: Git para iniciantes: commits, branches e merge. Este aqui assume que você já usa Git e quer usar melhor.
O formato Conventional Commits
Conventional Commits é uma especificação que define uma estrutura mínima para mensagens de commit. Não é obrigatório, mas é o padrão de facto em projetos open source sérios e em boa parte dos times de produto.
A estrutura é:
<tipo>(<escopo opcional>): <descrição curta>
<corpo opcional>
<rodapé opcional>
Tipos principais
| Tipo | Quando usar |
|---|---|
feat |
Nova funcionalidade visível ao usuário |
fix |
Correção de bug |
refactor |
Mudança interna sem alterar comportamento externo |
chore |
Tarefas de manutenção: CI, dependências, configs |
docs |
Apenas documentação |
test |
Adição ou correção de testes |
perf |
Melhoria de performance |
style |
Formatação, espaços, ponto e vírgula — sem lógica |
Exemplos reais:
feat(payments): adicionar suporte a Pix recorrente
fix(auth): corrigir refresh de token quando sessão expira idle
chore(deps): atualizar Node.js de 20 para 22 LTS
refactor(user): extrair validação de CPF para service separado
Breaking changes
Quando uma mudança quebra compatibilidade com versões anteriores, sinalize com ! depois do tipo ou com BREAKING CHANGE: no rodapé:
feat(api)!: remover endpoint /v1/users em favor de /v2/users
BREAKING CHANGE: O endpoint /v1/users foi removido. Migre para /v2/users
conforme documentado em docs/migration-v2.md
Isso é o que ferramentas como semantic-release usam para pular o major version automaticamente.
Modo imperativo: por que escrever assim
A convenção para a linha de descrição é o modo imperativo — como se você estivesse dando uma ordem ao codebase:
Certo:
adicionar validação de emailcorrigir race condition no worker de filasremover feature flag de A/B test encerrado
Errado:
adicionei validação de email— passadoadicionando validação de email— gerúndiovalidação de email adicionada— passivo
Por que imperativo? Porque o Git usa o mesmo estilo internamente: Merge branch 'feature/x', Revert "feat: add user auth". Quando o seu commit está no histórico ao lado de mensagens do próprio Git, consistência visual importa. E tem uma leitura que faz sentido: este commit, se aplicado, vai adicionar validação de email.
Título vs. corpo: quando usar os dois
A linha de título tem limite recomendado de 72 caracteres. Para a maioria dos commits, é suficiente. Mas há casos onde o porquê da mudança não cabe em uma linha — e é exatamente isso que o corpo serve para explicar.
Regra prática: se você precisaria responder "por que você fez isso assim?" no code review, coloque no corpo do commit.
fix(billing): arredondar valor de fatura para 2 casas decimais
JavaScript usa ponto flutuante IEEE 754, o que causa erros de arredondamento
em somas de múltiplos itens (ex: 0.1 + 0.2 = 0.30000000000000004).
Usado Math.round(value * 100) / 100 em vez de toFixed(2) porque toFixed
tem comportamento inconsistente entre engines para casos de .5.
Refs: #1847, MDN Float Precision
Esse corpo não vai aparecer no git log --oneline, mas vai aparecer quando alguém fizer git show <hash> ou abrir o commit no GitHub. É documentação de decisão — o tipo mais valioso de comentário que existe, porque é datado e está vinculado à mudança que motivou.
Separação: sempre deixe uma linha em branco entre o título e o corpo. Ferramentas como git log, git rebase e o GitHub dependem disso para renderizar corretamente.
Escopo: usar ou não usar
O escopo em feat(escopo): é opcional e vale quando o projeto tem módulos ou áreas claramente distintas. Em um monorepo ou em um backend com domínios separados, o escopo economiza tempo de leitura:
feat(checkout): adicionar cálculo de frete por CEP
fix(inventory): corrigir contagem negativa em estorno
chore(auth): rotacionar chave de signing do JWT
Em projetos pequenos ou onde tudo fica num módulo só, escopo pode ser ruído. Use quando acrescentar contexto real, não por obrigação.
O que não fazer: antipadrões comuns
Commits estilo diário: segunda, segunda 2, trabalho do dia — não é histórico de atividade, é histórico de mudanças. O que mudou, não quando você trabalhou.
Commits giant: um commit que toca cinquenta arquivos em cinco contextos diferentes. Se você tem dificuldade de escrever uma mensagem que descreva o commit em 72 caracteres, provavelmente o commit está grande demais. Quebre em pedaços que fazem sentido individual.
WIP como commit permanente: usar wip como mensagem de commit em uma branch que vai ser mergeada no main diretamente. WIP é aceitável em branches de feature de vida curta, não no histórico permanente.
Mensagens mentirosas: fix typo que na verdade refatora metade de um módulo. Além de confundir, atrapalha git bisect — quando você está caçando um bug por bissecção binária, a mensagem imprecisa te leva ao commit errado.
Ferramentas que ajudam
Commitlint: valida se as mensagens seguem Conventional Commits no pre-commit hook. Instala em segundos com Husky e para de aceitar "ajustes finais" no repositório.
npm install --save-dev @commitlint/cli @commitlint/config-conventional husky
Commitizen: CLI interativo que guia o dev pelo formato em vez de escrever na mão. Útil para times que estão adotando a convenção.
conventional-changelog: gera CHANGELOG.md automaticamente a partir do histórico. Vale a pena quando você já tem disciplina de commit — gera changelogs legíveis sem esforço manual.
Quando estou revisando um PR antes de aprovar, uso o Comparador de Texto para passar o olho no diff com mais calma — especialmente em mudanças grandes onde o contexto da mensagem de commit precisa bater com o que está no código.
Perguntas frequentes
Preciso seguir Conventional Commits em projetos pessoais?
Não é obrigatório. Mas mesmo em projetos solo, mensagens descritivas custam cinco segundos a mais e salvam horas quando você volta ao projeto depois de três meses. O que você vai preferir ler: fix ou fix(parser): corrigir parsing de datas no formato DD/MM? O benefício existe mesmo sem CI ou changelog automático.
Qual o tamanho ideal de uma mensagem de commit?
O título deve ter no máximo 72 caracteres — limite que garante legibilidade no terminal (git log) e no GitHub sem truncar. Para a descrição curta, 50 caracteres é um bom alvo (a maioria das interfaces exibe bem até aí). O corpo não tem limite formal, mas textos de 3-5 parágrafos já são mais do que suficiente para qualquer contexto.
Como corrigir a mensagem do último commit sem criar um novo?
git commit --amend -m "mensagem corrigida"
Isso reescreve o commit. Só use se o commit ainda não foi enviado ao remote — depois do push, --amend reescreve o histórico e vai causar divergência com a branch remota.
É possível forçar Conventional Commits no time sem gerar atrito?
A abordagem menos atritosa é começar com lint apenas nos CI pipelines (falha silenciosa em PR, não bloqueia o dev local), e depois migrar para pre-commit hook quando o time estiver confortável. Bloquear commits locais de entrada é o caminho mais rápido para resistência — introduce a convenção primeiro via revisão cultural, depois automatize.
Histórico de commits é decisão arquitetural comprimida
Cada commit bem escrito é uma decisão de produto ou de engenharia que vai poder ser entendida meses depois sem precisar interromper ninguém. Em times grandes, isso reduz ruído de comunicação. Em projetos open source, é o que permite contribuidores entenderem o projeto sem acesso direto aos autores originais.
O investimento é mínimo — alguns segundos a mais por commit. O retorno se acumula toda vez que alguém precisa entender o histórico de uma mudança e encontra contexto em vez de silêncio.
- 01 Como os LLMs geram respostas: tokens, predição e sampling explicados Tokenização, predição autorregressiva, temperatura e Top-P: a mecânica interna de como modelos de linguagem transformam um prompt em texto.
- 02 O que é inteligência artificial generativa LLMs geram texto token a token via predição estatística — sem plano, sem "saber". Entenda como funciona, onde performa bem e por que alucina.