如何修复因 git reset HASH 而混乱的本地副本

How to fix a local copy messed up with git reset HASH

我向本地文件添加了一些更改,然后提交了它们 (A),然后我添加了更多提交 (B&C),然后意识到,我不想要来自提交 (A) 的更改,所以我继续了并做了 git reset --hard <HASH>

git log 中删除了之前的提交,但保留了我在文件中 (A) 中添加的更改,当我现在将此文件更改回其原始状态时,它会将其标记为已更改,我不想。我希望文件反映 origin。我如何从这里完成这项工作?

(我认为这就是 reset --hard 所做的)

作为 , you can't get the final effect you want with git reset. The details here are, however, quite messy, because git reset is something of a kitchen-sink 命令。

I added some changes to a local file and then committed them (A) ...

这里要意识到的是,每个 Git 提交都是 每个文件 的完整快照。当您更改一个文件并进行新的提交时,您会创建一个新的 完整快照 .

这不会占用大量磁盘 space,因为 Git 提交中的文件不是普通文件。它们以特殊的、压缩的、Git 化和 去重 形式存储。所以新提交 A 简单地重新使用早期提交 α 中的所有旧文件,除了一个修改过的文件,它得到一个新的快照。1

I then added more commits (B&C) and then realized, that I didn't want the change from commit (A) so I went ahead and did a git reset --hard <HASH> ...

这种 git reset 是关于 进行特定提交

Git 中的提交有点像一串珍珠。

每个提交都有一个唯一但看起来随机的哈希 ID,它只是一个非常大的数字的 hexadecimal 编码。除非您拥有来自提交的所有数据,否则不可能知道某个提交的哈希ID,或者直接直接给出哈希ID,所以Git处理这个的方式是每个提交 存储其直接前身的哈希 ID(连同快照和其他元数据)。 Git 然后只需要一些方法来存储此链中 last 提交的哈希 ID,因为每个提交都指向前一个提交:

δ <-γ <-β <-α <-A <-B <-C   <-- your-branch

字母(您自己的问题中的 A、B 和 C,加上链中早期提交的一些倒序的希腊字母)在这里代表随机的哈希 ID。当你提交 A 链从:

δ <-γ <-β <-α   <-- your-branch

至:

δ <-γ <-β <-α <-A   <-- your-branch

因为Git添加了一个新提交A,持有一个完整的快照,加上链的前一端的哈希ID。然后,当您提交 B 时,您得到:

δ <-γ <-β <-α <-A <-B   <-- your-branch

等等,提交 C

git reset命令告诉Git:停止使用当前分支名称记住当前最后一次提交。相反,让这个分支名称指向我指定的提交。 因为你指定了提交 A,所以你:

                  B <-C   [no name to find C]
                 /
δ <-γ <-β <-α <-A   <-- your-branch

除了 git reset 的“移动分支名称”操作外,它还会影响 Git 的索引和您的工作树,除非您告诉它不要这样做。 git reset 命令还有其他模式可以做其他事情,但我们不要误入歧途;我的回答已经太长了。

如果您希望 BC 提交回来,它们 可恢复的,暂时。恢复它们的主要问题是找到提交 C 的哈希 ID。 (您不必找到提交 B 的哈希 ID,因为提交 C 为您保留了该哈希 ID。您只需找到 last在链中提交:Git 会从那里自动找到更早的所有内容。)

幸运的是,Git 有一个叫做 reflogs 的东西,它保留了 存储在某个名称中的哈希 ID ,默认至少 30 天。您可以查看 HEAD 的引用日志,或您的分支名称的引用日志,以查找提交 C 的哈希 ID。使用 git refloggit reflog <em>branch</em> 来执行此操作。

假设您将这两个放回去(与另一个 git reset --hard)以便您的 strand-of-pearls 提交回到:

δ <-γ <-β <-α <-A <-B <-C   <-- your-branch

你现在可以很容易地添加一个 new commit D 到你的分支,它只是 undoes 的效果使用 git revert 完全提交 A。或者,您可以创建一个新的提交 D,其快照与 C 的快照相同,只是一个特定文件的内容是从任何给定提交中提取的内容。


1这些单独的文件快照后来被进一步压缩,超出了它们的初始压缩和 Git 化,Git 称之为 pack files,这意味着即使是多个版本的大文本文件最终也不会占用太多space。不过,您无需关心这些细节:只要记住每个提交都充当完整存档就足够了,例如 tar 或 zip 或 rar 存档,每个 每个 文件。


使用git revert

git revert 命令通过将提交的内容与其父项的内容进行比较来工作。无论此处更改,该更改都将在当前文件集中未完成通过反向-或多或少地应用更改。2 因此,如果您在提交 A 中修改了文件 F,但未对任何其他文件执行任何操作文件,git revert <em>hash-of-A</em> 将撤消该更改。 Git 将对生成的文件进行新的提交。


2从技术上讲,还原是一种三向合并,当前提交一如既往地是当前提交,但是合并基础 是您在 git revert 命令中指定的子提交,三向合并的 other 提交 是该 parent/child 对的父提交.


使用 git restoregit checkout

如前所述,每次提交都有所有文件的完整快照。

要从一次特定提交中获取一个特定文件 out,您可以使用新的(自 Git 2.23 起)git restore 命令:

git restore -SW --source <commit> -- <path/to/file>

-S-W 选项,都在这里选择,告诉 git restore 将替换文件写入暂存区(这样你就不必 git add 之后的文件) 你的工作树。默认情况下只写入您的工作树,需要随后的 git add.

源提交可以是原始哈希 ID,或者您可以使用任何可以找到正确哈希 ID 的名称。如果 origin/main 选择了具有正确文件副本的提交,则可以使用 --source origin/main。您可以将 --source 缩写为 -s(注意小写,而 -S 大写表示临时区域)。

如果您的 Git 早于 2.23,您可以使用:

git checkout <commit> -- <path/to/file>

git restore -SW 命令具有相同的效果(写入索引/临时区域和工作树)。

无论如何,在使用这些方法之后,您将需要进行一次新的提交。

使用交互式变基

不是添加新的提交,而是可以用新的和改进的替代提交替换整个系列的提交git rebase 命令就是为了这个目的而设计的;当与 --interactive(或简称 -i)一起使用时,git rebase 是停止使用大量旧的和错误的提交的有效方法。旧的提交没有 gone: 就像 git reset --hard 一样,Git 仍然保留旧的提交一段时间(默认至少 30 天)。但是你的 Git 停止使用它们,支持 git rebase 在放弃旧提交之前构建的新的和改进的提交。

因为这 确实 放弃旧的提交,变基并不总是合适的。特别是,如果 other Git 存储库有旧提交的副本,可能很难说服 every Git 存储库切换到新的和改进的提交。旧的、糟糕的提交可能会不断地回来困扰你。添加还原提交没有这个问题,因为 Git 是为 add 提交而不是 drop 旧的(重置和rebase 是在 你的 存储库中工作的异常,但不会影响其他任何人的)。