Git 重置 --Hard git reset --explain
Reset 可能是最难理解的 git 命令之一,而且它还因危险而声名狼藉。这两种说法都有其合理性:是的,reset 命令确实比较难理解,在某些情况下甚至可能很危险。但其实它并没有那么难。因此,在这篇文章中,我将尽力为您呈现一个清晰精炼的 reset 命令教程。为了简洁明了,避免内容过于冗长,我提取了一些非必要的细节并进行了简化。如果您想进一步了解 git 的内部工作原理,也可以查看我的“理解 Git”系列文章,了解更多此处介绍的内容。
Git 树
在深入研究reset
命令之前,我们需要先了解一下 git 的树:工作目录、暂存区和存储库。
从 git 的角度来看,你可以将它们视为可以存储更改的区域:
- 工作目录 - 文件系统上的项目文件
- 暂存区——下一次提交的预览
- 存储库——git 保存所有(过去)提交的数据存储。
命令reset
作用于这三个/区域,但是首先,让我们看看我们日常使用的命令是如何影响这些区域的add
。commit
假设我们有一个 Web 应用,并对index.php
文件进行了一些重构。我们所做的更改会反映在工作目录中:
我们可以通过运行以下命令来确认这一点git status
:
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: index.php
现在我们使用以下add
命令将这些更改移动到暂存区:
现在运行status
命令将告诉我们:
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
modified: index.php
因为status
命令发现我们index.php
在工作目录和暂存区中都有相同版本的文件,但在存储库中却没有。
要将其添加到那里,我们使用以下commit
命令:
现在工作目录、暂存区和存储库都包含相同版本的index.php
,运行git status
会告诉我们:
nothing to commit, working tree clean
因此,该命令的工作方式status
是比较工作目录、暂存区和存储库中的文件版本,如果存在不同,则有文件需要暂存/提交。
假设我们现在index.php
进一步重构文件,并再次执行整个添加/提交循环。
现在我们的工作目录、暂存区和存储库都包含了文件的第二个版本index.php
。
但是第一个版本怎么办呢?如果你还记得的话,我们说过 Repository 会保留所有之前的提交,所以index.php
文件的第一个版本仍然存在:
为了跟踪我们index.php
文件的当前版本,存储库有一个特殊的指针,称为HEAD
指向当前版本(并且该命令仅在将其与暂存区的版本进行比较时status
查看指向的当前版本)。HEAD
现在我们已经了解了这些,我们终于可以转到我们的reset
命令并通过操纵这些区域的内容来查看它是如何工作的。
重置--软
第一种命令模式reset
只会做一件事:
- 移动
HEAD
指针。
在我们的例子中,我们将通过执行以下操作将其移动到上一个提交(的第一个版本index.php
):git reset --soft HEAD~1
git 的树现在看起来像这样:
如果我们运行,git status
我们会看到一条熟悉的消息:
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
modified: index.php
因此,运行git reset --soft HEAD~1
基本上撤消了我们的最后一次提交,但该提交中包含的更改并没有丢失 - 它们位于我们的暂存区和工作目录中。
重置 - 混合
该命令的第二种模式reset
将执行两件事:
- 移动
HEAD
指针 HEAD
更新暂存区(使用指向的内容)
所以,第一步和模式一样--soft
。第二步获取HEAD
指向的内容(在本例中是index.php
文件的第一个版本)并将其放入暂存区。
因此,运行后git reset --mixed HEAD~1
我们的区域如下所示:
如果我们现在运行,git status
我们会再次看到一条熟悉的消息:
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: index.php
因此,运行git reset - mixed HEAD~1
撤消了我们的最后一次提交,但这次来自该提交的更改(仅)在我们的工作目录中。
重置--hard
现在来看看臭名昭著的困难模式。跑步reset -- hard
会做三件事:
- 移动
HEAD
指针 HEAD
更新暂存区(使用指向的内容)- 更新工作目录以匹配暂存区
因此,前两个步骤与第三步相同--mixed
,使工作目录看起来像暂存区(已经填充了HEAD
指向的内容)。
因此,运行后git reset --hard HEAD~1
我们的区域如下所示:
跑步git status
会给我们带来:
nothing to commit, working tree clean
因此,运行git reset - hard HEAD~1
撤销了我们上一次提交,并且该提交中包含的更改现在既不在工作目录也不在暂存区了。但它们并没有完全丢失。Git 不会从仓库中删除提交(实际上,它有时会删除,但很少),所以这意味着我们的第二个版本的提交仍然在仓库中,只是有点难以找到(您可以通过查看reflog来跟踪它)。
那么,为什么说 reset 很危险呢?嗯,有一种情况可能会导致一些更改永久丢失。考虑这样一种情况:在第二次提交后,你对index.php
文件做了一些更改,但没有将它们暂存并提交:
现在运行git reset --hard HEAD~1
:
由于该reset
命令将覆盖您的工作目录的内容以匹配暂存区(即匹配HEAD
),并且您从未暂存和提交您的更改(存储库中没有对这些更改的提交),因此所有这些更改现在都会随着时间而丢失...就像雨中的泪水一样。
硬重置的危险之处在于它不安全于工作目录——这意味着如果你在工作目录中更改了文件,而这些文件在运行硬重置时会被覆盖(并丢失),它不会发出任何警告。所以,硬重置时要格外小心。
好了,这就是reset
命令。希望我解释得很清楚,并且你会同意它其实并不难。当然,它可能很危险,但前提是必须与--hard
选项一起使用。
正如开头所说,如果您想了解更多有关 git 内部工作原理的信息,您可以查看我的《理解 Git》系列,如果您想要更深入地解释该reset
命令,您可以查看git pro 书中的《重置揭秘》一章。
附录:
- 在示例中,我们使用了
HEAD~1
checksum 作为命令的参数。你可能已经知道,git 中的每个提交都有一个唯一的标识符,称为 checksum,我们也reset
可以将其用作命令的参数。reset
- 为了使示例更简单,我们只编辑并提交了一个文件,实际上,我们经常提交多个文件,因此特定的提交包含多个文件的不同版本。
- 特殊
HEAD
指针通常不直接指向提交(如示例中所示),而是指向一个分支指针,该分支指针指向特定的提交