我合并然后只提交了一些文件。我如何重新合并其他人?

I merged and then committed only some of the files. How do I re-merge the others?

我正在制作功能 b运行ch。从主b运行ch 运行 git merge之后,我有两个冲突。

我手动解决了它们,然后 只提交了这两个文件,运行 git reset --hard 删除了其他更改

结果 - git 认为其他文件已合并,而实际上,它们 100% 来自我的特征 b运行ch。

我该如何解决这个问题以及 "re-merge" 与主 b运行ch?

问题陈述本身有点偏差,因为 git commit 写入了 所有 文件,而不仅仅是其中的一些文件。我会先稍微调整一下,但请随时跳到下一部分。 :-)

更具体地说——这应该可以帮助你考虑 Git 在未来——Git 写入 索引 中的任何内容(必须完全合并). git merge 命令在索引中构建合并两个(或更多但我们坚持两个)提交的结果。如果存在冲突,Git 将冲突文件存储在工作树和索引中。

然后您可以编辑您所做的工作树中的文件。您可以使用 git add 将工作树文件复制到索引中,用单个已解决的版本替换冲突的版本。1 您也这样做了。您可以在其 git reset <em>paths</em> 形式中使用 git reset 来复制特定的 paths 来自 HEAD 提交到索引中,替换当前索引中的任何内容。

正是这最后一步造成了问题:Git 成功合并了其他文件(至少在 Git 的小脑袋里是这样 :-) )。然后你告诉 Git 它应该将这些文件的 HEAD(当前提交,合并前)版本复制到索引中,撤销合并对这些文件的影响。


1这里最重要的细节是,当发生冲突时,索引最终不会保存一个,而是 三个 个版本文件。例如,假设您有 运行 git merge other,并且 conflict.txt 有从基本提交到 HEAD 与基本提交到 other 的冲突更改。这些冲突出现在工作树副本中,但与此同时,Git 实际上将所有三个版本——基础、HEADother——放入索引中,在名称下conflict.txt。例如,您可以使用 git show :1:conflict.txtgit show :2:conflict.txtgit show :3:conflict.txt 查看它们。

运行 git add 告诉 Git 扔掉三个额外的索引版本,只存储一个 :0:conflict.txt 文件作为正确合并的索引版本,为下一个 git commit 命令。 Git 将允许在索引只有零编号版本的任何时候进行另一次提交。当时索引中的任何内容都成为新提交的内容。 (如果这些内容与当前或 HEAD 提交完全匹配,Git 要求您添加 --allow-empty 标志。这使得索引看起来像是空的,但事实并非如此:它是 diff 那是空的。)


绘制问题图

有几种方法可以解决这个问题。为了找到最好最简单的,我们真的需要画一些图。

具体来说,在您执行要重新执行的合并之前,您进行了一系列提交:

...--o--B--o--L   <-- mainbranch (HEAD)
         \
          o--o--R   <-- feature

您在 mainbranch(因此是 HEAD)。这意味着 HEAD 表示提交 L(对于本地或左侧)。你 运行 git merge feature,它标识了 commit R(对于 Remote or Right-side or otheR or featuRe;反正我一般称它为 R)。 Git 然后计算合并基础提交 B,这是两个选定的左右提交历史记录的第一个位置。

Git 然后 运行,实际上:

git diff --find-renames B L
git diff --find-renames B R

并结合了两个差异。有一些冲突,你解决了,还有一些不冲突,你 git reset 离开 L 版本。由于冲突,Git 让你成为 运行 git commit 自己,当你这样做时,你得到了这个:

...--o--B--o--L---M   <-- mainbranch (HEAD)
         \       /
          o--o--R   <-- feature

新的合并提交 M 的内容是 运行 git commit.

时索引中的内容。

我猜你是后来才发现这个问题的。你可能做了另一个 git checkout feature 并写了更多的提交,给出:

...--o--B--o--L---M   <-- mainbranch
         \       /
          o--o--R--o--S   <-- feature

然后你又回到了mainbranch和运行git merge feature。我不能再调用新提示 LR,所以我会坚持使用 MS。让我们通过同时从 MS 向后(向左,包括向左和向下)跟踪所有连接来找到 MS 的合并基础:

  • 不是MS本身,我们必须从M返回然后前进。
  • 也不是S之前的o
  • 但是 R 看起来不错:我们也可以从 M 向下退一步到 R;我们可以从 S 返回两步到达 R.

所以合并基础是 R,Git 运行s git diffR MR S 上。 Git 然后看到 R-to-Mfeature 代码中拿走了特征,而 R-to-S 添加或修改了特点。 Git 尝试将这些更改结合起来,结果是一团糟。如果合并本身有效,我们会得到:

...--o--B--o--L---M-----M2   <-- mainbranch (HEAD)
         \       /     /
          o--o--R--o--S   <-- feature

尽管由于 "undoing" 从 RM 与 "doing" 从 RS.

但我们甚至可能有这个,这取决于主 b运行ch 上发生的事情:

...--o--B--o--L---M--N--M2   <-- mainbranch
         \       /     /
          o--o--R--o--S   <-- feature

(在这种情况下,合并基础仍然是 R,但第一个差异是将 RN 进行比较)。

选项 1:完全删除 M

如果我们根本没有提交N如果"remove"是安全的完全提交 M,我们可以使用 git reset 来做到这一点。我们可以 git checkout mainbranch(将 HEAD 附加到它),git reset --hard 提交 L,然后:

...--o--B--o--L   <-- mainbranch (HEAD)
         \
          o--o--R--o--S   <-- feature

然后 git merge feature 将 运行 diff on B L 和第二个 diff on B S,并尝试合并 diffs。您将遇到与 B LB R 几乎相同的合并冲突,也许还有更多。

您可以用同样的方法解决它们(重复之前的工作)。或者您可以保存提交的身份 M 并从 M 中提取决议。虽然我们已经从绘图中删除了 M,但它实际上仍在存储库中。只是隐藏;如果您不做任何其他事情,大约一个月后它就会真正消失。由于 M 仍然在那里,我们可以从中提取文件:

git show <hash-id>:<path>

我们甚至可以在 git reset 之前给 M 添加一个名字。例如,使用标签名称:

git tag temp-save-merge <hash-id>

然后我们可以:

git show temp-save-merge:<path>

获取保存的文件。完成后,我们可以 git tag -d temp-save-merge 删除提交的特殊名称 M (这将像以前一样,让 Git 在 30 天后的某个时间自动删除它,现在没有我们可以 see/reach 它的名字。

选项 2:保留 M

如果提交 M 已被推送到其他地方,但是,and/or 如果提交 N 存在 and/or 已被推送,这可能不是一个好主意试图从存在中删除 M。我们可能会成功地将它从 我们的 存储库中删除,但稍后它可能 从另一个存储库中恢复 。我们必须强制推送以使其远离任何中央存储库,并说服使用该存储库的其他所有人丢弃 他们M 副本。如果我们需要 N 我们也必须把它带回来。

相反,我们可能希望重建我们希望没有的文件 git reset,并将这些更改放在 N 之上。从这里开始,我将在图纸中留下 commit N;如果 N 实际上不存在,我们只是在 M 之上构建,结果都是一样的。

首先,如果我们还没有 made M2 yet, or by git reset --hard if we have, so that we have a clean index and work-tree and have this graph:

...--o--B--o--L---M--N    <-- mainbranch
         \       /
          o--o--R--o--S   <-- feature

接下来,让我们创建一个 new b运行ch 指向提交 L:

git checkout -b temp-merge <hash-of-L>

这给了我们这张图:

                ----M--N   <-- mainbranch
               /   /
...--o--B--o--L  <-- temp-merge (HEAD)
         \       /
          o--o--R--o--S   <-- feature

我们将暂时停止绘制 mainbranch,因为它变得很乱。我们现在可以 运行:

git merge feature

重复我们之前做错的相同合并。这将使我们得到一个 "new and improved" 合并,尽管它会像以前一样停止并出现相同的冲突。我们将从 M 中获取正确合并的文件,添加它们并提交:

git show <hash-of-M>:<path1> > <path1>
git show <hash-of-M>:<path2> > <path2>
git add <path1> <path2>
git diff --cached   # inspect carefully
git commit

现在我们有这个:

...--o--B--o--L---M2  <-- temp-merge (HEAD)
         \       /
          o--o--R--o--S   <-- feature

现在让我们添加一个 N 的副本,使用 git cherry-pick mainbranch,并尝试绘制它:

                ----M--N   <-- mainbranch
               /   /
...--o--B--o--L---/-M2--N2   <-- temp-merge (HEAD)
         \       //
          o--o---R--o--S   <-- feature

这个最终提交 N2 是我们 喜欢 在 b运行ch mainbranch 中拥有的。然而,我们做所有这些繁琐的事情的原因是为了保留现有的哈希 ID MN,所以我们现在想要的是 Git 不完全提供的东西:a "theirs strategy"合并。 (参见 Is there a "theirs" version of "git merge -s ours"?

但是我们无论如何都可以得到我们需要的东西,使用另一个答案中的一种方法。这是最明显的一个,假设您位于工作树的顶层:

git checkout mainbranch
git merge -s ours --no-commit temp-merge
git rm -rf .
git checkout temp-merge -- .
git commit

git merge 步骤开始合并,但 --no-commit 从未完成。我们使用 --ours 只是为了让它运行得更快:这意味着 "keep the contents of N",这是完全错误的。然后我们删除所有内容!索引现在真的是空的。

现在我们使用真正的技巧:我们从 N2 提交 重新填充索引 git checkout temp-merge -- . 命令用名称 temp-merge 指向的提交内容替换我们拥有的所有内容,即 N2。这在索引和工作树中都会发生,所以现在我们都准备提交最终的合并结果。当我们这样做时,我们得到这张图:

                ----M--N---M3   <-- mainbranch (HEAD)
               /   /      /
...--o--B--o--L---/-M2--N2   <-- temp-merge
         \       //
          o--o---R--o--S   <-- feature

其中内容——提交M3的树与N2的完全相同,这是更正后的树。

我们现在可以删除 b运行ch 名称 temp-merge。历史充满了我们的双重合并,所以图表总是画得很乱。那是因为我们选择了选项 2,在该选项中我们重写历史以消除我们之前的错误。这个错误一直存在,但这也意味着我们只添加 new 提交(M3 和它的侧链,它曾经被称为 temp-merge ),因此使用此存储库的克隆的其他人可以正常工作。

总结

如果您可以使用选项 1(重写历史,假装错误的合并从未发生),您可能应该这样做。

您甚至可以使用一种混合方法:从临时合并 b运行ch 方法开始,提交 M2,然后 cherry-pick NN2 如果 N 存在。但是,不是将其与 mainbranch 合并,而是 重命名 旧的 mainbranch 并将临时合并重命名为 mainbranch,给出:

                ----M--N   <-- mistake
               /   /
...--o--B--o--L---/-M2--N2   <-- mainbranch (HEAD)
         \       //
          o--o---R--o--S   <-- feature

你可能不得不强制推送重命名的临时b运行ch,这会让其他用户头疼,如果有的话,但是如果我们删除mistake b运行ch 之后绘制图表,我们得到:

...--o--B--o--L--M2--N2   <-- mainbranch (HEAD)
         \      /
          o--o-R--o--S   <-- feature

这看起来很正常。 (并且,请注意,我们现在可以以通常的方式将 S 合并到 N2 之上。)这确实意味着我们重写了历史,但我们以一种简单的方式做到了:我们重新 -合并 LR,从提交 M 中抓取正确合并的文件,然后我们将提交 MN 扔到一边以支持M2N2。这实际上与选项 1 相同,除了我们保留错误 b运行ch 直到我们完成它。