Git版本回退
Git详细工作流程的可视化
参考博客: Git 工具 - 重置揭密
新建仓库并提交文件
假设我们进入到一个新目录, 其中有一个文件。 我们称其为该文件的 v1 版本, 将它标记为蓝色。 现在运行 git init
, 这会创建一个 Git 仓库, 其中的 HEAD 引用指向未创建的 master
分支。

此时, 只有工作目录有内容。
现在我们想要提交这个文件, 所以用 git add
来获取工作目录中的内容, 并将其复制到索引中。

接着运行 git commit
, 它会取得索引中的内容并将它保存为一个永久的快照, 然后创建一个指向该快照的提交对象, 最后更新 master
来指向本次提交。

此时如果我们运行 git status
, 会发现没有任何改动, 因为现在三棵树完全相同。
修改文件并提交至仓库
现在我们想要对文件进行修改然后提交它。 我们将会经历同样的过程;首先在工作目录中修改文件。 我们称其为该文件的 v2 版本, 并将它标记为红色。

如果现在运行 git status
, 我们会看到文件显示在 “Changes not staged for commit” 下面并被标记为红色, 因为该条目在索引与工作目录之间存在不同。 接着我们运行 git add
来将它暂存到索引中。

此时, 由于索引和 HEAD 不同, 若运行 git status
的话就会看到 “Changes to be committed” 下的该文件变为绿色 ——也就是说, 现在预期的下一次提交与上一次提交不同。
最后, 我们运行 git commit
来完成提交。

现在运行 git status
会没有输出, 因为三棵树又变得相同了。
版本回退
参考博客: Git 工具 - 重置揭密
参考博客: git reset 命令 | 菜鸟教程
为了演示这些例子, 假设我们再次修改了 file.txt
文件并第三次提交它。 现在的历史看起来是这样的:

让我们跟着 reset
看看它都做了什么。 它以一种简单可预见的方式直接操纵这三棵树。 它做了三个基本操作。
第 1 步:移动 HEAD
reset
做的第一件事是移动 HEAD 的指向。 这与改变 HEAD 自身不同(checkout
所做的);reset
移动 HEAD 指向的分支。 这意味着如果 HEAD 设置为 master
分支(例如, 你正在 master
分支上), 运行 git reset 9e5e6a4
将会使 master
指向 9e5e6a4
。

无论你调用了何种形式的带有一个提交的 reset
, 它首先都会尝试这样做。 使用 reset --soft
, 它将仅仅停在那儿。
现在看一眼上图, 理解一下发生的事情:它本质上是撤销了上一次 git commit
命令。 当你在运行 git commit
时, Git 会创建一个新的提交, 并移动 HEAD 所指向的分支来使其指向该提交。 当你将它 reset
回 HEAD~
(HEAD 的父结点)时, 其实就是把该分支移动回原来的位置, 而不会改变索引和工作目录。 现在你可以更新索引并再次运行 git commit
来完成 git commit --amend
所要做的事情了(见 修改最后一次提交)。
第 2 步:重置索引(–mixed)
注意, 如果你现在运行 git status
的话, 就会看到新的 HEAD 和以绿色标出的它和索引之间的区别。
接下来, reset
会用 HEAD 指向的当前快照的内容来更新索引。

如果指定 --mixed
选项, reset
将会在这时停止。 这也是默认行为, 所以如果没有指定任何选项(在本例中只是 git reset HEAD~
), 这就是命令将会停止的地方。
现在再看一眼上图, 理解一下发生的事情:它依然会撤销一上次 提交
, 但还会 取消暂存 所有的东西。 于是, 我们回滚到了所有 git add
和 git commit
的命令执行之前。
第 3 步:重置工作目录(–hard)
reset
要做的的第三件事情就是让工作目录看起来像索引。 如果使用 --hard
选项, 它将会继续这一步。

现在让我们回想一下刚才发生的事情。 你撤销了最后的提交、git add
和 git commit
命令 以及 工作目录中的所有工作。
必须注意, --hard
标记是 reset
命令唯一的危险用法, 它也是 Git 会真正地销毁数据的仅有的几个操作之一。 其他任何形式的 reset
调用都可以轻松撤消, 但是 --hard
选项不能, 因为它强制覆盖了工作目录中的文件。 在这种特殊情况下, 我们的 Git 数据库中的一个提交内还留有该文件的 v3 版本, 我们可以通过 reflog
来找回它。但是若该文件还未提交, Git 仍会覆盖它从而导致无法恢复。
版本回退流程总结
reset
命令会以特定的顺序重写这三棵树, 在你指定以下选项时停止:
- 移动 HEAD 分支的指向 (若指定了
--soft
, 则到此停止) - 使index看起来像 HEAD (若指定
--mixed
或未指定参数, 则到此停止) - 使工作目录看起来像index (若指定了
--hard
, 则到此停止)
通过路径来重置
前面讲述了 reset
基本形式的行为, 不过你还可以给它提供一个作用路径。 若指定了一个路径, reset
将会跳过第 1 步, 并且将它的作用范围限定为指定的文件或文件集合。 这样做自然有它的道理, 因为 HEAD 只是一个指针, 你无法让它同时指向两个提交中各自的一部分。 不过索引和工作目录 可以部分更新, 所以重置会继续进行第 2、3 步。
现在, 假如我们运行 git reset file.txt
(这其实是 git reset --mixed HEAD file.txt
的简写形式, 因为你既没有指定一个提交的 SHA-1 或分支, 也没有指定 --soft
或 --hard
), 它会:
- 移动 HEAD 分支的指向 (已跳过)
- 让索引看起来像 HEAD (到此处停止)
所以它本质上只是将 file.txt
从 HEAD 复制到索引中。

它还有 取消暂存文件 的实际效果。 如果我们查看该命令的示意图, 然后再想想 git add
所做的事, 就会发现它们正好相反。

我们可以不让 Git 从 HEAD 拉取数据, 而是通过具体指定一个提交来拉取该文件的对应版本。 我们只需运行类似于 git reset eb43bf file.txt
的命令即可。

它其实做了同样的事情, 也就是把工作目录中的文件恢复到 v1 版本, 运行 git add
添加它, 然后再将它恢复到 v3 版本(只是不用真的过一遍这些步骤)。 如果我们现在运行 git commit
, 它就会记录一条“将该文件恢复到 v1 版本”的更改, 尽管我们并未在工作目录中真正地再次拥有它。
版本回退总结
-
git reset
命令用于回退版本, 可以指定回退到某一次提交的版本1
$ git reset [--soft | --mixed | --hard] [HEAD]
-
--soft
参数只改变HEAD的指向即本地仓库中的版本会被回退, 而暂存区和工作区的内容不变
1
2
3$ git reset --soft HEAD~
# 这个命令将HEAD指向当前版本的上一个版本
# 暂存区和工作区的内容不会改变 -
--mixed
参数将改变HEAD和index的指向即本地仓库和暂存区的版本会被回退, 而工作目录的内容不会改变
1
2
3
4
5$ git reset [--mixed] HEAD~
# 该命令会先将HEAD指向上一个版本的快照
# 然后将index的内容回退到HEAD所指向版本的内容
# 本地仓库和暂存区的内容会改变,并且它们的版本一致,工作区的内容不会改变
# --mixed为默认参数, 可以不用指定 -
--hard
参数将改变HEAD和index的指向, 并改变工作区的内容1
2
3
4$ git reset --hard HEAD~
# 这个命令将会回退本地仓库+暂存区+工作区到上一个提交版本
# 注意,对于工作区未提交的修改,使用这个命令会将其覆盖,无法找回
# 已提交的文件可以通过reflog命令找回 -
不指定HEAD回退的版本号和回退次数可以跳过移动HEAD的步骤
1
2
3
4$ git reset file.txt
$ git reset --mixed HEAD file.txt
# 这两个命令是一样的,--mixed是默认参数,HEAD后没有跟版本号
# 相当于只是撤销了一次add,并不改变HEAD的指向 -
^和~
1
2
3
4
5HEAD #表示当前版本
HEAD^ #上一版本
HEAD^^ #上上版本
HEAD^^^ #上上上一版本
...1
2
3
4
5HEAD~0 #当前版本
HEAD~1 #上一版本
HEAD~2 #上两版本
HEAD~3 #上三版本
... -
回到未来版本
1
$ git reset [--soft | --mixed | --hard] 67bfc
-
查看被reset的提交日志
1
$ git reflog