引言
在版本控制管理中,开发者常面临两个核心诉求:
第一,主分支历史必须是线性的。git log 展示的是一条时间线,而非复杂的网状结构。
第二,每个提交都应由本人签名。GitHub 上应显示绿色 Verified,而非灰色的 Unverified。
然而,GitHub 提供了三个合并按钮:Merge、Squash、Rebase。选择这些选项实际上是在决定签名以何种方式被处理。
签名机制的本质
在讨论合并策略前,需明确一点:签名签的不是「改动内容」,而是「commit 对象本身」。
commit 对象包含父提交、作者、提交者、时间戳等元数据。任何字段的变动都会导致 SHA 变化,进而使旧签名失效。这意味着你签的是数据的'身份证',一旦对象重建,原签名即作废。
三种合并策略分析
1. Create a merge commit
这是最传统的方式。它不重写 PR 分支上的提交,SHA 不变,签名大概率保留。代价是主分支出现 merge 节点,历史不再线性。
2. Squash and merge
此方式将 PR 中的多个提交压缩为一个新提交,主分支保持线性。虽然显示 Verified,但这通常是 GitHub 为新创建的对象重新签署的,原有的提交签名并未保留进主分支。
3. Rebase and merge
这种方式看似完美满足线性需求,将 PR 中的提交在 main 顶端重放。但重放会导致所有 commit 的 SHA 改变,原有签名直接作废。由于 GitHub 没有用户的私钥,无法自动重签,只能显示 Unverified。
理想方案与妥协
要同时满足线性历史和保留原始签名,关键在于不重写对象,只做 fast-forward。
Fast-forward 仅移动指针,不创建新 commit,SHA 不变,签名自然保留。但 GitHub 的 UI 中缺少直接的 Fast-forward only 按钮。
路线 A:线性 + 签名都保住
维护者在本地执行 --ff-only 合并,将 main 快进到 PR 分支头,再 push main。这需要手动操作,但能确保签名有效。
路线 B:签名优先,线性放宽
使用 merge commit。提交原封不动进入 main,签名不会失效,但历史会有 merge 节点。
路线 C:线性优先,签名当装饰品
使用 squash。主分支超级线性,GitHub 提供 Verified 标记(实为代签),但丢失了中间提交的历史上下文。
总结
GitHub 的合并策略往往用'重写对象'换取线性,用'无私钥'导致签名失效。想要线性历史又想要自己 signed commits 的 Verified,需要绕开默认的 Rebase and merge 按钮,采用更底层的合并方式或接受一定的妥协。


