Tim*_*Tim 4 git git-merge git-revert
我对 Git 比较陌生,在误解了帮助文章后犯了一个(愚蠢的)错误,我不确定如何使用 Git 完全解决问题,而不是手动将更改重新引入目标分支。
S---sc1---sc2---sc3-----sc4----M4---R1---M5---sc5
\ \ / /
T1-------------------M2---M3--------R2
\ \ /
F---fc1---fc2---M1
Run Code Online (Sandbox Code Playgroud)
一些注意事项:S在这种情况下主要分支,T1是从拉队分支S,并且F是我的特性分支从拉T1。
我设置了自动合并,所以当提交到T1分支时,它们通过持续集成运行,然后自动合并到S. T1分支中有一个文件与S另一个团队成员的提交发生合并冲突,所以我决定在完成F.
我合并T1成F(M1),然后F进入T1(M2)。考虑到我过去遇到的合并冲突解决方案不符合我的预期的问题,我想我会尝试一些新的东西:将冲突文件从SintoT1合并,解决那里的合并冲突,从合并,然后允许持续集成将所有内容合并到S
我在没有提交的情况下开始了从S到T1( M3)的合并,解决了冲突,从合并中删除了其他 (~200) 个文件,然后提交。这会自动合并到S( M4)。
我立即注意到,排除这大约 200 个文件似乎已经完全消除了更改,这相当于 2 个团队大约一个月的工作价值。我(错误地)决定最好的行动方案是迅速采取行动并恢复合并提交,M4并且M3在我的错误进入其他任何人的本地存储库之前。我首先还原M4( R1),一旦提交,我就还原M3( R2)。我认为这是正确的顺序,因为当自动合并开始时,我不确定相反的方式是否会引入问题。最终R2提交并自动合并到S( M5)。
这解决了其他人的更改被清除的问题,但是我的所有更改F以及最初具有合并冲突的文件都从S. 我能够将单个文件的更改直接提交到S( sc5),但 中的更改F要复杂得多。他们T1仍然生活,但由于他们S作为 的一部分被恢复R1,我不能只是将他们提交回来。
我花了一天的大部分时间试图找出如何最好地获得这些变化最多S,但git rebase和git cherry-pick似乎并不像他们会做什么,我需要,但我很清楚,我可能是错上。如果有比我更擅长 Git 的人至少可以提出一个起点,那就太棒了。谢谢!
编辑:从图中删除了无用/令人困惑的点。M2没有自动合并,S因为我试图用M3.
编辑 2:在阅读了 torek 的精彩解释后,我开始尝试重新设置基准。我忘记了我在整个历史中多次T1将F分支合并到分支中,F因为这个功能分支跨越了多少时间。这意味着有许多合并冲突需要解决。
在 torek 对此的回应中,我尝试了合并壁球。我最初的想法是我需要将新分支从合并壁球合并到T1分支,然后将T1分支合并到S,但我遇到了同样的问题,它看不到更改。我认为这是因为更改已经存在,T1所以它基本上只是将相同的、先前还原的更改反馈回S.
编辑 3:感谢 torek 的解释非常详细、详细的回答(非常感谢!),我正在处理合并壁球,然后S在解决冲突后将结果合并到分支。
这很长,所以请随意跳过您已经知道的部分(或一直滚动到最后)。每个部分都有设置信息来解释正在发生的事情,或者我们正在做的事情,在后面的部分。
让我以我喜欢的方式重新绘制这个图(我认为它是一个部分图,但它包含我们需要的关键提交):
S0--sc1---sc2---sc3-----sc4----M4---R1---M5---sc5 <-- branch-S
\ \ / /
T0-------------o----M2---M3--------R2 <---- branch-T1
\ \ /
F0--fc1---fc2---M1 <------------------- branch-F
Run Code Online (Sandbox Code Playgroud)
在这里,分支名称是branch-S,branch-T1和branch-F,而这些名字目前确定的提交其哈希ID是什么不能发音,也不可能对人类记忆,但我们呼吁sc5,R2和M1分别。任何o节点都是没有以任何方式特别区分的提交,并且实际上可能代表一些任意数量的提交。命名的fc<number>s 是功能分支上的一些提交,M<number>提交是合并的。我重命名了第一个提交S0, T0,F0只是为了将它们与分支名称区分开来。
一些合并是手动进行的:
$ git checkout <branch-name>
$ git merge [options] <other-branch>
... fix up conflicts if necessary, and git commit (or git merge --continue)
Run Code Online (Sandbox Code Playgroud)
其他合并是由软件进行的,并且只有在没有冲突时才会发生。在R提交的运行:
git checkout <branch>
git revert -m 1 <hash ID of some M commit>
Run Code Online (Sandbox Code Playgroud)
where<branch>是T1or S,并且-m 1是因为git revert在恢复合并时您总是必须告诉使用哪个父级,并且它几乎总是父级 #1。
最简单的 Git 提交图是一条直线,有一个分支名称,通常是master:
A--B--C <-- master (HEAD)
Run Code Online (Sandbox Code Playgroud)
在这里,我们需要提到 Git 的index。索引可能最好被描述为 Git 构建下一次提交的地方。它最初包含保存在当前提交中的每个文件(此处C):您检查此提交,使用 commit 中的文件填充索引和工作树C。namemaster指向此提交,并且 nameHEAD附加到 name master。
然后修改工作树中的文件,用于git add将它们复制回索引,用于git add将新文件复制到索引(如果需要),然后运行git commit. 通过将这些索引副本冻结到快照中来进行新的提交。然后,Git 添加快照元数据——您的姓名和电子邮件、您的日志消息等——以及当前提交的哈希 ID,以便新提交指向现有提交。结果是:
A--B--C <-- master (HEAD)
\
D
Run Code Online (Sandbox Code Playgroud)
有了新的提交,有了新的唯一哈希 ID,就在半空中闲逛,什么都不记得了。因此,进行新提交的最后一步是将新提交的哈希 ID 写入分支名称:
A--B--C--D <-- master (HEAD)
Run Code Online (Sandbox Code Playgroud)
现在当前提交是D,并且索引和当前提交匹配。如果你git add-ed 工作树中的所有文件,它也匹配当前提交和索引。如果没有,您可以添加git add更多文件并再次提交,使名称master指向新提交E,依此类推。在任何情况下,新的(单个)父提交是任何当前提交是。
让我概述一下git merge实际工作原理。在某些情况下和某些方面非常简单,让我们使用最简单的真正合并案例开始。考虑一个看起来像这样的图:
o--...--L <-- mainline (HEAD)
/
...--o--*
\
o--...--R <-- feature
Run Code Online (Sandbox Code Playgroud)
我们已经运行了git checkout mainline; git merge feature,所以我们告诉 Git 将 branch feature/commit合并R到 branch mainline/commit 中L。为此,Git 必须首先找到合并基础提交。粗略地说,合并基础是两个分支共有的“最近”提交——即,可从两个分支访问。在这个简单的例子中,我们从 at 开始L向后走到较旧的提交,从 at 开始R向后走,我们遇到的第一个地方是 commit *,这就是合并基础。
(有关可达性的更多信息,请参阅Think Like (a) Git。)
找到合并基础后,Git 需要将L(左侧 / 本地 / --ours)和R(右侧 / 远程 / --theirs)快照转换为变更集。这些变更集告诉 Git 我们做了什么, on mainline,自合并 base 以来*,以及他们做了什么, on feature,自合并 base 以来。这三个提交都有哈希ID,这是三个提交的真实名称,因此Git可以在内部运行相当于:
git diff --find-renames <hash-of-*> <hash-of-L> # what we changed
git diff --find-renames <hash-of-*> <hash-of-R> # what they changed
Run Code Online (Sandbox Code Playgroud)
合并只是简单地组合了两组更改,并将组合的集应用于 .snap 文件中的快照中的文件*。
当一切顺利时,Git 以通常的方式进行新的提交,除了新的提交有两个父项。这使当前分支指向新的合并提交:
o--...--L
/ \
...--o--* M <-- mainline (HEAD)
\ /
o--...--R <-- feature
Run Code Online (Sandbox Code Playgroud)
的第一个父级M是L,第二个是R。这就是为什么 reverts 几乎总是使用 parent #1,以及为什么git log --first-parent只“看到”主线分支,从Mup 到遍历L而R完全忽略分支。(请注意,这里的分支这个词指的是图的结构,而不是像这样的分支名称feature:此时,我们可以完全删除名称 feature。另请参阅“分支”究竟是什么意思?)
如果两个变更集以“糟糕的方式”重叠,合并将停止,并发生合并冲突。特别地,假设 base-vs-L 说要更改 file 的第 75 行F,而 base-vs-R也说要更改 file 的第 75 行F。如果两个更改集都说要进行相同的更改,则 Git 可以这样做:两个更改的组合是进行一次更改。但是如果他们说要进行不同的更改,Git 就会声明合并冲突。在这种情况下,Git 会在它自己能做的事情之后停下来,让你清理烂摊子。
由于有三个输入,此时 Git 会将文件的所有三个版本都保留F在索引中。通常,索引对每个要提交的文件都有一个副本,但在此冲突解决阶段,它最多有三个副本。(“最多”部分是因为您可能会有其他类型的冲突,由于篇幅原因,我不会在这里介绍。)同时,在file的工作树副本中F,Git 将其近似值留给了合并,工作树文件中的两组或全部三组行带有<<<<<<</>>>>>>>标记。(要获得所有三个,请设置merge.conflictStyle为diff3。我更喜欢这种解决冲突的模式。)
如您所见,您可以以任何您喜欢的方式解决这些冲突。Git 假定无论你做什么都是解决问题的正确方法:这会产生完全正确的最终合并文件,或者在某些情况下缺少文件。
不管你做什么,最终的合并——假设你没有中止它,并且没有使用合并的非合并变体之一——仍然会在图中产生相同的结果,无论你在索引中放入什么,通过解决冲突,是合并的结果。这是合并提交中的新快照。
当图形像上图一样非常简单时,合并基很容易看到。但是图表并不简单,你的也不简单。有一些合并的图的合并基础比较棘手。例如,仅考虑以下片段:
...--sc4----M4---R1
\ /
...--M2---M3--------R2
Run Code Online (Sandbox Code Playgroud)
如果R1和R2是两个提示提交,它们的合并基础是什么?答案是M3,不是sc4。其原因是,虽然M3和sc4都提交是由起始于两个到达R1和R2向后工作,M3是“接近” R2(退一万步)。从R1to 或M3or的距离sc4是两跳——转到M4,然后再退一步——但是从R2to的距离M3是一跳,而从R2to的距离sc4是两跳。所以M3是“较低”(在图形方面),因此赢得了比赛。
(幸运的是,你的图没有的情况下,那里是一个平局。如果是平局,Git的默认方法是合并所有捆绑提交,每次两个,以产生一种“虚拟合并基础”,这实际上是一个实际的,尽管是临时的,提交。然后它使用这个通过合并合并基所做的临时提交。这是递归策略,它的名字来自于 Git 递归合并合并基以获得合并基的事实。您可以选择该解决策略,它只是挑选在基地的一个看似随机,取其基地在算法的前弹出有很少任何优势在于:递归方法通常要么做同样的事情,或者是改进了随机选择获胜者。)
这里的关键点是进行合并提交更改,提交将来的合并将选择作为它们的合并基础。即使在进行简单的合并时,这也很重要,这就是我将其用粗体显示的原因。这就是我们进行合并提交的原因,而不是非合并的挤压“合并”操作。(但壁球合并仍然很有用,我们稍后会看到。)
有了上面的方法,现在我们可以看看真正的问题了。让我们从这个开始(稍微编辑以使用更新的提交和分支名称):
我合并
branch-T1成branch-F(M1),然后branch-F进入branch-T1(M2)。
我在这里假设合并fc2(作为 的 then-tip branch-F)和o(作为 的 then-tip branch-T1)进行得很顺利,并且 Git 能够M1自己制作。正如我们之前看到的,合并实际上不是基于分支,而是基于提交。这是一个调整分支名称的新提交的创建。所以this创建M1,所以branch-F指向M1。 M1本身指向现有的提示branch-T1——我现在标记的提交o——fc2作为它的第二个父级,作为它的第一个父级。Git通过-ing 的内容、合并基础、反对和反对来计算此提交的正确内容:git diffT0ofc2
T0-------------o <-- branch-T1
\
F0--fc1---fc2 <--- branch-F (HEAD)
Run Code Online (Sandbox Code Playgroud)
一切顺利,Git 现在可以M1自己制作:
T0-------------o <-- branch-T1
\ \
F0--fc1---fc2---M1 <--- branch-F (HEAD)
Run Code Online (Sandbox Code Playgroud)
现在你git checkout branch-T1and git merge --no-ff branch-F(没有--no-ffGit 只会做一个快进,这不是图片中的内容),所以 Git 找到了oand的合并基础M1,这就是o它本身。这种合并很容易:与oto的区别o是什么,没有与oto的区别M1等于 的内容M1。因此M2,作为快照,与 完全相同M1,Git 轻松创建它:
T0-------------o----M2 <-- branch-T1 (HEAD)
\ \ /
F0--fc1---fc2---M1 <--- branch-F
Run Code Online (Sandbox Code Playgroud)
到目前为止,一切都很好,但现在事情开始变得非常糟糕:
T1分支中有一个文件存在合并冲突S... fromSintoT1,解决那里的合并冲突,从合并中删除所有其他文件,然后允许持续集成将所有内容合并到S.
所以,此时你所做的是:
git checkout branch-T1
git merge branch-S
Run Code Online (Sandbox Code Playgroud)
因合并冲突而停止。此时的图形与上面的图形相同,但有更多的上下文:
S0--sc1---sc2---sc3-----sc4 <-- branch-S
\
T0-------------o----M2 <-- branch-T1 (HEAD)
\ \ /
F0--fc1---fc2---M1 <-- branch-F
Run Code Online (Sandbox Code Playgroud)
合并操作找到合并基础 ( S0),将其与两个提示提交 (M2和sc4) 进行比较,组合产生的更改,并将它们应用于 的内容S0。一个冲突的文件现在作为三个输入副本在索引中,在工作树中作为 Git 的合并努力,但带有冲突标记。同时所有无冲突的文件都在索引中,准备被冻结。
唉,您现在git rm在冲突合并期间删除了一些文件 ( )。这将从索引和工作树中删除文件。结果提交,M3,会说组合提交M2和sc4基于合并基础的正确方法S0是删除这些文件。(这当然是错误的。)
这会自动合并到
S(M4)。
在这里,我假设这意味着系统,使用它拥有的任何预编程规则,做了相当于:
git checkout branch-S
git merge --no-ff branch-T1
Run Code Online (Sandbox Code Playgroud)
其中发现提交的合并基础sc4(尖branch-S)和M3,其是M3,以同样的方式的合并基础o和M1为M1早。所以新的提交,M4,M3在内容方面匹配,此时我们有:
S0--sc1---sc2---sc3-----sc4----M4 <-- branch-S
\ \ /
T0-------------o----M2---M3 <-- branch-T1
\ \ /
F0--fc1---fc2---M1 <-- branch-F
Run Code Online (Sandbox Code Playgroud)
我立即注意到,排除这大约 200 个文件似乎已经完全消除了更改,这相当于 2 个团队大约一个月的工作价值。我(错误地)决定最好的行动方案是迅速采取行动并恢复合并提交,
M4并且M3在我的错误进入其他任何人的本地存储库之前。我首先还原M4(R1),一旦提交,我就还原M3(R2)。
事实上,这是一件好事!它获得了正确的内容,当您立即执行时,这非常有用。使用git checkout branch-s && git revert -m 1 branch-S(或git revert -m 1 <hash-of-M4>) 创建R1fromM4基本上取消了内容方面的合并,因此:
git diff <hash-of-sc4> <hash-of-R1>
Run Code Online (Sandbox Code Playgroud)
根本不应该产生任何东西。同样,使用git checkout branch-T1 && git revert -m 1 branch-T1(或与哈希相同)R2从M3合并内容的撤消创建:比较M2和R2,您应该看到相同的内容。
现在的问题是 Git 认为您的功能分支中的所有更改都已正确合并。Any git checkout branch-T1orgit checkout branch-S后跟git merge <any commit within branch-F>将查看图表,遵循从提交到提交的向后指向链接,并看到此提交branch-F- 例如fc2or M1-已经合并。
诀窍让他们在为做一个新的承诺,做同样的事情,在提交序列的F0通过M1确实,这是不是已经合并。最简单(尽管最丑陋)的方法是使用git merge --squash. 更难,也许更好的方法是使用git rebase --force-rebase创建一个新的功能分支。(注意:这个选项有三个拼写,最简单的一个是-f,但是Linus Torvalds 的描述中的一个是--no-ff。我认为最令人难忘的是--force-rebase版本,但我实际上会使用-f自己。)
让我们快速浏览一下两者,然后考虑使用哪个以及为什么使用。无论哪种情况,一旦完成,这次您必须正确合并新提交,而不能删除文件;但既然您知道git merge真正在做什么,那么做起来应该容易多了。
我们首先创建一个新的分支名称。我们可以重复使用branch-F,但我认为如果我们不这样做会更清楚。如果我们想使用git merge --squash,我们创建这个指向提交的新分支名称T0(忽略后面有提交的事实——T0记住,任何分支名称都可以指向任何提交):
T0 <-- revised-F (HEAD)
\
F0--fc1--fc2--M1 <-- branch-F
Run Code Online (Sandbox Code Playgroud)
如果我们想使用git rebase -f,我们创建这个指向 commit 的新名称fc2:
T0-----....
\
F0--fc1--fc2--M1 <-- branch-F, revised-F (HEAD)
Run Code Online (Sandbox Code Playgroud)
我们这样做:
git checkout -b revised-F <hash of T0> # for merge --squash method
Run Code Online (Sandbox Code Playgroud)
或者:
git checkout -b revised-f branch-F^1 # for rebase -f method
Run Code Online (Sandbox Code Playgroud)
取决于我们要使用哪种方法。(^1或~1后缀 - 您可以使用任何一个 - 排除M1自身,将第一个父步骤后退到fc2。这里的想法是排除提交o和可从 到达的任何其他提交o。branch-F沿着 的底部行不需要其他合并提交,在这里。)
现在,如果我们想使用“挤压合并”(它使用 Git 的合并机制而不进行合并提交),我们运行:
git merge --squash branch-F
Run Code Online (Sandbox Code Playgroud)
这使用我们当前的提交,加上branch-F(commit M1)的尖端,作为合并的左侧和右侧,找到它们共同的提交作为合并基础。普通提交当然只是F0,所以合并结果是M1. 然而,新的提交只有一个父提交:它根本不是一个合并提交,它看起来像这样:
fc1--fc2--M1 <-- branch-F
/
F0-------------F3 <-- revised-F (HEAD)
Run Code Online (Sandbox Code Playgroud)
该快照在F3那场比赛中M1,但承诺本身是全新的。它会收到一条新的提交消息(您可以编辑),并且当 Git 将其F3视为提交时,其效果是对从F0到所做的相同更改集M1。
如果我们选择 rebase 方法,我们现在运行:
git rebase -f <hash-of-T0>
Run Code Online (Sandbox Code Playgroud)
(你可以使用,而不是哈希o,这是branch-F^2即第二父M1。在这种情况下,你可以开始revised-F指向M1自身。这可能是我会做什么,以避免剪切和粘贴大量的散列ID的用潜在的拼写错误,但除非您进行了大量图形操作练习,否则它的工作原理并不明显。)
也就是说,我们希望F0通过fc2包含新的哈希 ID将提交复制到新的提交中。这就是这样git rebase做的(请参阅上面的其他 StackOverflow 答案和/或 Linus 的描述):我们得到:
F0'-fc1'-fc2' <-- revised-F (HEAD)
/
T0-----....
\
F0--fc1--fc2--M1 <-- branch-F
Run Code Online (Sandbox Code Playgroud)
现在我们已经revised-F指向单个提交 ( F3) 或提交链(以 结尾的链fc2', 的副本fc2),我们可以使用git checkout其他分支和git merge revised-F。
在这一点上,我假设您有一个压缩合并结果(单亲提交不是合并,但确实包含所需的快照,我在F3这里称之为)。我们也需要稍微修改一下重新绘制的图,基于表明有更多合并到 的评论branch-F:
S0--sc1---sc2---sc3-----sc4----M4---R1---M5---sc5 <-- branch-S
\ \ / /
T0-----o-------o----M2---M3--------R2 <---- branch-T1
\ \ \ /
F0--fc1-o-fc2---M1 <--------------- branch-F
Run Code Online (Sandbox Code Playgroud)
现在我们将添加revised-F分支,它应该有一个提交,它是F0或的后代T0。哪一个并不重要。由于我F0之前使用过,让我们在这里使用它:
S0--sc1---sc2---sc3-----sc4----M4---R1---M5---sc5 <-- branch-S
\ \ / /
T0-----o-------o----M2---M3--------R2 <---- branch-T1
\ \ \ /
F0--fc1-o-fc2---M1 <--------------- branch-F
\
---------------------------------F3 <-- revised-F
Run Code Online (Sandbox Code Playgroud)
提交的内容F3匹配M1(所以git diff branch-F revised-F什么也没说),但F3这里的父级是F0. (注意:有创建F3using 的快捷方式git commit-tree,但只要它已经存在并且与M1内容匹配,我们就可以使用它。)
如果我们现在这样做:
git checkout branch-T1
git merge revised-F
Run Code Online (Sandbox Code Playgroud)
Git 将在提交R2(分支 T1 的F3尖端)和( 的尖端revised-F)之间找到合并基础。如果我们从所有的向后(向左)链接跟随R2,我们可以得到T0通过M3,然后M2再一定数量的o秒和最终T0,或者我们可以得到F0通过M3,然后M2再M1接着fc2上回F0。同时,我们可以F3直接从到F0,只需一跳,因此合并基数可能是F0。
(要确认这一点,请使用git merge-base:
git merge-base --all branch-T1 revised-F
Run Code Online (Sandbox Code Playgroud)
这将打印一个或多个哈希 ID,每个合并基础一个。理想情况下,只有一个合并基础,即 commit F0。)
现在的Git将运行两个git diffS,以内容比F0对F3-即,我们所做的一切都是以完成功能和内容比较F0于那些R2在前端branch-T1。当两个差异更改相同文件的相同行时,我们会遇到冲突。在其他地方,Git 将获取 的内容F0,应用合并的更改,并将结果准备好提交(在索引中)。
解决这些冲突并提交会给你一个新的提交,结果是:
S0--sc1---sc2---sc3-----sc4----M4---R1---M5---sc5 <-- branch-S
\ \ / /
T0-----o-------o----M2---M3--------R2-----M6 <---- branch-T1
\ \ \ / /
F0--fc1-o-fc2---M1 <-- branch-F /
\ /
---------------------------------F3 <-- revised-F
Run Code Online (Sandbox Code Playgroud)
现在M6,也许可以合并到branch-S.
或者,我们可以直接合并到branch-S. 哪个提交是合并基础不太明显,但可能F0又是一次。这里又是同一张图:
S0--sc1---sc2---sc3-----sc4----M4---R1---M5---sc5 <-- branch-S
\ \ / /
T0-----o-------o----M2---M3--------R2 <---- branch-T1
\ \ \ /
F0--fc1-o-fc2---M1 <--------------- branch-F
\
---------------------------------F3 <-- revised-F
Run Code Online (Sandbox Code Playgroud)
从 commit 开始sc5,我们向后工作到M5to R2,我们现在处于与以前相同的情况。所以我们可以git checkout branch-S做同样的合并,解决类似的冲突——这次我们比较的是F0tosc5而不是 to R2,所以冲突可能会略有不同——并最终提交:
S0--sc1---sc2---sc3-----sc4----M4---R1---M5---sc5----M6 <-- branch-S
\ \ / / /
T0-----o-------o----M2---M3--------R2 <------ / -- branch-T1
\ \ \ / /
F0--fc1-o-fc2---M1 <-- branch-F /
\ /
---------------------------------------F3 <-- revised-F
Run Code Online (Sandbox Code Playgroud)
要验证它F0是合并基础,请git merge-base像以前一样使用:
git merge-base --all branch-S revised-F
Run Code Online (Sandbox Code Playgroud)
并查看您必须合并的内容,git diff从合并基础运行两个s 到两个提示。
(要进行哪种合并取决于您。)
| 归档时间: |
|
| 查看次数: |
1480 次 |
| 最近记录: |