将之前的提交点恢复到 Git 顶部的最简单方法

Easiest way to bring back a previous commit point to the top in Git

好的,这就是我想要的,非常喜欢:

假设我的 git 日志是这样的:

detour C
detour B
detour A
Last good point

我想退回"Last good point",同时还保留了历史上的弯路,但不像,我想重新登顶。所以之后我的 git 日志会是:

Revert to last good point
detour C
detour B
detour A
Last good point

我知道官方的方式是

git revert HEAD~3

不过,我得到了

error: could not revert f755e55... Last good point
hint: after resolving the conflicts, mark the corrected paths
hint: with 'git add <paths>' or 'git rm <paths>'

也就是说,我需要解决那些很乱的冲突,这是我想尽可能避免的。我知道

git checkout HEAD~3

会马上带我到那里,但我读到 git 将处于分离阶段或类似的东西,我不知道如何再次将这个阶段复制回顶部。请帮忙。谢谢。

这是 git revert 的替代方法,它会给您留下相同的信息,但会避免混乱的合并冲突。只需从当前点签出一个新分支,然后将该分支硬重置为较早的点:

git checkout your_branch
git checkout -b new_branch
git reset --hard HEAD~3

现在 new_branch 在功能上是你想要的,而且你仍然有那三个提交坐在真正的 your_branch 分支中。

您可以按照相反的顺序逐一还原您的提交以避免修复合并冲突并最终达到所需的提交。

假设您最初有以下历史记录。

7a6c2cc detour C
dc99368 detour B
1cf4eb4 detour A
22769c2 Last good point

您可以 运行 以下 git revert 命令序列。

git revert 7a6c2cc
git revert dc99368
git revert 1cf4eb4

这将为您留下如下所示的提交历史记录。

3b67aaf Revert "detour A"
9028879 Revert "detour B"
6d9bcce Revert "detour C"
7a6c2cc detour C
dc99368 detour B
1cf4eb4 detour A
22769c2 Last good point

最后,您的代码将与 Last good point 提交时的代码相同。

您还可以将 --no-commit 标志与 git revert 一起使用,以避免每次还原都单独提交。

git revert --no-commit 7a6c2cc
git revert --no-commit dc99368
git revert --no-commit 1cf4eb4
git commit -m "Revert detour C, B and A"

您可以简单地从 Last good point 提交中读取树,然后提交它:

git read-tree -m -u HEAD~3
git commit

-m -u read-tree 选项将从给定树更新工作目录。

TL;DR

有三种相当直接的方法可以达到你想要的效果:

  1. git revert --no-commit HEAD~3..HEAD && git commit
  2. git read-tree --reset -u HEAD~3 && git commit
  3. git rm -rf -- . && git checkout HEAD~3 -- . && git commit

这三个都要求您输入新的提交信息;您可以将 -C HEAD~3 --edit 添加到 git commit 命令,以便您可以从 HEAD~3 中的消息开始进行编辑。这三个中的最后一个要求您位于(cd-ed 到)存储库的顶层。如果你还没有,你应该先使用:

cd $(git rev-parse --show-toplevel)

或:

git rev-parse --show-toplevel

然后将输出剪切并粘贴到 cd 命令以到达顶层。

更长:为什么以上是正确的,从#3

开始

关键词是这样的:

I want to revert to "Last good point"

(强调我的:恢复,而不仅仅是恢复,这是一个Git 命令有点不同)。

你也应该警惕 stage 这个词,它在 Git 中有技术定义的含义(指复制到staging area,这是 Git 调用的另一个短语,不同的是 indexcache ,当然还有暂存区)。 [编辑:由于现在标题已调整,已删除]

执行此操作的低级命令是 git read-tree,如 PetSerAl's answer。我会推荐 git read-tree --reset -u,因为 -m 意味着执行 merge 并且您想要重置索引。但是有一种方法可以做到这一点,虽然有点笨拙,但对人类来说可能更有意义,使用 git checkout。那就是command-set3,我们先来看。

正如您所注意到的,git checkout HEAD~3 将使所需的提交成为当前提交——但它是通过 "detaching HEAD" 实现的,这是一个可怕的短语,只意味着您不再处于命名分支。 (你 "re-attach" 你的 HEAD 通过 运行ning git 结帐 <em>branchname</em>,这会再次设置你在那个分支上,通过检查那个分支的提示提交,这当然意味着你不再使用所需的提交。)发生这种情况是因为 all 提交或多或少是永久性的,1 完全 read-only: 你不能改变过去,你只能re-visit它。

git checkout 命令可以做的不仅仅是 re-visit 过去(通过检查特定的过去提交)或切换到其他分支(通过检查任何命名的分支)。也许,这些操作中的许多或大部分应该有一个不同的 front-end 命令,因为将它们全部集中在 git checkout 下只会使 Git 更加混乱;但这就是我们所拥有的:git checkout <em>commit-specifier</em> -- <em>paths</em>告诉 git checkout 将给定的 paths(文件或目录名称)提取到索引中,然后再提取到 work-tree 中,覆盖当前的任何内容在索引和 work-tree 中, 没有 更改提交。

因此:

git checkout HEAD~3 -- .

告诉 Git 从提交 HEAD~3 中提取目录 .(从你现在所在的位置向后退三步)。如果您位于 Git 存储库的顶层,. 为存储库中的每个文件命名。

更准确地说,. 为存储库的 特定提交 中的每个文件命名。这就是为什么你应该首先 运行:

git rm -rf -- .

这告诉 Git 删除 每个文件(Git 知道,即现在在索引中)从索引和work-tree。这一点是……好吧,假设在三个迂回提交期间,您添加了一个 new 文件 newfile.ext。该新文件至少在提交 detour C 中,如果不是在所有这三个文件中的话。但 HEAD~3 中的 不是 ,它命名为提交 22769c2,您要恢复的最后一个好提交。因此,当您告诉 git git checkout 22769c2 -- . 或等效项时,Git 会查看 22769c2,找到提交 拥有的所有文件 ——这并没有't include newfile.txt——并用良好提交的文件替换当前文件,但 在索引中留下 newfile.ext 和 work-tree.

通过首先删除 一切 Git 在 detour C 提交中知道的内容,您为 git checkout ... -- . 命令提供了一个干净的石板提取所有内容。

因此,命令集 3 表示:

  • 删除 Git 知道的所有内容,以生成 clean-slate 索引和 work-tree。 (Git 不知道的文件,例如编译器构建的 .o 文件,或来自 .pyc byte-code 的文件Python 或其他通过 .gitignore 忽略的内容不会被删除。)

  • 将好的提交中的所有内容提取到索引中,然后work-tree:用好的东西填满白纸。

  • 提交:进行 new 提交,不是 22769c2 而是其他一些哈希 ID,其 parent 是 detour C commit 但其内容是现在索引,这是我们刚刚从 22769c2.

  • 中提取的内容

1 "more or less" 部分是因为你可以 放弃 提交,通过更改你的各种名称,这样就不会 name 不再定位那些提交。没有找到它们的名字,提交就会丢失并被遗弃。一旦它们被放弃足够长的时间——通常至少 30 天,因为有隐藏的 reflog 条目 名称仍然可以找到提交,但这些 reflog 条目最终会过期,对于此类提交通常会在 30 天内过期—Git 的 Grim Reaper 收集器,也称为 垃圾收集器git gc,实际上会删除它们。


git read-tree方法

git read-tree --reset所做的,简单来说就是把git rm -r --cached .步骤和git checkout HEAD~3 -- .步骤的大部分结合起来。当然,这些根本不是#3 中的内容:这种带有 --cached 的表单仅删除 index 条目。此外,git checkout 步骤填充 work-tree。这就是 -u 添加到命令的作用:它更新 work-tree 以匹配对索引所做的更改。删除一些条目,如果最终 removed,导致相应的 work-tree 文件被删除;更新其余条目,包括从正在读取的提交中添加新条目,会导致相应的 work-tree 文件被更新或创建。所以 git read-tree --reset -u HEAD~3 与我们的 remove-and-check-out 序列相同,只是它更有效。

(虽然你可能不记得了:git read-tree 不是一个经常使用的命令。另外,使用 -m 告诉 Git 到 merge 将目标树放入当前索引,这也不是您想要的,尽管它几乎肯定会在这里做正确的事情。)

或者您可以使用 git revert -n

上面的第一个命令使用git revert --no-commit。这是拼写 -n 的漫长方法,这意味着 在不提交结果的情况下执行每个还原。 通常,git revert 所做的是将每个 commit-to-be-reverted 变成 change-set,然后 "reverse apply" 的变化。给定一系列提交,如 HEAD~3..HEAD,Git 首先收集所有涉及的哈希 ID 的列表——在本例中它们是:

7a6c2cc detour C
dc99368 detour B
1cf4eb4 detour A

Git 然后 运行 以倒序排列,child-most 到 parent-most,即首先查看 detour C,然后查看 detour B,然后在 detour A.

这些提交中的每一个本身都是一个快照,但每个提交都有一个 parent 这也是一个快照。从 detour C 中的内容减去 detour B 快照中的内容告诉 Git,实际上, 更改了 的内容以便从 B 到 C。 Git 然后可以 "un-change" 正是这些更改:如果从 B 到 C 添加一行到 README.mdREADME.md 中删除 该行.如果它从 a.txt 中删除了一行, 将那行添加 回到 a.txt。如果它删除了整个文件,请将该文件放回去;如果它添加了新文件,请将其删除。

一旦所有更改都已 backed-out(结果与绕行 B 快照中的结果相匹配),git revert——显然应该称为 git backout——通常会使结果的新提交;但是对于 -n,它不会。相反,它将结果留在索引和 work-tree 中,准备提交。然后它移动到列表中的下一个提交,即绕行 B 的提交。Git 将其与其 parent 进行比较以查看更改的内容,并撤消这些更改。结果是,在这种情况下,与绕行 A 中的快照相同。

如果我们从绕行 C 快照以外的其他内容开始,则退回绕行 C 的更改将与绕行 B 不匹配,然后退回绕行 B 的更改将与绕行 A 中的内容不匹配。但是我们确实从绕行 C 快照中的内容开始。所以现在 Git 退出绕行 A 中的任何更改,留下 - 没错! - 最后一个好的提交中的任何内容。

此状态现在在索引中 work-tree,准备提交。所以现在我们简单地把它作为一个新的提交来提交。那就是 command-sequence 1:以相反的顺序恢复(退出)三个坏主意,因为我们从最后一个的快照开始,所以保证有效。不要提交任何中间结果。然后,一旦索引和 work-tree 匹配上一个好的提交,就进行新的提交。