在合并提交中如何撤销其中的一个提交?

3

我有一个pull request,其中包含250个文件更改,但实际上只有5~10个是被我修改过的。

它是4个commits的压缩,其中之一是回滚了与另一个分支的合并,这个commit包含了大约240个文件更改

看起来像这样:

<some commits I made after this>
<squashed commit of:

minor change

working on abcd..

Revert "Merge branch 'settlement-statement' into rents-proration"

This reverts commit 0e44eae, reversing
changes made to a39e7fd.

some more change>

我只想还原

Revert "Merge branch 'settlement-statement' into rents-proration"

This reverts commit 0e44eae, reversing
changes made to a39e7fd.

我原以为将settlement-statement分支合并到我的分支就能解决问题,但事实并非如此。
有什么方法可以修复这个问题,而不需要查看files changed中的所有文件并挑选出我的更改吗?
2个回答

4
更新 - 我分两个阶段写了原始答案,因此有点混乱。信息保持不变,但我会稍微整理一下...


根据您如何压缩提交,您可能会遇到两种非常相似的情况。粗略地说:

1) 如果您从一个分支压缩到另一个分支(例如通过 git merge --aquash),则您将拥有以下图形:

x -- AB!MCD -- E <--(branch_for_pr)

A -- B -- !M -- C -- D <--(work_branch)

其中,!M 是某个先前合并操作的撤销,而 AB!MCD 是对提交 AB!MCD 进行压缩的结果。

2) 如果您在一个分支上“原地”压缩了提交记录,例如通过使用命令 git rebase -i,那么图形将更像:

x -- AB!MCD -- E <--(branch_for_pr)

A -- B -- !M -- C -- D <--(branch_for_pr@{2})

这里的branch_for_pr@{2}是一个reflog条目,工具不像现有引用那样显式显示reflog条目,所以看起来A .. D好像"消失"了,但至少在发生压缩的本地克隆中,它们并没有消失。(确实,reflog条目最终会过期, 默认情况下大约三个月后会过期,或者当您明确地使其过期时。在reflog条目过期后,gc可能会删除旧提交,除非在此期间您已执行某些操作使其可访问。此外,reflogs在推送或拉取时不会共享。)
您可以使用类似以下命令查找reflog条目:
git reflog branch_for_pr

并且可以在知道不再需要它之前,将新的分支或标签放在它上面,以消除引用日志到期可能会隐藏它的风险。 如果这样做,结果看起来像第一个图表(来自上面的(1))。
本条目的其余部分假设实际分支指向 D。所以...怎么办?
最简单的方法是还原 branch_for_pr 上的 !M。这似乎是违反直觉的,但你几乎可以还原任何东西;它不必是 HEAD 的祖先。所以你需要一个解析为 !M 的表达式 - 它可以是其提交 ID,或者在本例中类似于 work_branch~2 的东西。
git checkout branch_for_pr
git revert work_branch~2

一些需要了解的背景知识:
提交是项目的快照,其中包含一小部分元数据。元数据包括“父级”列表,将快照相对于历史中的其他快照放置在哪里。许多 git 命令都操作提交的补丁 - 即该提交与其父提交之间的差异(在合并的情况下可能是其一个父提交)。提交的元数据不包括任何特殊关系,“此提交撤消的提交”或“合并到此提交的提交”,这些信息基本上已丢失。
如上所示,“撤消”只是一种汇编与其父提交以某种方式相关的提交的简便方法,由另一个提交与其父级的关系控制。git 不记录特定提交是通过撤消另一个提交生成的(更不用说是那个提交是什么了)。
同样,“压缩”只是指定要应用于 HEAD 以创建新提交的一组更改的简便方法。得到的提交并不“知道”它是一个压缩提交。
因此,这些事情可能会改变您对正在尝试做什么的看法,并影响您如何解决这个问题。对于“重新执行合并”的建议解决方案而言,一个更为紧迫的问题是,Git 在合并撤消方面有些奇怪。一旦您撤消了合并,重新合并它就不那么容易了。您要么必须从新提交生成分支(以使重新合并成为可能),要么只需撤消撤消 - 这正是我建议在您的最终分支上直接执行的操作。
有许多此想法的变体,可能会产生“更清晰”的历史记录;但这是最简单的方法,避免了可能会让事情变得更加混乱的情况。

抱歉,我没有在单独的分支上压缩它们。这些提交已经在我的工作分支上被压缩,并且已经被推送并制成了PR。 - Eric
@EricNa:这并没有真正改变情况。它只是意味着你使用reflog来定位!M,而不是第二个分支ref。 - Mark Adelsberger

2

无法从聚合中提取单个提交。你得到了你的愿望,它们被压缩成一个。

幸运的是,Git需要几周时间才会丢失任何东西。你未压缩的分支可能仍然存在。你的仓库希望看起来像这样。

A - B - C - D [master]
            |\
            | E [feature]
            \
             F - G - H - I

feature是你的分支。 F - G - H - I是你原始的、未压缩的提交。 EF - G - H - I全部压缩在一起的结果。我们想把feature放回I

I没有被任何分支或标签引用。我们可以通过reflog找到它。这是每次HEAD移动的历史记录。每次检出、变基和重置。你要找的是这样的东西。

c6a7e90 (HEAD -> master) HEAD@{0}: rebase -i (finish): returning to refs/heads/master
c6a7e90 (HEAD -> master) HEAD@{1}: rebase -i (squash): second
40c7031 HEAD@{2}: rebase -i (squash): # This is a combination of 2 commits.
4c79819 HEAD@{3}: rebase -i (start): checkout HEAD^^^
7fd34b9 HEAD@{4}: rebase -i (finish): returning to refs/heads/master
7fd34b9 HEAD@{5}: rebase -i (start): checkout master

7fd34b9是您压缩之前分支的提交ID,表示为I。 c6a7e90是压缩后的提交ID,表示为E。您可以使用git reset --hard 7fd34b9feature还原到压缩之前。

有关维护和数据恢复重置解密的更多信息,请参见Git Book。


这就是为什么不应该压缩整个特性分支的原因。重要的细节会丢失,无法被记录和审阅。为什么要更改那一行?可能有4种混在一起的原因之一。另一方面,您可以轻松地从未压缩的分支获得“压缩”视图;Github将为您的PR提供一个,或者您可以使用git diff master。但是反过来不行,您无法从压缩的分支获取未压缩的更改。关键信息会丢失。

压缩应该用于消除无趣的更改,例如拼写错误和“我想在三个提交之前进行的更改不同”。


根据压缩的方式,这里的一般事实内容大多是正确的 - 除非它建议重写几乎肯定是推送分支的内容,而这并不必要。关于工作流程的主观评论也不太好;根据团队和个人的实践,压缩功能分支有许多原因。 - Mark Adelsberger
1
@MarkAdelsberger 我经常进行代码考古。压缩提交记录是我非常熟悉的操作。当有选择性地进行时,它非常有用,但如果一个提交记录真的没有意义,那么使用 fixup 更为合适。当成为一种习惯性的工作流程时,我会感到不安。通常我会在那些并不真正理解 Git 的项目中发现这种情况(我不怪他们),他们只是希望问题迎刃而解。总之,你向专家免费咨询,可能会得到一些额外的意见。 :) - Schwern

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接