阅读历史
你做的每一次提交都会被保留下来,连同作者、日期和你写的说明。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 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。想知道某个文件是何时、为何改动的,就从这里入手。
查看改动
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 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 会练到这一点。
撤销尚未提交的改动
错误在进入提交之前最容易撤销。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 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 所建议的命令。
修改最近一次提交
有时你刚提交完就发现了错误:说明里有错字,或者忘了暂存某个文件。只要你还没有把这次提交分享出去,就可以用 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(+)
看看这些哈希。最初的提交是 087bb2c;第一次修补之后,最新的提交是 3fc0d24,第二次之后是 31783ba。修补并不是原地编辑一次提交,因为提交一旦生成就不会再改变。Git 会生成一次新的提交,其父提交与旧提交相同,然后把分支移到这次新提交上。旧提交不再属于这个分支,所以 git log --oneline -n 2 显示 31783ba 紧接在 1ad5842 之上。git show --stat 确认新提交包含了 bread.md 的全部五行。
每次修补的输出中,Date: 一行都是原始提交的时间:修补后的提交保留原来的作者和日期。
只修补还没有分享出去的提交。如果别人已经拿到了旧提交,修补之后你的历史就会与他们的不同。第 8 模块会讲改写历史,以及为什么不要在共享的分支上这样做。
撤销一次提交
--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 revert HEAD 生成了一次新的提交 4407f3a,说明为 Revert "Use more flour"。在你的电脑上,Git 会先打开编辑器,里面是这段现成的说明,后面还有一行指出它撤销的是哪次提交。保存并关闭编辑器即可接受;本页的动手环节会自动接受。这里的 Date: 一行只是新提交的时间。
日志中现在两次提交都在:错误的提交 654de4f,以及撤销它的提交 4407f3a。这看起来也许不够整洁,但它如实记录了发生的事,在别人共享的分支上也很安全:没有人的历史被改写,他们只是多收到一次提交。cat 确认食谱又变回了 200 g 面粉。
你可以撤销任何一次提交,不仅仅是最新的,只要给出它的哈希。如果后来的提交改动过同样的行,Git 可能会因冲突而停下,等你解决,第 5 模块会讲到这一点。
查看旧版本
有时你想看看项目早先的样子:找出某个功能最后一次正常工作是在什么时候,或者把一段旧文字复制回来。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 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 分离,并在这样做时打印一段更长的说明。
练习
用哪条命令查看? 说出查看下列内容的命令。
- 你已做出但尚未暂存的改动。
- 你已为下一次提交暂存的改动。
- 最近一次提交改动了什么。
- 所有改动过
pancakes.md的提交。
查看解答
git diffgit diff --stagedgit showgit log --oneline pancakes.md(或用git log pancakes.md查看完整条目)
比较两次提交。 在食谱仓库中,查看从第一次提交到最新一次提交之间的全部改动。
查看解答
先列出提交,看看第一次提交在多久之前,再把它与 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
这里 HEAD~2 就是第一次提交。差异显示了 README.md 中改写的那一行,以及以新增行形式出现的整个 pancakes.md,因为第一次提交时这个文件还不存在。
选择撤销方式。 下列每种情况分别适用哪条命令?
- 你编辑了
README.md,想恢复为已提交的版本。 - 你误暂存了
notes.md,但想保留你的修改。 - 最近一次提交(尚未共享)的说明中有错别字。
- 上周的一次提交已与团队共享,它弄坏了面包食谱。
查看解答
git restore README.mdgit restore --staged notes.mdgit commit --amend -m "…",写上改正后的说明git revert <哈希>,用一次新提交撤销那次提交,而不改写共享的历史
停止跟踪文件。 假设 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 rm --cached 把文件从下一次提交中移除,但保留在磁盘上,ls 的输出说明了这一点。密码仍然保存在更早的提交中,所以要按第 2 模块的建议更换密码。
漏了一个文件。 你提交了 bread.md,随后发现属于同一项改动的 butter.md 没有添加。请在不做第二次提交的情况下修正。什么情况下不应这样做?
查看解答
暂存该文件并修补提交,保留原来的说明:
git add butter.md
git commit --amend --no-edit
不要修补已经共享的提交:修补会用一次新提交替换它,已拿到旧提交的人会因此得到不同的历史(第 8 模块)。
在分离的 HEAD 上提交。 你运行了 git switch --detach HEAD~2,编辑了一个文件并做了一次提交。运行 git switch main 时会发生什么?怎样才能保留这次提交?
查看解答
Git 会切换过去,但会警告你留下了一次不属于任何分支的提交,并给出一条带有其哈希的 git branch 命令。这次提交仍然存在,只是没有分支指向它。
要保留它,在切换离开之前先创建一个分支:git switch -c rescue。如果已经切换走了,可以用 Git 警告中的哈希(或第 8 模块介绍的引用日志)补建分支。
自测
git diff 比较工作区与暂存区;--staged 比较暂存区与最近一次提交。+ 开头的行表示什么?+ 表示新增的行,- 表示删除的行,以空格开头的是未改动的上下文。README.md 尚未提交的修改。应使用哪条命令?git restore <文件> 用暂存的版本替换工作区中的文件;--staged 只取消暂存。git commit --amend 的作用是什么?git revert 添加一次撤销旧提交的新提交,不会改写共享的历史。HEAD~2 指的是什么?~2 表示“从 HEAD 往回数两次提交”。延伸阅读
- Scott Chacon、Ben Straub,《Pro Git》中文版第 2.3 节“查看提交历史”和第 2.4 节“撤消操作”。
- git log、git diff、git restore 和 git revert 的参考文档(英文)。