Git学习笔记——3. 撤销操作
现在关注Git的常见撤销操作。
撤销操作在某些情形下是危险的,这指的不是 Git 数据库中的内容丢失(这几乎很难办到),而是最新的修改可能丢失:
- 如果修改被正式提交了,那么即使回滚了版本,也可以通过哈希值或历史记录来恢复
- 如果使用
git add添加到 index,那么即使撤回了,也可以通过哈希值或历史记录来恢复 - 如果最新的修改没有添加到 index,那么这些本地修改确实有可能直接被 Git 的某个切换操作或撤销操作覆盖,导致修改的丢失
Git 撤销选项
reset
git reset 是一个非常重要的重置操作,原型如下
1 | git reset [-q] [<tree-ish>] [--] <pathspec>… |
reset 命令主要关注 HEAD 移动所对应的版本切换或撤销操作:假设我们希望将 HEAD 指向的分支(例如 main)移动到其他结点(例如回滚到前一个结点,记作 HEAD^ ),
那么除了修改分支的指向,还需要考虑 index 和工作目录是否随之改变,可以进行从低到高三个级别的操作:
--soft:不影响 index 和工作目录,只是 HEAD 指向分支的移动
1 | git reset --soft HEAD^ |
--mixed(默认)移动分支之后,重置 index,但是不影响工作目录
1 | git reset HEAD^ |
--hard移动分支之后,重置 index 和工作目录,这很危险,因为可能丢失工作目录中的数据
1 | git reset --hard HEAD^ |
如果我们让 HEAD 的指向不变,此时如果使用--soft是没有任何效果的,没有移动分支,但是如果使用其它两个选项,就相当于利用它进行一次撤销操作:
- 使用
--mixed选项,使用 HEAD 指向的提交快照来重置 index,这相当于撤销了所有的git add操作(HEAD $\Rightarrow$ index)
1 | git reset HEAD |
- 利用
-hard选项,使用 HEAD 指向的提交快照来重置 index 和工作目录,这相当于完全回到上一次正式提交的状态(HEAD $\Rightarrow$ index $\Rightarrow$ Working Directory)
1 | git reset --hard HEAD |
注意:
git reset的命令都是从 HEAD 出发的,因此无法做到基于 index 重置工作目录的效果。
上述操作都是针对仓库整体,如果限制在具体的目录或文件上,那么并不支持这么灵活的控制,实际上只能基于某个快照重置 index,例如:
- 用指定的提交版本(例如前一个版本 HEAD^ )的快照重置 index 中的对应文件,但是不影响工作目录的内容,效果相当于撤销了当前版本对文件的修改
1 | git reset HEAD^ filename |
- 用 HEAD 重置 index 中的对应文件,效果相当于撤销了
git add filename
1 | git reset HEAD filename |
git reset在全局的操作中更常见,局部的撤销行为更建议使用下面的git restore命令。
restore
git restore是另一个撤销操作,原型如下
1 | git restore [<options>] [--source=<tree>] [--staged] [--worktree] [--] <pathspec>… |
git restore 在逻辑上比 git reset 更清晰,使用更加友好:
- 它不支持 HEAD 指向分支的移动,只是专门用于撤销操作;
- 没有默认的范围,必须指定具体的文件或路径(当然,使用
.也相当于对所有项进行操作)。
git restore 命令会对某个具体的项,使用参考源的快照来重置目的地的对应项(如果参考源没有这一项,而目的地存在,会在目的地将其移除)
关于重置的参考源和目的地:
--worktree:(默认)index $\Rightarrow$ Working Directory;--staged:HEAD $\Rightarrow$ index;--staged和--worktree:HEAD $\Rightarrow$ index $\Rightarrow$ Working Directory。
除了指定重置的参考源和目的地,还必须限制作用到具体文件上,限制范围不能省略。
例如下面的命令用 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 重置工作目录的这个文件(index $\Rightarrow$ Working Directory)
1 | git checkout -- filename |
- 如果指定 HEAD,则会用 HEAD 的状态重置 index 和工作目录的这个文件(HEAD $\Rightarrow$ index $\Rightarrow$ Working Directory)
1 | git checkout HEAD -- filename |
注:
--在 Git 中通常用于区分选项和参数的消歧义,在其后的项不会被理解为选项,而是视作文件名或路径名称,这里如果缺少--,filename 会被理解为分支名称,并尝试进行分支切换- 这里撤回的参考源默认是 HEAD 指向的状态或 index,也可以是其他的正式提交结点
- 如果参考源没有指定的项,
git checkout可能会报错(这一点与git restore不同,后者会在目的地删除这一项)
Git 撤销操作
前文介绍了几个撤销相关的命令的各种操作,现在从需求的角度记录一下应该如何使用这些命令,这里省略了过时的 git checkout 命令的相关操作。
撤销工作目录的修改
$$
\text{index} \Rightarrow \text{Working Directory}
$$
- 使用
git restore默认基于 index 重置工作目录,作用范围不可省略
1 | git restore filename |
撤销 index 的修改
$$
\text{HEAD} \Rightarrow \text{index}
$$
git reset撤销整个 index 的修改,默认是--mixed级别,默认基于 HEAD 进行重置
1 | git reset --mixed HEAD |
git restore加--staged选项才能基于 HEAD 重置 index,作用范围不可省略
1 | git restore --staged filename |
撤销工作目录和 index 的修改
$$
\text{HEAD} \Rightarrow \text{index} \Rightarrow \text{Working Directory}
$$
git reset在--hard级别可以对工作目录和 index 进行重置
1 | git reset --hard HEAD |
git restore加两个选项才能基于 HEAD 重置 index 和工作目录,作用范围不可省略
1 | git restore --staged --worktree filename |
修正上一次提交
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 的效果。
除此之外,还有一个
git revert命令,它也可以达到提交回退的目的,但是做法和这里不同,git revert会生成一个新的提交,在其中实质性地撤销修改。
