Ran Wei/Git 系列
English
Git 与版本控制 — Ran Wei

查看历史与撤销

阅读项目的历史,准确查看改动内容,并在各个阶段撤销错误:暂存之前、暂存之后、最近一次提交中以及更早的提交中。

3 小时3 个时段6 个动手环节Git 2.50.1 (Apple Git-155)

完成后你能够

  • 用 git log 列出并筛选历史。
  • 读懂差异(diff),并使用 git diff、git diff --staged 和 git show。
  • 用 git restore 丢弃未提交的改动,以及取消暂存文件。
  • 用 git commit --amend 修改最近一次提交,用 git revert 撤销更早的提交。
  • 安全地查看旧版本,并解释什么是分离的 HEAD。

开始之前

第 2 模块:创建仓库、用 git add 暂存,以及写清楚说明进行提交。

目录

学习计划

3 小时

时间为估计值,包含输入命令的时间。进度保存在当前浏览器,中英文版本共享。

时段 160 分钟
回顾历史
时段 260 分钟
撤销提交
时段 360 分钟
练习
1

阅读历史

你做的每一次提交都会被保留下来,连同作者、日期和你写的说明。git log 就是阅读这段历史的方式。不带选项时,它按从新到旧的顺序列出提交,显示每次提交的完整哈希、作者、日期和说明。历史一长,文字就很多,下面几个选项会有帮助:

  • --oneline 每次提交只打印一行:短哈希和说明。
  • -n 2 只显示最新的两次提交。任何数字都可以;-n 1 只显示最新的一次。
  • --stat 额外显示每次提交改动了哪些文件,以及改动了多少。
  • 在末尾加上文件名,例如 git log pancakes.md,只列出改动过这个文件的提交。

选项可以自由组合:git log --oneline -n 5 以每次一行的形式显示最新的五次提交。

本页的每个动手环节都从第 2 模块主示例结束时的食谱仓库开始,其中有三次提交。如果你还提交过 .gitignore 或做了练习,你自己的仓库可能多几次提交;而且你的哈希会不同,因为 Git 计算哈希时除了内容,还会用到每次提交的时间。请始终使用你自己的 git log 打印出的哈希。随着你学习本模块,你的仓库也会不断增加提交,所以你看到的输出可能与页面上略有不同;命令的用法是一样的。

提示

当历史超出窗口高度时,git log 会用分页器(pager)显示,也就是一次显示一屏的查看器。用方向键或空格键滚动,按 q 退出并回到命令提示符。本系列页面中的动手环节会一次性打印全部内容。

动手试试阅读历史
git log --oneline
输出
1ad5842 Say what the notes are for
8bf3c2d Add a pancake recipe
907a979 Add a README
git log -n 1
输出
commit 1ad584283c1471035cb8e8538e2ea2cc2ce54824
Author: Alex Smith <alex@example.com>
Date:   Mon Jan 5 09:09:00 2026 +0000

    Say what the notes are for
git log --oneline --stat
输出
1ad5842 Say what the notes are for
 README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
8bf3c2d Add a pancake recipe
 pancakes.md | 5 +++++
 1 file changed, 5 insertions(+)
907a979 Add a README
 README.md | 3 +++
 1 file changed, 3 insertions(+)
git log --oneline pancakes.md
输出
8bf3c2d Add a pancake recipe
上面的输出来自 git version 2.50.1 (Apple Git-155),在构建本页时实际运行得到。其他版本的 Git 提示措辞可能略有不同。

git log -n 1 完整显示最新的提交:40 个字符的哈希、作者、日期和说明。加上 --stat 后,每次提交都附带一段简短的改动摘要。README.md | 2 +- 表示这次提交改动了 README.md 中的两行:增加一行(+),删除一行(-),Git 就是这样记录改写过的一行的。pancakes.md | 5 +++++ 表示增加了五行。

最后一条命令 git log --oneline pancakes.md 只列出了 8bf3c2d Add a pancake recipe,因为没有其他提交改动过 pancakes.md。想知道某个文件是何时、为何改动的,就从这里入手。

2

查看改动

git log 告诉你有哪些提交。另外三条相关的命令则显示具体改了什么:

  • git diff 比较工作区与暂存区,显示你已经做了但还没有暂存的改动。
  • git diff --staged 比较暂存区与最近一次提交,显示你的下一次提交将包含的内容。(--cached 是同一个选项的另一个名字。)
  • git show 显示一次提交:它的哈希、作者、日期和说明,后面跟着它的改动。不带参数时显示最新的提交;给它一个哈希,就能查看另一次提交。

git status 告诉你哪些文件改动了;这几条命令则告诉你怎样改动了,让你在改动进入历史之前逐一检查。在这个动手环节中,你把煎饼食谱改为少用一些牛奶并加一小撮盐,然后跟着这个改动从工作区一路走进提交。

动手试试查看改动
创建或编辑 pancakes.md,使其内容为:
# Pancakes

- 200 g flour
- 2 eggs
- 250 ml milk
- a pinch of salt
git diff
输出
diff --git a/pancakes.md b/pancakes.md
index 871d830..fcc716f 100644
--- a/pancakes.md
+++ b/pancakes.md
@@ -2,4 +2,5 @@

 - 200 g flour
 - 2 eggs
-- 300 ml milk
+- 250 ml milk
+- a pinch of salt
git add pancakes.md
git diff
git diff --staged
输出
diff --git a/pancakes.md b/pancakes.md
index 871d830..fcc716f 100644
--- a/pancakes.md
+++ b/pancakes.md
@@ -2,4 +2,5 @@

 - 200 g flour
 - 2 eggs
-- 300 ml milk
+- 250 ml milk
+- a pinch of salt
git commit -m "Use less milk and add salt"
输出
[main b7b26af] Use less milk and add salt
 1 file changed, 2 insertions(+), 1 deletion(-)
git show
输出
commit b7b26af362cd6db7ab58150f347b1c07ccfa83e5
Author: Alex Smith <alex@example.com>
Date:   Mon Jan 5 09:14:00 2026 +0000

    Use less milk and add salt

diff --git a/pancakes.md b/pancakes.md
index 871d830..fcc716f 100644
--- a/pancakes.md
+++ b/pancakes.md
@@ -2,4 +2,5 @@

 - 200 g flour
 - 2 eggs
-- 300 ml milk
+- 250 ml milk
+- a pinch of salt
上面的输出来自 git version 2.50.1 (Apple Git-155),在构建本页时实际运行得到。其他版本的 Git 提示措辞可能略有不同。

git diff 的输出称为差异(diff),每段差异都由同样的几部分组成:

  • diff --git a/pancakes.md b/pancakes.md 给出文件名:a/ 是旧版本,b/ 是新版本。后面的 index、--- 和 +++ 几行以更详细的方式说明同一件事,可以跳过。
  • @@ -2,4 +2,5 @@ 开始一个区块(hunk),即一组改动。-2,4 表示这一块涵盖旧版本中从第 2 行开始的四行;+2,5 表示它涵盖新版本中同样从第 2 行开始的五行:新版本多了一行。
  • 再往下,每一行都以一个标记开头。以 - 开头的行被删除了,以 + 开头的行是新增的,以空格开头的行没有改动,是帮你定位的上下文。这里,- 300 ml milk 被换成了 - 250 ml milk,并新增了 - a pinch of salt。每行的第一个字符是差异的标记;紧随其后的 - 属于食谱中的列表。

运行 git add pancakes.md 之后,第二次 git diff 什么也没有打印,因为工作区和暂存区现在一致了。改动并没有消失:git diff --staged 显示出完全相同的差异,因为这个改动现在位于暂存区和最近一次提交之间。提交之后,git show 再次显示这个改动,上方还有这次提交的哈希、作者、日期和说明。

git diff 也可以比较两次提交:git diff <较早的提交> <较新的提交>,哈希取自 git log --oneline。这时差异显示的是较新的提交与较早的提交有何不同:标为 - 的行只存在于较早的提交中,标为 + 的行只存在于较新的提交中。练习 2 会练到这一点。

3

撤销尚未提交的改动

错误在进入提交之前最容易撤销。git restore 的两种用法分别对应两种情况:

  • git restore <file> 丢弃你对某个文件的编辑。它用暂存区中的版本替换工作区中的文件;除非你之后又暂存过改动,这就是最近一次提交中的版本。
  • git restore --staged <file> 取消暂存某个文件。它把改动从暂存区中移出,使其不会进入下一次提交,但你的编辑仍保留在工作区中。

两者都不必死记:git status 的提示中就会建议这两条命令,动手环节里可以看到。首先,README 的简介被不小心删掉了。然后,对煎饼食谱的一处改动(用三个鸡蛋而不是两个)在还没准备好时就被暂存了。

动手试试丢弃尚未提交的改动
创建或编辑 README.md,使其内容为:
# Family recipes

Oops: the description is gone.
git status
输出
On branch main
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:   README.md

no changes added to commit (use "git add" and/or "git commit -a")
git restore README.md
git status
输出
On branch main
nothing to commit, working tree clean
cat README.md
输出
# Family recipes

Recipes we cook again and again, with notes on what worked.
创建或编辑 pancakes.md,使其内容为:
# Pancakes

- 200 g flour
- 3 eggs
- 300 ml milk
git add pancakes.md
git status
输出
On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
	modified:   pancakes.md
git restore --staged pancakes.md
git status
输出
On branch main
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:   pancakes.md

no changes added to commit (use "git add" and/or "git commit -a")
上面的输出来自 git version 2.50.1 (Apple Git-155),在构建本页时实际运行得到。其他版本的 Git 提示措辞可能略有不同。

运行 git restore README.md 之后,git status 报告工作区是干净的,cat 也显示简介回来了。运行 git restore --staged pancakes.md 之后,这个改动回到了 “Changes not staged for commit” 之下:三个鸡蛋仍在文件中,只是不再处于暂存状态。如果你在跟着操作,现在运行 git restore pancakes.md,把这处编辑也丢弃掉,让工作区在下一节开始时保持干净。

陷阱

不带 --staged 的 git restore <file> 会丢弃从未提交过的编辑。Git 从来没有保存过它们的副本,所以无法找回。如果拿不准,先看看 git diff。

较早的教程和网上的许多回答用 git checkout -- <file> 丢弃编辑,用 git reset HEAD <file> 取消暂存。这两种写法仍然可用,但 Git 2.23 加入的 git restore 从名字上就说明了自己的作用,也是 git status 所建议的命令。

4

修改最近一次提交

有时你刚提交完就发现了错误:说明里有错字,或者忘了暂存某个文件。只要你还没有把这次提交分享出去,就可以用 git commit --amend 修补它,也就是用一次新的提交替换最近一次提交:

  • git commit --amend -m "新的说明" 为这次提交换上新的说明。
  • git commit --amend --no-edit 保留原来的说明,并加入提交之后你暂存的所有内容。

两个选项都不用时,Git 会打开编辑器,里面是原来的说明,供你修改。在这个动手环节中,你提交了一份苏打面包食谱,说明里有错字;你先改正说明,然后发现食谱里缺了膨松剂。

动手试试修改最近一次提交
创建或编辑 bread.md,使其内容为:
# Soda bread

- 450 g flour
- 400 ml buttermilk
git add bread.md
git commit -m "Add a sdoa bread recipe"
输出
[main 087bb2c] Add a sdoa bread recipe
 1 file changed, 4 insertions(+)
 create mode 100644 bread.md
git commit --amend -m "Add a soda bread recipe"
输出
[main 3fc0d24] Add a soda bread recipe
 Date: Mon Jan 5 09:11:00 2026 +0000
 1 file changed, 4 insertions(+)
 create mode 100644 bread.md
创建或编辑 bread.md,使其内容为:
# Soda bread

- 450 g flour
- 400 ml buttermilk
- 1 teaspoon bicarbonate of soda
git add bread.md
git commit --amend --no-edit
输出
[main 31783ba] Add a soda bread recipe
 Date: Mon Jan 5 09:11:00 2026 +0000
 1 file changed, 5 insertions(+)
 create mode 100644 bread.md
git log --oneline -n 2
输出
31783ba Add a soda bread recipe
1ad5842 Say what the notes are for
git show --stat
输出
commit 31783bacd01cd94e817a9f878b5a49d36be8dbf3
Author: Alex Smith <alex@example.com>
Date:   Mon Jan 5 09:11:00 2026 +0000

    Add a soda bread recipe

 bread.md | 5 +++++
 1 file changed, 5 insertions(+)
上面的输出来自 git version 2.50.1 (Apple Git-155),在构建本页时实际运行得到。其他版本的 Git 提示措辞可能略有不同。

看看这些哈希。最初的提交是 087bb2c;第一次修补之后,最新的提交是 3fc0d24,第二次之后是 31783ba。修补并不是原地编辑一次提交,因为提交一旦生成就不会再改变。Git 会生成一次新的提交,其父提交与旧提交相同,然后把分支移到这次新提交上。旧提交不再属于这个分支,所以 git log --oneline -n 2 显示 31783ba 紧接在 1ad5842 之上。git show --stat 确认新提交包含了 bread.md 的全部五行。

每次修补的输出中,Date: 一行都是原始提交的时间:修补后的提交保留原来的作者和日期。

陷阱

只修补还没有分享出去的提交。如果别人已经拿到了旧提交,修补之后你的历史就会与他们的不同。第 8 模块会讲改写历史,以及为什么不要在共享的分支上这样做。

5

撤销一次提交

--amend 只能作用于最新的提交,而且会改写历史。对于更早的提交,或者别人已经拿到的提交,请使用 git revert <commit>。它不删除任何东西:它生成一次新的提交,抵消旧提交的改动。旧提交增加的行会被删除,旧提交删除的行会被加回来。

你可以用哈希指定要撤销的提交,也可以用 HEAD,这是 Git 对你当前所在提交的称呼,通常就是你所在分支上最新的提交。git revert 要求工作区是干净的,所以请先提交或恢复你的编辑。在这个动手环节中,一个错误混了进来:煎饼食谱要求用 2000 g 面粉,而不是 200 g。

动手试试用一次新提交撤销旧提交
创建或编辑 pancakes.md,使其内容为:
# Pancakes

- 2000 g flour
- 2 eggs
- 300 ml milk
git add pancakes.md
git commit -m "Use more flour"
输出
[main 654de4f] Use more flour
 1 file changed, 1 insertion(+), 1 deletion(-)
git revert HEAD
输出
[main 4407f3a] Revert "Use more flour"
 Date: Mon Jan 5 09:12:00 2026 +0000
 1 file changed, 1 insertion(+), 1 deletion(-)
git log --oneline -n 3
输出
4407f3a Revert "Use more flour"
654de4f Use more flour
1ad5842 Say what the notes are for
cat pancakes.md
输出
# Pancakes

- 200 g flour
- 2 eggs
- 300 ml milk
上面的输出来自 git version 2.50.1 (Apple Git-155),在构建本页时实际运行得到。其他版本的 Git 提示措辞可能略有不同。

git revert HEAD 生成了一次新的提交 4407f3a,说明为 Revert "Use more flour"。在你的电脑上,Git 会先打开编辑器,里面是这段现成的说明,后面还有一行指出它撤销的是哪次提交。保存并关闭编辑器即可接受;本页的动手环节会自动接受。这里的 Date: 一行只是新提交的时间。

日志中现在两次提交都在:错误的提交 654de4f,以及撤销它的提交 4407f3a。这看起来也许不够整洁,但它如实记录了发生的事,在别人共享的分支上也很安全:没有人的历史被改写,他们只是多收到一次提交。cat 确认食谱又变回了 200 g 面粉。

你可以撤销任何一次提交,不仅仅是最新的,只要给出它的哈希。如果后来的提交改动过同样的行,Git 可能会因冲突而停下,等你解决,第 5 模块会讲到这一点。

6

查看旧版本

有时你想看看项目早先的样子:找出某个功能最后一次正常工作是在什么时候,或者把一段旧文字复制回来。Git 提供了两种安全的查看方式。

要查看单个文件,使用 git show <commit>:<file>。它打印该文件在那次提交中的内容,不做任何改动。你可以用哈希指定提交,也可以从 HEAD 往回数:HEAD~1 是你所在提交的前一次提交,HEAD~2 是再往前的一次。

要查看整个项目,使用 git switch --detach <commit>。它把工作区中的每个文件都恢复到那次提交时的样子,方便你四处查看、打开文件或运行项目,而你的分支保持不变。HEAD 是 Git 用来标记你当前位置的指针,此时它直接指向一次提交,而不是指向一个分支。Git 把这种状态称为分离的 HEAD(detached HEAD)。

你现在已经做了更多提交,所以在你的仓库中,HEAD~2 是另一次提交。要跟着操作,请改用 git log --oneline 中你的第一次提交 Add a README 的哈希。

动手试试安全地查看旧版本
git show HEAD~2:README.md
输出
# Family recipes

Recipes we cook again and again.
git switch --detach HEAD~2
输出
HEAD is now at 907a979 Add a README
git status
输出
HEAD detached at 907a979
nothing to commit, working tree clean
cat README.md
输出
# Family recipes

Recipes we cook again and again.
* 1ad5842 (main) Say what the notes are for
* 8bf3c2d Add a pancake recipe
* 907a979 (HEAD) Add a README
git switch main
输出
Previous HEAD position was 907a979 Add a README
Switched to branch 'main'
git status
输出
On branch main
nothing to commit, working tree clean
上面的输出来自 git version 2.50.1 (Apple Git-155),在构建本页时实际运行得到。其他版本的 Git 提示措辞可能略有不同。

git show HEAD~2:README.md 打印出了 README 最初的措辞,git switch --detach HEAD~2 报告了这次移动:HEAD is now at 907a979 Add a README。git status 不再显示 On branch main,而是显示 HEAD detached at 907a979;cat README.md 显示工作区中的文件本身也是旧的措辞。

提交图显示了一切所处的位置。main 仍然指向最新的提交 1ad5842,而 HEAD 单独停在 907a979 上。没有任何东西丢失或改变:较新的提交都还在,git switch main 就能带你回去。Git 会告诉你从哪里回来(Previous HEAD position was 907a979),git status 也再次显示 On branch main。

提示

分离的 HEAD 很适合四处查看。如果你在这种状态下做了提交并想保留它们,请在切换走之前用 git switch -c <name> 为它们创建一个分支。练习 6 会说明原因。

较早的教程用 git checkout <commit> 做同样的事。它同样会让 HEAD 分离,并在这样做时打印一段更长的说明。

7

练习

练习 1★★★概念6 分钟

用哪条命令查看? 说出查看下列内容的命令。

  1. 你已做出但尚未暂存的改动。
  2. 你已为下一次提交暂存的改动。
  3. 最近一次提交改动了什么。
  4. 所有改动过 pancakes.md 的提交。
查看解答
  1. git diff
  2. git diff --staged
  3. git show
  4. git log --oneline pancakes.md(或用 git log pancakes.md 查看完整条目)
练习 2★★★编程8 分钟

比较两次提交。 在食谱仓库中,查看从第一次提交到最新一次提交之间的全部改动。

查看解答

先列出提交,看看第一次提交在多久之前,再把它与 HEAD 比较:

参考解答比较两次提交
git log --oneline
输出
1ad5842 Say what the notes are for
8bf3c2d Add a pancake recipe
907a979 Add a README
git diff HEAD~2 HEAD
输出
diff --git a/README.md b/README.md
index 2ecc1f2..130cb17 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,3 @@
 # Family recipes

-Recipes we cook again and again.
+Recipes we cook again and again, with notes on what worked.
diff --git a/pancakes.md b/pancakes.md
new file mode 100644
index 0000000..871d830
--- /dev/null
+++ b/pancakes.md
@@ -0,0 +1,5 @@
+# Pancakes
+
+- 200 g flour
+- 2 eggs
+- 300 ml milk
上面的输出来自 git version 2.50.1 (Apple Git-155),在构建本页时实际运行得到。其他版本的 Git 提示措辞可能略有不同。

这里 HEAD~2 就是第一次提交。差异显示了 README.md 中改写的那一行,以及以新增行形式出现的整个 pancakes.md,因为第一次提交时这个文件还不存在。

练习 3★★★概念7 分钟

选择撤销方式。 下列每种情况分别适用哪条命令?

  1. 你编辑了 README.md,想恢复为已提交的版本。
  2. 你误暂存了 notes.md,但想保留你的修改。
  3. 最近一次提交(尚未共享)的说明中有错别字。
  4. 上周的一次提交已与团队共享,它弄坏了面包食谱。
查看解答
  1. git restore README.md
  2. git restore --staged notes.md
  3. git commit --amend -m "…",写上改正后的说明
  4. git revert <哈希>,用一次新提交撤销那次提交,而不改写共享的历史
练习 4★★★编程10 分钟

停止跟踪文件。 假设 passwords.txt 像第 2 模块最后一道练习中那样被误提交了。请把文件保留在磁盘上,但从现在起停止跟踪它。要动手试试,请先创建 passwords.txt(内容随意)并提交它。

查看解答

把文件写进 .gitignore,只从暂存区中删除它,然后提交。动手环节中的仓库还没有 .gitignore;如果你的仓库已经有了,就把 passwords.txt 这一行加进去,并保留原有的各行。

参考解答停止跟踪文件
创建或编辑 .gitignore,使其内容为:
passwords.txt
git rm --cached passwords.txt
输出
rm 'passwords.txt'
git status
输出
On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
	deleted:    passwords.txt

Untracked files:
  (use "git add <file>..." to include in what will be committed)
	.gitignore
git add .gitignore
git commit -m "Stop tracking the passwords file"
输出
[main 908ddf5] Stop tracking the passwords file
 2 files changed, 1 insertion(+), 1 deletion(-)
 create mode 100644 .gitignore
 delete mode 100644 passwords.txt
git status
输出
On branch main
nothing to commit, working tree clean
ls
输出
README.md  pancakes.md  passwords.txt
上面的输出来自 git version 2.50.1 (Apple Git-155),在构建本页时实际运行得到。其他版本的 Git 提示措辞可能略有不同。

git rm --cached 把文件从下一次提交中移除,但保留在磁盘上,ls 的输出说明了这一点。密码仍然保存在更早的提交中,所以要按第 2 模块的建议更换密码。

练习 5★★★编程7 分钟

漏了一个文件。 你提交了 bread.md,随后发现属于同一项改动的 butter.md 没有添加。请在不做第二次提交的情况下修正。什么情况下不应这样做?

查看解答

暂存该文件并修补提交,保留原来的说明:

git add butter.md
git commit --amend --no-edit

不要修补已经共享的提交:修补会用一次新提交替换它,已拿到旧提交的人会因此得到不同的历史(第 8 模块)。

练习 6★★★概念7 分钟

在分离的 HEAD 上提交。 你运行了 git switch --detach HEAD~2,编辑了一个文件并做了一次提交。运行 git switch main 时会发生什么?怎样才能保留这次提交?

查看解答

Git 会切换过去,但会警告你留下了一次不属于任何分支的提交,并给出一条带有其哈希的 git branch 命令。这次提交仍然存在,只是没有分支指向它。

要保留它,在切换离开之前先创建一个分支:git switch -c rescue。如果已经切换走了,可以用 Git 警告中的哈希(或第 8 模块介绍的引用日志)补建分支。

8

自测

1
哪条命令显示已暂存但尚未提交的改动?
2
在差异中,以 + 开头的行表示什么?
3
你想丢弃对 README.md 尚未提交的修改。应使用哪条命令?
4
git commit --amend 的作用是什么?
5
你上周共享的一次提交引入了缺陷。安全的撤销方法是什么?
6
“HEAD detached at 907a979” 是什么意思?
7
HEAD~2 指的是什么?
9

延伸阅读