从其他分支挑选特定提交进行精选

11

这个可能吗?从其他分支挑选到主分支的特定提交。例如:假设我有两个分支:

 -> master : <commit1>, <commit2>, <commit3>
 -> test: <c1>, <c2>, <c3>, <c4>

现在我想从 test 分支中挑选一个提交(例如:<c3>),并将其应用到主分支的一个 <commit2> 上。

编辑:我知道可以通过 rebase 命令来完成,但是否只能通过 cherry-pick 命令来完成?


你的意思是想将另一个分支中特定提交的更改添加到主分支中现有的提交中吗? - Michael Mior
是的,但如果可能的话,通过cherry-pick进行选择合并可能更好,因为我听说cherry-pick是rebase命令的低级工具。 - Ashish Rawat
你为什么反对使用rebase?它是完成工作的正确工具。 - Michael Mior
我目前正在学习Git,所以现在我只想保持我的工具集尽可能小。这就是为什么我避免使用rebase,因为它有更多的选项。 - Ashish Rawat
我知道原问题是关于执行cherry-pick的,但我在类似问题上找到了这个答案,至少可以提供另一种处理/解决这种情况的观点:https://dev59.com/WXNA5IYBdhLWcg3wpfmu#62402568 - Guy Avraham
4个回答

34

你对 Git 的工作原理有一个基本错误。

这是可能的吗?从其他分支 cherry picking 到特定的 master 分支提交中。

不可以:Git 不能 更改 任何 提交。您可以添加新的提交,并可以复制现有提交以制作一个新的提交,即添加一个新的提交,该提交是现有提交的副本(其中,副本中至少有一件事情与原提交不同)。后者就是 git cherry-pick 所做的。

您还可以更改任何分支 名称 对您记忆的特定提交哈希 ID。

绘制分支

比如,我有两个分支:

-> master : <commit1>, <commit2>, <commit3>
-> test: <c1>, <c2>, <c3>, <c4>
这不是绘制分支的好方法。下面是更好的方法:
...--D--E--F--G   <-- master
      \
       H--I--J--K   <-- test

这里的提交E对应于您的<commit1>,我只是使用单个字母来缩短它们的长度。同样,F就像您的<commit2>J就像您的<c3>。 我还包括了另一个早期的提交D,从该提交扩展出两个分支,以表明在EH之前可能存在更早的提交。

请注意,每个提交都记住其提交: G的父提交是F,因此我们说G“指向”FF指向EE指向D,依此类推。同样,提交K指向JJ指向II指向H。请注意,仅在分支test上的H也指向D

(提交D特别有趣:它位于两个分支上。这也是Git称为两个分支的合并基础的原因。但这不是我们在这里关心的问题; 这只是一个有趣的侧面说明,在Git中,提交可以同时位于多个分支上。)

在本示例中,分支名称mastertest,每个名称都指向一个特定的提交。这里的名称master指向提交G,而名称test指向提交K。这就是我们如何确定哪些提交在哪些分支上:我们像Git一样,从分支名称开始查找分支尖端提交。从这些尖端提交开始,我们(或者Git)向后工作:从任何一个提交开始,到其父提交,然后到该提交的父提交,以此类推。这个过程只有在我们跟着父提交感到疲倦(例如退出git log),或被告知停止,或遇到根提交时才会停止,根提交是没有父提交的提交。(您在存储库中创建的第一个提交是根提交,因为没有早期的提交可用作父提交。)(可能会创建额外的根提交,但在这里我们不会看到它们。)

挑选提交

现在我想从测试分支中挑选一个提交记录(例如:<c3>),并将其“复制”到主分支中的<commit2>位置。你可以使用cherry-pick来实现这一点,但是我加粗和斜体的部分(“到一个提交记录”)是无意义的。cherry-pick意味着复制一个提交记录,这个副本是一个新的提交记录,通常会添加到分支上。

既然你的<c3>就是我的J,那么让我们看看如果你使用以下命令会发生什么:

git checkout master
git cherry-pick test~1

第一步让你“进入”分支master,也就是说,你现在添加的新提交将被添加到master中,通过将名称master指向它们。

第二步通过从test的末端向后走一步来确定提交J。名称test标识提交K:一个分支名称“意味着”该分支的末端提交。后缀~1表示“后退一级”,因此我们从提交K移动到提交J。那就是我们要求Git复制的提交。

暂时忽略进行此复制的确切机制,只看结果。 Git创建了一个“类似于”J新提交;让我们称这个新提交为J'。原始J和新的副本J'之间存在几个差异,目前最重要的差异是复制品的父节点master的现有末端提交,即提交G。因此,J'指向G

完成复制并永久存储新的J'后,Git会更新master以指向我们刚刚创建的新提交:

...--D--E--F--G--J'  <-- master
      \
       H--I--J--K   <-- test

请注意,没有现有的提交受到影响。这是因为更改任何现有提交是完全不可能的。

变基(Rebase)

我知道可以通过rebase命令来完成...

无法通过rebase命令实现,这真的很重要。变基所做的不是修改提交,而是复制——可能会复制很多次。

让我们考虑这样一种情况,你在master上有一个D-E-F-G序列,并且想要复制J并在F之后但G之前插入它。也就是说,你想要得到这个结果,即使这是不可能的:

...--D--E--F--J'-G   <-- master
      \
       H--I--J--K   <-- test

之所以不可能是因为G 无法更改,而且G已经指回了F。但如果我们这样做呢:

原因在于G无法更改,并且G已经指回到F。但如果我们做了这个:

             J'-G'  <-- new-master
            /
...--D--E--F--G   <-- old-master
      \
       H--I--J--K   <-- test

换句话说,假设我们将 J 复制到新的 new-master 分支上的 J',并将 J' 放在 F 之后,然后将 G 复制到 G',并将 G' 放在 J' 之后,这样会怎样呢? 我们可以使用 git cherry-pick 来完成此操作,我们只需挑选两个提交,先选择 J,然后再选择 G,复制到一个新分支上:

git checkout -b new-master master~1    # make the new-master branch at F
git cherry-pick test~1                 # copy J to J'
git cherry-pick master                 # copy G to G'

假设我们完成了这样的操作,我们 擦除 旧的 master 名称,只需将新名称设置为 master,像这样:

And, suppose once we have done that, we erase the old master name entirely and just call the new one master, like this:

             J'-G'  <-- master
            /
...--D--E--F--G   [abandoned]
      \
       H--I--J--K   <-- test

现在,如果我们停止绘制已废弃的原始 G,并直接画出master,它看起来像是我们在FG之间插入了J'...但实际上我们没有:我们将G复制到了G'

git rebase 命令本质上是一个自动化的、高级的“挑选很多提交”命令,紧接着在末尾进行一些分支名称移动操作。

为什么这很重要

这只有在某些情况下才会关系到,但当它确实关系到时,就非常重要。

假设我们从以下内容开始,这与之前非常相似,只是还有另一个分支:

...--D--E--F--G   <-- master
      \        \
       \        L   <-- develop
        \
         H--I--J--K   <-- test

现在我们将 J 复制到 J',放在 F 之后,然后将 G 复制到新的提交 G' 中,使用 J' 作为 G' 的父提交,并使名称 master 记住新提交 G' 的哈希 ID。 (这与我们之前执行的 rebase-with-copy 相同;我们只是有了这个额外的 develop 分支。)

             J'-G'  <-- master
            /
...--D--E--F--G
      \        \
       \        L   <-- develop
        \
         H--I--J--K   <-- test

现在让我们重新绘制这个布局,使其更加清晰:

...--D--E--F--J'-G'  <-- master
      \     \
       \     G--L   <-- develop
        \
         H--I--J--K   <-- test
尽管我们让 Git“忘记”了位于主分支上的原始提交 G,转而使用具有 J' 作为其父对象的全新G’,但是原始的 G 仍然存在于 develop 分支上。实际上,现在看起来好像 G 一直都在 develop 分支上,并且我们可能是从那里将其合并到 master 分支中去的。
关键点在于:虽然你可以复制提交并忘记原始版本,但如果原始版本在其他地方已知 - 通过你自己的存储库中的另一个分支,或者(更糟糕的情况)通过 git push 推送到另一个存储库中 - 如果你想要所有其他用户使用新版本,则必须让所有其他用户使用新版本。 你无法更改原始版本,只能将它们复制到新版本并让每个用户切换到新版本。

1
点赞,不仅因为你提供了详细的答案,而且还解释了我思考错误的原因。 - Ashish Rawat

2

是的,您可以使用git cherry-pick <test分支中的提交>来应用更改到master分支。

要将测试分支中的提交在非最新的主分支上挑选出来,您可以使用以下方法:

git checkout <a commit on master you want to cherry pick>
git cherry-pick <a commit from test branch>
git rebase --onto HEAD <the commit you used in step1> master

我不想将更改应用于主分支的头部。我只想通过 cherry-pick 应用到主分支中的特定提交。 - Ashish Rawat
你可以首先检出到提交,然后挑选一个注释,接着使用 git rebase --onto HEAD <你检出的提交> master。 - Marina Liu
如果你在第一步中使用了 git checkout my-branch,那么可能不需要使用 git rebase --onto HEAD。实际上,如果你执行 git rebase --onto HEAD my-branch,它什么也不会做。 - Nawaz

1

您将重新编写存储库历史记录,这不是推荐的做法。

  • 首先,在主分支中必要的提交处创建一个新分支。
  • 检出新分支。
  • 从测试分支中将必要的提交挑选到新分支中。
  • 检出主分支。
  • 将主分支合并到新分支中。

如果需要将主分支推送到远程存储库,则必须强制推送主分支。如果其他人已经提交了主分支,他们的主分支将因此失效。


重点是我不想使用rebase,只想用cherry-pick。如果我想这样做,那么你的第二点就不必要了。 - Ashish Rawat
@AshishRawat:不幸的是,你必须使用一个临时分支("checkout the new branch",第2步),因为Git只会将新的提交添加到分支中。你可以使用Git所谓的“分离头指针”来完成这个操作——这就是git rebase的做法——以避免需要一个临时分支名称,但你仍然需要一个临时分支。 - torek

0
如果您想更改现有的提交,可以使用rebasecherry-pick结合使用。
GIT_SEQUENCE_EDITOR='sed -i s/^pick/edit/' git rebase -i <commit2>
git cherry-pick -n <c3>
git commit --amend -C HEAD
git rebase --continue

首先运行rebase以选择要编辑的主分支提交。然后应用来自其他提交的更改,修改主分支上的提交,并完成rebase。当然,这实际上并不会更改提交,但它将创建一个新的提交,其中包含来自<commit2><c3>的更改。

使用GIT_SEQUENCE_EDITOR只是避免在开始rebase时手动选择单个提交进行编辑。


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