为什么需要 -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 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 比,差出来的正是 feature 带来的改动。| 选项 | 基准 | 计算的差异 | 带到当前分支的内容 |
|---|
-m 1 | parent 1(C,main 侧) | C → M | feature 的全部改动(A、B,以及合并时的冲突解决) |
-m 2 | parent 2(B,feature 侧) | B → M | main 在 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> | 撤销整个合并带来的改动 |