Git学习笔记——2. 撤销操作
现在关注Git的常见撤销操作。 撤销操作在某些情形下是危险的,这指的不是 Git 数据库中的内容丢失(这几乎很难办到),而是最新的修改可能丢失:
- 如果修改被正式提交了,那么即使回滚了版本,也可以通过哈希值或历史记录来恢复
- 如果使用
git add
添加到 index,那么即使撤回了,也可以通过哈希值或历史记录来恢复 - 如果最新的修改没有添加到 index,那么这些本地修改确实有可能直接被 Git 的某个切换操作或撤销操作覆盖,导致修改的丢失
Git 撤销原理
reset
git reset
是一个非常重要的重置操作,原型如下
1
2
3
4git reset [-q] [<tree-ish>] [--] <pathspec>…
git reset [-q] [--pathspec-from-file=<file> [--pathspec-file-nul]] [<tree-ish>]
git reset (--patch | -p) [<tree-ish>] [--] [<pathspec>…]
git reset [--soft | --mixed [-N] | --hard | --merge | --keep] [-q] [<commit>]
首先介绍第一类用法,可以用指定的提交版本(例如前一个版本
HEAD^
)的快照刷新 index
中的对应文件,但是不影响工作目录的内容 1
git reset HEAD^ filename
如果选择HEAD,则相当于用HEAD刷新index中的对应文件,效果相当于撤销了git add filename
1
git reset HEAD filename
然后介绍第二类用法,现在是一个整体性的版本切换或撤销操作:假设我们希望将 HEAD 指向的分支(例如 main)移动到其他结点(例如回滚到前一个结点,记作 HEAD^ ), 那么除了修改分支的指向,还需要考虑index和工作目录是否随之改变,可以进行从低到高三个级别的操作:
--soft
:不影响 index 和工作目录,只是 HEAD 指向分支的移动1
git reset --soft HEAD^
--mixed
移动分支之后,刷新 index,但是不影响工作目录(默认操作)1
2git reset HEAD^
git reset --mixed HEAD^--hard
移动分支之后,刷新 index 和工作目录,很危险,会丢失工作目录的所有数据1
git reset --hard HEAD^
第二类用法有一个特别的用途:我们可以不移动HEAD指向的分支,仍然指向HEAD,只是利用它进行一次撤销操作
使用
--mixed
选项,使用 HEAD 指向的提交快照来刷新 index,这相当于撤销了所有的git add
操作1
git reset HEAD
利用
-hard
选项,使用 HEAD 指向的提交快照来刷新 index 和工作目录,这相当于完全回到上一次正式提交的状态1
git reset --hard HEAD
(此时如果使用--soft
是没有任何效果的,没有移动分支)
restore
git restore
是另一个撤销操作,原型如下 1
2
3git restore [<options>] [--source=<tree>] [--staged] [--worktree] [--] <pathspec>…
git restore [<options>] [--source=<tree>] [--staged] [--worktree] --pathspec-from-file=<file> [--pathspec-file-nul]
git restore (-p|--patch) [<options>] [--source=<tree>] [--staged] [--worktree] [--] [<pathspec>…]
git restore
在逻辑上比git reset
更清晰:它不支持HEAD指向分支的移动,只是专门用于撤销操作,并且默认不是全局的,必须指定具体的文件或路径(当然用.
也相当于对所有项进行操作)。
git restore
命令会对某个具体的项,使用参考源的快照来刷新目的地的对应项(如果参考源没有这一项,而目的地存在,则会在目的地移除它)
关于刷新的参考源:
- 在不使用
--staged
选项时,参考源为 index - 在使用
--staged
选项时,基于当前 HEAD 指向的提交结点(因为这个选项意味着 index 也要被刷新) - 也可以使用
--source=...
指定其他版本。
关于刷新的目的地:(通常为index或工作目录)
- 默认情形只刷新工作目录,相当于使用
--worktree
- 如果使用
--staged
选项,只刷新 index - 如果使用
--staged
和--worktree
,则刷新 index 和工作目录
除了指定刷新的参考源和目的地,还必须限制作用到具体文件上,而不是全部,例如下面的命令用
HEAD 刷新 index 的指定文件,相当于撤回了 git add filename
1
git restore --staged filename
git restore
其实是从git checkout
剥离出来的新命令,由于原本的git checkout
过于复杂,在逻辑上明显包括了分支切换和撤销两个部分,在较新的Git版本中,这两个命令被单独拆分给了两个新命令:git switch
和git restore
。
简单记录一下git checkout
在撤销时的语法:如果
git checkout
命令没有加上分支名称,会被视为一种撤销操作,此时HEAD指向的分支不发生变化,但是可能将
index 或工作目录的某个文件撤回到某个正式提交的状态,例如
对单个文件,撤销在工作目录中的修改,用 index 刷新工作目录的这个文件
1
git checkout -- filename
如果指定 HEAD,则会用 HEAD 的状态刷新index和工作目录的这个文件
1
git checkout HEAD -- filename
注:
--
在 Git 中通常用于区分选项和参数的消歧义,在其后的项不会被理解为选项,而是视作文件名或路径名称,这里如果缺少--
,filename 会被理解为分支名称,并尝试进行分支切换- 这里撤回的参考源默认是HEAD指向的状态或index,也可以是其他的正式提交结点
- 如果参考源没有指定的项,
git checkout
可能会报错(这一点与git restore
不同,后者会在目的地删除这一项)
Git 撤销操作
前文介绍了几个撤销相关的命令的原理,现在从需求的角度记录一下应该如何使用这些命令。
撤销单个文件
撤销工作目录的单个文件(基于index)
基于
git restore
实现1
git restore filename
(过时)基于
git checkout
当前分支实现,必须加上--
,这个操作是老版本的建议,新版本建议是使用git restore
1
git checkout -- filename
撤销index的单个文件(基于HEAD),但不改动工作目录
基于
git restore
实现,这里的--staged
是必要的1
git restore --staged filename
基于
git reset HEAD
实现1
2git reset HEAD filename
git reset filename
撤销工作目录和index的单个文件(基于HEAD)
基于
git restore
实现,这里的--staged
和--worktree
是必要的1
git restore --staged --worktree filename
(过时)基于
git checkout
实现1
git checkout HEAD -- filename
撤销所有文件
撤销工作目录的所有文件(基于index):
- 使用
git restore
默认基于 index 刷新工作目录,但是需要加范围.
代表所有文件1
git restore .
撤销index的所有文件(基于HEAD)
git reset
不添加文件名,撤销整个 index 的修改,默认是--mixed
级别,默认基于 HEAD 进行刷新1
2
3git reset --mixed HEAD
git reset HEAD
git resetgit restore
加额外选项才能基于HEAD刷新 index,需要加范围.
代表所有文件1
git restore --staged .
撤销工作目录和index的所有文件(基于HEAD)
git reset
在--hard
级别可以对工作目录和 index 的刷新1
git reset --hard HEAD
git restore
加额外选项才能基于HEAD刷新 index 和工作目录,需要加范围.
代表所有文件1
git restore --staged --worktree .
修正上一次提交
git commit
会生成新的提交对象,并移动 HEAD
和分支到最新的提交上,
在进行了一次正式提交之后,如果发现某些内容遗漏,或者提交的 message
需要修改,可以使用 --amend
对HEAD指向的提交进行修正
1 | git commit -m "last commit" |
再次生成的提交会完全替换上一次的提交,上一次的提交不会出现在日志中。(但是仍然在数据库中,处于游离状态)
注意:这个操作只建议在本地进行,如果已经推送到了远程仓库,那么重新添加一个提交比修正更合适,因为会出现合并冲突。
回退上一次提交
如果需要完全舍弃最近的一次提交,可以通过 git reset
实现撤销提交,回到上一次的提交状态
1 | git reset --soft HEAD^ |
由于使用了--soft
选项,这里只是对 HEAD
和分支的移动,并没有用 HEAD^
的内容刷新 index
和工作目录。此时如果重新提交,或者对 index 进行处理后再提交,也可以达到
git commit --amend
的效果。