首页 > 解决方案 > 如何在合并期间提交合并分支

问题描述

在实际提交合并之前,我经常git merge --no-ff --no-commit <feature_branch>对我的主人做一个检查一切是否按预期工作。

虽然在那里修复合并冲突很好,但有时我会发现更严重的问题需要修复。如果我只是在合并期间修复它们,这些更改将隐藏在合并提交中。如果他们也从功能分支合并,其他人可能不会注意到它们并错过它们。

所以我改为取消合并(git reset --hard),失去我已经做过的所有冲突解决,切换回功能分支(git checkout <feature_branch>),实施修复(直到这里我必须记住并且不能在合并的上下文中测试),git commit( + git push),然后切换回 master ( git checkout master) 并重新进行合并。

该过程繁琐且容易出错。

有没有办法从合并解决方案中提交对功能分支的更改,或者,如果不可能,提交到另一个终端中的功能分支,然后更新合并以合并新的更改集而不丢失现有进度?

或者是否有其他工作流程可以解决该问题?

标签: gitmerge

解决方案


有没有办法从合并解决方案中提交对功能分支的更改

否:合并冲突使用索引来存储每个冲突文件的所有三个版本(基本、--ours--theirs,分别在索引槽 1、2 和 3 中)。Git 从索引中的任何内容构建新提交,并要求索引中的每个文件都位于其正常的“已解决”槽(槽零)中,而不是作为合并冲突解决的三个非零槽。

(文件的工作树副本保留了 Git 在合并这三个输入方面的最大努力,但 Git 仅在您修复并运行后再次使用它。这会将内容复制回索引中,写入插槽零和清空插槽 1-3。现在文件已解析并准备好提交。)git add filefile

由于任何给定的工作树只有一个索引1并且该索引对合并“忙碌”,因此您没有索引(就此而言也没有工作树)来更改功能分支. 不过,这个措辞——特别是在任何给定的工作树中——是一个很大的提示:

或者,如果这不可能,请提交到另一个终端中的功能分支......

是的,这是可能的。自从 Git 2.5 版学习了一个新特性之后,它变得容易多了:git worktree add.


1可以设置临时索引,并且在 2.5 之前的 Git 版本中,有一些 hacky 脚本可以执行等同于git worktree add使用符号链接和临时索引文件以及许多其他魔法的操作。


使用git worktree add

每个添加的工作树都有自己的索引(并非巧合,也有自己的索引HEAD)。如果您正在合并featuremaster,以便存储库的主要工作树和索引正在处理 branch master,您可以从工作树的顶层运行:

git worktree add ../feature

或者:

git worktree add /path/to/where/I/want/feature

或者:

git worktree add /path/to/dir feature

(但不只是git worktree add /path/to/dir,因为这会尝试签出一个名为 的分支dir)。

这会:

  • 创建一个新目录../feature/path/to/where/I/want/feature/path/to/dir;和
  • 本质上,git checkout feature在这条道路上运行。

您现在有一个与当前存储库关联的额外工作树。这个添加的工作树在 branch 上feature。(您的主要工作树仍在 on master,正在进行的合并仍在进行中。)在另一个工作树中,您可以修改文件,git add将它们添加到索引中,git commit并将新提交添加到 branch feature

但是,仍然存在问题。如果您使用单独的克隆,则更明显;现在让我们介绍一下。

使用单独的克隆

如果您有 2.5 之前的 Git,或者担心其中的各种错误git worktree add(有几个,包括一些仅在 2.18 或 2.19 左右才修复的相当重要的错误),您可以简单地重新克隆您的存储库。您可以将原始存储库重新克隆到新的克隆,或者将您的克隆重新克隆到新的克隆。无论哪种方式,你都会得到一个的存储库,它有自己的分支、索引和工作树,在那个克隆中你可以做任何你想做的事情。

显然,您在这个新克隆中所做的任何事情都不会影响您现有的克隆(至少在您获取或推送以将提交从克隆转移到克隆之前不会影响)。同样,您在原始克隆中所做的事情不会影响新克隆(直到您传输提交)。

转移提交会将提交放入原始克隆中,这很好;但显然,您正在执行的合并使用了您在开始合并时所做的提交,而不是您在另一个克隆中所做的任何新提交。但即使使用 也是如此git worktree add,我们稍后会看到。

添加了工作树与单独的克隆

当您使用git worktree add时,两个工作树共享底层存储库。这意味着您从一工作树所做的提交立即可供您自己在另一工作树中工作。但是,它们也共享分支名称,这导致了一个限制,git worktree add其中包括一个单独的克隆没有。

特别是,每个添加的工作树都有“锁定”对该分支名称的访问的副作用。也就是说,一旦您添加了feature-branch 工作树,其他工作树就不能使用 branch feature。如果主工作树打开master,则没有添加的工作树可以使用分支master。每个分支名称对每个添加的工作树都是专有的。(注意:您可以git checkout在添加的工作树中使用来更改分支,前提是您保持排他性属性。)

当然,您可以只删除添加的工作树。由于该工作树现已消失,它拥有专有权的分支现在可用于任何其他工作树。有关详细信息,请参阅文档

最近修复的错误与旧添加的工作树有关。如果您添加工作树,建议您在几周内完成添加的工作树中的工作,然后放弃。换句话说,将它用于相对快速的项目。(在我看来,最可怕的错误是git gc有时会认为一个对象没有被使用,因为它无法检查添加的工作树的 HEAD 和索引文件中的对象 ID。默认的 2 周修剪时间意味着从您开始在添加的工作树中工作的时间起,您至少可以在两周内免受此错误的影响。只要添加的工作树不在分离的 HEAD 上,无论何时提交,您都可以获得更多时间,重置时钟.)

搭便车

我提到,无论你在这里做什么,仍然存在问题。这与 Git 如何在“幕后”工作有关。2

然后更新合并以合并新的变更集而不丢失现有进度?

您正在进行的合并,合并featuremaster,是 - 至少在某种意义上 - 不是合并分支。它正在合并commit

请记住,在 Git 中,分支名称只是包含哈希 ID 的人类可读标识符。Git 真的是关于提交的,提交的真实名称是一个大的、丑陋的、显然是随机的、对人类不友好的哈希 ID 8858448bb49332d353febc078ce4a3abcc962efe(这是 Git 存储库中提交的哈希 ID)。

每个提交与其所有其他数据一起存储一个哈希 ID 列表,通常只有一个哈希 ID。我们可以并且 Git 确实可以使用这些将提交链接到反向链中。如果我们有一个只有三个提交的存储库,我们可能会这样绘制它,使用单个大写字母代表实际的提交哈希 ID:

A <-B <-C

由于提交A是第一次提交,它的父列表是空的:它没有父。 B但是,A作为其(唯一)父级C列出,并B作为其父级列出。Git 只需要以某种方式找到提交C,然后C找到B哪个找到A. (找到后A,没有父母了,我们可以休息了。)

分支名称在 Git 中的主要作用是允许 Git 找到分支上的最后一次提交:

A <-B <-C   <--master

名称master找到 commit C。Git对此进行了定义,因此无论里面有什么ID master都是. 因此,要向 中添加提交,我们让 Git 写出新提交的内容,将提交的父级设置为. 新的提交获得了一些新的大而丑陋的哈希 ID,但我们将称之为. 然后我们让 Git 将哈希 ID 写入名称:mastermasterCDDmaster

A <-B <-C <-D   <--master

名称仍然是master,但表示的哈希IDmaster发生了变化。 实际上,当我们进行新提交时,名称从 commit到 commit已移动。CD

这些提交内部的箭头总是向后指向,3与任何提交的任何其他部分一样不可更改。所以我们不需要绘制它们:我们知道它们附加到链接对的第二次提交。然而,从分支名称中出来的箭头会四处移动,所以我们应该画出它们。这给了我们在即将运行时看到的内容git merge

...--F--G--H   <-- master (HEAD)
      \
       I--J--K--L   <-- feature

此时,HEAD附加到master,我们运行git merge feature。吉特:

  • H使用HEAD;定位我们当前的提交
  • L使用名称定位另一个提交feature
  • 使用图表本身来查找两个分支上的最佳提交,即 commit F
  • 实际上,运行两个 git diffs:

    git diff --find-renames <hash-of-F> <hash-of-H>   # what we changed on master
    git diff --find-renames <hash-of-F> <hash-of-L>   # what they changed on feature
    
  • 尝试合并这两个差异并将生成的更改应用到来自的快照F

  • 如果成功,则进行新的提交,但如果没有,则以合并冲突停止。

的提交将有两个父级——两个向后看的链接,指向HL。一旦 Git 成功了——要么是因为合并成功而自动完成,要么是因为我们解决了冲突并使用git merge --continuegit commit自己完成了合并——我们将拥有:

...--F--G--H------M   <-- master (HEAD)
      \          /
       I--J--K--L   <-- feature

但是假设合并因冲突而停止,所以我们仍然有:

...--F--G--H   <-- master (HEAD)
      \
       I--J--K--L   <-- feature

我们的索引和包含部分合并结果的工作树?如果我们现在使用git worktree add创建第二个工作树,其HEAD附加到feature. 添加的工作树文件是来自 commit 的文件L;它的索引还包含来自L. 如果我们然后使用该工作树添加一个的提交——我们称之为这个,因为我们已经为最终的合并保留了——我们得到:NM

...--F--G--H   <-- master (HEAD of main work-tree)
      \
       I--J--K--L--N   <-- feature (HEAD of feature work-tree)

然而,正在进行的 merge仍在合并提交HL. 当我们最终完成合并时,我们得到:

...--F--G--H------M   <-- master (HEAD of main work-tree)
      \          /
       I--J--K--L--N   <-- feature (HEAD of feature work-tree)

这不是你想要的!


2此链接转到有关“幕后”一词的 Quora 答案。(Google 为我提供的第一个链接是Urban Dictionary,它有一个相当……不同的定义,咳咳。这篇 Quora 文章声称,Python 初学者不需要了解 Python 有点特殊的方法对于变量,所有对象总是被装箱并且变量仅被绑定到盒子,我不同意这种说法——或者至少,只会说非常开始——但“引擎盖下”的描述仍然很好。 )

3不管出于什么原因,我喜欢用英式英语区分“backward”和“backwards”:backward 是形容词,backwards 加上-s 是副词。再说一次,我也喜欢用 E 拼写“grey”。 但我省略了“color”中的 U。


处理重新合并

想要的是:

然后更新合并以合并新的变更集而不丢失现有进度?

这有效地要求我们重新合并. 也就是说,你想要:

...--F--G--H---------M   <-- master (HEAD)
      \             /
       I--J--K--L--N   <-- feature

你所拥有的——假设你此时删除了添加的工作树,以简化绘图——是:

...--F--G--H------M   <-- master (HEAD)
      \          /
       I--J--K--L--N   <-- feature

无论如何,提交M将指向Hand 。L但是你现在可以做更多的事情。

例如,您可以只允许M继续存在,然后git merge feature再次运行。Git 现在将执行与上次相同的操作:解析HEAD提交 ( M),解析feature提交 ( N),找到它们的合并基础- 最佳共享提交 - 这将是 commit L,并让 Git 组合两组差异。效果将是拾取您刚刚在 中进行的修复N,这甚至通常可以在没有冲突的情况下工作,尽管细节取决于您在L-vs-中所做的精确修复N。Git 将进行新的合并提交M2

...--F--G--H------M--M2   <-- master (HEAD of main work-tree)
      \          /  /
       I--J--K--L--N   <-- feature (HEAD of feature work-tree)

请注意,这M2 取决于 MforM2的存在。你不能M完全放弃,至少现在还不能。但是如果你放弃M支持M2呢?

好吧,M2将正确合并的结果作为快照M2因此,让我们使用另一个分支或标签名称保存某处的哈希 ID :

$ git tag save

现在让我们用git reset --hard HEAD~2来强行剥离两者 M M2从图中。 HEAD~2表示“从当前提交返回两个第一父链接”。is的第一个父级和M2isM的第一个父级,所以这告诉 Git 让名称再次指向:MHmasterH

             .............<-- master (HEAD)
            .
...--F--G--H------M--M2   <-- tag: save
      \          /  /
       I--J--K--L--N   <-- feature

如果我们没有保留标签M2M可见标签,那么这些提交看起来就好像完全消失了。同时,这--hard部分git reset告诉 Git 也用提交的内容覆盖我们的索引和工作树H

现在我们可以运行:

git merge feature

它告诉 Git 使用HEAD提交来查找提交H,使用名称feature来查找提交N,找到它们的合并基础 ( F),并开始合并过程。这将像以前一样遇到所有相同的冲突,您不得不重新解决它们 - 但您可以在 commit 中获得解决方案M2。所以现在你只需要告诉 Git:M2. 把它们放在我的索引和工作树中。 为此,请运行:

$ git checkout save -- .

(从工作树的顶层开始)。该名称save指向 commit M2,因此这是git checkout将从中提取文件的提交。-- .告诉git checkout不要直接检查该提交相反,不要HEAD理会,而是从该提交中获取带有 name的文件.。将这些文件复制到索引中,在插槽 0 处,清除插槽 1-3 中的任何合并冲突信息。将文件从索引复制到工作树。

由于.表示此目录中的所有文件,并且您位于顶级目录中,因此这会将您的所有索引和工作树文件替换为M2.

警告:如果您的索引和工作树中现在有一个文件在 中丢失M2则不会删除该文件。也就是说,假设您所做的修复之一N是删除名为bogus. 该文件bogus存在于M但不在M2. 如果bogus也在H其中,它现在就在您的索引和工作树中,因为您从文件开始F- 有bogus或没有 - 并从H保留bogus或添加的文件中获取所有更改bogus。要解决此问题,请使用git rm -r .before git checkout save -- .。删除步骤从索引和工作树中删除每个文件,这没关系,因为我们只想要其中的任何内容M2,这git checkout save -- .一步将得到所有这些。

(有一个更短的方法来做这一切,但它不是很“教程”,所以我在这里使用更长的方法。实际上,有两种方法,一种git read-tree在合并中间使用,另一种使用git commit-tree两个-p参数来回避运行的需要git merge。两者都需要高度的舒适和对 Git 内部工作的熟悉。)

save从(commit )中提取所有文件后M2,您就可以像往常一样使用git merge --continueor完成合并了git commit。这将创建一个新的合并 M3:

             -------------M3   <-- master (HEAD)
            /            /
...--F--G--H------M--M2 /  <-- tag: save
      \          /  /__/
       I--J--K--L--N   <-- feature

您现在可以删除标签save,这会使提交M变得M2不可见(它们需要一段时间才能真正消失,因为它们的哈希 ID 也存储在HEADreflog 中)。一旦我们停止绘制它们,我们就可以开始M3调用M

...--F--G--H---------M   <-- master (HEAD)
      \             /
       I--J--K--L--N   <-- feature

就是你想要的。

使用git rerere

还有另一种方法可以避免标记并保存合并。Git 有一个叫做“rerere”的功能,它代表re - use re - corded re solution。我自己实际上并没有使用此功能,但它是为这种事情设计的。

要使用它,您可以在开始合并之前git config rerere.enabled true运行(或者git config rerere.enabled 1,两者的意思相同),可能有也可能没有冲突,可能需要重新完成,也可能不需要重新完成。因此,您必须为此提前计划。

启用 rerere 后,您只需像往常一样进行合并,像往常一样解决任何冲突 - 可能还会向功能分支添加工作树和更多提交,即使它们不会在合并中 - 然后完成合并照常。然后你做:

git reset --hard HEAD^    # or HEAD~, use whichever spelling you prefer

这会剥离合并,丢失您的分辨率 - 但完成合并的较早步骤保存了分辨率。具体来说,Git 在合并停止时保存了冲突,然后在您告诉 Git合并完成时只保存了它们的解决方案(而不是整个文件!)。

现在你可以git merge feature再次运行。你可以等到你添加了更多的提交到feature; 保存的rerere决议将保留几个月。(如文档所述

默认情况下,超过 15 天的未解决冲突和超过 60 天的已解决冲突是 [垃圾收集]

每当 Git 运行时git rerere gc,它git gc都会为你运行。Git 本身会git gc自动为你运行,但通常不是每天都运行,所以这些可能会持续超过 15 到 60 天。)

这一次,在你运行之后,Git 会注意到它已经记录了解决方案git merge,而不是只记录冲突,并将使用这些解决方案来修复冲突。然后您可以检查结果,如果一切正常,则检查文件并完成合并。(您甚至可以使用另一个配置项来完全解析 Git 自动文件;有关详细信息,请参阅文档。)git addaddrereregit config

(这可能是最友好的开发模式,如果你必须重新做很多合并。我自己不太喜欢它,因为很难看到记录了哪些分辨率以及它们何时到期,以及自动重用即使你希望它也可能发生。你可以用它git rerere forget来清除记录的决议,但这也是一种痛苦。所以我更喜欢保持完整的提交,它有更清晰的生命周期。但我也不必重新 -合并很多。)


推荐阅读