Git 笔记系列(六)—— Git常用命令-Reset
时间 更新备注
2018-03-02 新建文章
2018-06-10 添加和revert&checkout的对比
2019-01-18 更新链接

引言

在提交层面上,reset将一个分支的末端指向另一个提交。这可以用来移除当前分支的一些提交。比如,下面这两条命令让 hotfix 分支向后回退了两个提交。

1
2
git checkout hotfix
git reset HEAD~2

hotfix 分支末端的两个提交现在变成了悬挂提交。也就是说,下次 Git 执行垃圾回收的时候,这两个提交会被删除。换句话说,如果你想扔掉这两个提交,你可以这么做。reset 操作如下图所示:

如果你仔细研究reset命令本身就知道,它本身做的事情就是重置HEAD(当前分支的版本顶端)到另外一个commit

目录

Reset解惑

让我们跟着 reset 看看它都做了什么。它以一种简单可预见的方式直接操纵这三棵树。它做了三个基本操作。 第 1 步:移动 HEAD

reset 做的第一件事是移动 HEAD 的指向。这与改变 HEAD 自身不同(checkout 所做的); reset 移动 HEAD 指向的分支。这意味着如果 HEAD 设置为 master 分支(例如,你正在 master 分支上),运行 git reset 9e5e64a将会使master指向9e5e64a。

reset补充

总的来说,git reset命令是用来将当前branch重置到另外一个commit的,而这个动作可能会将index以及work tree同样影响。比如如果你的master branch(当前checked out)是下面这个样子:

1
- A - B - C (HEAD, master)

HEAD和master branch是在一起的,而你希望将master指向到B,而不是C,那么你执行

git reset B以便移动master branch到B那个commit:

1
- A - B (HEAD, master)      # - C is still here, but there's no branch pointing to it anymore

注意:git reset和checkout是不一样的。如果你运行git checkout B,那么你讲得到:

1
- A - B (HEAD) - C (master)

这时HEAD和master branch就不在一个点上了,你进入detached HEAD State. HEAD,work tree,index都指向了B,但是master branch却依然指向C。如果在这个点上,你执行一个新的commit D,那么你讲得到下面(当然这可能并不是你想要的,你可能想要的是创一个branch做bug fix):

1
2
3
- A - B - C (master)
\
D (HEAD)

记住git reset不会产生commits,它仅仅更新一个branch(branch本身就是一个指向一个commit的指针)指向另外一个commit(Head和branch Tip同时移动保持一致).其他的仅剩对于index和work tree(working directory)有什么影响。git checkout xxxCommit则只影响HEAD,如果xxxCommit和一个branch tip是一致的话,则HEAD和branch相匹配,如果xxxCommit并不和任何branch tip相一致,则git进入detached HEAD 状态。

reset用法

重置命令(git reset)是Git最常用的命令之一,也是最危险,最容易误用的命令。来看看git reset命令的用法。

用法一:git reset [-q] [<commit>] [--] <paths>...

用法二:git reset [--soft | --mixed | --hard | --merge | --keep] [-q] [<commit>]

上面列出了两个用法,其中<commit>都是可选项,可以使用引用或者提交ID,如果省略 <commit>则相当于使用了HEAD的指向作为提交ID
上面列出的两种用法的区别在于,第一种用法在命令中包含路径<paths>。为了避免路径和引用(或者提交ID)同名而冲突,可以在<paths>前用两个连续的短线(减号)作为分隔。

第一种用法(包含了路径<paths>的用法)不会重置引用,更不会改变工作区,而是用指定提交状态(<commit>)下的文件(<paths>)替换掉暂存区中的文件。例如命令git reset HEAD <paths>相当于取消之前执行的git add <paths>命令时改变的暂存区。

第二种用法(不使用路径的用法)则会重置引用。根据不同的选项,可以对暂存区或者工作区进行重置。参照下面的版本库模型图,来看一看不同的参数对第二种重置语法的影响。

reset注意

当你传入HEAD 以外的其他提交的时候要格小心,因为 reset 操作会重写当前分支的历史。正如 rebase 黄金法则所说的,在公共分支上这样做可能会引起严重的后果

当执行 “git reset HEAD” 命令时,暂存区的目录树会被重写,被 master 分支指向的目录树所替换,但是工作区不受影响。

reset Parameters

  1. soft

reset 做的第一件事是移动 HEAD 的指向。
–soft参数告诉Git重置HEAD到另外一个commit,但也到此为止。如果你指定–soft参数,Git将停止在那里而什么也不会根本变化。这意味着index,working copy都不会做任何变化,所有的在original HEAD和你重置到的那个commit之间的所有变更集都放在stage(index)区域中。

  1. mixed(default默认选项)

接下来,reset 会用 HEAD 指向的当前快照的内容来更新索引。
–mixed是reset的默认参数,也就是当你不指定任何参数时的参数。它将重置HEAD到另外一个commit,并且重置index以便和HEAD相匹配,但是也到此为止。working copy不会被更改。所有该branch上从original HEAD(commit)到你重置到的那个commit之间的所有变更将作为local modifications保存在working area中,(被标示为local modification or untracked via git status),但是并未staged的状态,你可以重新检视然后再做修改和commit

  1. hard

reset 要做的的第三件事情就是让工作目录看起来像索引。如果使用 –hard 选项,它将会继续这一步。

Git reflog

–hard参数将会blow out everything.它将重置HEAD返回到另外一个commit(取决于~12的参数),重置index以便反映HEAD的变化,并且重置working copy也使得其完全匹配起来。这是一个比较危险的动作,具有破坏性,数据因此可能会丢失!如果真是发生了数据丢失又希望找回来,那么只有使用:git reflog命令了。makes everything match the commit you have reset to.你的所有本地修改将丢失。如果我们希望彻底丢掉本地修改但是又不希望更改branch所指向的commit,则执行git reset --hard = git reset --hard HEAD. i.e. don’t change the branch but get rid of all local changes.另外一个场景是简单地移动branch从一个到另一个commit而保持index/work区域同步。这将确实令你丢失你的工作,因为它将修改你的work tree!

限定reset重置范围

前面讲述了 reset 基本形式的行为,不过你还可以给它提供一个作用路径。若指定了一个路径,reset 将会跳 过第 1 步,并且将它的作用范围限定为指定的文件或文件集合。这样做自然有它的道理,因为 HEAD 只是一个指 针,你无法让它同时指向两个提交中各自的一部分。不过索引和工作目录 可以部分更新,所以重置会继续进行 第2、3步。

现在,假如我们运行git reset file.txt(这其实是git reset --mixed HEAD file.txt的简写形 式,因为你既没有指定一个提交的 SHA-1 或分支,也没有指定 –soft 或 –hard),它会:

  1. 移动 HEAD 分支的指向 (已跳过)
  2. 让索引看起来像 HEAD (到此处停止)

所以它本质上只是将 file.txt 从 HEAD 复制到索引中。

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

压缩提交

我们来看看如何利用这种新的功能来做一些有趣的事情 - 压缩提交。

假设你的一系列提交信息中有 “oops.”、“WIP” 和 “forgot this file”,聪明的你就能使用 reset 来轻松快 速地将它们压缩成单个提交,也显出你的聪明。(压缩提交 展示了另一种方式,不过在本例中用 reset 更简 单。)

假设你有一个项目,第一次提交中有一个文件,第二次提交增加了一个新的文件并修改了第一个文件,第三次提

交再次修改了第一个文件。由于第二次提交是一个未完成的工作,因此你想要压缩它。

那么可以运行git reset --soft HEAD~2来将HEAD分支移动到一个旧一点的提交上(即你想要保留的第 一个提交):

注意,这时候因为是在上次提交的后,的Index暂存区和Working Directory是保持一致的,所以可以直接提交。

然后只需再次运行git commit:

现在你可以查看可到达的历史,即将会推送的历史,现在看起来有个 v1 版 file-a.txt 的提交,接着第二个提 交将 file-a.txt 修改成了 v3 版并增加了 file-b.txt。包含 v2 版本的文件已经不在历史中了。

reset和checkout

checkout这个命令做的不过是将HEAD移到一个新的分支,然后更新工作目录。因为这可能会覆盖本地的修改,Git 强制你提交或者缓存工作目录中的所有更改,不然在 checkout 的时候这些更改都会丢失。和 git reset 不一样的是,git checkout 没有移动这些分支。这对于快速查看项目旧版本来说非常有用。

checkout对工作目录是安全的,它会通过检查来确保不会将已更改的文件吹走。

  • checkout不会去修改你在Working Directory里修改过的文件
  • checkout则把HEAD移动到另一个分支
  • reset会不做检查把working directory里的所有内容都更新掉
  • reset把branch移动到HEAD指向的地方

reset和revert

  • git revert可以用在公共分支上,git reset应该用在私有分支上.

git revert用于记录一些新的提交以反转一些早期提交的影响(通常只是一个错误的提交)。如果你想扔掉工作目录中所有未提交的更改,你应该看到git-reset,特别是–hard选项。如果你想提取特定文件,就像在另一个提交中那样,你应该看到git-checkout,特别是git checkout <commit> -- <filename>语法。请谨慎使用这些替代方法,因为它们都会丢弃工作目录中的未提交更改。

reset是用来修改提交历史的,想象这种情况,如果你在2天前提交了一个东西,突然发现这次提交是有问题的。

这个时候你有两个选择,要么使用git revert(推荐),要么使用git reset。

上图可以看到git reset是会修改版本历史的,他会丢弃掉一些版本历史。

git revert是根据那个commit逆向生成一个新的commit,版本历史是不会被破坏的。
相比git reset,它不会改变现在的提交历史。因此,git revert可以用在公共分支上,git reset应该用在私有分支上

你也可以把git revert当作撤销已经提交的更改,而git reset HEAD用来撤销没有提交的更改。

就像git checkout 一样,git revert 也有可能会重写文件。所以,Git会在你执行revert之前要求你提交或者缓存你工作目录中的更改。

如果你的更改还没有共享给别人,git reset 是撤销这些更改的简单方法。当你开发一个功能的时候发现「糟糕,我做了什么?我应该重新来过!」时,reset 就像是 go-to 命令一样。

除了在当前分支上操作,你还可以通过传入这些标记来修改你的缓存区或工作目录:

  • –soft – 缓存区和工作目录都不会被改变
  • –mixed – 默认选项。缓存区和你指定的提交同步,但工作目录不受影响
  • –hard – 缓存区和工作目录都同步到你指定的提交

把这些标记想成定义 git reset 操作的作用域就容易理解多了

相比 git reset,它不会改变现在的提交历史。因此,git revert 可以用在公共分支上,git reset 应该用在私有分支上。

总结

简单总结一下,其实就是–soft 、–mixed以及–hard是指代三个不同的恢复等级。使用–soft就仅仅将Head头指针恢复,已经add的缓存以及工作空间的所有东西都不变。如果使用–mixed,就将Head头指针恢复掉,已经add的缓存也会丢失掉,工作空间的代码什么的是不变的。如果使用–hard,那么一切就全都恢复了,Head头指针变,add的缓存消失,本地工作区的代码的也恢复到指定之前版本的状态。

命令 作用域 常用情景
git reset 提交层面 在私有分支上舍弃一些没有提交的更改
git reset 文件层面 将文件从缓存区中移除
git checkout 提交层面 切换分支或查看旧版本
git checkout 文件层面 舍弃工作目录中的更改
git revert 提交层面 在公共分支上回滚更改
git revert 文件层面 (然而并没有)
1
2
3
4
5
6
7
8
9
10
                         head    index   work dir  wd safe
Commit Level
reset --soft [commit] REF NO NO YES
reset [commit] REF YES NO YES
reset --hard [commit] REF YES YES NO
checkout [commit] HEAD YES YES YES

File Level
reset (commit) [file] NO YES NO YES
checkout (commit) [file] NO YES YES NO

参考

  1. git reset soft,hard,mixed之区别深解
  2. Git - git-reset Documentation
  3. 代码回滚:git reset、git checkout和git revert区别和联系 - houpy - 博客园
  4. Pro Git(中文版)

Git Revert

  1. Git Revert | Atlassian Git Tutorial
  2. Learn how to undo changes in Git using Bitbucket Cloud
  3. 5.2 代码回滚:Reset、Checkout、Revert 的选择 · geeeeeeeeek/git-recipes Wiki
文章作者: MichaelMao
文章链接: http://frizzlefur.com/2018/03/02/Git 笔记系列(六)—— Git常用命令-Reset/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 MMao
我要吐槽下