您至少有三个选项。请参阅Tim Biegeleisen's answer 了解其中之一:它是最简单的,如果您没有其他提交并且没有推送合并(或合并和恢复),则可以使用它。
如果其余的是 TL;DR,请使用其他答案。 :-)
让我以这种方式重新绘制他的master-and-feature 图表:
A - B - C <-- master
\ (pre-merge)
D <-- feature
和:
A - B - C <-- master
\ \ (post-merge)
D --- M <-- feature
使合并提交成为合并提交的原因在于它有两个父级:“主线”父级(在本例中为D)和被合并的提交( C)。
合并的工作原理
当您要求 git 执行合并时,git 首先找到一个共同的祖先,在这种情况下提交 A。然后它做了两个差异:A vs D(在主线上发生了什么,feature)和A vs C(在被合并的分支上发生了什么,master)。然后它结合这两组差异,以便它可以进行一个新的提交,其中包含两行中所做的每个更改的一个副本。1 如果一切顺利,它会自动提交结果;如果不是,它会因合并冲突而停止,让您解决问题,并让您提交最终结果。无论哪种方式,最终提交 M 都将提交 C 和 D 列为其(两个)父提交,而不仅仅是一个父提交。在 git 中,这就是合并的定义:具有两个2 父级的提交。
上一次合并后
为了便于说明,我们现在在 feature 上添加一个新的普通提交:
A - B - C <-- master
\ \
D --- M - E <-- feature
如果在分支 feature 上,你要求 git 再次合并 master,会发生什么?
Git 必须首先找到feature 和master (也称为“合并基础”)的(提示)的最近共同祖先。提交E 的祖先,feature 的尖端,按距离顺序是E 本身(后退零步); M(退一步); C 和 D(都退后两步:都是 M 的父母);和A 和B(都退了三步)。 (还有另一种到达A 的方法,即后退四步,但我们采取更短的路线。)提交C 的祖先,master 的尖端,从C 本身开始(后退零步)。所以这是合并基础,要合并master,git 应该区分C(合并基础)与C ......但显然那里没有变化,所以没有什么可以合并,你得到“最新”消息。
但是,还原呢?好吧,就 git 而言,revert 只是一个普通的提交。使它成为一个回归的原因是它应用的更改“撤消”了先前提交的更改。也就是说,要进行还原,git 本质上会执行 git diff (对于合并的任一侧),但是无论差异显示“添加一行”,git deletes 代替该行,或者它说“删除一行”,git 放回被删除的内容。 (当您恢复合并时,这有点棘手,因为 git 必须 避免 撤消在您进行合并时已在 双方 双方完成的事情;但是 git做对了。)
重新合并
回到手头的问题,即重新进行合并:您可以使用建议的方法,即(实际上)删除不需要的还原和不需要的合并。使用git reset --hard 可以做到这一点,让您回到原来的合并前状态:
A - B - C <-- master
\
D <-- feature
(额外的提交 M 和 R 仍存储在存储库中,但从视图中隐藏并最终将被垃圾收集。)现在您可以重新进行合并,大概这次可以正确.
什么时候做一些更有趣的事情
最简单方法的一个缺点是,当您进行新的(更正的)合并时,它将有一个新的不同的底层 SHA-1 ID:
A - B - C <-- master
\ \
D --- M2 <-- feature
这本身就是必要的,甚至是可取的。但是,如果您在某处推送(或以其他方式发布)合并提交 M(同时推送或不推送回复 R),其他人可能拥有原始错误合并的副本。推送新的feature 将需要使用--force 来“重写历史”。任何选择了错误合并 M 的人都可能会依赖它,重写历史会让他们感到困难。
(你当然可以选择对他们说“运气不好”,让他们处理你的重置和重新合并。这是最简单的方法,有时甚至是最好的方法。但如果不是,我将继续按下。)
如果你做了一些好的次提交,你可能不想这样做的另一个原因是:
A - B - C <-- master
\ \
D --- M - E - R - F <-- feature
假设提交 M 是错误的合并,R 是它的反转,但 E 和 F 都是好的和可取的。如果你使用git reset --hard 消灭M,你也消灭了E 和F。那么现在呢?
更有趣的事情要做
Git 是 git,有很多方法可以处理这个问题。您必须选择:
- 您是否想要“快进”历史记录
- 您是想完全重新进行合并,还是只是修复它
- 您希望如何保留“好的”提交
让我们先解决最后一个问题。您可以使用git format-patch 将这些提交保存为补丁。这样您就可以清除它们(使用git reset --hard),然后重新应用它们(使用git am)。或者,您可以将它们保留在存储库中,以便您可以使用git cherry-pick 复制它们,或者将它们保留在原始的 git 提交链中,这样您就可以获得可快速转发的历史记录。
接下来让我们看一下可快速前进的项目。这基本上意味着每个已发布 提交(您曾经推送过的每个提交,或通过git pull 提供给其他人的每个提交)必须保持不变:您只能添加 到这样的历史。这就是它的全部内容(当然,这意味着将你的错误留给其他人查看)。
最后,让我们看看中间的那一项:你真的想重新进行合并,还是只取现有的东西并修复它?实际上还有第三种选择,起初看起来有点愚蠢:您可以完全重新进行合并,然后然后(使用那个)修复损坏的那个。这种方法的优势在于它可以让您非常轻松地生成可快速转发的历史记录。
在不重置的情况下重新合并
实际上有几种方法可以做到这一点,但我只特别展示一种。即使有合并,您也希望 git 看到与“合并前”相同的设置。我将再次绘制相同的图表,但将其拉长一点:
A - B - C <-- master
\ \
D \
\ \
----- M - E - R - F <-- feature
现在让我们做一些 git 容易做的事情,即检查提交 D。我们可以找到它的 ID(78cefa3 或其他),或者在 feature 上计算提交:F 是 0 后,R 是 1 后,E 是 2 后,M 是 3 后, D ... 嗯,这有点棘手:D 和 C 都是 4 后退,但 D 在“主线”上,所以 feature~4 将命名为 commit D。
无论哪种方式,我们都会检查它...但是我们为它制作了一个新分支标签,我们称之为feature-alt:
$ git checkout -b feature-alt 78cefa3 # by ID
现在我们有了这个图表:
A - B - C <-- master
\ \
D ....\................ <-- feature-alt
\ \
----- M - E - R - F <-- feature
(我输入.s 以表明feature-alt 直接指向提交D)。现在,即使有一个feature 分支,让我们简单地忽略它一段时间,然后再绘制一次图形:
A - B - C <-- master
\
D <-- feature-alt
这应该看起来很熟悉:这是我们进行错误合并时的图表。所以现在,当我们在feature-alt 上时,我们只需运行:
$ git merge master
这次正确地进行合并(如果你愿意,可以使用git merge --no-commit,以防止 git 提交自动合并结果,所以你可以先修复它)。
一旦合并完成并提交,我们将得到:
A - B - C___ <-- master
\ \ \
D ----\-- M2 <-- HEAD=feature-alt
\ \
----- M - E - R - F <-- feature
我在此处添加了HEAD=,以表明我们仍在使用feature-alt,并带有新的合并提交M2。请注意,M 有父级 D 和 C(按此顺序),M2 也是如此,但 M2 的 内容 (可能)不同(合并为这次更正)。
此时,您可以简单地git cherry-pick 提交E 和F,然后删除分支feature 并将feature-alt 重命名为feature,并使用--force 推送结果。这为您提供了正确的合并和两次良好提交的干净历史记录。它不能快进,但它保留了您在 E 和 F 中所做的更改。
或者,如果M2 是正确的合并结果,并且您想要一个可以快速转发的历史记录,那么您现在需要将M2 中的更改应用到现有分支feature 的尖端。由于R 还原了M,您现在可以这样做:
$ git checkout feature
$ git cherry-pick -m 1 feature-alt
这与任何其他樱桃选择一样,只是您必须指定合并的主线(就像git revert 一样)。这实际上是说“将D(第一个父级)与M2 进行比较,并将这些更改应用到当前分支feature 的尖端”:也就是说,放入正确的合并,而不是错误的合并.
如果一切顺利,你现在有这个:
A - B - C___ <-- master
\ \ \
D ----\-- M2 <-- feature-alt
\ \
----- M - E - R - F - M3 <-- HEAD=feature
其中M3 是M2 的副本(但是是常规的非合并提交)。
如果一切顺利,您现在可以完全删除 feature-alt 分支。错误的合并被保留在历史中,它的恢复也是如此,合并的更正版本就像普通提交一样神奇。
如果所有这些都太复杂并且您不想重复合并,您可以简单地恢复合并,恢复错误的合并:
A - B - C <-- master
\ \
D --- M - E - R - F <-- feature
变成:
A - B - C <-- master
\ \
D --- M - E - R - F - R2 <-- feature
其中R2 反转R。现在你可以进行任何需要的修复,git commit --amend 来调整 R2(或者甚至只是 git commit 而没有 --amend 只是在顶部添加另一个修复,然后可能使用 git rebase -i 在稍后推送之前将其压扁)。它的作用是使R2 与M3 得到的完全相同,M2 的精挑细选副本,更正的合并。您避免重新进行合并,但当然,您必须弄清楚如何修复 bad 合并。
无论如何,所有这一切的最终结果都可以在没有-f 的情况下推送,因为它只是在所有先前的提交之上添加了新次提交。
1例如,如果 A-to-D 将一行文本添加到 README.txt 和 A-to-C 将不同的文本行添加到main.c,新的提交 M 会将每一行添加到每个文件中。但是,如果 A-to-C 将 same 行添加到 README.txt,在同一个地方,git 会知道只添加该新行的一个副本,而不是每个副本组的变化。新的README.txt 将只添加一行,不再重复。
2实际上是两个或更多,但多臂“章鱼”合并的工作方式基本相同。