如何挑选多个提交

1493
我有两个分支。一个分支的头是提交 a,而另一个分支在 a 之上有 bcdef。我想将 cdef 移动到第一个分支,而不包括提交 b。使用 cherry-pick 很容易实现:切换到第一个分支,逐个 cherry-pick cf,然后将第二个分支变基到第一个分支上。但是否有一种方法可以用一条命令来 cherry-pick 所有的 c-f
以下是该场景的可视化描述(感谢 JJD):

A commit showing a 'before' and 'after' state. In the 'before' state, commits a through f are connected in one contiguous sequence. In the after state, commits c through f have been relocated to directly connect to a without reordering, leaving the b commit behind.


17
你提到的变基对于这个问题并不是真正相关的,对吧?(我知道你可能希望以后将'b'基于'f',但这与挑选提交无关。) - Superole
还有一种与此相反的推论:在git中还原一系列提交 - Gabriel Staples
17个回答

2279

Git 1.7.2引入了选择一系列提交(commit)的能力。从发行说明中可以看到:

git cherry-pick 学习了如何挑选一系列的提交(例如 cherry-pick A..Bcherry-pick --stdin),而 git revert 也是这样;不过,它们没有像 rebase [-i] 那样良好的序列控制。

要挑选所有从提交A到提交B之间的提交(其中AB更旧),请运行:

git cherry-pick A^..B

如果你想要忽略 A 自身,运行以下命令:

git cherry-pick A..B

评论中的注意事项:

  • A 应该比 B 更早,或者 A 应该来自另一个分支。
  • 在 Windows 上,应该使用 A^^..B,因为插入符号需要转义,或者应该使用双引号包裹 "A^..B"
  • zsh shell 中,应该使用单引号包裹 'A^..B',因为插入符号是一个特殊字符。
  • 有关详细说明,请参见Gabriel Staples 的答案

(感谢评论区的 damian、J. B. Rainsberger、sschaef、Neptilo、Pete 和 TMin。)


308
在“挑选A..B”格式中,A 应该比 B 更旧。如果它们的顺序错误,命令将会悄无声息地失败。 - damian
22
如果你使用的是Git 1.7.1或更早版本,并且无法更新,你可以通过运行“git cherry-pick f3”然后“git cherry-pick f2”等一直到“git cherry-pick f”来快速地挑选它们,以确保按顺序执行(在大多数控制台中,按上箭头可以获取先前命令,从而可以快速更改数字并运行它)。 - David Mason
36
了解一下,这个语法也适用于分支名。假设某分支已经重新基于主分支,那么git cherry-pick master..somebranch会挑选自主分支以来某分支上的所有提交,并将它们应用到当前分支上。注意不要改变原意。 - Tor Klingberg
18
在Windows的cmd.exe中,^是一个特殊字符,在A^..B中会被静默地忽略。你需要将其加倍(^^)或将提交引用放在引号中。 - Neptilo
10
请注意,A^会选择A的父提交记录之一。这意味着如果A有多个父提交记录(例如合并提交),您需要小心处理。 - jpmc26
显示剩余11条评论

220

如果您需要合并选择性的修订版本,例如从A、B、C、D、E、F、G、H、I和J的提交中选择A、C、F、J,则只需使用以下命令:

git cherry-pick A C F J

142

最简单的方法是使用rebase命令的onto选项。假设当前分支结束于a,该分支称为mybranch,您想将c-f移动到该分支。

# checkout mybranch
git checkout mybranch

# reset it to f (currently includes a)
git reset --hard f

# rebase every commit after b and transplant it onto a
git rebase --onto a b

1
谢谢!您能否将 git checkout secondbranch && git rebase mybranch 添加到完整答案中? - tig
2
这个答案对我非常有帮助,让我更好地理解了在这种情况下哪个提交是哪个。而且:你也可以使用rebase的交互模式。谢谢,@Charles! - Oliver
6
这种方法的美妙之处在于,您可以使用--interactive从序列中删除一些提交或在“cherry pick”之前对它们进行重新排序。+1 - Michael Merickel
2
很奇怪,你必须将不想变基的提交作为参数之一(在此示例中为“b”),但是是的,这对我有效。 - asontu
这个答案对我很有帮助。我使用rebase命令与--committer-date-is-author-date一起使用,以保留原始提交时间戳(与--interactive不兼容)。 - joH1

123

如何挑选单个提交、多个提交或一系列提交

...到您当前检出的分支上:

1. 挑选一个名为commit单个分支或提交

git cherry-pick commit

例子:

git cherry-pick my_branch                                 # by branch name
git cherry-pick 1e038f108a130831f108329b1083a8139813fabc  # by full hash
git cherry-pick 1e038f10                                  # by partial hash

2. 挑选多个提交

请注意,您可以一次挑选任意数量的提交哈希,并且可以按照任何顺序进行。它们将逐个应用,并按照您指定的顺序进行。如果出现冲突,您需要逐个解决它们,然后使用git add my_file命令,完成后使用git cherry-pick --continue命令继续挑选过程。

git cherry-pick commit1 commit2 commit3 commit4 commit5

3. 挑选一系列提交记录
我最初是从这里@Eric Darchis的最受赞同的答案中学到了这种风格的基础知识。
请注意,要挑选一系列提交记录,您必须指定起始和结束的提交哈希值,并在它们之间使用“..”。然而,在一系列提交记录中,起始提交不包括在内。因此,为了包括它,您必须指定起始提交之前的提交。指定前一个提交的语法是在您的提交后面加上“~”、“~1”或“^”,例如:beginning_commit~,意思是:“在beginning_commit之前的提交”。
# A. INCLUDING the beginning_commit
git cherry-pick beginning_commit~..ending_commit
# OR (same as above)
git cherry-pick beginning_commit~1..ending_commit
# OR (same as above)
git cherry-pick beginning_commit^..ending_commit 

# B. NOT including the beginning_commit
git cherry-pick beginning_commit..ending_commit

注意:commit~,commit~1和commit^都表示“相对于commit之前的一次提交”,或者换句话说:“commit之前的提交”。
要指定相对于commit之前的两次提交,可以使用以下语法:
commit~~
commit~2  # my preferred syntax
commit^^

为了指定在 commit 之前的三个提交,您可以这样做:
commit~~~  
commit~3   # my preferred syntax
commit^^^

这个不起作用。
commit^3   # INVALID syntax

要自己测试上述的“先前提交语法”概念,最简单的方法是使用git log命令。例如:
git log commit
git log commit~
git log commit~1
git log commit^
git log commit~~
git log commit~5
# etc.

4. 将同事的提交范围选择到您的分支上
...当他们的分支peer_branch从您的分支my_branch的早期版本分叉出来时。

快速摘要

# you cherry-pick all of their extra commits from their `peer_branch` onto 
# your `my_branch` (note: the 3 dots below are very important!)

git fetch origin peer_branch  # get their latest changes from the remote
git checkout my_branch        # ensure you're on your branch
# cherry-pick their range of commits
git cherry-pick my_branch...origin/peer_branch  
git log                       # review the commits you just chery-picked
git push                      # push your changes to the remote

完整的细节和工作流程演示

假设你正在处理你的特性分支my_branch,而你的同事想要帮助你进行一些修改以支持你的特性。你已经将my_branch推送到名为origin的远程仓库。所以,他们将会从远程仓库中获取你的名为my_branch的分支到他们的本地计算机上,然后从这个分支中创建一个名为peer_branch的自己的分支,并将其推送到自己的分支peer_branch上。一旦他们完成这个操作,你将一次性挑选出他们所有的修改内容。这是整个过程的第一部分:

# **your peer** does this

# peer fetches your branch named `my_branch` and forks their `peer_branch`
# off of it

# they fetch your latest work from remote `my_branch` into their locally-stored
# remote-tracking "hidden" branch named `origin/my_branch`
# (note: you can see all locally-stored remote-tracking "hidden" branches
# with `git branch -r`)
git fetch origin my_branch
# create `peer_branch` as a fork off of `origin/my_branch`, and check it out
git checkout -b peer_branch origin/my_branch

# Now they can add their changes and commits and `git push` to remote `origin`
# as their own `peer_branch` when done.

现在他们已经将所有更改推送到远程的origin,作为他们自己的分支peer_branch。你可以像这样挑选出他们在你的工作之上添加的所有提交:
# **you** do this to cherry-pick your peer's helpful changes they added to 
# your work

# you fetch their latest work from their branch named `peer_branch` on remote
# `origin` into your locally-stored remote-tracking "hidden" branch named 
# `origin/peer_branch` 
# (note: you can see all locally-stored remote-tracking "hidden" branches
# with `git branch -r`)
git fetch origin peer_branch
# ensure you are on `my_branch` (if you aren't already)
git checkout my_branch
# you cherry-pick all of their extra commits from their `peer_branch` onto 
# your `my_branch` (note: the 3 dots here are very important!)
git cherry-pick my_branch...origin/peer_branch

git log                       # review the commits you just chery-picked
git push                      # push your changes to the remote

为了您的理解,上面那个带有3个点的cherry-pick命令,与下面这个更长的命令是完全等效的:
git cherry-pick $(git merge-base my_branch origin/peer_branch)..origin/peer_branch

git merge-base my_branch origin/peer_branch部分找到分支my_branch和分支origin/peer_branch之间的共同父提交哈希。这是它们在你的my_branch上从其peer_branch分叉的提交。然后,当然,您要挑选从该点到(..)他们在origin/peer_branch的最终提交的一段提交

要了解更多关于3个点语法的内容,请参阅此处:Git diff commit范围中双点".. "和三点"..."之间的区别?[重复]。有关git checkout -b new_branch from_branch的帮助,请参阅我的答案:使用其他分支在git中创建分支的各种方法

官方Git文档

  • https://git-scm.com/docs/gitrevisions - 提到 git commit 的三个点(...)与两个点范围语法,^commit("非" commit),commit^(commit 的父节点),等等。
  • 还有一件事需要知道:`git rebase`只是一系列连续的`git cherry-pick`。请参阅我在这里的其他答案(根据Git,"我们"和"他们"是谁?),其中我展示了一个ASCII图,说明了`git rebase`的工作原理和它在执行什么操作。
  • Git diff提交范围中双点".. "和三点"..."之间的区别是什么?[重复]
  • 我的回答在不同的方式从另一个分支在git中创建一个分支

  • 关于:commit~3 # 我偏爱的语法 也许更好的做法是为了与 Git 命名保持一致,将其称为“修订”。 - Janusz 'Ivellios' Kamieński
    1
    我同意使用 commit~3 # my preferred syntax。这是我能记住的少数几件事情之一,HEAD~1HEAD 提交早一次。在相同的概念下,我认为 git cherry-pick beginning_commit~1..ending_commit 是最好的语法。由于我很少使用 cherry-pick,所以有额外的明确性非常有用,这样当我在我的 bash 历史记录中搜索它时,我不必再去搜索互联网来确定 ^ 的确切含义。 - icc97

    99

    或者可以使用请求的单行代码:

    git rebase --onto a b f
    

    12
    如果 f 是一个提交(而不是分支),则 Upvoted 但将使您处于游离的 HEAD 状态 - 您应该编辑以添加应检出一个分支,如下面的答案所述。 - Mr_and_Mrs_D
    为什么要费心处理其他的事情呢? - Jason S
    2
    需要注意的是,挑选一堆提交并不等同于将它们变基。从图表上看,原帖作者似乎想要变基的效果,但如果你已经将提交发布到团队或公共仓库中,那么变基就不安全了。 - Ivan Kurmanov
    我喜欢这个,但一旦你进入了分离的HEAD状态,那么你应该 git branch tmp(保存你的分离的HEAD状态)+ git checkout destination-branch(切换到目标分支,可能是develop)+ git merge tmp(将你的分离的HEAD合并到你的目标分支中)。 - BigRon

    80
    您可以使用 git rebasegit branch 的串行组合将一组提交应用到另一个分支上。正如回答所述,第一个命令实际上是复制提交。然而,在将分支名称添加到组中最上面的提交之前,更改是不可见的。

    请在新标签页中打开图片...

    Workflow

    以文本形式总结命令:

    1. 使用命令 gitk --all &gitk 作为独立进程打开。
    2. 运行 git rebase --onto a b f
    3. gitk 中按 F5。没有任何变化,但没有标记 HEAD
    4. 运行 git branch selection
    5. gitk 中按 F5。带有其提交内容的新分支出现。

    这应该澄清问题:

    • 提交 a 是组的新根目标。
    • 提交 b 是组中第一个提交之前的提交(不包括)。
    • 提交 f 是组中最后一个提交(包括)。

    之后,您可以使用 git checkout feature && git reset --hard bfeature 分支中删除提交 cf

    除了这个答案外,我还写了一篇博客文章,描述了另一个情景下的命令,可以帮助你更好地使用它。


    2
    如果不再需要mybranch(即a..f提交),可以简化为:git rebase --onto a b mybranch 另外,那个很酷的Git图片是用哪个程序做的? - Mr_and_Mrs_D
    2
    @Mr_and_Mrs_D 感谢您的评论。我想我使用了http://cacoo.com来绘制这些图片。 - JJD

    58

    为了回答问题,根据J.B.Rainsberger和sschaef的评论,可以使用此示例中的cherry-pick范围:

    git checkout a
    git cherry-pick b..f
    
    或者
    git checkout a
    git cherry-pick c^..f
    

    5
    我使用 git 2.7.0.windows.1,发现当我尝试精选一系列提交时,一切都很好,但是 git 并没有告诉你在再次提交/精选之前需要执行 git cherry-pick --continue | --abort | --quit。因此,如果你精选了一系列的提交,每次准备好(解决冲突或其他)给定范围内的提交时,你都需要运行 git cherry-pick --continue - kuskmen
    我做了完全相同的事情,但是出现了致命错误:无法找到'a..b'。 - Amitkumar Karnik
    我不知道哪里出了问题,但是当我在我的端上执行 'git cherry-pick c^..f' 命令时,它会包含提交 f,但不包括提交 c。但是根据我所读到的,它应该将 c 和 f 定义为包含关系。难道我错了吗? - Samuel
    @Samuel 是的,没错。实际上,c 后面的 ^ 表示“c 的前一个提交”,在这种情况下是 b。这就是为什么 c^..fb..f 是同义词。尝试执行 git log c^..f 命令,你应该会看到从 c 到 f 的提交记录,与执行 git log b..f 命令完全相同。 - Andy

    33

    要从提交ID选择到分支末尾,您可以使用:

    git cherry-pick commit_id^..branch_name
    

    这已经是答案的一部分了 https://dev59.com/t3I-5IYBdhLWcg3wy7wd#31640427 - tig
    10
    这个答案与其他答案不同,对我有所帮助。它明确指出了分支名称而非最终提交的 SHA。 - Subtletree
    我收到了“fatal: bad revision”错误。 - Dentrax
    @Dentrax 我也遇到了同样的错误,你找到原因了吗? - osacognitive
    1
    没事了,问题是我用了“-”而不是“...”。 - osacognitive

    28
    git rev-list --reverse b..f | xargs -n 1 git cherry-pick
    

    git rev-list 打印从分支 bf 的所有修订版本(顺序相反),因此当按顺序传递每行(提交哈希)时,它将把每个提交应用于当前的 git HEAD。即 git cherry-pick {hash of c}; git cherry-pick {hash of d}; ...

    -- @coderatchet comment


    2
    如果没有冲突,它可以完美地工作,否则“rebase onto”可能更容易,因为您不必弄清楚它停在哪里并重新应用其余的补丁。 - Ruslan Kabalin
    12
    请添加注释以解释这是在做什么。 - Mr_and_Mrs_D
    9
    因为没有人做出解释,所以现在需要翻译的是:因为没有人解释,所以git rev-list打印从分支b到f的所有修订版本(反向),这样当按顺序传递每行(提交哈希)时,它将会把每个提交都择优地应用于当前的Git HEAD。即 git cherry-pick {c的哈希}; git cherry-pick {d的哈希}; ... - coderatchet
    1
    这与 git cherry-pick b..f 有何不同?也许在2009年不存在 cherry-pick b..f 语法? - icc97
    如果您的系统有paste,在合并冲突时可以使用以下命令完成操作(不使用xargs): git cherry-pick -x $(git rev-list --reverse b..f | paste -sd ' ' -) - site

    19

    还有一种值得一提的变体是,如果你想要从一个分支中获取最后 n 次提交,那么 ~ 语法将会很有用:

    git cherry-pick some-branch~4..some-branch
    
    在这种情况下,上述命令会从名为some-branch的分支选择最后4次提交记录(尽管您也可以使用提交哈希代替分支名称)。

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接