bigywater.tech
Git · 51

cherry-pick 与 patch-id

cherry-pick 摘过去的提交和原来的是同一个提交吗?git 怎么知道两个提交是“同一个补丁”?这篇验证 cherry-pick 的三方合并本质,算 patch-id,再看 git cherry 和 log --cherry-pick 依据什么判断,以及哪些情况会让 patch-id 失效。

b2ad9fde…
同一补丁摘到别处,原提交与新提交的 patch-id 完全相同(行号从 10 变成 12)
+ / −
git cherry:+ 表示上游还没有等价补丁,- 表示已经有
手工合并
冲突后手工解决的结果与原补丁的 patch-id 不同,git cherry 认不出

cherry-pick 做了什么

实测 · 本机执行输出
  $ git cherry-pick -x 595215f35d377425cb3635144db29426ffa25682 2>&1 | head -2
  Auto-merging f.txt
  [main 2128741] 修复:第 10 行
  $ git log -1 --format='%s%n  author=%an committer=%cn%n  parent: %p' | sed -E 's/[0-9a-f]{7}/<sha>/'; git log -1 --format=%b | sed -E 's/[0-9a-f]{40}/<sha40>/'
  修复:第 10 行
    author=Alice committer=Bob
    parent: <sha>
  (cherry picked from commit <sha40>)
  原提交 parent 是 基础提交,新提交 parent 是 main 的上一个提交;两个提交的 tree 不同(新的带有 main 加的两行): True

把一个提交的改动在当前分支上再做一遍:新提交保留原作者和说明,提交者换成执行者,parent 不同,hash 当然不同;-x 在说明里记下来源。

就是三方合并

实测 · 本机执行输出
  merge-tree 算出的 tree: 8d786e2a105f   cherry-pick 之后 HEAD 的 tree: 8d786e2a105f   相同: True

基础是那个提交的 parent,ours 是当前 HEAD,theirs 是那个提交,结果 tree 配上原作者和说明做成新提交。用 git merge-tree 手工做同样的合并,tree 与 cherry-pick 的结果完全一致。

patch-id

实测 · 本机执行输出
  原提交的 patch-id  : b2ad9fde2a9c9a6afc8e26dd60cd97206555b913
  新提交的 patch-id  : b2ad9fde2a9c9a6afc8e26dd60cd97206555b913
  相同: True(hash 不同: True;行号不同: 原来改的是第 10 行,新提交里变成第 12 行)
  $ git show HEAD | grep "^@@"; git show 595215f35d377425cb3635144db29426ffa25682 | grep "^@@"
  @@ -9,7 +9,7 @@
  @@ -7,7 +7,7 @@
  对照:补丁里多改一个字,patch-id 就不同;只改空白,默认的 --stable 会忽略空白:
    A(改第 1 行) 5e3837421ef7  | 同样内容但行号是 10 5e3837421ef7  | 多一个感叹号 4c68380e8a0e  | 行尾多空白 5e3837421ef7

git patch-id 对补丁的增删内容取指纹,忽略行号;--stable 还忽略空白。所以同一个补丁摘到文件的不同位置仍有相同的 patch-id,改动内容一变就不同。

判断重复

实测 · 本机执行输出
  $ git cherry -v main feature | sed -E "s/[0-9a-f]{40}/<sha40>/"
  + <sha40> 修复:第 10 行
  + <sha40> 后续提交
  $ git cherry -v main feature | sed -E "s/[0-9a-f]{40}/<sha40>/"; echo "(- 表示 main 里已经有等价的补丁,+ 表示还没有)"
  - <sha40> 修复:第 10 行
  + <sha40> 后续提交
  (- 表示 main 里已经有等价的补丁,+ 表示还没有)
  $ git log --left-right --cherry-pick --oneline main...feature | sed -E "s/[0-9a-f]{7} //"; echo "(--cherry-pick 把互相等价的提交过滤掉了)"
  < main 在文件开头加两行
  > 后续提交
  (--cherry-pick 把互相等价的提交过滤掉了)

git cherry、git log --cherry-pick、rebase 跳过重复补丁都用 patch-id:相同则认为“上游已经有这个改动”。

盲点

实测 · 本机执行输出
  $ git cherry-pick f4ff8a4be3ff7a9b1bb5c1c938fe7b39a89ddfd1 2>&1 | head -2; ls .git | grep CHERRY_PICK_HEAD
  Auto-merging f.txt
  CONFLICT (content): Merge conflict in f.txt
  CHERRY_PICK_HEAD
  原提交 patch-id e24b90ae0805070fda29f8fa6906dd632636dab4 ;解决冲突后的提交 patch-id 45ea0bf645fd8a0595247bb4a0cbdb9d206b1094;相同: False
  $ git cherry -v main feature | sed -E "s/[0-9a-f]{40}/<sha40>/"
  + <sha40> 改第 10 行

冲突后手工合并的结果往往不再是原来的补丁,patch-id 随之改变,上面这些命令就认不出它们是同一个改动;所以 -x 记下来源很有用。

空提交

实测 · 本机执行输出
  $ git cherry-pick 595215f35d377425cb3635144db29426ffa25682 2>&1 | head -3
  Auto-merging f.txt
  The previous cherry-pick is now empty, possibly due to conflict resolution.
  If you wish to commit it anyway, use:

补丁已经在当前分支里时,cherry-pick 的结果为空,git 会停下来让你选择跳过或保留空提交。

分步演示

cherry-pick 与 patch-id
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / git 2.54.0(Apple Git-157);每个实验在临时目录里新建仓库,HOME 和全局配置指向实验专用的空目录(不读用户自己的 git 配置),提交的作者与时间固定以便复现;需要网络协议的实验用本机的 file:// 或 git daemon 回环端口(9800 以上),不碰任何远程仓库;计时用 time.perf_counter,数字每次运行会有小幅抖动。作者与时间固定;patch-id 来自 git patch-id --stable;“手写补丁”的 patch-id 是直接构造文本得到的
    • 没有验证:cherry-pick 合并提交需要的 -m;cherry-pick 一个范围 A..B 与 --no-commit;patch-id 对二进制补丁、重命名补丁的处理;rebase 靠 patch-id 跳过重复(上一篇已经看到);git cherry-pick 的 --strategy 与 -X 选项;context 行不同时 patch-id 的变化(只验证了行号变化,上下文内容相同)
    京ICP备2026067502号-1京公网安备11010502063487号