我合并然后只提交了一些文件。我如何重新合并其他人?
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 实际上将所有三个版本——基础、HEAD
和 other
——放入索引中,在名称下conflict.txt
。例如,您可以使用 git show :1:conflict.txt
、git show :2:conflict.txt
和 git 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
。我不能再调用新提示 L
和 R
,所以我会坚持使用 M
和 S
。让我们通过同时从 M
和 S
向后(向左,包括向左和向下)跟踪所有连接来找到 M
和 S
的合并基础:
- 不是
M
或S
本身,我们必须从M
返回然后前进。
- 也不是
S
之前的o
。
- 但是
R
看起来不错:我们也可以从 M
向下退一步到 R
;我们可以从 S
返回两步到达 R
.
所以合并基础是 R
,Git 运行s git diff
在 R M
和 R S
上。 Git 然后看到 R
-to-M
从 feature
代码中拿走了特征,而 R
-to-S
添加或修改了特点。 Git 尝试将这些更改结合起来,结果是一团糟。如果合并本身有效,我们会得到:
...--o--B--o--L---M-----M2 <-- mainbranch (HEAD)
\ / /
o--o--R--o--S <-- feature
尽管由于 "undoing" 从 R
到 M
与 "doing" 从 R
到S
.
但我们甚至可能有这个,这取决于主 b运行ch 上发生的事情:
...--o--B--o--L---M--N--M2 <-- mainbranch
\ / /
o--o--R--o--S <-- feature
(在这种情况下,合并基础仍然是 R
,但第一个差异是将 R
与 N
进行比较)。
选项 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 L
和 B 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 M
和 N
,所以我们现在想要的是 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 N
到 N2
如果 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
之上。)这确实意味着我们重写了历史,但我们以一种简单的方式做到了:我们重新 -合并 L
和 R
,从提交 M
中抓取正确合并的文件,然后我们将提交 M
和 N
扔到一边以支持M2
和 N2
。这实际上与选项 1 相同,除了我们保留错误 b运行ch 直到我们完成它。
我正在制作功能 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 实际上将所有三个版本——基础、HEAD
和 other
——放入索引中,在名称下conflict.txt
。例如,您可以使用 git show :1:conflict.txt
、git show :2:conflict.txt
和 git 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
。我不能再调用新提示 L
和 R
,所以我会坚持使用 M
和 S
。让我们通过同时从 M
和 S
向后(向左,包括向左和向下)跟踪所有连接来找到 M
和 S
的合并基础:
- 不是
M
或S
本身,我们必须从M
返回然后前进。 - 也不是
S
之前的o
。 - 但是
R
看起来不错:我们也可以从M
向下退一步到R
;我们可以从S
返回两步到达R
.
所以合并基础是 R
,Git 运行s git diff
在 R M
和 R S
上。 Git 然后看到 R
-to-M
从 feature
代码中拿走了特征,而 R
-to-S
添加或修改了特点。 Git 尝试将这些更改结合起来,结果是一团糟。如果合并本身有效,我们会得到:
...--o--B--o--L---M-----M2 <-- mainbranch (HEAD)
\ / /
o--o--R--o--S <-- feature
尽管由于 "undoing" 从 R
到 M
与 "doing" 从 R
到S
.
但我们甚至可能有这个,这取决于主 b运行ch 上发生的事情:
...--o--B--o--L---M--N--M2 <-- mainbranch
\ / /
o--o--R--o--S <-- feature
(在这种情况下,合并基础仍然是 R
,但第一个差异是将 R
与 N
进行比较)。
选项 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 L
和 B 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 M
和 N
,所以我们现在想要的是 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 N
到 N2
如果 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
之上。)这确实意味着我们重写了历史,但我们以一种简单的方式做到了:我们重新 -合并 L
和 R
,从提交 M
中抓取正确合并的文件,然后我们将提交 M
和 N
扔到一边以支持M2
和 N2
。这实际上与选项 1 相同,除了我们保留错误 b运行ch 直到我们完成它。