如何挑选一系列的提交并将它们合并到另一个分支?

866
我有以下仓库布局:
  • master分支(生产)
  • integration分支
  • working分支
我想做的是从working分支挑选一系列提交并将其合并到integration分支。我对git很新手,无法确切地做到这一点(在一个操作中挑选一系列的提交,而不是合并),以免捣乱。你们有什么指导或想法吗?谢谢!
12个回答

1032

说到一系列的提交,挑选樱桃曾经不切实际的。

正如下面提到的Keith Kim所说,Git 1.7.2+引入了挑选一系列提交的能力(但你仍然需要注意挑选樱桃对未来合并的影响

"git cherry-pick"学会了挑选一系列的提交
(例如"cherry-pick A..B"和"cherry-pick --stdin"),"git revert"也是如此;然而,它们不支持更好的顺序控制,就像"rebase [-i]"那样。

damian 评论并警告我们:

在"cherry-pick A..B"的形式中,A应该比B
如果它们的顺序错误,命令将会静默失败
如果你想选择范围BD(包括B,那么应该使用B~..D(而不是B..D)。 参见"Git create branch from range of previous commits?"以获得示例。
正如Jubobs评论中提到的:

这假设B不是一个根提交;否则你将会得到一个"未知修订"的错误。

注意:从Git 2.9.x/2.10(2016年第三季度)开始,您可以直接在孤立分支(空头)上挑选一系列提交:请参阅“如何将现有分支变为Git中的孤立分支”。

原始答案(2010年1月)

一个rebase --onto会更好,你可以在你的集成分支上重新播放给定范围的提交,就像Charles Bailey在这里描述的那样
(另外,在git rebase手册页中查找“这是如何将一个基于一个分支的主题分支移植到另一个分支”的实际示例,以了解git rebase --onto的用法)

如果你当前的分支是integration:

# Checkout a new temporary branch at the current location
git checkout -b tmp

# Move the integration branch to the head of the new patchset
git branch -f integration last_SHA-1_of_working_branch_range

# Rebase the patchset onto tmp, the old location of integration
git rebase --onto tmp first_SHA-1_of_working_branch_range~1 integration

这将重新播放以下内容:

  • first_SHA-1_of_working_branch_range的父级开始(因此是~1):您想要重新播放的第一个提交
  • 直到“integration”(指向您打算重新播放的最后一个提交,来自working分支)

到“tmp”(指向integration之前的位置)

如果在重新播放其中一个提交时出现冲突:

  • 要么解决冲突并运行“git rebase --continue”。
  • 要么跳过此补丁,而是运行“git rebase --skip
  • 要么取消所有操作并运行“git rebase --abort”(并将integration分支放回tmp分支)

在那个rebase --onto之后,integration将回到集成分支的最后一个提交(即“tmp”分支+所有重新播放的提交)

使用樱桃拣选或 rebase --onto 时,不要忘记它对后续合并操作有影响,如此处所述
一个纯粹的“挑选”解决方案在这里讨论,可能涉及以下内容:
如果你想使用补丁方法,那么“git format-patch|git am”和“git cherry”是你的选择。 目前,git cherry-pick只接受一个提交,但如果你想选择范围B到D,那么在Git术语中应该是B^..D(实际上是B~..D,见下文),所以:
git rev-list --reverse --topo-order B~..D | while read rev 
do 
  git cherry-pick $rev || break 
done 

但无论如何,当你需要“重播”一系列的提交时,这个词“重播”应该引导你使用 Git 的“rebase”功能。

pridmorej评论中提到:

警告:不要被上面使用插入符号(^)来使范围包含的建议所迷惑!

如果父提交是一个合并提交,那么这不会包括CommitId1,例如:git cherry-pick CommitId1^..CommitId99
在这种情况下,cherry-pick仍然从CommitId2开始 - 不知道为什么,但这是我经历过的行为。

然而,我发现使用波浪符号(~)可以按预期工作,即使CommitId1的父提交是一个合并提交:git cherry-pick CommitId1~..CommitId99

真的:这突显了在使用Git的cherry-picking命令与提交范围时的一个重要细微差别,尤其是在处理合并提交时。
在Git中,^和~在与提交引用一起使用时有特定的含义:
- ^指的是一个提交的父提交,在范围中使用它可能会导致意外结果,尤其是在处理合并提交时。 - 另一方面,~用于表示线性历史中一个提交的第一个父提交,这使得在cherry-picking范围的上下文中更可预测。
在cherry-picking一系列提交时,尤其是涉及合并提交的情况下,最好使用~来确保范围包括预期的提交。
所以,关于git cherry-pick CommitId1^..CommitId99:当指定范围CommitId1^..CommitId99时,Git将其解释为“从CommitId1的父提交开始,包括提交直到CommitId99”。
  • 如果CommitId1是一个普通的提交,它的唯一父提交(假设为CommitId0)将成为范围的起点,有效地排除了CommitId1本身。
  • 如果CommitId1是一个合并提交,CommitId1^仍然指向它的第一个父提交。这可能特别令人困惑,因为合并提交本质上合并了两条开发线,而第一个父提交可能在线性意义上不直观地被认为是“前一个”提交。

在涉及合并提交的非线性历史中,合并提交的第一个父提交可能不是同一分支中的直接前任。

当使用波浪符(~)表示法时,如CommitId1~..CommitId99,它实际上表示“包括CommitId1并从那里回退一个提交”,在大多数情况下将包括CommitId1在范围内,正如预期的那样。

考虑以下提交历史,其中M表示合并提交,每个字母表示不同的提交:

A---B---C-------D---E--CommitId99   <- master
     \         /
      X---Y---M                     <- feature (CommitId1 is M)
  • ABCDE 是在 master 分支上的提交。
  • XY 是在一个 feature 分支上的提交。
  • M 是一个合并提交,在 feature 分支上将 master 分支的更改合并到 feature 分支上。在这里,MCommitId1

当你运行以下命令时:

git cherry-pick CommitId1^..CommitId99
  • CommitId1M
  • CommitId1^ 指的是 M 的第一个父提交。
  • 在这种情况下,M 的第一个父提交是 D,因为合并提交按照它们合并的顺序列出其父提交。

因此,范围 CommitId1^..CommitId99 转换为 D..CommitId99。M(CommitId1)被排除在外!

但是,如果你使用 git cherry-pick CommitId1~..CommitId99,当在范围的上下文中使用时,如 CommitId1~..CommitId99,它的解释是“从 CommitId1 之前的提交开始,包括到 CommitId99。”

所以CommitId1~指的是在feature分支中M之前的提交,即Y(因为M是通过从masterfeature的合并在feature分支上创建的)。
范围CommitId1~..CommitId99翻译为Y..CommitId99
在使用git cherry-pick时,范围中的~有效地将范围的起始点移至CommitId1之前的提交,从而将CommitId1包括在复制的范围内。这种行为在想要在复制操作中包括合并提交时特别有用。

2
如果您有需要使用-m选项的父提交,您该如何处理这些提交呢?或者是否有一种方法可以过滤掉这些提交呢? - aug
@aug -m 应该为您处理它们,通过选择您为此挑选的樱桃选取所引用的主线。 - VonC
问题在于,如果您正在挑选一系列提交,它将正确地挑选父提交,但是当它遇到普通提交时,它会失败并显示提交不是合并。我想我的问题更好地表达方式是如何使其在挑选一系列提交时仅在遇到父提交时通过“-m”选项?现在,如果我像这样传递“-m”:git cherry-pick a87afaaf..asfa789 -m 1,它将应用于范围内的所有提交。 - aug
1
啊,我正在运行git版本2.6.4(Apple Git-63)。我看到的错误可能是这样的:error: Commit 8fcaf3b61823c14674c841ea88c6067dfda3af48 is a merge but no -m option was given. 我实际上意识到你可以只需使用 git cherry-pick --continue 就可以解决问题(但它不会包括父提交)。 - aug
愿你永远不再痒,愿你的眼睛只看到尾巴蓬松的兔子。谢谢! - undefined
显示剩余11条评论

167

从git v1.7.2开始,cherry pick可以接受一系列的提交:

git cherry-pick 学会了选择一系列的提交(例如 cherry-pick A..Bcherry-pick --stdin),同样的也适用于 git revert。但是这些命令不像 rebase [-i] 命令那样支持更好的序列控制。

正如Gabe Moothart指出的,cherry-pick A..B 不会选取提交 A(你需要使用 A~1..B),如果有冲突,git 不会像 rebase 那样自动继续(至少在 1.7.3.1 版本中是这样)。


114
假设您有两个分支:
"branchA":包括您要复制的提交(从“commitA”到“commitB”)
"branchB":您想要从“branchA”转移提交的分支
1)
 git checkout <branchA>

2) 获取“commitA”和“commitB”的ID。

3)

git checkout <branchB>

4)

git cherry-pick <commitA>^..<commitB>

5) 如果您遇到冲突,请解决并输入

git cherry-pick --continue

继续进行cherry-pick过程。


4
在"git cherry-pick <commitA>^..<commitB>"命令中,符号"^"的作用是选择从<commitA>的父提交开始,并且包括<commitA>本身在内的一系列提交。这个范围是通过指定<commitA>的简写形式和<commitB>的提交对象标识符来定义的。 - JVM
3
有人请编辑帖子,指出 cherry-pick 的范围是不包括最后一个提交的。 - lolololol ol
3
@JVM 当您在使用 cherry-pick 时在范围内不使用 ^,第一个提交将不包含在内。 - Tomas Vancoillie
2
@JVM中,^表示前一次提交。正如@Tomas所指出的,cherry-pick的行为会留下提交A。因此,要包括commitA,你需要选择相应的前一个提交,使用commitA^ - SKPS
1
对于那些不知道的人,如果你在使用zsh,你需要用引号括起来你的范围:git cherry-pick "<commitA>^..<commitB>" - Drew Daniels
显示剩余2条评论

36

你确定你不想合并这些分支吗?如果工作分支有一些你不想要的最近提交,你可以创建一个新的分支来指向你想要的那个点。

现在,如果你真的想挑选一系列提交,出于某种原因,一个优雅的方法就是拉取补丁集并将其应用到你的新集成分支上:

git format-patch A..B
git checkout integration
git am *.patch

本质上,这正是git-rebase正在做的事情,但无需玩弄技巧。如果需要合并,可以在git-am中添加--3way。 如果您完全按照说明操作,请确保在执行此操作的目录中没有其他*.patch文件...


3
请注意,与其他修订范围一样,需要使用A^来包括A - Gino Mempin

25

git cherry-pick start_commit_sha_id^..end_commit_sha_id

例如:git cherry-pick 3a7322ac^..7d7c123c

假设您在“branchA”上并想要从“branchB”中选择提交记录(给出了范围的起始和结束提交 SHA,左边的提交 SHA 更旧)。整个提交记录范围(包括两端的提交)将被精选到“branchA”中。

官方文档中提供的示例非常有用。


9

我将VonC的代码封装成一个简短的bash脚本git-multi-cherry-pick,以便于轻松运行:

#!/bin/bash

if [ -z $1 ]; then
    echo "Equivalent to running git-cherry-pick on each of the commits in the range specified.";
    echo "";
    echo "Usage:  $0 start^..end";
    echo "";
    exit 1;
fi

git rev-list --reverse --topo-order $1 | while read rev 
do 
  git cherry-pick $rev || break 
done 

我正在使用这个工具来重建一个项目的历史记录,该项目包含第三方代码和自定义内容混合在同一个svn主干中。现在,为了更好地理解未来的自定义内容,我将核心第三方代码、第三方模块和自定义内容分别放到它们自己的git分支中。由于我在同一个存储库中有两棵树,但没有共同的祖先,因此git-cherry-pick在这种情况下非常有用。


8

git cherry-pick FIRST^..LAST 只适用于简单的情况。

为了在将其合并到集成分支时获得良好的“舒适度”,例如自动跳过已集成的挑选,移植钻石合并,交互控制等等,最好使用rebase。这里有一个答案指出了这一点,但该协议包括一个棘手的git branch -f和一个临时分支的搬弄。这里提供了一种直接而健壮的方法:

git rebase -i FIRST LAST~0 --onto integration
git rebase @ integration

-i 允许交互式控制。 ~0 确保在 LAST 是分支名称时进行分离的变基(不移动/另一个分支)。否则可以省略。第二个变基命令只是以安全的方式将 integration 分支引用向前移动到中间分离的头部,而不会引入新的提交。要重新对包含合并菱形等复杂结构的分支进行变基,请考虑在第一个变基中使用 --rebase-merges--rebase-merges=rebase-cousins


6

我在几天前测试过这个方法,是在阅读了Vonc非常清晰的解释之后。

我的步骤

开始

  • 分支dev:A B C D E F G H I J
  • 分支target:A B C D
  • 我不需要EH

在分支dev_feature_wo_E_H中复制功能而不包括步骤E和H的步骤

  • git checkout dev
  • git checkout -b dev_feature_wo_E_H
  • git rebase --interactive --rebase-merges --no-ff D,在交互式编辑器中放置“drop”在EH前面
  • 解决冲突,继续并commit

在目标上复制分支dev_feature_wo_E_H的步骤。

  • git checkout target
  • git merge --no-ff --no-commit dev_feature_wo_E_H
  • 解决冲突,继续并commit

一些备注

  • 我这样做是因为在之前的几天里使用了太多的cherry-pick

  • git cherry-pick很强大,也很简单,但它

    • 会创建重复的提交
    • 当我想要合并时,我必须解决初始提交和重复提交的冲突,所以对于一个或两个cherry-pick,它可以“挑选”,但对于更多的提交,它过于冗长,分支将变得太复杂

3
所有上述选项都会提示您解决合并冲突。如果您正在合并团队提交的更改,则很难从开发人员那里解决合并冲突并继续进行。但是,“git merge”将在一次操作中完成合并,但您无法将修订范围作为参数传递。我们必须使用“git diff”和“git apply”命令来执行修订范围的合并。我注意到,“git apply”将在补丁文件中有太多文件差异时失败,因此我们必须为每个文件创建一个补丁,然后应用。请注意,脚本将无法删除源分支中已删除的文件。这是一个罕见的情况,您可以手动从目标分支中删除这些文件。如果“git apply”的退出状态不为零,则无法应用补丁,但是如果使用-3way选项,它将回退到三方合并,您不必担心此失败。
以下是脚本。
enter code here



  #!/bin/bash

    # This script will merge the diff between two git revisions to checked out branch
    # Make sure to cd to git source area and checkout the target branch
    # Make sure that checked out branch is clean run "git reset --hard HEAD"


    START=$1
    END=$2

    echo Start version: $START
    echo End version: $END

    mkdir -p ~/temp
    echo > /tmp/status
    #get files
    git --no-pager  diff  --name-only ${START}..${END} > ~/temp/files
    echo > ~/temp/error.log
    # merge every file
    for file in `cat  ~/temp/files`
    do
      git --no-pager diff --binary ${START}..${END} $file > ~/temp/git-diff
      if [ $? -ne 0 ]
      then
#      Diff usually fail if the file got deleted 
        echo Skipping the merge: git diff command failed for $file >> ~/temp/error.log
        echo Skipping the merge: git diff command failed for $file
        echo "STATUS: FAILED $file" >>  /tmp/status
        echo "STATUS: FAILED $file"
    # skip the merge for this file and continue the merge for others
        rm -f ~/temp/git-diff
        continue
      fi

      git apply  --ignore-space-change --ignore-whitespace  --3way --allow-binary-replacement ~/temp/git-diff

      if [ $? -ne 0 ]
       then
#  apply failed, but it will fall back to 3-way merge, you can ignore this failure
         echo "git apply command filed for $file"
       fi
       echo
       STATUS=`git status -s $file`


       if [ ! "$STATUS" ]
       then
#   status is null if the merged diffs are already present in the target file
         echo "STATUS:NOT_MERGED $file"
         echo "STATUS: NOT_MERGED $file$"  >>  /tmp/status
       else
#     3 way merge is successful
         echo STATUS: $STATUS
         echo "STATUS: $STATUS"  >>  /tmp/status
       fi
    done

    echo GIT merge failed for below listed files

    cat ~/temp/error.log

    echo "Git merge status per file is available in /tmp/status"

2
如果你只有几个提交并且想要挑选其中的某些提交,你可以简单地执行以下操作:
git cherry-pick <commit> -n

在这些提交上,然后将它们合并为一个新的提交。 -n 不会自动创建一个提交,而只是暂存更改,因此您可以继续挑选或对该提交中的文件进行更改。

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