一文看懂 git cherry-pick -m 1:如何摘取一个合并提交

为什么需要 -m 1

直接 cherry-pick 一个合并提交会失败:它有两个父提交,Git 不知道该拿哪一个来比较。-m 1 的作用,就是告诉 Git“以第一个父提交(通常是主线)为基准”。

1
2
3
$ git cherry-pick 8865ad0
error: commit 8865ad060c1c64fbc3c0196e12302b1d96e8bf30 is a merge but no -m option was given.
fatal: cherry-pick failed

最常见的场景是:功能分支 feature 已经通过一个合并提交进入 main,现在要把同一个功能补到维护中的 release 分支,而 main 上的其他提交不能跟过去。

逐个 cherry-pick feature 上的提交也能做到,但提交一多就很繁琐,而且合并时手动解决过的冲突不会被带上。直接摘取那个合并提交,一条命令就能拿到功能的完整改动。

核心概念:合并提交有两个父提交

普通提交只有一个父提交,合并提交有两个:parent 1 是执行合并时所在的分支,parent 2 是被合进来的分支。

在 main 上执行 git merge feature 时,main 当时的最新提交成为 parent 1,feature 的最新提交成为 parent 2。在 GitHub、GitLab 上用“创建合并提交”的方式合并 PR 也是同样的顺序:目标分支在前。

合并提交 M 的两个父提交:C 是 parent 1,B 是 parent 2initABCMparent 1parent 2releasefeaturemain时间 →
箭头沿时间方向画出:两条彩色箭头进入 M,分别来自 C(parent 1)和 B(parent 2)。

想确认顺序,看 git log -1 <sha> 输出里的 Merge: 行即可,写在前面的就是 parent 1:

1
2
3
4
5
6
7
$ git log -1 8865ad0
commit 8865ad060c1c64fbc3c0196e12302b1d96e8bf30
Merge: 06233b4 d9b8793
Author: Demo <[email protected]>
Date:   Fri Jun 26 17:56:50 2026 +0800

    M: merge feature

也可以用 git rev-parse <sha>^1 <sha>^2 分别打印两个父提交:

1
2
3
$ git rev-parse 8865ad0^1 8865ad0^2
06233b41967aea22699e607028a268615027526c
d9b87938de6bd17532f3156c05e7cfd0b475a787

-m 1 与 -m 2 分别算出了什么

cherry-pick 的本质,是把“某个提交相对其父提交的差异”应用到当前分支。-m(全称 --mainline)只是在两个父提交里选一个作为基准,选谁就带走另一侧的改动。

-m 1 拿 C 和 M 比,差异是 login.txt;-m 2 拿 B 和 M 比,差异是 README.md-m 1:以 parent 1(C)为基准Capp.txtREADME.md差异Mapp.txtREADME.md+ login.txt带到当前分支的是+ login.txtfeature 的改动(A、B)-m 2:以 parent 2(B)为基准Bapp.txtlogin.txt差异Mapp.txtlogin.txt+ README.md带到当前分支的是+ README.mdmain 侧的改动(C)
左边是常用的 -m 1:拿 C 和 M 比,差出来的正是 feature 带来的改动。
选项基准计算的差异带到当前分支的内容
-m 1parent 1(C,main 侧)C → Mfeature 的全部改动(A、B,以及合并时的冲突解决)
-m 2parent 2(B,feature 侧)B → Mmain 在 feature 分出之后的改动(C)

在“把已合并的功能搬到另一个分支”这类场景里,几乎总是用 -m 1。

动手实验:搭一个示例仓库

下面的命令可以直接复制运行,它会造出上文的仓库:release 停在 init,feature 有两个提交,main 另有一个无关提交,最后用合并提交 M 把 feature 合进 main。

分四步来,每一步之后都用 git log --oneline --graph --all 看一眼提交图。下面是实际输出(你自己运行时 SHA 会不同)。

第 1 步:初始提交,切出 release

1
2
3
4
5
6
git init -b main demo && cd demo
echo "v1" > app.txt
git add app.txt && git commit -m "init"

# 从这里切出 release 分支(之后不再动它)
git branch release
1
2
$ git log --oneline --graph --all
* f643d1c (HEAD -> main, release) init

此时只有一个提交,main 和 release 都指向它。

第 2 步:feature 分支上做两个提交

1
2
3
4
5
git checkout -b feature
echo "login" > login.txt
git add login.txt && git commit -m "A: add login"
echo "logout" >> login.txt
git commit -am "B: add logout"
1
2
3
4
$ git log --oneline --graph --all
* d9b8793 (HEAD -> feature) B: add logout
* f7858bc A: add login
* f643d1c (release, main) init

feature 往前走了两步,main 和 release 还留在 init。

第 3 步:main 上另有一个无关提交

1
2
3
git checkout main
echo "readme" > README.md
git add README.md && git commit -m "C: add readme"
1
2
3
4
5
6
$ git log --oneline --graph --all
* d9b8793 (feature) B: add logout
* f7858bc A: add login
| * 06233b4 (HEAD -> main) C: add readme
|/
* f643d1c (release) init

历史在 init 之后分了叉:一侧是 feature 的 A、B,另一侧是 main 的 C。

第 4 步:用合并提交把 feature 合进 main

1
git merge --no-ff feature -m "M: merge feature"

--no-ff 保证一定会生成合并提交。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
$ git merge --no-ff feature -m "M: merge feature"
Merge made by the 'ort' strategy.
 login.txt | 2 ++
 1 file changed, 2 insertions(+)
 create mode 100644 login.txt
$ git log --oneline --graph --all
*   8865ad0 (HEAD -> main) M: merge feature
|\
| * d9b8793 (feature) B: add logout
| * f7858bc A: add login
* | 06233b4 C: add readme
|/
* f643d1c (release) init

M(8865ad0)有两条线进来:竖直的那条来自 C(06233b4,parent 1),右边那条来自 B(d9b8793,parent 2)。这正是前面 Merge: 06233b4 d9b8793 的顺序。

执行 cherry-pick 并验证

切到 release,用 -m 1 摘取 M。main 此刻正指向 M,所以直接写分支名也可以:

1
2
git checkout release
git cherry-pick -m 1 main      # 等价于 git cherry-pick -m 1 8865ad0

在终端里执行并验证,实际输出如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
$ git checkout release
Switched to branch 'release'
$ git cherry-pick -m 1 main
[release 20de01d] M: merge feature
 Date: Fri Jun 26 17:56:50 2026 +0800
 1 file changed, 2 insertions(+)
 create mode 100644 login.txt
$ ls
app.txt  login.txt
$ git log --oneline --graph --all
*   8865ad0 (main) M: merge feature
|\
| * d9b8793 (feature) B: add logout
| * f7858bc A: add login
* | 06233b4 C: add readme
|/
| * 20de01d (HEAD -> release) M: merge feature
|/
* f643d1c init
$ git log -1 --format='%h parents: %p'
20de01d parents: f643d1c

release 拿到了 feature 的 login.txt,而 main 上的 README.md 没有跟过来。注意新生成的 20de01d 是一个普通的单父提交(父提交只有 f643d1c),不是合并提交,只是沿用了 M 的提交信息。

对比实验:换成 -m 2 会怎样

换成 -m 2,拿到的反而是 main 侧的 README.md,feature 的改动一个也没有。从 init 新开一个分支试试:

1
2
git checkout -b release-m2 release~1   # 回到 cherry-pick 之前的 init
git cherry-pick -m 2 main
1
2
3
4
5
6
7
8
9
$ git checkout -b release-m2 release~1
Switched to a new branch 'release-m2'
$ git cherry-pick -m 2 main
[release-m2 220b106] M: merge feature
 Date: Fri Jun 26 17:56:50 2026 +0800
 1 file changed, 1 insertion(+)
 create mode 100644 README.md
$ ls
README.md  app.txt

原因是基准换成了 B:B 已经包含 login.txt,所以 B → M 的差异只剩 C 引入的 README.md。

-m 2 并非没有用。如果有人先在 feature 上执行 git merge main,再把 main 快进到这个合并,父提交顺序就反了过来,这时要用 -m 2 才能拿到 feature 的改动。

常见坑与最佳实践

最大的坑是想当然地认为 parent 1 就是主线,动手前先确认父提交顺序,其余问题都好处理。

  • 先看顺序再动手。 用 git log -1 <sha> 的 Merge: 行确认哪个是 parent 1,尤其是别人做的合并。
  • 加上 -x 记录来源。 git cherry-pick -x -m 1 <sha> 会在提交信息末尾追加 (cherry picked from commit ...),日后排查时一眼就能找到出处。
  • 遇到冲突照常处理。 修改冲突文件后 git add,再 git cherry-pick --continue;想放弃就 git cherry-pick --abort,分支会回到操作前的状态。
  • 之后再合并要留意。 cherry-pick 生成的是新 SHA,Git 并不记得这些改动已经在 release 里。以后若把 feature 或 main 再合进 release,完全相同的改动通常能自动合并,但 feature 之后又改过的地方可能出现冲突。
  • 撤销的方式不同。 cherry-pick 出来的是普通提交,git revert <新 sha> 即可;如果要撤销原始的合并提交 M,则同样需要 git revert -m 1 M。
  • 批量摘取时可以统一加 -m 1。 从 Git 2.21 起,-m 也能用在普通提交上,所以一个混有合并提交的范围可以用一条命令处理。

加上 -x 之后,提交信息的最后一行会记下来源合并提交的完整 SHA:

1
2
git checkout -b release-x release~1
git cherry-pick -x -m 1 main
1
2
3
4
5
6
7
8
$ git log -1
commit bd76f2d148bac366ad55b388915292d25daead59
Author: Demo <[email protected]>
Date:   Fri Jun 26 17:56:50 2026 +0800

    M: merge feature

    (cherry picked from commit 8865ad060c1c64fbc3c0196e12302b1d96e8bf30)

速查表

一句话记住:-m N 选第 N 个父提交当基准,带走的是另一侧的改动;搬运已合并的功能,用 -m 1。

命令作用
git cherry-pick -m 1 <M>把 M 相对 parent 1 的差异(被合入分支的改动)应用到当前分支
git cherry-pick -m 2 <M>把 M 相对 parent 2 的差异(主线一侧的改动)应用到当前分支
git cherry-pick -x -m 1 <M>同上,并在提交信息里记录来源 SHA
git log -1 <M>查看 Merge: 行,确认父提交顺序
git rev-parse <M>^1 <M>^2分别打印 parent 1 和 parent 2
git cherry-pick --continue解决冲突并 git add 后继续
git cherry-pick --abort放弃本次 cherry-pick,回到操作前
git revert -m 1 <M>撤销整个合并带来的改动
使用 Hugo 构建
主题 Stack 由 Jimmy 设计