章节 ▾ 第二版

2.4 Git 基础 - 撤销操作

撤销操作

在任何阶段,你都可能想要撤销某些操作。在这里,我们将回顾一些用于撤销已做修改的基本工具。请务必小心,因为有些撤销操作是不可逆的。这是 Git 中少数几个如果操作不当可能会丢失数据的地方。

一种常见的撤销场景是:提交得太早,可能忘了添加某些文件,或者提交信息写错了。如果你想重做这次提交,可以进行遗漏的修改,暂存它们,然后使用 --amend 选项进行再次提交:

$ git commit --amend

此命令会将暂存区中的内容提交。如果你自上次提交后没有做任何修改(例如,在上一次提交后立即运行此命令),那么快照将完全保持不变,你唯一能改变的就是提交信息。

同样会启动提交信息编辑器,但其中已经包含了上一次提交的信息。你可以像往常一样编辑该信息,但这会覆盖掉上一次的提交。

例如,如果你执行了提交,然后意识到忘记暂存某个你想添加到本次提交中的文件,你可以执行以下操作:

$ git commit -m 'Initial commit'
$ git add forgotten_file
$ git commit --amend

最终你只会得到一次提交 —— 第二次提交会替换掉第一次提交的结果。

注意

理解这一点很重要:当你修改(amend)上一次提交时,并不是在“修复”它,而是将其完全 替换 为一个新的、经过改进的提交,旧的提交会被移除。实际上,就像之前的提交从未发生过一样,它不会出现在你的仓库历史中。

修改提交的显而易见的好处是,可以对上一次提交进行微小的改进,而不会在仓库历史中留下类似“哎呀,忘了添加一个文件”或“糟糕,修改上一次提交的拼写错误”这样的提交信息。

注意

只能修改那些尚在本地且未推送出去的提交。修改已经推送过的提交并强制推送该分支,会给你的协作开发者带来问题。关于当你这样做时会发生什么,以及如果你身处接收端该如何恢复,请阅读 变基的风险

取消暂存已暂存的文件

接下来的两节将演示如何处理暂存区和工作目录中的变更。好消息是,用来确定这两个区域状态的命令,也会提醒你如何撤销对它们的修改。例如,假设你修改了两个文件,想把它们作为两次独立的修改分别提交,但你不小心输入了 git add *,结果把两个文件都暂存了。该如何撤销其中一个文件的暂存呢?git status 命令会提醒你:

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

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

在“Changes to be committed”(要提交的变更)文本下方,它提示可以使用 git reset HEAD <file>…​ 来取消暂存。那么,我们按照这个建议来取消 CONTRIBUTING.md 文件的暂存:

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

    renamed:    README.md -> README

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

    modified:   CONTRIBUTING.md

该命令看起来有点奇怪,但确实有效。现在 CONTRIBUTING.md 文件处于已修改但未暂存的状态。

注意

诚然,git reset 是一个危险的命令,特别是当你提供 --hard 参数时。然而,在上述场景中,工作目录中的文件并未受到影响,所以它是相对安全的。

目前,关于 git reset 命令,你只需要知道这个“魔法指令”即可。我们将在 重置揭秘 中深入探讨 reset 的原理,以及如何通过它掌握真正高级的技巧。

撤销对文件的修改

如果你意识到不想保留对 CONTRIBUTING.md 文件的修改该怎么办?如何轻松地取消修改 —— 即将其还原为上一次提交(或刚克隆时,或者无论你通过什么方式将其放入工作目录时)的样子?幸运的是,git status 也告诉了你如何做。在上一个示例的输出中,未暂存区域看起来是这样的:

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

    modified:   CONTRIBUTING.md

它非常明确地告诉你如何丢弃所做的修改。让我们按照它的指示操作:

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

    renamed:    README.md -> README

可以看到,修改已经被还原了。

重要提示

理解 git checkout -- <file> 是一个危险命令非常重要。你对该文件所做的任何本地修改都会消失 —— Git 只是用最近一次暂存或提交的版本替换了该文件。除非你确定自己确实不需要那些未保存的本地修改,否则永远不要使用这个命令。

如果你想保留所做的修改但又需要暂时将其搁置,我们将在 Git 分支 一章中介绍储藏(stashing)和分支(branching);通常那些方法是更好的选择。

请记住,Git 中任何 已提交 的东西几乎总是可以恢复的。即使是已删除分支上的提交,或者被 --amend 提交覆盖的提交,都是可以恢复的(参见 数据恢复)。然而,任何未提交而丢失的内容,很可能就永远找不回来了。

使用 git restore 撤销操作

Git 2.23.0 版本引入了一个新命令:git restore。它基本上是 git reset(我们刚刚介绍过)的替代方案。从 Git 2.23.0 版本开始,对于许多撤销操作,Git 将使用 git restore 而不是 git reset

让我们重走一遍之前的步骤,这次改用 git restore 而不是 git reset 来撤销操作。

使用 git restore 取消暂存文件

接下来的两节将演示如何使用 git restore 处理暂存区和工作目录中的变更。好消息是,用来确定这两个区域状态的命令,也会提醒你如何撤销对它们的修改。例如,假设你修改了两个文件,想把它们作为两次独立的修改分别提交,但你不小心输入了 git add *,结果把两个文件都暂存了。该如何撤销其中一个文件的暂存呢?git status 命令会提醒你:

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

在“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 撤销对文件的修改

如果你意识到不想保留对 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 restore --staged <file>..." to unstage)
	renamed:    README.md -> README
重要提示

理解 git restore <file> 是一个危险命令非常重要。你对该文件所做的任何本地修改都会消失 —— Git 只是用最近一次暂存或提交的版本替换了该文件。除非你确定自己确实不需要那些未保存的本地修改,否则永远不要使用这个命令。