在使用 Git 时,较少被理解(和欣赏)的方面之一是,它非常容易回到你之前的状态——也就是说,即使是存储库中的重大更改,也很容易撤消。在本文中,我们将快速了解如何使用单个 Git 命令的简洁性和优雅性来重置、还原和完全返回到之前的状态。
如何重置 Git 提交
让我们从 Git 命令 reset
开始。实际上,你可以将其视为“回滚”——它将你的本地环境指向之前的提交。 “本地环境”指的是你的本地存储库、暂存区和工作目录。
看看图 1。这里我们展示了 Git 中一系列提交的表示。 Git 中的分支只是一个命名的、可移动的指针,指向特定的提交。在本例中,我们的 master 分支是指向链中最新提交的指针。

图 1:本地 Git 环境,包含存储库、暂存区和工作目录
如果我们看看现在 master 分支中的内容,我们可以看到到目前为止所做的一系列提交。
$ git log --oneline
b764644 File with three lines
7c709f0 File with two lines
9ef9173 File with one line
如果我们想要回滚到之前的提交会发生什么?很简单——我们可以直接移动分支指针。 Git 提供了 reset
命令来为我们执行此操作。例如,如果我们想将 master 重置为指向当前提交之前的两个提交,我们可以使用以下任一方法
$ git reset 9ef9173
(使用绝对提交 SHA1 值 9ef9173)
或
$ git reset current~2
(使用相对值 -2 在“current”标签之前)
图 2 显示了此操作的结果。之后,如果我们在当前分支(master)上执行 git log
命令,我们将只看到一次提交。
$ git log --oneline
9ef9173 File with one line

图 2:reset
之后
git reset
命令还包含一些选项,用于使用你最终到达的提交的内容来更新本地环境的其他部分。这些选项包括:hard
用于重置存储库中指向的提交,使用提交的内容填充工作目录,并重置暂存区;soft
仅重置存储库中的指针;以及 mixed
(默认)用于重置指针和暂存区。
在特定情况下,使用这些选项可能很有用,例如 git reset --hard <commit sha1 | reference>
.
这会覆盖你尚未提交的任何本地更改。实际上,它会重置(清除)暂存区,并使用你重置到的提交中的内容覆盖工作目录中的内容。在使用 hard
选项之前,请确保这确实是你想要做的,因为该命令会覆盖任何未提交的更改。
如何还原 Git 提交
git revert
命令的最终效果与 reset 类似,但其方法不同。 reset
命令将分支指针向后移动到链中(通常)以“撤消”更改,而 revert
命令在链的末尾添加一个新的提交以“取消”更改。通过再次查看图 1,最容易看到效果。如果我们在链中每次提交的文件中添加一行,那么回到只有两行的版本的一种方法是重置到该提交,即 git reset HEAD~1
。
获得两行版本的另一种方法是添加一个新的提交,其中删除了第三行——有效地取消了该更改。这可以使用 git revert
命令来完成,例如
$ git revert HEAD
因为这会添加一个新的提交,Git 将提示输入提交消息
Revert "File with three lines"
This reverts commit b764644bad524b804577684bf74e7bca3117f554.
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# On branch master
# Changes to be committed:
# modified: file1.txt
#
图 3(下图)显示了 revert
操作完成后的结果。
如果我们现在执行 git log
,我们将看到一个新的提交,反映了上一次提交之前的内容。
$ git log --oneline
11b7712 Revert "File with three lines"
b764644 File with three lines
7c709f0 File with two lines
9ef9173 File with one line
以下是工作目录中文件的当前内容
$ cat <filename>
Line 1
Line 2

还原还是重置?
为什么你会选择执行 revert
而不是 reset
操作?如果你已经将你的提交链推送到远程存储库(其他人可能已经拉取了你的代码并开始使用它),那么 revert 是一种为他们取消更改的更友好的方式。这是因为 Git 工作流程非常适合在分支末尾获取额外的提交,但是如果当有人将分支指针重置回去时,在链中不再看到一组提交,则可能会带来挑战。
这引出了以此方式使用 Git 时的基本规则之一:在你本地存储库中对尚未推送的代码进行此类更改是可以的。但是,如果提交已经推送到远程存储库,并且其他人可能正在使用它们,则应避免进行重写历史记录的更改。
简而言之,如果你回滚、撤消或重写其他人正在使用的提交链的历史记录,那么当你的同事尝试合并基于他们拉取的原始链的更改时,他们可能会有更多的工作要做。如果你必须对已经推送并被其他人使用的代码进行更改,请考虑在进行更改之前进行沟通,并给人们机会先合并他们的更改。然后,他们可以在侵权操作后拉取一个全新的副本,而无需合并。
你可能已经注意到,在我们执行 reset 后,原始的提交链仍然存在。我们移动了指针并将代码重置回之前的提交,但它没有删除任何提交。这意味着,只要我们知道我们最初指向的提交,我们就可以通过简单地重置回分支的原始 head 来“恢复”到之前的点
git reset <sha1 of commit>
当我们替换提交时,我们在 Git 中执行的大多数其他操作中也会发生类似的事情。创建了新的提交,并且适当的指针被移动到新的链。但是旧的提交链仍然存在。
Rebase
现在让我们看看分支 rebase。假设我们有两个分支——master 和 feature——以及下图 4 中显示的提交链。 Master 具有链 C4->C2->C1->C0
,feature 具有链 C5->C3->C2->C1->C0
。

图 4:分支 master 和 feature 的提交链
如果我们查看分支中提交的日志,它们可能如下所示。(提交消息的 C
标识符用于使这更容易理解。)
$ git log --oneline master
6a92e7a C4
259bf36 C2
f33ae68 C1
5043e79 C0
$ git log --oneline feature
79768b8 C5
000f9ae C3
259bf36 C2
f33ae68 C1
5043e79 C0
我告诉人们将 rebase 视为 Git 中的“合并历史记录”。本质上,Git 所做的是获取一个分支中的每个不同的提交,并尝试将差异“重播”到另一个分支上。
因此,我们可以将 feature rebase 到 master 上以获取 C4
(例如,将其插入到 feature 的链中)。使用基本的 Git 命令,它可能看起来像这样
$ git checkout feature
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: C3
Applying: C5
之后,我们的提交链将如图 5 所示。

图 5:rebase
命令后的提交链
再次,查看提交日志,我们可以看到更改。
$ git log --oneline master
6a92e7a C4
259bf36 C2
f33ae68 C1
5043e79 C0
$ git log --oneline feature
c4533a5 C5
64f2047 C3
6a92e7a C4
259bf36 C2
f33ae68 C1
5043e79 C0
请注意,我们有 C3'
和 C5'
——由于将原始的更改“放在”master 中现有链的“顶部”而创建的新提交。但也要注意,“原始”的 C3
和 C5
仍然存在——它们只是不再有分支指向它们。
如果我们执行了此 rebase,然后决定我们不喜欢结果并想撤消它,这将非常简单,只需
$ git reset 79768b8
通过这个简单的更改,我们的分支现在将指向与 rebase
操作之前相同的提交集——有效地撤消了它(图 6)。

图 6:撤消 rebase
操作后
如果你不记得分支在操作之前指向哪个提交怎么办?幸运的是,Git 再次帮助了我们。对于大多数以这种方式修改指针的操作,Git 会为你记住原始提交。实际上,它将其存储在 .git
存储库目录中名为 ORIG_HEAD
的特殊引用中。该路径是一个文件,其中包含修改之前的最新引用。如果我们 cat
该文件,我们可以看到其内容。
$ cat .git/ORIG_HEAD
79768b891f47ce06f13456a7e222536ee47ad2fe
我们可以像以前一样使用 reset
命令,以指向回原始链。然后日志将显示这个
$ git log --oneline feature
79768b8 C5
000f9ae C3
259bf36 C2
f33ae68 C1
5043e79 C0
获取此信息的另一个地方是 reflog。 reflog 是本地存储库中引用开关或更改的逐个操作列表。要查看它,你可以使用 git reflog
命令
$ git reflog
79768b8 HEAD@{0}: reset: moving to 79768b
c4533a5 HEAD@{1}: rebase finished: returning to refs/heads/feature
c4533a5 HEAD@{2}: rebase: C5
64f2047 HEAD@{3}: rebase: C3
6a92e7a HEAD@{4}: rebase: checkout master
79768b8 HEAD@{5}: checkout: moving from feature to feature
79768b8 HEAD@{6}: commit: C5
000f9ae HEAD@{7}: checkout: moving from master to feature
6a92e7a HEAD@{8}: commit: C4
259bf36 HEAD@{9}: checkout: moving from feature to master
000f9ae HEAD@{10}: commit: C3
259bf36 HEAD@{11}: checkout: moving from master to feature
259bf36 HEAD@{12}: commit: C2
f33ae68 HEAD@{13}: commit: C1
5043e79 HEAD@{14}: commit (initial): C0
然后,你可以使用你在日志中看到的特殊相对命名格式重置为该列表中的任何项目
$ git reset HEAD@{1}
一旦你理解了 Git 在操作“修改”链时保留原始提交链,那么在 Git 中进行更改就变得不那么可怕了。这是 Git 的核心优势之一:能够快速轻松地尝试一些事情,并在它们不起作用时撤消它们。
Brent Laster 将在 7 月 16 日至 19 日在俄勒冈州波特兰举行的第 20 届年度 OSCON 大会上展示 Power Git:Rerere、Bisect、Subtrees、Filter Branch、Worktrees、Submodules 和更多。有关在任何级别使用 Git 的更多提示和说明,请查看 Brent 的著作《Professional Git》,可在亚马逊上购买。
1 条评论