Git学习笔记——3. 撤销操作
现在关注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>]
reset 命令主要关注 HEAD 移动所对应的版本切换或撤销操作:假设我们希望将 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
的指向不变,此时如果使用--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
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
命令会对某个具体的项,使用参考源的快照来重置目的地的对应项(如果参考源没有这一项,而目的地存在,会在目的地将其移除)
关于重置的参考源和目的地:
--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
命令的相关操作。
撤销工作目录的修改
\[ \color{red} \text{index} \Rightarrow \text{Working \,Directory} \]
- 使用
git restore
默认基于 index 重置工作目录,作用范围不可省略1
2git restore filename
git restore .
撤销 index 的修改
\[ \color{red} \text{HEAD} \Rightarrow \text{index} \]
git reset
撤销整个 index 的修改,默认是--mixed
级别,默认基于 HEAD 进行重置1
2
3git reset --mixed HEAD
git reset HEAD
git resetgit restore
加--staged
选项才能基于 HEAD 重置 index,作用范围不可省略1
2git restore --staged filename
git restore --staged .
撤销工作目录和 index 的修改
\[ \color{red} \text{HEAD} \Rightarrow \text{index} \Rightarrow \text{Working \,Directory} \]
git reset
在--hard
级别可以对工作目录和 index 进行重置1
git reset --hard HEAD
git restore
加两个选项才能基于 HEAD 重置 index 和工作目录,作用范围不可省略1
2git restore --staged --worktree filename
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
的效果。