git reset,git checkout,和git revert命令是Git工具箱中最有用的几个工具之一。他们都用来撤销仓库中的某种修改,其中前两个命令可以用来撤销针对提交或者单个文件的修改。
因为如此相似,在特定开发场景下很容易出现不知道该使用那个命令的情况。在本文中我们会比较git reset,git checkout和git revert命令最常见的使用方式。希望在本文结束时读者能够在自己的项目中胸有成竹地使用对应的命令。
为了深入理解,我们需要考虑每个命令在Git仓库的三种状态管理机制中所产生的不同效果:工作目录,暂存快照,和提交历史。有时候这些状态被称为Git的三棵树。在阅读本文过程中请谨记这三种不同的工作机制。
checkout操作会将当前HEAD指针指向指定的提交。请参见下图:
上图展示了main分支中一系列的提交。此时的HEAD指针和main分支的当前指针都指向提交d。接下来执行git checkout b
这个操作会影响到“提交历史”树。git checkout命令可以用于提交,甚至在文件层级上执行。对于文件进行checkout操作会改变该文件的内容到某一次指定提交。
revert操作撤销指定提交并创建一个新的提交,其内容为指定提交的所有逆向修改。git revert只能运行在提交层面,不能对指定文件操作。
reset操作接受一次commit作为参数,并将git的三棵树状态重置到指定的这次commit的相同状态。reset操作可以在三棵树的不同状态下执行。
checkout和reset通常用于本地或者私有分支的撤销操作。修改之后的提交历史,在推送到共享的远程仓库时会引发冲突。反之revert操作的“公共撤销”通常被认为是安全的。因为revert操作会为撤销动作创建一次提交,而这个撤销历史也会被其他人得到,并且revert操作也不会覆盖团队其他成员可能依赖的提交历史。
Git Reset vs Revert vs Checkout
下表总结了这些命令的常用场景。
命令 | 影响范围 | 常见实用场景 |
git reset | 提交 | 丢弃私有分支或者未提交的变更 |
git reset | 文件 | 反暂存一个文件 |
git checkout | 提交 | 切换分支或者查看历史快照 |
git checkout | 文件 | 丢弃工作目录下的变更 |
git revert | 提交 | 撤销公共分支的提交 |
git revert | 文件 | (N/A) |
关于提交的操作
向git reset和git checkout命令传递的参数决定了其影响范围。使用命令时不含文件路径则会让操作作用于整个提交。接下来这部分我们会主要讨论相关内容。注意git revert没有文件层面的操作。
重置指定提交
在提交层面,重置可以移动分支顶端到其他提交。基于这一特性,重置可以用于删除分支中的提交。比如,下面的命令将hotfix分支的顶端向前移动了两个提交。
git checkout hotfix git reset HEAD~2
hotfix分支的最后两个提交现在称为孤立的提交。这意味着下次Git执行垃圾回收时会删除他们。换句话说,如此操作意味着你要丢弃这些提交。这一过程可以通过下图表示:
如此使用git reset撤销那些还未与他人共享过的变更相当简便。如果你开始开发一个功能做了几次提交之后,突然发现“卧槽,我在干嘛?从头来吧。”的时候可以直接使用这个命令。
除了移动当前分支以外,你还可以传递以下选项,用git reset来变更暂存快照或者工作目录:
- --soft – The staged snapshot and working directory are not altered in any way.
- --soft – 暂存快照和工作目录不会改变
- --mixed – The staged snapshot is updated to match the specified commit, but the working directory is not affected. This is the default option.
- --mixed – 暂存快照更新为指定提交,但是工作目录不受影响。这是默认选项。
- --hard – The staged snapshot and the working directory are both updated to match the specified commit.
- --hard – 暂存快照和工作目录都被更新为指定提交。
checkout旧提交
git checkout命令用于更新仓库状态到指定的项目提交历史。当传递的参数是一个分支名称,则用于切换分支。
git checkout hotfix
在命令内部,以上所有命令都是移动HEAD指针到不同分支,并相应的更新工作目录。由于这一操作具有潜在的覆盖本地变更的可能性,因此Git会强制你在checkout操作之前执行commit或者stash命令,以便存储可能由于checkout丢失的变更。与git reset不同,git checkout不会移动分支本身的指针。
你也可以通过传递提交引用作为参数,checkout出指定提交而不是分支。其内部执行方式与checkout分支一摸一样:移动HEAD指针到指定提交。举个例子,下面的命令会checkout出当前提交的祖父节点。
git checkout HEAD~2
这一操作经常用于查看某一个旧版本的项目快照。然而由于当前HEAD指针并不指向任何分支,这一操作会让你处于游离HEAD状态。由于在这个状态下提交新的更新之后,当切换为其他分支之后无法在回到新的提交,所以在游离状态下新建提交是危险的。基于这个原因,如果希望在游离状态下做新的提交,应该先基于此提交创建新的分支。
使用revert撤销公共提交
revert命令通过新建一个提交来撤销之前的一个提交。因为这一操作不会重写提交历史所以被认为是一种安全的撤销操作。比如下面的例子中,Git会搞清楚倒数第二次提交的内容,然后创建一个新的提交用于撤销这些内容,并且将新提交的撤销动作提交到当前的项目中。
git checkout hotfix git revert HEAD~2
此过程图示如下:
与git reset相反,git revert没有改变已有提交历史。基于此,git revert应该被用于撤销公共分支上的变更,而git reset应该被限制于撤销私有分支的变更。
你也可以理解为git revert用于撤销已提交的变更,git reset用于撤销未提交的变更。
与git checkout一样,git revert操作也会导致潜在的文件覆盖,所以Git也会要求在revert之前先进行commit或者stash操作。
关于文件的操作
git reset和git checkout命令也接受文件路径作为可选参数。这也让其行为与上面所介绍的功能完全不一样。相比于操作整个快照,附加的文件路径参数限制相应操作的影响范围到单个文件。
Git-reset指定文件
当附加了文件路径作为参数时,git reset会根据指定提交更新暂存快照。比如下面的命令会获取foo.py文件在倒数第二次提交时的快照,根据快照内容变更文件,并暂存它,等待下一次提交:
git reset HEAD~2 foo.py
相比于针对提交的git reset命令,上面的命令更多地用于HEAD。执行git reset HEAD foo.py会取消foo.py的暂存,但其中的改变仍然在工作目录中。
--soft,--mixed和--hard选项在文件层面的git reset操作没有任何作用,因为暂存快照总是最新的,并且工作目录总是不更新的。
Git checkout 文件
checkout一个文件与使用git reset命令传递文件路径类似,除了checkout更新的是工作目录,而不是更新暂存快照。另外,与执行checkout命令关于提交的操作不同,checkout一个提交会改变HEAD的指向,而checkout文件不改变HEAD,仅改变文件内容。也就意味着执行这个命令不会切换分支。
比如下面的命令更新工作目录中的foo.py文件,将其内容同步为倒数第二次提交时的样子。
git checkout HEAD~2 foo.py
就像操作提交时使用git checkout,这也可以用于查看项目的旧版本——不过这次是查看指定文件的旧版本。
如果你暂存并提交了checkout出来的旧版本文件,其执行结果也含有将指定文件revert到旧版本的效果。不过请注意这一操作也同时移除了该文件从旧版本之后的所有后续变更历史,然而revert命令仅撤销指定提交的变更。
就像git reset,这种情况也通常与HEAD搭配使用。比如,git checkout HEAD foo.py执行的结果就含有丢弃foo.py文件未暂存的变更的效果。这个行为类似于git reset HEAD --hard,只不过影响范围仅限于指定文件。
总结
到现在为止,你应该已经拥有用于在Git仓库中撤销变更所需的所有知识了。git reset,git checkout,和git revert命令容易混淆,但是当你考虑到他们分别在工作目录,暂存快照和提交历史上的可能产生的影响,就不难在开发中分辨出应该使用哪个命令。
原文地址:https://www.atlassian.com/git/tutorials/resetting-checking-out-and-reverting