107 commits. Conventional commits impecáveis desde o primeiro dia. Feat, fix, refactor, chore — tudo perfeitamente etiquetado. E o CHANGELOG? Vazio. Inexistente. Um arquivo que “já vou escrever amanhã” durante dois meses.
Se isso soa familiar, você não está sozinho. Escrever um changelog manualmente é um saco de categoria olímpica. Não é que seja difícil — é que é tedioso, repetitivo, e sempre há algo mais urgente para fazer. E justamente por isso existe o git-cliff.
O que é git-cliff (em 30 segundos)
É um gerador de changelogs escrito em Rust que lê seus commits do git, os processa seguindo conventional commits, e produz um CHANGELOG.md agrupado por versão e tipo. Sem dependências estranhas, sem plugins, sem magia negra. Um binário, um arquivo de configuração, e pronto.
Em outras palavras: você dá seus commits e ele te devolve o arquivo que você vem adiando há meses.
| |
Essas duas linhas são literalmente tudo que você precisa para começar. Se seus commits seguem a convenção tipo: descrição, o git-cliff os entende sem configuração adicional.
O caso real: changelog retroativo para 8 versões
Em Tokamak (meu app de menu bar para monitorar a cota do Claude) eu tinha exatamente esse problema: 107 commits perfeitos e um CHANGELOG em branco. O app já estava na v1.3.0 mas só existia uma tag v0.1 do começo dos tempos.
O plano era simples:
Passo 1: Criar tags retroativas nos commits de cada versão.
| |
Passo 2: Executar git-cliff.
| |
Resultado: um CHANGELOG.md completo com 8 versões, cada uma com suas features, bug fixes, refactors e chores agrupados. 189 linhas. 30 segundos.
A configuração: cliff.toml
Git-cliff usa um arquivo TOML com duas seções principais: [changelog] para o formato de saída e [git] para como interpretar os commits.
Esta é a configuração que uso:
| |
Três coisas a notar:
trim_start_matches(pat="v")— As tags sãov1.3.0mas no changelog quero1.3.0. Este filtro do Tera faz isso.filter_unconventional = true— Descarta commits que não seguem a convenção. Se você tem um repo com commits antigos tipo “fixed stuff”, ponha comofalsee adicione um parser catch-all:{ message = ".*", group = "Other" }.sort_commits = "oldest"— Os commits dentro de cada grupo vão em ordem cronológica. Para ordem inversa:"newest".
Templates: a linguagem que você não sabia que precisava
O body usa Tera, um motor de templates inspirado no Jinja2. A curva de aprendizado não é zero, mas também não é Haskell. Alguns filtros úteis:
| Filtro | O que faz | Exemplo |
|---|---|---|
trim_start_matches | Remove prefixo | v1.0 → 1.0 |
upper_first | Capitaliza | features → Features |
split + first | Primeira linha | Ignora corpo do commit |
date | Formata data | %Y-%m-%d |
group_by | Agrupa | Por tipo de commit |
O truque mais útil: commit.message | split(pat="\n") | first extrai apenas a primeira linha da mensagem do commit. Se você escreve corpos longos em seus commits (e deveria), isso evita que o changelog se torne um romance.
Pegadinha: o whitespace do Tera
Este foi o que me custou mais tempo. Os templates do Tera são sensíveis ao whitespace. Uma quebra de linha a mais ou a menos no template e seu changelog sai com espaços estranhos ou com seções grudadas.
As chaves:
{%-(com hífen): elimina o whitespace antes da tag-%}(com hífen): elimina o whitespace depois da tagtrim = trueem[changelog]: elimina whitespace de cada linha gerada
O problema é que trim = true também remove as quebras de linha que você quer — como a separação entre versões. A solução é deixar um {% endfor %} sem hífen no final do loop exterior, para que o Tera emita a quebra extra.
Não é exatamente intuitivo. Mas uma vez que funciona, funciona.
Casos de uso que não são óbvios
Git-cliff não é apenas “gera um CHANGELOG e pronto”. Há usos que valem a pena conhecer:
Gerar apenas a última versão
| |
Perfeito para release notes. Em vez do changelog completo, só te dá a seção da versão mais recente. Ideal para colar na descrição de um release do GitHub ou Gitea.
Preview sem tag
| |
Gera a seção de uma versão que você ainda não taggeou. Útil para revisar o que vai incluir seu próximo release antes de criá-lo.
Bump automático
| |
Git-cliff analisa os commits desde a última tag e te diz qual versão deveria ser a próxima segundo semver: se só há fix, sobe o patch; se há feat, sobe o minor; se há breaking changes, sobe o major.
Changelog por range
| |
Apenas os commits entre duas tags. Útil para gerar as notas de um hotfix ou para auditar o que mudou entre duas versões específicas.
Monorepos
Se seu repo tem vários projetos, o git-cliff suporta --include-path para filtrar commits que afetam um caminho específico:
| |
Cada subprojeto pode ter seu próprio cliff.toml e seu próprio changelog.
Integrar no seu fluxo (sem esquecer)
O maior risco do git-cliff não é técnico — é humano. Você vai esquecer de executá-lo. Eu sei porque já aconteceu comigo.
A solução mais pragmática: integrá-lo como dependência do passo que você vai se lembrar de fazer. No meu caso, o Makefile:
| |
Agora make archive regenera o changelog automaticamente antes de compilar para release. Não preciso me lembrar de nada — quando vou publicar, o changelog se atualiza sozinho.
Outras opções razoáveis:
- GitHub Actions:
git cliff --latestno workflow de release para gerar as notas automaticamente. - Pre-push hook: regenerar antes de cada push. Mais agressivo, mais barulhento.
- Post-tag hook: regenerar ao criar uma tag. O mais lógico semanticamente, mas git não tem um hook nativo para tags (precisaria de um wrapper).
Minha recomendação: amarre o changelog ao passo de release. É onde realmente importa. Um changelog desatualizado no main entre releases não importa para ninguém.
Conventional commits: o requisito prévio
Tudo isso assume que seus commits seguem a convenção tipo(scope): descrição:
feat: add login with OAuth
fix: prevent crash on empty response
refactor: extract auth logic to separate module
chore: update dependencies
docs: add API reference
test: add edge cases for parser
Se seus commits são do tipo “wip”, “stuff”, “asdf” e “fix things maybe”, o git-cliff não vai fazer milagres. Lixo entra, lixo sai.
Mas a boa notícia é que você não precisa de ferramentas para fazer conventional commits. É apenas uma convenção de nomenclatura. Isso sim, se você quer validação, commitlint ou um pre-commit hook simples garantem que ninguém (incluindo você às 3 da manhã) pule o formato.
Por que git-cliff e não outra coisa
Há alternativas: standard-version, semantic-release, conventional-changelog, release-please. Todas funcionam. Mas o git-cliff tem três vantagens que para mim são decisivas:
É um binário.
brew installe funciona. Sem Node, sem Python, sem ecossistema. Em um projeto Swift como o meu, não quero adicionarpackage.jsonsó pelo changelog.É rápido. 120 milissegundos para 10.000 commits (segundo benchmarks reais). As alternativas baseadas em Node levam 30 segundos para o mesmo repo.
O template é seu. Se quer emojis, você põe. Se quer links para issues, você põe. Se quer o changelog em YAML, pode. O motor Tera te dá controle total sobre o output.
O resultado
De 0 a changelog completo em menos de 5 minutos:
## 1.3.0 — 2026-02-22
### Features
- QuotaFetchStrategy pipeline with fallback for resilient quota fetching
- semi-automatic screenshot capture for App Store (8 locales × 3 scenarios)
### Bug Fixes
- attack phrase wrapping in exhausted view (TOK-115)
- resolve all notarization signing issues (TOK-93/94/95/96)
### Refactor
- QuotaProvider protocol for multi-provider support (TOK-53)
## 1.2.0 — 2026-02-22
...
8 versões, 107 commits, agrupados por tipo, com datas e referências a issues. Sem escrever uma linha sequer manualmente.
Da próxima vez que alguém perguntar “o que mudou na última versão?”, você não vai ter que vasculhar entre commits nem improvisar de memória. Vai ter um arquivo. Gerado automaticamente. Que se atualiza sozinho quando você publica.
E se algum dia mudar de opinião sobre o formato, muda o template. Os commits — seus commits de sempre — são a fonte da verdade. O git-cliff só os apresenta de forma bonita.