作为already answered in a comment,你不能用git reset得到你想要的最终效果。然而,这里的细节相当混乱,因为git reset 有点像kitchen-sink 命令。
我在本地文件中添加了一些更改,然后提交了它们 (A) ...
这里要意识到的是,每个 Git 提交都是每个文件的完整快照。当您更改一个文件并进行新的提交时,您会创建一个新的完整快照。
这不会占用大量磁盘空间,因为 Git 提交中的文件不是普通文件。它们以特殊的、压缩的、Git 化的和去重复 的形式存储。所以新的提交 A 只是重新使用了之前提交 α 中的所有旧文件,除了一个修改过的文件,它会获得一个新的快照。1
然后我添加了更多提交 (B&C),然后意识到,我不希望提交 (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 的 reflog 或分支名称的 reflog,以找到提交 C 的哈希 ID。使用git reflog 或git reflog <em>branch</em> 来执行此操作。
假设您将这两个放回(与另一个 git reset --hard),以便您的 strand-of-pearls 提交返回到:
δ <-γ <-β <-α <-A <-B <-C <-- your-branch
您现在可以非常轻松地向您的分支添加一个新提交D,使用git revert完全撤消提交A的效果.或者,您可以创建一个新的提交 D,其快照与 C 的快照相同,只是一个特定文件的内容是从任何给定提交中提取的内容。
1这些单独的文件快照后来在 Git 所谓的 pack files 中进一步压缩,超出了它们的初始压缩和 Git 化,这意味着即使是多个版本的大文本文件最终不会占用太多空间。不过,您无需关心这些细节:只需记住每个提交都充当每个文件的完整存档,例如 tar、zip 或 rar 存档。
使用git revert
git revert 命令通过将提交的内容与其父级的内容进行比较来工作。无论在此处更改,该更改将通过或多或少地反向应用更改而在当前 文件集中取消。 2 因此,如果您在提交 A 中修改了文件 F,但没有对任何其他文件执行任何操作,git revert <em>hash-of-A</em> 将退出该更改。 Git 将从结果文件中进行新的提交。
2从技术上讲,revert 是一种三向合并,当前提交一如既往地是当前提交,但 merge base 是子提交您在 git revert 命令中指定,并且三向合并的 其他提交 是该父/子对的父级。
使用git restore 或git checkout
如前所述,每次提交都有所有文件的完整快照。
要获取一个特定提交的特定文件输出,您可以使用新的(从 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 是一种有效的方式来停止使用一大堆陈旧的提交。旧的提交并没有消失: 就像git reset --hard 一样,Git 仍然会保留旧的提交一段时间(默认情况下至少 30 天)。但是您的 Git 停止使用它们,转而支持 git rebase 在放弃旧提交之前构建的新的和改进的提交。
因为这确实放弃了旧的提交,rebase 并不总是合适的。特别是,如果 other Git 存储库有旧提交的副本,则很难说服 每个 Git 存储库改用新的和改进的提交。旧的、糟糕的提交可能会继续困扰您。添加还原提交没有这个问题,因为 Git 是为 add 提交而不是 drop 旧的(重置和变基是在 你的 存储库,但不影响其他人的)。