撤销操作
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 的最简方式,就是将它看做该分支上的最后一次提交的快照。
其实,查看快照的样子很容易。下例就显示了 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-file
和 ls-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 reset
和 git 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 -> 工作树 |
--staged | HEAD -> 暂存区 | tree -> 暂存区 |
--worktree | 暂存区 -> 工作树 | tree -> 工作树 |
--staged --worktree | HEAD -> 工作树&暂存区 | tree -> 工作树&暂存区 |