教程 > Git 教程 > Git 撤销更改 阅读:2519

Git reset 重置命令详解

git reset命令是用于撤消更改的方法。它具有三种主要的调用形式。这些形式对应于命令行参数--soft, --mixed--hard。这三个参数分别对应于 Git 的三个内部状态管理机制,即提交树 ( HEAD)、暂存索引和工作目录。

Git reset 和 Git 的“三棵树”

要正确理解git reset用法,首先要了解 Git 内部的状态管理系统。有时这些机制被称为 Git 的“三棵树”。树可能用词不当,因为它们不是严格的传统树数据结构。然而,它们是基于节点和指针的数据结构,Git 使用它们来跟踪编辑的时间线。了解这些机制的最好方法是在仓库中创建一个变更集并通过三棵树来跟踪它。

首先,我们将使用以下命令创建一个新存储库:

$ mkdir git_reset_test
$ cd git_reset_test/
$ git init .
Initialized empty Git repository in /git_reset_test/.git/
$ touch reset_lifecycle_file
$ git add reset_lifecycle_file
$ git commit -m"initial commit"
[master (root-commit) fcee6bd] initial commit
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 reset_lifecycle_file

上面的示例代码创建了一个新的 git 仓库,其中包含一个空文件reset_lifecycle_file。此时,示例仓库有一个来自添加 reset_lifecycle_file 的提交 (fcee6bd ) 。


工作目录

我们将检查的第一棵树是“工作目录”。该树与本地文件系统同步,代表对文件和目录中的内容所做的即时更改。

$ echo 'hello git reset' > reset_lifecycle_file
$ git status 

在我们的演示仓库中,我们修改并添加了一些内容到reset_lifecycle_file文件。调用 git status 查看对文件的更改。这些更改目前是第一棵树“工作目录”的一部分。git status可用于显示对工作目录的更改。它们将以红色显示,并带有“modified”前缀。

git status查看reset示例仓库


暂存区索引

接下来是“暂存索引”树。这棵树是用来跟踪工作目录的更改的,这些更改已被 git add 命令提交到暂存区,从而存储在下一次提交中。这棵树是一个复杂的内部缓存机制。Git 通常会尝试向用户隐藏 Staging Index 的实现细节。

为了准确地查看暂存索引的状态,我们必须使用一个鲜为人知的 Git 命令git ls-files。git ls-files命令本质上是一个调试程序,用于检查暂存索引树的状态。

$ git ls-files -s

git ls-files 命令查看索引状态

这里我们在 git ls-files 命令后面使用-s 或者 --stage选项。如果没有-s选项,则git ls-files的输出内容只是当前属于索引一部分的文件名和路径的列表。-s选项显示暂存索引中文件的其他元数据。此元数据是暂存内容的模式位、对象名称和阶段编号。这里我们对对象名称感兴趣,即第二个值 ( e69de29bb2d1d6434b8b29ae775ad8c2e48c5391)。这是一个标准的 Git 对象的 SHA-1 哈希值。它是对文件的内容进行的散列。提交历史存储其自己的对象 SHA,用于标识指向提交和引用的指针,并且临时索引具有自己的对象 SHA,用于跟踪索引中文件的版本。

接下来,我们会将修改后的reset_lifecycle_file提交到暂存索引中。

$ git add reset_lifecycle_file 
$ git status 

git-status-查看提交到暂存索引的仓库的状态

在这里,我们调用使用 git add 命令将reset_lifecycle_file文件添加到暂存索引。调用git status查看状态,现在reset_lifecycle_file在“Changes to be committed”下显示为绿色。需要注意是,git status 并不能真实的反映 Staging Index 的状态。git status命令的输出显示提交历史和分段指数之间的变化。现在让我们查看暂存区索引的内容。

$ git ls-files -s
100644 d7d77c1b04b5edd5acfc85de0b592449e5303770 0    reset_lifecycle_file

git ls-files查看新的暂存索引状态

我们可以看到reset_lifecycle_file 对象的 SHA 已从 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 变为 d7d77c1b04b5edd5acfc85de0b592449e5303770。

提交历史

最后一棵树是提交历史。git commit命令将更改添加为一个永久快照,该快照位于提交历史记录中。此快照还包括提交时暂存索引的状态。

$ git commit -am"update content of reset_lifecycle_file"
[master 091ff5f] update content of reset_lifecycle_file
 1 file changed, 1 insertion(+)
$ git status
On branch main
nothing to commit, working tree clean

在这里,我们创建了一个带有消息 "update content of resetlifecyclefile" 的新提交。变更已添加到提交历史记录中。此时调用git status可以看出任何树都没有挂起的更改。执行git log将显示提交历史。

$ git log
commit 091ff5f5c4b5e046033644e94b2267d25b2a5dba (HEAD -> master)
Author: jiyik <jiyi_onmpw@163.com>
Date:   Thu Sep 2 22:40:28 2021 +0800

    update content of reset_lifecycle_file

commit fcee6bd8608d6c99a660de54967fe6a9d8ab1d17
Author: jiyik <jiyi_onmpw@163.com>
Date:   Thu Sep 2 21:56:34 2021 +0800

    initial commit

现在我们已经通过三棵树完成了这个变更,接下来我们可以开始使用 git reset 了。


git reset 工作原理

从表面上看,git reset在行为上类似于git checkout。其中git checkout仅对HEAD ref 指针进行操作,git reset将移动HEAD ref 指针和当前分支 ref 指针。为了更好理解此行为,可以看下面示例:

![](git reset命令是用于撤消更改的通用方法。它具有三种主要的调用形式。这些形式对应于命令行参数--soft, --mixed--hard。这三个参数分别对应于 Git 的三个内部状态管理机制,即提交树 ( HEAD)、暂存索引和工作目录。

Git reset 和 Git 的“三棵树”

要正确理解git reset用法,首先要了解 Git 内部的状态管理系统。有时这些机制被称为 Git 的“三棵树”。树可能用词不当,因为它们不是严格的传统树数据结构。然而,它们是基于节点和指针的数据结构,Git 使用它们来跟踪编辑的时间线。了解这些机制的最好方法是在仓库中创建一个变更集并通过三棵树来跟踪它。

首先,我们将使用以下命令创建一个新存储库:

$ mkdir git_reset_test
$ cd git_reset_test/
$ git init .
Initialized empty Git repository in /git_reset_test/.git/
$ touch reset_lifecycle_file
$ git add reset_lifecycle_file
$ git commit -m"initial commit"
[master (root-commit) fcee6bd] initial commit
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 reset_lifecycle_file

上面的示例代码创建了一个新的 git 仓库,其中包含一个空文件reset_lifecycle_file。此时,示例仓库有一个来自添加 reset_lifecycle_file 的提交 (fcee6bd ) 。


工作目录

我们将检查的第一棵树是“工作目录”。该树与本地文件系统同步,代表对文件和目录中的内容所做的即时更改。

$ echo 'hello git reset' > reset_lifecycle_file
$ git status 

在我们的演示仓库中,我们修改并添加了一些内容到reset_lifecycle_file文件。调用 git status 查看对文件的更改。这些更改目前是第一棵树“工作目录”的一部分。git status可用于显示对工作目录的更改。它们将以红色显示,并带有“modified”前缀。

git status查看reset示例仓库


暂存区索引

接下来是“暂存索引”树。这棵树是用来跟踪工作目录的更改的,这些更改已被 git add 命令提交到暂存区,从而存储在下一次提交中。这棵树是一个复杂的内部缓存机制。Git 通常会尝试向用户隐藏 Staging Index 的实现细节。

为了准确地查看暂存索引的状态,我们必须使用一个鲜为人知的 Git 命令git ls-files。git ls-files命令本质上是一个调试程序,用于检查暂存索引树的状态。

$ git ls-files -s

git ls-files 命令查看索引状态

这里我们在 git ls-files 命令后面使用-s 或者 --stage选项。如果没有-s选项,则git ls-files的输出内容只是当前属于索引一部分的文件名和路径的列表。-s选项显示暂存索引中文件的其他元数据。此元数据是暂存内容的模式位、对象名称和阶段编号。这里我们对对象名称感兴趣,即第二个值 ( e69de29bb2d1d6434b8b29ae775ad8c2e48c5391)。这是一个标准的 Git 对象的 SHA-1 哈希值。它是对文件的内容进行的散列。提交历史存储其自己的对象 SHA,用于标识指向提交和引用的指针,并且临时索引具有自己的对象 SHA,用于跟踪索引中文件的版本。

接下来,我们会将修改后的reset_lifecycle_file提交到暂存索引中。

$ git add reset_lifecycle_file 
$ git status 

git-status-查看提交到暂存索引的仓库的状态

在这里,我们调用使用 git add 命令将reset_lifecycle_file文件添加到暂存索引。调用git status查看状态,现在reset_lifecycle_file在“Changes to be committed”下显示为绿色。需要注意是,git status 并不能真实的反映 Staging Index 的状态。git status命令的输出显示提交历史和分段指数之间的变化。现在让我们查看暂存区索引的内容。

$ git ls-files -s
100644 d7d77c1b04b5edd5acfc85de0b592449e5303770 0    reset_lifecycle_file

git ls-files查看新的暂存索引状态

我们可以看到reset_lifecycle_file 对象的 SHA 已从 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 变为 d7d77c1b04b5edd5acfc85de0b592449e5303770。

提交历史

最后一棵树是提交历史。git commit命令将更改添加为一个永久快照,该快照位于提交历史记录中。此快照还包括提交时暂存索引的状态。

$ git commit -am"update content of reset_lifecycle_file"
[master 091ff5f] update content of reset_lifecycle_file
 1 file changed, 1 insertion(+)
$ git status
On branch main
nothing to commit, working tree clean

在这里,我们创建了一个带有消息 "update content of resetlifecyclefile" 的新提交。变更已添加到提交历史记录中。此时调用git status可以看出任何树都没有挂起的更改。执行git log将显示提交历史。

$ git log
commit 091ff5f5c4b5e046033644e94b2267d25b2a5dba (HEAD -> master)
Author: jiyik <jiyi_onmpw@163.com>
Date:   Thu Sep 2 22:40:28 2021 +0800

    update content of reset_lifecycle_file

commit fcee6bd8608d6c99a660de54967fe6a9d8ab1d17
Author: jiyik <jiyi_onmpw@163.com>
Date:   Thu Sep 2 21:56:34 2021 +0800

    initial commit

现在我们已经通过三棵树完成了这个变更,接下来我们可以开始使用 git reset 了。


git reset 工作原理

从表面上看,git reset在行为上类似于git checkout。其中git checkout仅对HEAD ref 指针进行操作,git reset将移动HEAD ref 指针和当前分支 ref 指针。为了更好理解此行为,可以看下面示例:

git 提交历史记录
git 提交历史记录

此示例演示了main 分支上的一系列提交。HEAD ref和main 分支ref 目前指向提交 d。现在让我们对 git checkout b和 git reset b进行一个比较

git checkout b

git checkout 检出提交b
git checkout 检出提交b

git checkout 执行之后,main ref 仍然指向d。在HEAD ref 已经被移动,现在指向提交b。仓库现在处于“分离HEAD”状态。

git reset b

git checkout 检出提交b
git 对提交b重置

相比之下,git reset 将HEAD分支引用和 main 分支引用都移动到指定的提交。

除了更新提交引用指针外,git reset还会修改三棵树的状态。ref 指针总是被修改,并且是对提交树的更新。

git reset命令的参数包括 --soft, --mixed--hard指示如何修改临时索引和工作目录树。

主要选项

默认情况下,如果在调用git reset命令时,后面没有跟任何参数的话,则其执行后的效果就相当于在命令后面跟上了参数--mixed和HEAD。这意味着执行 git reset 等同于执行 git reset --mixed HEAD。HEAD是指定的提交。HEAD可以使用任何的 Git 的提交哈希值(SHA-1 )来代替。

git reset模型的范围
git reset模型的范围

--hard

该选项是最直接、最危险但也是最常用的选项。使用--hard时,提交历史引用指针将更新为指定的提交。然后,临时索引和工作目录被重置来匹配指定的提交的索引和工作目录。任何先前对暂存索引和工作目录的未提交的更改都会被重置来匹配提交树的状态。这意味着暂存索引和工作目录中的任何待处理的工作都将丢失。

为了证明这一点,让我们继续我们之前建立的“三棵树”的示例仓库。首先让我们对仓库的内容进行一些修改。执行以下命令:

$ echo 'new file content' > new_file
$ git add new_file
$ echo 'changed content' >> reset_lifecycle_file

这些命令创建了一个名为 new_file 的新文件,并将其添加到仓库中。此外,修改reset_lifecycle_file文件的内容。有了这些更改,现在让我们使用git status查看一下当前仓库的状态

$ git status

git status 查看添加新文件之后仓库状态

我们可以看到仓库中现在有待处理的更改。暂存索引树有一个挂起的用于添加 new_file文件的更改,而工作目录中有一个挂起的用于修改 reset_lifecycle_file 文件的更改。

在继续之前,让我们还检查一下暂存索引树的状态:

$ git ls-files -s
100644 8e66654a5477b1bf4765946147c49509a431f963 0    new_file
100644 d7d77c1b04b5edd5acfc85de0b592449e5303770 0    reset_lifecycle_file

我们可以看到 new_file 已经添加到索引中了。我们已经更新了reset_lifecycle_file,但它的暂存索引中的 SHA (d7d77c1b04b5edd5acfc85de0b592449e5303770 ) 值保持不变。这是符合我们预期行为的,因为尚未使用 git add将此文件的更改提交到暂存索引中。这些更改目前仅存在于工作目录中。

现在让我们执行 git reset --hard 并检查仓库库的新状态。

$ git reset --hard
HEAD is now at 091ff5f update content of reset_lifecycle_file
$ git status
On branch main
nothing to commit, working tree clean
$ git ls-files -s
100644 d7d77c1b04b5edd5acfc85de0b592449e5303770 0    reset_lifecycle_file

在这里,我们使用--hard选项进行了 “硬重置” 。执行命令后的输出表明 HEAD指向最新的提交 091ff5f。接下来,我们使用 git status来检查仓库的状态。从输出信息我们可以看到没有挂起的更改。同时我们还检查了暂存索引的状态,并看到它已被重置为添加 new_file 之前的某个点。我们对 reset_lifecycle_file 的修改和添加的new_file已经被破坏。这种数据丢失是无法找回的,这一点非常重要。

--mixed

该选项是默认的选项。引用指针已更新。暂存索引重置为指定提交的状态。已从暂存索引中撤消的任何更改都将移至工作目录。让我们继续下面的操作。

$ echo 'new file content' > new_file
$ git add new_file
$ echo 'append content' >> reset_lifecycle_file
$ git add reset_lifecycle_file
$ git status
On branch main
Changes to be committed:
    (use "git reset HEAD ..." to unstage)
    new file:   new_file
    modified:   reset_lifecycle_file

$ git ls-files -s
100644 8e66654a5477b1bf4765946147c49509a431f963 0    new_file
100644 0fcccd36ac649422bbe0226d9c0a11d12699f174 0    reset_lifecycle_file

在上面的示例中,我们对存储库进行了一些修改。同样,我们添加了new_file并修改了 reset_lifecycle_file 的内容。然后将这些更改使用 git add 命令添加到到暂存索引。我们现在将执行 reset。

$ git reset --mixed
Unstaged changes after reset:
M    reset_lifecycle_file
$ git status
$ git ls-files -s

执行git reset mixed 命令之后的仓库状态

在这里,我们执行了“混合重置”。 重申一下, --mixed 是默认选项,与执行 git reset 的效果相同。 通过查看 git status 和 git ls-files 的输出,暂存索引的状态已被重置,当前 reset_lifecycle_file 是索引中唯一文件。 reset_lifecycle_file 的对象 SHA (d7d77c1b04b5edd5acfc85de0b592449e5303770)已重置为以前的版本。

这里需要注意的重要事项是 git status 向我们显示了对 reset_lifecycle_file 的修改,并且有一个未跟踪的文件:new_file。 这是显式的 --mixed 行为。 暂存索引已重置,待定更改已移至工作目录中。 将此与--hard 重置情况相比,--hard重置执行之后,临时索引被重置并且工作目录也被重置,丢失了这些更新。

--soft

当传递--soft参数时,ref指针将被更新,重置将停止。暂存索引和工作目录保持不变。这种行为很难清楚地表现出来。让我们继续操作上面创建的仓库,并为 soft 重置做好准备。

$ git add reset_lifecycle_file 
$ git ls-files -s 
100644 0fcccd36ac649422bbe0226d9c0a11d12699f174 0    reset_lifecycle_file
$ git status 

git status查看reset soft之前的仓库状态

在这里,我们再次使用git add将修改后的reset_lifecycle_file 添加到暂存索引中。我们 使用git ls-files -s确认索引已经进行了更新。git status的输出现在以绿色显示“Changes to be committed”。我们前面示例中的 new_file 作为未跟踪文件在工作目录中被忽略。让我们执行rm new_file来删除该文件,因为在接下来的示例中我们不需要它。

在此状态的仓库中,我们现在执行 soft 重置。

$ git reset --soft
$ git status
$ git ls-files -s
100644 0fcccd36ac649422bbe0226d9c0a11d12699f174 0    reset_lifecycle_file

git status查看reset soft之后的仓库状态

我们执行了“软重置”。 使用 git status 和 git ls-files 检查仓库状态发现没有任何变化。 这是预期的行为。 软重置只会重置提交历史记录。 默认情况下,使用 HEAD 作为目标提交调用 git reset。 由于我们的 提交历史已经位于 HEAD ,并且我们隐式重置为HEAD ,那什么也不会发生。

为了更好地理解和利用 --soft,我们需要一个不是HEAD的目标提交。我们已重置暂存索引中的 reset_lifecycle_file。让我们创建一个新的提交。

$ git commit -m"prepend content to reset_lifecycle_file"

此时,我们的仓库应该有三个提交。 我们将回到第一次提交。 为此,我们需要第一个提交的 ID。 这可以通过查看 git log 的输出找到。

$ git log
commit dff4ecabc836afcc11595df3ad8d1be7df6c6052 (HEAD -> master)
Author: jiyik <jiyi_onmpw@163.com>
Date:   Fri Sep 3 19:24:23 2021 +0800

    prepend content to reset_lifecycle_file

commit 091ff5f5c4b5e046033644e94b2267d25b2a5dba
Author: jiyik <jiyi_onmpw@163.com>
Date:   Thu Sep 2 22:40:28 2021 +0800

    update content of reset_lifecycle_file

commit fcee6bd8608d6c99a660de54967fe6a9d8ab1d17
Author: jiyik <jiyi_onmpw@163.com>
Date:   Thu Sep 2 21:56:34 2021 +0800

    initial commit

请记住,提交历史记录ID对于每个系统都是唯一的。这意味着本例中的提交ID与您在个人计算机上看到的不同。本例中我们想要的提交ID是 fcee6bd8608d6c99a660de54967fe6a9d8ab1d17。这是对应于“初始提交”的ID。一旦我们找到了这个ID,我们将使用它作为软重置的目标。

$ git reset --soft fcee6bd8608d6c99a660de54967fe6a9d8ab1d17
$ git status && git ls-files -s

查看reset soft指定提交之后的仓库和暂存索引状态

上面的代码执行“软重置”并调用 git status 和 git ls-files 组合命令,该命令输出仓库的状态。 我们可以检查仓库的状态输出并注意一些有趣的观察结果。 首先, git status 显示对 reset_lifecycle_file 进行了修改,并突出显示它们表示它们是为下一次提交准备的更改。 其次,git ls-files 输出显示暂存索引没有改变并保留了我们之前的 SHA 0fcccd36ac649422bbe0226d9c0a11d12699f174。

为了进一步看一下这次重置中发生了什么,让我们检查一下 git log:

$ git log
Author: jiyik <jiyi_onmpw@163.com>
Date:   Thu Sep 2 21:56:34 2021 +0800

    initial commit

log 输出现在显示提交历史记录中有一个提交。 这可以很清楚地说明 --soft 做了什么。 与所有 git reset 调用一样,reset 采取的第一个操作是重置提交树。 我们之前使用 --hard 和 --mixed 的例子都是针对 HEAD 的,并没有及时移动提交树。 在 soft reset 期间,这就是所有发生的事情。

这可能会混淆为什么 git status 显示有修改过的文件。 --soft 不涉及暂存索引,因此对暂存索引的更新会在提交历史中及时跟踪。 这可以通过 git ls-files -s 的输出来确认,该输出显示 reset_lifecycle_file 的 SHA 未更改。 提醒一下, git status 不显示“三棵树”的状态,它本质上显示了它们之间的差异。 在这种情况下,它显示暂存索引领先于提交历史中的更改,就好像我们已经暂存了它们一样。


reset 与 revert

如果git revert是撤销更改的“安全”方法,那么可以将git reset视为危险的方法。使用 git reset 确实存在丢失工作的风险。git reset永远不会删除提交,但是,提交可能会成为“孤立的”,这意味着没有从引用直接访问它们的路径。这些孤立的提交通常可以使用 git reflog 找到和恢复。Git将在运行内部垃圾收集器后永久删除所有孤立的提交。默认情况下,Git配置为每30天运行一次垃圾收集器。提交历史是“三棵git树”之一,另外两棵,暂存索引和工作目录不像提交那样是永久存储的。使用此工具时必须小心,因为它是可能丢失工作的Git命令之一。

revert 旨在安全地撤消公共提交,而git reset旨在撤消对临时索引和工作目录的本地更改。由于目标不同,这两个命令的实现方式不同:重置将完全删除变更集,而恢复将保留原始变更集,并使用新的提交应用来实现撤消。


不要对公共历史执行 git reset

当任何快照被推送到公共仓库后,你永远不应该使用 git reset 。 发布提交后,你必须假设其他开发人员也在使用它。

删除团队其他成员继续开发的提交会给协作带来严重的问题。 当他们尝试与你的仓库同步时,看起来就像是项目历史的一大块突然消失了。 下面的演示了当尝试重置公共提交时会发生什么。 origin/main 分支是本地 main 分支的中央仓库版本。

对公共历史进行reset操作的情况
对公共历史进行reset操作的情况

一旦你在重置后添加新的提交,Git 就会认为你的本地历史已经偏离了 origin/main,并且同步你的仓库所需的合并提交可能会让你的团队感到困惑和沮丧。

关键是,请确保你在出错的本地实验中使用 git reset ,而不是在已发布的更改上。 如果需要修复公共提交,git revert 命令是专门为此目的而设计的。


取消暂存文件

在准备暂存快照时经常会遇到 git reset 命令。 下一个示例假设您已经将两个名为 hello.txt 和 main.txt 的文件添加到仓库中。

# 编辑 hello.txt 和 main.txt

# 将工作目录中修改添加到暂存索引
$ git add .

# 取消暂存索引中的 main.txt
$ git reset main.txt

# 只提交 hello.txt
$ git commit -m "Make some changes to hello.txt"

# 使用单独的快找提交 main.txt
$ git add main.txt
$ git commit -m "Edit main.txt"

如您所见,git reset 通过让您取消暂存与下一次提交无关的更改来帮助您保持提交高度集中。


删除本地提交

下一个示例显示了一个更高级的用例。 当您进行了一段时间的新代码的编写,但在提交了一些快照后决定将其完全丢弃,我们看会发生什么。

# 创建一个 foo.txt 新文件 并且在其中写入一些内容

# 将其添加到提交历史中
$ git add foo.txt
$ git commit -m "Start developing a crazy feature"

# 再次编辑 `foo.txt` 

# 提交另一个快照
$ git commit -a -m "Continue my crazy feature"

# 移除所有相关的提交
git reset --hard HEAD~2

git reset HEAD~2 命令将当前分支向后移动两次提交,有效地从项目历史中删除了我们刚刚创建的两个快照。

再次警告 - 这种重置只能用于未发布的提交。 如果您已经将提交推送到共享仓库,请不要执行上述操作。

在本篇中,我们利用了几个其他的 Git 命令来帮助演示 git reset 的过程。在各自的页面上了解关于这些命令的更多信息:git statusgit loggit addgit checkoutgit refloggit revert

查看笔记

扫码一下
查看教程更方便