[控场AI]
· 3 分钟阅读· 1,965 字

Git变更日志生成器踩坑:合并提交掩盖了真相

Git变更日志生成器踩坑:合并提交掩盖了真相

git变更日志生成器因线性假设丢失合并引入的提交,揭示git历史是DAG而非列表。

作者构建了一个基于diff两个commit的git变更日志生成器,第一版直接遍历`git log A..B`,在标准PR工作流下只能看到合并提交,却看不到被合并的feature提交,导致发布说明丢失实质内容。修复方案是用`git log --first-parent`获取分支级摘要,再对每个合并提交用`git log ^parent1 parent2`展开真实提交并去重。文章进一步指出`A..B`的语义是“从B可达但不从A可达”,会包含已revert的提交,不等于实际发布内容。核心教训是git历史是DAG,任何线性假设的工具都会在无声中丢信息,图才是事实来源。

一个看似简单的需求,却暴露了git的本质

作者构建了一个git变更日志生成器,通过diff两个commit来输出结构化的发布说明。第一版实现直接遍历git log A..B,把每个提交当作直线上的一个点处理——干净、确定性、但错了。

问题出在采用标准PR工作流的仓库上:对main分支执行git log A..B时,能看到合并提交,却看不到这些合并所引入的各个feature提交。生成的变更日志只会写一句"Merge pull request #42",读者对实际发布了什么毫无感知。

git changelog generator

这个bug的隐蔽性在于:工具本身运行正常,输出格式也正确,但信息在悄无声息中被吞掉了。直到有人读发布说明时问出"v2.3到底改了什么?",问题才会浮出水面。

修复的关键不是更大的diff

作者的修复思路很有启发性——问题不在于diff范围不够大,而在于遍历的对象选错了。具体做法分两层:

  • 用git log --first-parent A..B获取分支级别的摘要,把每个合并提交当作一个结构性事件
  • 对每个合并提交,再用git log ^parent1 parent2枚举该合并真正引入的提交

最终输出先呈现合并事件,再展开其下的真实提交,并与基线已有内容去重。这样读者既能看到结构,也能看到实质内容。

为什么A..B会骗人

原文指出了一个容易被忽视的语义陷阱:git log A..B的含义是"从B可达但不从A可达"。在图上,这个集合包含了那些被合并、后来又被revert的分支上的提交——它们"可达",但不代表"已发布"。

如果想要的是"实际交付了什么",需要的是:从B可达、减去从A可达、与first-parent边取交集,再加上每次合并拖进来的侧分支提交。作者直言这不是一行命令能解决的,而是一个小型图遍历。

--first-parent 是 git log 的一个遍历选项,它只沿着每个合并提交的第一个父提交向下走。在标准的 GitHub/GitLab PR 工作流中,第一个父提交通常对应目标分支(如 main),第二个父提交对应被合并的 feature 分支。因此 git log --first-parent 天然地跳过所有侧分支提交,只把合并提交本身当作里程碑,从而给出“分支级别”的线性摘要。这适合用来回答“主线发生了什么大事”,但它不是完整的发布清单——被合并进来的具体提交全都被折叠掉了。

与之相对,git log ^parent1 parent2 是 Rev 范围语法的一种写法:^parent1 排除从第一个父提交可达的所有提交,parent2 表示从第二个父提交可达的集合。两者相减,就得到“只在 feature 分支上、尚未进入主线的提交”。对每个合并提交做一次这样的枚举,就能把被合并拖进来的真实提交全部找出来。这正是原文修复方案的核心机制。

更深层的教训:历史是DAG

git历史是一个有向无环图,不是列表。任何假设线性结构的工具都在对用户撒谎,而且这个谎言在你读发布说明之前是不可见的。图才是事实来源,--first-parent只是一个视图,把两者都走一遍再对账,才是诚实的答案。

这个案例的普适价值在于:很多围绕git构建的工具(变更日志、影响分析、版本对比)都容易掉进线性假设的坑。表面上跑得通,实际上丢信息。作者的做法是坚持确定性——没有用LLM去"重写"历史,只是让图做图该做的事。

在 git 的底层模型中,每个提交都是一条记录,包含一个树对象(代表当时的目录快照)和零个或多个父提交。没有父提交的是根提交,有两个或以上父提交的是合并提交。所谓“可达”,是指从某个引用出发,沿着父提交链不断回溯所能到达的所有提交的集合。A..B 就是“从 B 可达”与“从 A 可达”两个集合的差集。

之所以会出现“被 revert 的提交仍被包含”的现象,是因为 revert 只是新增了一个反向补丁的提交,原提交依然存在于历史图中,依然从 B 可达。可达 ≠ 生效,也 ≠ 已发布。理解这一点,就能明白为什么仅靠 A..B 无法回答“实际交付了什么”:它描述的是图的拓扑关系,而不是发布语义。

对工具开发者的启示

如果你在构建任何依赖git历史的工具,有两个检查点值得提前确认:默认的A..B语义是否符合你的"发布"定义;合并提交在你的输出中是被当作噪声忽略、还是被当作结构信号处理。这两个决定会直接影响最终用户对"改了什么"的理解质量。

#git changelog generator#git log first-parent#合并提交#git DAG#变更日志生成#git历史遍历
分享:

相关推荐