问题描述
不熟悉git的人在他的分支上提交了,然后在develop
分支上做了一个合并提交。合并时,他:
现在我想保留第 1 和第 2 部分,但还原第 3rd 部分,我该怎么办?注意到他的分支已经推送到远程所以我希望可以避免reset
。
我尝试过的:
-
git revert <commit-id> -m 1
并在合并前返回提交 - 再次尝试合并,但被告知“已经是最新的”。并且丢弃的更改仍然消失了。
我在这里所期望的应该与 git reset head^; git merge develop
相同,但似乎我没有正确理解 revert
。
解决方法
对于这个特定的问题没有正确的答案。只有答案会留下一些问题,而答案会留下许多问题。每个问题的严重程度取决于您的具体情况:
-
例如,使用
git reset
剥离合并,然后使用git push --force
,会给使用远程克隆的其他人带来问题。但也许只有另一个人在使用该克隆,而另一个人已经知道该做什么,或者可以被指示该做什么。在这种情况下,剥离坏合并并重新开始的“坏处”相对较小,特别是因为您可以保持良好的分辨率(尽管这需要手动工作和大量 Git 知识)。完成后,没有人需要再次处理错误的合并,从而使事情处于良好状态。
-
但也许许多人正在使用该远程存储库,并且剥离错误的合并会造成无法弥补的损害。在这种情况下,剥离坏合并的“坏处”是巨大的,您应该使用另一种策略。
要记住的主要事情是,Git 存储库归根结底只是提交 的集合。存储库中的提交是历史,是存储库。1所以,无论你最终做什么,你都会添加提交到存储库。要修复错误的合并提交,您必须添加更多提交。
这些不一定是合并提交。您可以保留现有的合并,只需将其记住(或将其标记为 - 请参阅 git notes
)为“不好,不要使用” .然后,您可以添加解决问题的普通(非合并)提交。
每次提交都存储每个文件的完整快照。 提交不包含与前一次提交的差异。因此,错误的合并提交只是一些文件内容错误的提交。随后的非合并提交可以存储具有正确内容的文件。
因此您的问题可以归结为两部分:
-
您必须决定是否删除错误的合并。这是一种价值判断,没有正确答案。
-
您必须提出更正的内容。这是一个机械问题:您将如何生成正确的文件?在这里,Git 可以提供帮助。
让我先放一个脚注,然后描述 Git 如何提供帮助。
1这有点夸大其词:可能有git notes
,尽管从技术上讲,它们无论如何都存储在提交和标签中;并且人类重视分支名称,它们也在存储库中,但相当短暂,不应该被如此严重地依赖。
Git 如何执行真正的合并
在 Git 中,真正的合并是对 三个输入提交的操作。2 三个提交包括您选择的当前提交通过您的当前分支名称和特殊名称 HEAD
。您在命令行上给 Git 另一个提交:当您运行 git merge other-branch-name
或 git merge hash-id
时,Git 使用它来定位另一个分支提示提交。有关分支提示如何工作以及HEAD
如何工作的更多信息,请参阅Think Like (a) Git。此站点也将有助于理解下一部分。
鉴于这两个分支提示提交,Git 现在使用提交图自行查找三个输入提交中的第三个——或者在某种意义上,第一个 .每个普通的非合并提交向后连接到某个较早的提交。这一系列的向后连接最终必须到达某个共同的起点,在那里两个分支最后共享某个特定的提交。
我们可以把这种情况画成这样:
I--J <-- our-branch (HEAD)
/
...--G--H
\
K--L <-- their-branch
我们最近的提交,我画成提交 J
,向后指向一些早期的提交,我画成提交 I
。他们最近的提交 L
向后指向某个较早的提交 K
。但是 I
和 K
向后指向某个提交——这里是 H
——同时在两个分支。 Think Like (a) Git 有很多关于它是如何工作的,但为了我们在这里的目的,我们只需要看到 Git 自己找到提交 H
,并且它在两个分支上。
当我们运行 git merge
时,提交 J
作为我们的提交——Git 调用 --ours
或 HEAD
或 local 提交——并提交L
作为他们的提交——Git 称其为 --theirs
或 远程 提交,通常——Git 发现提交 H
作为合并基础。然后它:
-
将提交
H
中的快照与我们提交J
中的快照进行比较。这会找出我们更改了哪些文件,以及我们对这些文件进行了哪些更改。 -
将
H
中的快照与L
中的快照进行比较。这会找出他们更改了哪些文件,以及他们对这些文件进行了哪些更改。 -
合并更改。这是辛苦的部分。 Git 使用简单的文本替换规则进行这种组合:它不知道真正应该使用哪些更改。在规则允许的情况下,Git 会自行进行这些更改;如果规则声称存在冲突,Git 会将冲突传递给我们,让我们修复。在任何情况下,Git 都会应用对起始提交中的快照的组合更改:merge base
H
。这样可以在添加更改的同时保留我们的更改。
因此,如果合并本身进行得很好,Git 将进行新的合并提交 M
,如下所示:
I--J
/ \
...--G--H M <-- our-branch (HEAD)
\ /
K--L <-- their-branch
新提交 M
有一个快照,就像任何提交一样,以及日志消息和作者等等,就像任何提交一样。 M
的唯一特别之处在于它不仅链接回提交 J
——我们开始时的提交——而且还链接到提交 L
,我们告诉了其哈希 ID 的提交 git merge
{1}} about(使用原始哈希 ID,或使用名称 their-branch
)。
如果我们必须自己修复合并,我们会这样做并运行 git add
,然后运行 git commit
或 git merge --continue
,以使合并提交 M
。当我们这样做时,我们可以完全控制进入 M
的内容。
2这种合并会导致合并提交,即有两个父项的提交。 Git 还可以执行它所谓的快进合并,这根本不是合并并且不会产生新的提交,或者它所谓的章鱼合并,它需要超过三个输入提交。 Octopus 合并有一定的限制,这意味着它们不适用于这种情况。真正的合并可能涉及进行递归合并,这也会使图片复杂化,但我将在这里忽略这种情况:复杂性与我们将要做的事情没有直接关系。 >
重做错误的合并
我们这里的情况是,我们开始于:
I--J <-- our-branch (HEAD)
/
...--G--H
\
K--L <-- their-branch
然后有人——大概不是我们?——运行了 git merge their-branch
或类似的,遇到了合并冲突,并错误地解决了它们并提交了:
I--J
/ \
...--G--H M <-- our-branch (HEAD)
\ /
K--L <-- their-branch
要重新执行合并,我们只需要检出/切换到提交 J
:
git checkout -b repair <hash-of-J>
例如,或:
git switch -c repair <hash-of-J>
使用新的(自 Git 2.23 起)git switch
命令。然后我们运行:
git merge <hash-of-L>
要获得两个哈希 ID,我们可以在合并提交 git rev-parse
上使用 M
,并带有时髦的 ^1
和 ^2
语法后缀;或者我们可以运行 git log --graph
或类似的并找到两个提交并直接查看它们的哈希 ID。或者,如果名称 their-branch
仍然找到提交 L
,我们可以运行 git merge their-branch
。 Git 只需要定位正确的提交即可。
此时,Git 将按照完全相同的规则重复之前尝试的合并尝试。这将产生完全相同的冲突。我们现在的工作是解决这些冲突,但这一次,我们做对了。
如果我们喜欢其他人在提交 M
中做出的解析,我们可以要求 git checkout
(所有版本的 Git)或 git restore
(Git 2.23 及更高版本)提取解析的其他人提交的文件 M
:
git checkout <hash-of-M> -- <path/to/file>
例如。即使我们不喜欢整个分辨率,我们仍然可以这样做,然后修复文件并运行 git add
;只有当我们不喜欢任何的决议,并且想要自己完成整个修复时,我们是否必须自己完成整个修复。
无论如何,我们只是修复了每个文件,然后git add
将结果告诉 Git 我们已经修复了文件。 (git checkout hash -- path
技巧使我们可以在某些情况下跳过 git add
步骤,但无论如何运行 git add
也无妨。)当我们全部完成后,我们运行 git merge --continue
或 git commit
以完成此合并:结果是新的合并提交 M2
或 N
,在我们的新分支 repair
或任何我们称之为它的地方当我们创建它时:
I--J-----M2 <-- repair (HEAD)
/ \ /
...--G--H M / <-- our-branch
\ /_/
K--L <-- their-branch
我们现在可以通过 git checkout our-branch
提交 M
,并直接从 repair
获取文件:
git checkout our-branch
git checkout repair -- path/to/file1
git checkout repair -- path/to/file2
...
然后我们准备 git commit
进行新的提交 N
。或者,我们可以从 M2
:
git checkout repair -- .
并在此时运行 git status
、git diff --cached
和/或 git commit
,具体取决于我们是否确定一切顺利。
上面的结果是:
I--J-----M2 <-- repair
/ \ /
...--G--H M-/--N <-- our-branch (HEAD)
\ /_/
K--L <-- their-branch
我们现在可以完全删除分支名称 repair
:commit N
只是“神奇地固定”。
如果我们打算保持提交M2
,我们可以使用git merge
将repair
合并到M
中。我们可能想要运行 git merge --no-commit
以便我们获得完全控制:这将阻止 git merge
进行实际提交,以便我们可以检查即将进入新合并的快照。然后最后的 git merge --continue
或 git commit
将 N
作为新的合并提交:
I--J-----M2 <-- repair
/ \ / \
...--G--H M-/----N <-- our-branch (HEAD)
\ /_/
K--L <-- their-branch
我们可以再次删除名称repair
;它不再增加任何价值。
(我通常只是自己做一个简单的非合并修复提交,而不是另一个合并。使 N
as 成为合并的合并基础是两个提交 {{1 }} 和 J
,这意味着除非我们指定 L
,否则 Git 将进行递归合并。递归合并往往很混乱,有时会出现奇怪的冲突。)
如果自错误合并以来有提交
发生在之后 bad-merge--s resolve
的提交只需要将它们的更改推进到我在上面绘制的作为最终提交 M
的内容中。你如何实现这一点并不是非常重要,尽管有些方法可能会让 Git 为你做更多的工作。这里要记住的是我之前说过的:最后,重要的是存储库中的提交。这包括图表——从提交到较早提交的向后连接——和快照。该图对 Git 本身很重要,因为它是 N
的工作方式以及 git log
如何找到合并基础。快照对你很重要,因为它们是 Git 存储你关心的内容的方式。
好的,我设法自己解决了。如果幸运的人遇到类似情况,我会发布解决方案。
- 从
develop
签出一个新分支,我们称之为fix
- 将错误的分支合并到
fix
中,选择正确的部分并丢弃错误的部分 - 将
fix
合并到错误的分支中,因为我想保持分支干净
看起来很简单,为什么我要花这么多时间才能想出解决方案......?