撤销操作

本节部分内容参考自 Pro Git 的《撤销操作》《重置揭秘》,在原文基础上有一定修改。

git restore

取消暂存

有的时候,我们需要取消一些文件的暂存状态。例如,你已经修改了两个文件,并且想要将它们作为两次独立的修改提交,但是却意外地输入 git add * 暂存了它们两个。如何只取消暂存两个中的一个呢?git status 命令提示了你:

$ git add *
$ git status
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)

    renamed:    README.md -> README
    modified:   CONTRIBUTING.md

在 “Changes to be committed” 文字正下方,提示使用 git restore --staged <file>... 来取消暂存。所以,我们可以这样来取消暂存 CONTRIBUTING.md 文件:

$ git restore --staged CONTRIBUTING.md
$ git status
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)

    renamed:    README.md -> README

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)

    modified:   CONTRIBUTING.md

正如你看到的一样,CONTRIBUTING.md 文件已经是修改未暂存的状态了。

git restore 是在 Git 2.23.0 中引入的新命令。在以前的 Git 版本中,git status 给出的提示命令是 git reset HEAD <file>...。这条命令和上面的命令的效果和用法都是一样的,只不过它更多涉及到一些底层的复杂操作,而 git restore 对这些操作给出了一个易于理解的上层抽象(详见下文)。

撤消对文件的修改

如果你并不想保留对 CONTRIBUTING.md 文件的修改怎么办?你该如何方便地撤消修改——将它还原成上次提交时的样子(或者刚克隆完的样子,或者刚把它放入工作目录时的样子)?幸运的是,git status 也告诉了你应该如何做。在最后一个例子中,未暂存区域是这样:

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)

    modified:   CONTRIBUTING.md

它非常清楚地告诉了你如何撤消之前所做的修改。让我们来按照提示执行:

$ git restore CONTRIBUTING.md
$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    renamed:    README.md -> README

可以看到那些修改已经被撤消了。

请务必记得 git restore <file> 是一个危险的命令。你对那个文件在本地的任何修改都会消失——Git 会用最近提交的版本覆盖掉它。除非你确实清楚不想要对那个文件的本地修改了,否则请不要使用这个命令。

记住,在 Git 中任何已提交的东西几乎总是可以恢复的。甚至那些被删除的分支中的提交或使用 --amend 选项覆盖的提交也可以恢复。然而,任何你未提交的东西丢失后很可能再也找不到了。

同样,在 Git 的旧版本中,完成这一任务的命令是 git checkout -- <file>...。用法和效果和上面也是一样的。

详细用法

点击展开
git restore <file>...

将文件恢复为暂存区的状态。这会覆盖文件当前的内容。

git restore --staged <file>...

将暂存区的文件恢复为上一次提交的状态,不会影响工作树中的文件。

git restore --staged --worktree <file>...

将暂存区和工作树的文件恢复为上一次提交的状态。

git restore -s <tree> <file>...

将文件恢复到 <tree> 所对应的快照的状态。

撤销的原理

快照

理解 git restore 的最简方法,就是以 Git 的思维框架(将其作为内容管理器)来管理三棵不同的「树」。

用途
HEAD上一次提交的快照
Index(索引/暂存区)预期的下一次提交的快照
工作目录沙盒

这里涉及到一个 Git 的概念:快照。快照是对 Git 中工作目录的状态的一次完整记录。Git 所管理的版本历史便是由一个个快照组成的。

必须要澄清一个误区:虽然 Git 似乎无时无刻不在“比较”两个文件的版本,但在 Git 中,保存的并非是文件的差异,而是每一次都保存文件的完整内容。事实上,我们在 Git 中看到的各种比较结果,都真的是在比较两个文件,文件的差异是临时算出来的

你可能会疑惑:如果每一次提交都保存了整个工作区的内容,那么储存库不是会飞速地膨胀吗?其实不用担心。对于两次提交中没有改变的文件,Git 在储存库中只会保存一份内容。所以 Git 的储存方式还是比较精简的。

顺带一提,Git 的这种保存策略,无意中鼓励我们把大文件拆分为多个小文件。大多数情况下,能够进行这种拆分,是程序模块化设计良好的表象。

三棵树

HEAD 是当前分支引用的指针,它总是指向该分支上的最后一次提交。这表示 HEAD 将是下一次提交的父结点。通常,理解 HEAD 的最简方式,就是将它看做该分支上的最后一次提交的快照

其实,查看快照的样子很容易。下例就显示了 HEAD 快照实际的目录列表,以及其中每个文件的 SHA-1 校验和:

$ git cat-file -p HEAD
tree cfda3bf379e4f8dba8717dee55aab78aef7f4daf
author Scott Chacon  1301511835 -0700
committer Scott Chacon  1301511835 -0700

initial commit

$ git ls-tree -r HEAD
100644 blob a906cb2a4a904a152...   README
100644 blob 8f94139338f9404f2...   Rakefile
040000 tree 99f1a6d12cb4b6f19...   lib

Git 的 cat-filels-tree 是底层命令,它们一般用于底层工作,在日常工作中并不使用。不过它们能帮助我们了解到底发生了什么。

索引(暂存区)

索引是你的预期的下一次提交。我们也会将这个概念引用为 Git 的「暂存区」,这就是当你运行 git commit 时 Git 看起来的样子。

Git 将上一次检出到工作目录中的所有文件填充到索引区,它们看起来就像最初被检出时的样子。之后你会将其中一些文件替换为新版本,接着通过 git commit 将它们转换为树来用作新的提交。

$ git ls-files -s
100644 a906cb2a4a904a152e80877d4088654daad0c859 0	README
100644 8f94139338f9404f26296befa88755fc2598c289 0	Rakefile
100644 47c6340d6459e05787f644c2447d2595f5d3a54b 0	lib/simplegit.rb

再说一次,我们在这里又用到了 git ls-files 这个幕后的命令,它会显示出索引当前的样子。

确切来说,索引在技术上并非树结构,它其实是以扁平的清单实现的。不过对我们而言,把它当做树就够了。

工作目录

最后,你就有了自己的工作目录(通常也叫工作区工作树)。另外两棵树以一种高效但并不直观的方式,将它们的内容存储在 .git 文件夹中。工作目录会将它们解包为实际的文件以便编辑。你可以把工作目录当做一个沙盒。在你将修改提交到暂存区并记录到历史之前,可以随意更改。

$ tree
.
├── README
├── Rakefile
└── lib
    └── simplegit.rb

1 directory, 3 files

git restore 的工作

git restore 是一条很新的命令。在它诞生之前,完成对应的工作的命令是 git resetgit checkout。这两个命令的功能很强大,同时原理也很复杂,需要到下一章再详细讲解。git restore 从它们的功能中抽取了一个子集,并建立了一种易于理解的接口。

git restore 所做的工作只有一个:把文件从一棵树复制到另一棵树。比如说,当我们执行 git restore <file>... 时,是把文件从索引树,也就是暂存区,复制到工作目录中。当我们执行 git restore --staged <file>... 时,是把文件从 HEAD 指向的树复制到暂存区中。因为复制的来源总是比目标要旧,表现出来的就是撤销了对文件的修改。

git restore--staged--worktree 参数分别指定了复制的目标是索引树和工作目录。复制的来源一般是索引树,而如果指定了 --staged,则以 HEAD 指向的树为来源。用 -s <tree> 或者 --source=<tree> 可以自行指定来源。

下表展示了 git restore 在不同参数下的复制来源与目标的设定。

默认-s <tree>
默认暂存区 -> 工作树tree -> 工作树
--stagedHEAD -> 暂存区tree -> 暂存区
--worktree暂存区 -> 工作树tree -> 工作树
--staged --worktreeHEAD -> 工作树&暂存区tree -> 工作树&暂存区