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

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".

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, 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:

  1. Debugando uma regressão. git bisect te leva até o commit causador. Se a mensagem for "fix", você ainda não sabe nada. Se for fix(auth): corrigir loop infinito quando token JWT expira com fuso UTC-3, você já entende o contexto antes de abrir o diff.

  2. Gerando changelogs automáticos. Ferramentas como conventional-changelog e semantic-release leem o histórico para gerar notas de versão e calcular semver. Sem estrutura nas mensagens, não tem automação.

  3. 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 email
  • corrigir race condition no worker de filas
  • remover feature flag de A/B test encerrado

Errado:

  • adicionei validação de email — passado
  • adicionando validação de email — gerúndio
  • validaçã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.

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