107 个提交。从第一天开始就是完美的约定式提交。Feat、fix、refactor、chore — 所有内容都完美标记。那么 CHANGELOG 呢?空的。不存在。一个"明天再写"的文件,已经拖了两个月。
如果这听起来很熟悉,你不是一个人。手动编写变更日志是奥林匹克级别的苦差事。不是说它难 — 而是它乏味、重复,总有更紧急的事情要做。这就是为什么 git-cliff 存在的原因。
什么是 git-cliff(30 秒版本)
这是一个用 Rust 编写的变更日志生成器,它读取你的 git 提交,根据约定式提交解析它们,然后输出按版本和类型分组的 CHANGELOG.md。没有奇怪的依赖,没有插件,没有黑魔法。一个二进制文件,一个配置文件,就完了。
简单来说:你给它你的提交,它返回你拖延了几个月的文件。
| |
这两行字面上就是开始所需的全部。如果你的提交遵循 类型: 描述 约定,git-cliff 无需额外配置就能理解它们。
真实案例:8 个版本的回溯性变更日志
在 Tokamak(我的监控 Claude 配额的菜单栏应用)中,我正好遇到了这个问题:107 个完美提交和一个空白的 CHANGELOG。应用已经是 v1.3.0 版本,但只有一个开始时的 v0.1 标签。
计划很简单:
步骤 1:在每个版本的提交上创建回溯性标签。
| |
步骤 2:运行 git-cliff。
| |
结果:一个包含 8 个版本的完整 CHANGELOG.md,每个版本都有其功能、bug 修复、重构和杂务分组。189 行。30 秒。
配置:cliff.toml
Git-cliff 使用一个 TOML 文件,有两个主要部分:[changelog] 用于输出格式,[git] 用于如何解释提交。
这是我使用的配置:
| |
需要注意三点:
trim_start_matches(pat="v")— 标签是v1.3.0,但在变更日志中我想要1.3.0。这个 Tera 过滤器做到了这一点。filter_unconventional = true— 丢弃不遵循约定的提交。如果你有一个包含旧式提交(如"fixed stuff")的仓库,将其设置为false并添加一个全捕获解析器:{ message = ".*", group = "Other" }。sort_commits = "oldest"— 每组内的提交按时间顺序排列。要反序:"newest"。
模板:你不知道自己需要的语言
body 使用 Tera,一个受 Jinja2 启发的模板引擎。学习曲线不是零,但也不是 Haskell。一些有用的过滤器:
| 过滤器 | 功能 | 示例 |
|---|---|---|
trim_start_matches | 删除前缀 | v1.0 → 1.0 |
upper_first | 首字母大写 | features → Features |
split + first | 第一行 | 忽略提交正文 |
date | 格式化日期 | %Y-%m-%d |
group_by | 分组 | 按提交类型 |
最有用的技巧:commit.message | split(pat="\n") | first 只提取提交消息的第一行。如果你在提交中写长正文(你应该这样做),这可以防止变更日志变成小说。
陷阱:Tera 的空白字符
这是最耗费我时间的。Tera 模板对空白字符敏感。模板中多一个或少一个换行符,你的变更日志就会出现奇怪的空隙或部分粘在一起。
关键点:
{%-(带破折号):删除标签之前的空白字符-%}(带破折号):删除标签之后的空白字符[changelog]中的trim = true:删除每个生成行的空白字符
问题是 trim = true 也会吃掉你确实想要的换行符 — 比如版本之间的分隔。解决方案是在外层循环结尾留一个没有破折号的 {% endfor %},让 Tera 输出额外的换行符。
不是很直观。但一旦工作起来,就一直工作。
不明显的用例
Git-cliff 不只是"生成一个 CHANGELOG 就完了"。有一些值得了解的用途:
只生成最新版本
| |
非常适合发布说明。不是完整的变更日志,只给你最新版本的部分。非常适合粘贴到 GitHub 或 Gitea 发布描述中。
无标签预览
| |
生成你还没有标记的版本部分。在创建之前预览下一个发布将包含什么内容很有用。
自动版本号递增
| |
Git-cliff 分析自上次标签以来的提交,并根据semver告诉你下一个版本应该是什么:如果只有fix,递增patch;如果有feat,递增minor;如果有破坏性变更,递增major。
范围变更日志
| |
只有两个标签之间的提交。对生成热修复说明或审计两个特定版本之间的变更很有用。
单仓库多项目
如果你的仓库有多个项目,git-cliff 支持 --include-path 来过滤影响特定路径的提交:
| |
每个子项目可以有自己的 cliff.toml 和自己的变更日志。
集成到你的工作流程(不要忘记)
Git-cliff 最大的风险不是技术性的 — 是人为的。你会忘记运行它。我知道因为这发生在我身上了。
最实用的解决方案:将其作为你肯定会记得做的步骤的依赖集成。在我的情况下,Makefile:
| |
现在 make archive 在编译发布版之前自动重新生成变更日志。我不需要记住任何事情 — 当我要发布时,变更日志自己更新。
其他合理选项:
- GitHub Actions:在发布工作流程中
git cliff --latest自动生成说明。 - Pre-push hook:每次推送前重新生成。更激进,更嘈杂。
- Post-tag hook:创建标签时重新生成。语义上最合理,但 git 没有标签的原生钩子(需要包装器)。
我的建议:将变更日志绑定到发布步骤。那才是真正重要的地方。发布之间 main 分支上过时的变更日志没人在乎。
约定式提交:先决条件
所有这些都假设你的提交遵循 类型(范围): 描述 约定:
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
如果你的提交是"wip"、“stuff”、“asdf"和"fix things maybe"类型,git-cliff 不会创造奇迹。垃圾进,垃圾出。
但好消息是你不需要工具来做约定式提交。这只是一个命名约定。不过,如果你想要验证,commitlint或简单的pre-commit hook可以确保没有人(包括凌晨 3 点的你)跳过格式。
为什么是 git-cliff 而不是其他
有替代方案:standard-version、semantic-release、conventional-changelog、release-please。它们都有效。但 git-cliff 有三个对我来说决定性的优势:
它是一个二进制文件。
brew install就能工作。没有 Node,没有 Python,没有生态系统。在像我的 Swift 项目中,我不想只为了变更日志添加package.json。它很快。10,000 个提交 120 毫秒(根据真实基准测试)。基于 Node 的替代方案对相同仓库需要 30 秒。
模板是你的。如果你想要表情符号,就加上。如果你想要 issue 链接,就加上。如果你想要 YAML 格式的变更日志,可以。Tera 引擎给你对输出的完全控制。
结果
从 0 到完整变更日志,不到 5 分钟:
## 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 个版本,107 个提交,按类型分组,带有日期和 issue 引用。没有手写一行。
下次有人问你"最新版本改变了什么?",你不需要在提交中搜索或凭记忆即兴发挥。你会有一个文件。自动生成的。在你发布时自己更新。
如果哪天你改变了对格式的想法,就改变模板。提交 — 你一如既往的提交 — 是真相的来源。git-cliff 只是把它们呈现得漂亮。