如何修复因 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
命令还有其他模式可以做其他事情,但我们不要误入歧途;我的回答已经太长了。
如果您希望 B
和 C
提交回来,它们 是 可恢复的,暂时。恢复它们的主要问题是找到提交 C
的哈希 ID。 (您不必找到提交 B
的哈希 ID,因为提交 C
为您保留了该哈希 ID。您只需找到 last在链中提交:Git 会从那里自动找到更早的所有内容。)
幸运的是,Git 有一个叫做 reflogs 的东西,它保留了 曾 存储在某个名称中的哈希 ID ,默认至少 30 天。您可以查看 HEAD
的引用日志,或您的分支名称的引用日志,以查找提交 C
的哈希 ID。使用 git reflog
或 git 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 restore
或 git 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 是在 你的 存储库中工作的异常,但不会影响其他任何人的)。
米
我向本地文件添加了一些更改,然后提交了它们 (A),然后我添加了更多提交 (B&C),然后意识到,我不想要来自提交 (A) 的更改,所以我继续了并做了 git reset --hard <HASH>
从 git log
中删除了之前的提交,但保留了我在文件中 (A) 中添加的更改,当我现在将此文件更改回其原始状态时,它会将其标记为已更改,我不想。我希望文件反映 origin
。我如何从这里完成这项工作?
(我认为这就是 reset --hard
所做的)
作为 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
命令还有其他模式可以做其他事情,但我们不要误入歧途;我的回答已经太长了。
如果您希望 B
和 C
提交回来,它们 是 可恢复的,暂时。恢复它们的主要问题是找到提交 C
的哈希 ID。 (您不必找到提交 B
的哈希 ID,因为提交 C
为您保留了该哈希 ID。您只需找到 last在链中提交:Git 会从那里自动找到更早的所有内容。)
幸运的是,Git 有一个叫做 reflogs 的东西,它保留了 曾 存储在某个名称中的哈希 ID ,默认至少 30 天。您可以查看 HEAD
的引用日志,或您的分支名称的引用日志,以查找提交 C
的哈希 ID。使用 git reflog
或 git 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 restore
或 git 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 是在 你的 存储库中工作的异常,但不会影响其他任何人的)。