我有一个特征分支A。然后我开始开发依赖于A的第二个特征,因此我将新的特征分支B基于A:
git checkout A
git checkout -B B
我在B上做了一些工作,所以现在在B上我有提交1(来自A)和新的提交2。 我们公司总是尽可能地将一个PR的所有提交压缩在一起,所以在某一点上我要强制A,以便A仅提交1'。现在,我想将B重新设置为A(或在A合并后为主),但是由于我强制按下A,因此git尝试应用commit 1,这显然失败了。
2种解决方法,但都不是很好:
使用git cherry-pick:
git checkout B
git checkout -B B2
git log // copy latest commit id
git checkout B
git reset --hard A
git cherry-pick <commit-id>
使用软重置:
git checkout B
git reset --soft HEAD~1
git stash
git reset --hard A
git stash pop
git commit -a -m "msg"
是否存在用于解决此问题的“ git方法”?我知道始终压缩提交可能不是最佳实践,但是我不能改变。还是有更好的方法将一个分支建立在另一个分支上?
答案 0 :(得分:1)
最终,您需要git rebase --onto
。有时候,您不需要做任何特别的事情。
让我们来介绍一下您的初始情况:
...--A--B <-- master
\
C <-- feature/A
\
D <-- feature/B
也就是说,在某条主线上有一系列提交(我在这里将其称为master
,但可能是develop
或其他),然后在您的feature/A
上进行一次提交,然后对您的feature/B
进行一次提交。 D
上的提交feature/B
的父级是C
和feature/B
上的提交feature/A
。
稍后,您已经向feature/A
添加了第二个提交,得到:
...--A--B <-- master
\
C--E <-- feature/A
\
D <-- feature/B
最终,feature/A
将被合并到master
,并且根据某些策略规则,您已经进行了一次新的提交F
,它是C
和E
,以便您现在拥有:
F <-- feature/A
/
...--A--B <-- master
\
C--E [abandoned]
\
D <-- feature/B
在这一点上,您想将D
复制到一些新的提交D'
上,该提交与D
与其父项的区别完全相同,但其中{{1} }的父级是D'
,而不是F
。
Git提供了一种简单的方法来获取您想要的东西:
C
问题是git checkout feature/B
git rebase --onto feature/A something-goes-here
部分。那里有什么?
实际上,something-goes-here
命令只是一系列git rebase
命令,后跟一个分支标签动作。正如您已经发现的那样,git cherry-pick
执行您想要的操作:它复制一个提交。实际上,它可以复制多个提交(使用Git内部称为 sequencer 的东西)。
也就是说,它会将要复制的每个提交与提交的 parent 进行比较,以查看更改的内容。然后,它对 current 提交进行相同的更改,如果一切顺利,则提交结果。
例如,让我们从这种情况开始。目前,我使用一个新标签git cherry-pick
来记住提交saved-A
,并添加了名称E
并在括号中添加了new-B
显示当前分支是HEAD
,而当前提交是提交new-B
:
F
我们现在可以运行 F <-- feature/A, new-B (HEAD)
/
...--A--B <-- master
\
C--E <-- saved-A
\
D <-- feature/B
。我们告诉Git:比较提交git cherry-pick feature/B
与它的父D
,然后对现在的位置进行相同的更改,提交C
,然后提交结果。 / em>如果一切顺利,我们将得到:
F
我们现在要做的就是将名称 D' <-- new-B (HEAD)
/
F <-- feature/A
/
...--A--B <-- master
\
C--E <-- saved-A
\
D <-- feature/B
移至指向提交feature/B
的位置,然后删除名称D'
:
new-B
同样,第一部分就是 D' <-- feature/B (HEAD)
/
F <-- feature/A
/
...--A--B <-- master
\
C--E <-- saved-A
\
D [abandoned]
所做的:复制一个提交。其中的 last 部分是git cherry-pick
所做的:移动分支标签,例如git rebase
。
此处的关键是feature/B
复制 some 个提交。 哪个? 默认答案对您来说是错误的答案!
git rebase
的作用简而言之让我们看一下稍有不同的图形:
git rebase
在这里,我们在分支...--A--B <-- target
\
C--D--E <-- current (HEAD)
上,即current
会说git status
。 on branch current
的最先提交是提交current
:E
的哈希ID是存储在名称E
中的哈希ID。
如果我们现在运行:
refs/heads/current
Git会将git rebase target
提交复制到新提交C-D-E
并将新提交放置在C'-D'-E'
之上,然后移动分支名称,如下所示:>
target
通常这就是我们想要的。但是: C'-D'-E' <-- current (HEAD)
/
...--A--B <-- target
\
C--D--E [abandoned]
怎么知道要复制git rebase
但也不知道要复制C-D-E
?
答案是A
使用Git内部的“列出一些提交”操作git rebase
,并带有停止点。重新编制文档声称git rev-list
所做的是运行的:
git rebase
这是一个白色的谎言:它足够接近且具有说明性。确切的细节比较棘手,我们稍后将解决。现在,让我们看一下git rev-list target..HEAD
的{{1}}部分。这告诉Git:不要列出从目标开始并向后工作可以找到的任何提交。
由于target..
名提交target..HEAD
,这意味着:请勿复制提交target
。好吧,我们已经不打算复制commit B
,所以没什么大不了的。但这 的意思是:不要复制提交B
。为什么不?因为提交B
指向提交A
。提交B
在两个分支A
和A
上。所以我们将复制了target
,但我们没有复制,因为它在请勿复制列表中。也有之前 current
的提交,但是它们都在请勿复制部分中,因此都不会被复制。
因此,将A
的提交复制到此处:它们在“要复制”列表中,而不是由于不在“复制”列表中而终止。
因此,A
概括地说是这样的:
C-D-E
附加到哪个分支。git rebase
。HEAD
一样。HEAD
所附加的分支名称git cherry-pick
重新连接到已移动的分支。请注意,在第4步中可能会出错。特别是,复制提交,就像通过HEAD
一样(无论是否实际使用HEAD
)都可能发生合并冲突< / em>。如果是这样,则重新定位将在中间停止,带有分离的HEAD。因此,了解步骤3很重要。但是,我们会将其留给其他问题和答案(以及有关重新设置是否确实确实使用“自动选择”的详细信息:有时会使用,有时会伪造)。
我们提到上面的git cherry-pick
是一个谎言:一种简化,旨在使人们更容易理解要复制哪些提交。现在是时候了。
首先,git cherry-pick
通常忽略合并完全提交。如果是合并(具有两个或多个父级),则由以上target..HEAD
生成的所有提交都将被剔除。只要列表中没有合并提交,就没关系了。
第二,git rebase
也忽略了与某些其他提交 patch-ID等效的提交。这使用git rev-list
程序。除了观察要获得“其他提交”部分之外,我们在这里不做任何详细介绍,Git实际上必须使用带有三个点的git rebase
。这将产生一个对称差异列表,该列表可从git patch-id
到达,但不能成为目标,并且也可以从git rev-list target...HEAD
而到达,但不能到达HEAD
。有关可达性的更多信息,请参见Think Like (a) Git。然后,rebase命令在两个列表中的每个提交上使用target
(它是内部生成的,因此它知道哪个提交哈希与哪个列表一起使用),并剔除具有匹配补丁ID的列表。这样做的效果是,例如,如果提交HEAD
与提交git patch-id
已经已经相同(樱桃选择),而不是复制{{1} },我们只需复制B
,即可获得:
D
因为提交C-D-E
和C-E
“做同样的事情”。
最后,对我们来说最重要的是, C'-E' <-- current (HEAD)
/
...--A--B <-- target
\
C--D--E [abandoned]
使我们可以使用其他 target 。
在上面的示例中,我们运行了:
B
和D
既是--onto
的 stop参数,也是我们Git放置副本的目标的目标。但是我们可以运行:
git rebase target
,现在target
将对git rev-list stop..HEAD
的{{1}}部分使用我们的 stop 参数,同时继续使用我们的 target 副本所在位置的参数。
所以,假设我们现在得到了 this :
git rebase --onto target stop
然后我们运行:
git rebase
我们现在告诉Git,我们的基准站的 stop 参数是stop
,它选择提交git rev-list
。我们的基准将在...--A--B <-- target
\
C <-- another
\
D--E <-- current (HEAD)
或git rebase --onto target another
上使用another
,这意味着要复制的提交列表将仅由C
组成。
该列表将通过patch-id和no-merges规则进一步过滤,但是只要git rev-list
与another..HEAD
不同,我们最终会得到:
C..E
也就是说,我们将仅复制{em> 从{{1}可以访问的两个提交D-E
,而忽略{{1}可以访问的提交B
}}。
这是您要进行提交复制时的设置:
D
请注意,我们添加了名称 D'-E' <-- current (HEAD)
/
...--A--B <-- target
\
C <-- another
\
D--E [abandoned]
,以记住要复制的 not 。我们不想复制提交D-E
和current
。我们还是不会复制C
,但这是记住所有内容不复制的简单方法。
我们目前有another
个签出(提交 F <-- feature/A
/
...--A--B <-- master
\
C--E <-- saved-A
\
D <-- feature/B (HEAD)
)。我们不需要创建名称saved-A
,所以我们不需要这样做。现在我们运行:
C
Git现在将列出要复制的提交:当前分支E
上的每个提交,除了在E
上的每个提交。这就是提交feature/B
。
Git现在分离HEAD,移动到提交D
(我们的new-B
目标)并复制git rebase --onto feature/A saved-A
来产生feature/B
。这样就完成了要复制的提交列表,因此,在成功将saved-A
复制到D
之后,Git强制将名称F
移到--onto
并重新附加{{1 }},给我们:
D
这正是我们想要的。
我们现在可以删除名称D'
。
如果您已经重新建立了D
的基础,但是忘记将提交D'
的提交哈希ID保存在某个地方怎么办?
幸运的是,您没有保存了feature/B
或D'
的哈希ID。您可以:
HEAD
查找他们,或 D' <-- feature/B (HEAD)
/
F <-- feature/A
/
...--A--B <-- master
\
C--E <-- saved-A
\
D [abandoned]
查找saved-A
用来命名的哈希ID,或者原始哈希ID有效,因此您可以运行:
feature/A
找到哈希ID之后。 (使用剪切和粘贴或类似方法正确获得哈希ID;手工输入它甚至是它的唯一前缀是导致错误的方法。)
引用名称也可以,因此通常您可以这样做:
E
其中E
是提交C
的哈希ID时您看到的引用日志名称,当您运行git log
列出git reflog
的先前哈希ID时。 (feature/A
可能会提交git rebase --onto feature/A <hash-ID-of-E-or-C>
,因此也可以使用。)
关键是找到要忽略的提交,并将其与git rebase --onto feature/A feature/A@{1}
一起使用。根据副本应放置的位置设置 target ,并将停止点(the git rebase
documentation调用 upstream 参数的对象)设置为哈希ID,停止您不想要复制的提交。
如果压缩的提交具有与原始提交相同的补丁ID,则feature/A@{1}
的省去具有匹配补丁ID的提交将为您完成所有工作。通常,只有在您将一个提交压榨合并到其他分支的情况下,才会发生这种情况。
E
技巧始终有效,因此您实际上不必担心这种情况,但是如果发生的次数很多,很高兴知道。