Skip to content
This page has been auto-translated and may contain errors.View in English

用 log、show 和 diff 查看 Git 历史记录 ​

docs.scrimba.com

假设你周一早上打开 weather-app,却完全想不起来周五到底提交了什么。或者同事问你上周为什么改了温度换算逻辑,而你想给出真实的答案,而不是猜测。The commit loop 讲过一次提交是怎么保存下来的。这一章讲的是如何反过来读取这些历史:发生了什么、是谁做的、什么时候做的。用来做这件事的命令是 git log,下面是它在一个小项目上打印出的全部内容:

bash
$ git log
commit 4f2a1c9d8e7b6a5c4d3e2f1a0b9c8d7e6f5a4b3c
Author: Priya Nair <[email protected]m>
Date:   Tue Jul 14 09:12:03 2026 +0100

    Fix temperature conversion in Celsius-to-Fahrenheit formula

commit 5f3d8b21a90e7c6d5b4a3f2e1d0c9b8a7f6e5d4c
Author: Kwame Boateng <[email protected]m>
Date:   Mon Jul 13 16:45:22 2026 +0100

    Add five-day forecast to the dashboard

commit 1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b
Author: Kwame Boateng <[email protected]m>
Date:   Mon Jul 13 11:03:47 2026 +0100

    Set up project structure

只用一个命令,项目的整段故事就摆在你面前了:三次提交,最新的排在最前面,每一条都写清了作者、时间和改动原因。

用 git log 读历史 ​

git log 会从你当前所在的位置开始,沿着能追溯到的每一次提交往回走,最新的排在最前面。每一条记录都展示四样东西:哈希值(标识这次提交的一长串字母和数字)、作者、日期,以及作者写的提交信息。

commit(提交) 就是把这四样东西打包在一起:那一刻所有被跟踪文件的快照、描述改动的信息、改动者是谁,以及指向上一次提交的链接,也就是这次提交的父提交(parent commit)。正是这个指向父提交的链接,把一堆孤立的快照串成了一条真正的时间线。从上往下读 git log,其实就是在倒着读这条时间线,从现在读到最初。

哈希值乍一看挺吓人,但你几乎用不到完整的那一长串。4f2a1c9d8e7b6a5c4d3e2f1a0b9c8d7e6f5a4b3c 和 4f2a1c9 指向的是同一次提交。Git 只需要哈希值中足够区分它和仓库里其他提交的那一部分,通常前七八个字符就够了。你会在各处见到这种短格式,本章后面的命令也都会用它。

git log 默认的输出很详细,但也占地方:光是三次提交就把你的终端塞满了。有几种常用写法能让它在日常使用中更好用。

bash
$ git log --oneline
4f2a1c9 Fix temperature conversion in Celsius-to-Fahrenheit formula
5f3d8b2 Add five-day forecast to the dashboard
1a2b3c4 Set up project structure

--oneline 把每次提交压缩成短哈希加提交信息,一行一个提交,这也是你日常会最常用到的形式。--graph 会在日志旁边把历史的走向画出来,用线条和点表示。在一个还没有分支的项目上,它画出的只是一条直线,但最好现在就把它记在手上:一旦分支登场(下一章的内容),这张图才真正开始发挥作用。

你也可以对日志做筛选,而不是一条条翻。git log --author="Kwame" 只显示 Kwame 提交的记录,在你想看某个人在共享项目里做了哪些工作时很有用。git log -- src/index.js 只显示改动过这个特定文件的提交,忽略所有跟它无关的提交:

bash
$ git log --oneline -- src/index.js
4f2a1c9 Fix temperature conversion in Celsius-to-Fahrenheit formula
1a2b3c4 Set up project structure

第二次提交,就是加预报功能那次,从来没有动过 src/index.js,所以没有出现在结果里。这两种筛选方式都可以干净地和 --oneline 组合使用。

上面这些筛选方式回答的是"哪些提交动过这个文件"。而真正在你遇到麻烦时会去翻历史的问题更尖锐:"这段代码具体是什么时候出现的,或者是什么时候消失的?" 这正是 git log -S(也就是 pickaxe(探针))要解决的问题。给它一个字符串,它只保留那些让这个字符串出现次数发生变化的提交,也就是新增或删除了这个字符串的提交:

bash
$ git log --oneline -S "showLoadingSpinner"
7d1e8c3 Add loading spinner while forecast data loads

整段历史里只有一次提交命中:那正是这个调用第一次进入代码库的那一刻。这是回答"这行有问题的代码是什么时候引入的"最快的方式,而且完全不需要切换任何东西。这种计数规则有一个值得知道的后果:如果一次提交只是把某一行在文件内挪了个位置,出现次数并不会变,所以 -S 会跳过它。如果你想找出 diff 里只要碰到这段文字(包括挪动位置)的每一次提交,可以改用 git log -G 配合正则表达式。但通常先用 -S,它对你平时最常问的这类问题更精准。

Juno读日志git log 会列出每一次提交,最新的在最前面,附带哈希值、作者、日期和提交信息。一次提交就是那个快照,加上提交信息,再加上一条指向上一次提交的链接。你几乎用不到完整的哈希值,前面几个字符就足够让 Git 准确知道你说的是哪次提交了。
Juno读日志git log --oneline 是你日常会一直用到的形式,一行一次提交。加上 --author 或者末尾加 -- path,就能筛选到某个人的工作或者某个文件的历史,等分支登场以后再用 --graph。
Juno读日志 每次提交的哈希值都是它自身内容加上父提交哈希值的指纹,所以两次提交不会意外撞在一起,写个短前缀也是安全的。而当你想知道"这一行是什么时候出现或消失的",git log -S "文本" 会帮你翻遍整段历史,只留下新增或删除它的那些提交。加上父提交这条链接,这一章几乎就是我调试工具箱里最常用的部分。

用 git show 查看单个提交 ​

git log 告诉你有一次提交发生了。git show <hash> 告诉你它到底做了什么:

bash
$ git show 4f2a1c9
commit 4f2a1c9d8e7b6a5c4d3e2f1a0b9c8d7e6f5a4b3c
Author: Priya Nair <[email protected]m>
Date:   Tue Jul 14 09:12:03 2026 +0100

    Fix temperature conversion in Celsius-to-Fahrenheit formula

diff --git a/src/index.js b/src/index.js
index 3f2c1a9..7b8e2d4 100644
--- a/src/index.js
+++ b/src/index.js
@@ -12,7 +12,7 @@ function celsiusToFahrenheit(celsius) {
-  return celsius * (9 / 5 + 32)
+  return (celsius * 9) / 5 + 32

上半部分和 git log 展示的元信息一样。下面才是真正的改动:以 - 开头的行是这次提交删掉的行,以 + 开头的行是新增的行,没有标记的行是保持不变的上下文。这里能看得很清楚:旧的那一行把 32 也算在括号里,导致 32 在乘法运算之前就被加进去了。修复的方式是把括号重新围在 celsius * 9 外面,让 32 最后才被加上,这正是 Priya 提交信息里说的运算符优先级问题。

加一个路径可以只看一次大提交中的一部分:git show 4f2a1c9 -- src/index.js 只显示这个文件相关的那部分改动,当一次提交动了好几个文件、而你只关心其中一个时,这一点就很有用。

Juno查看单个提交git show <hash> 完整展示一次提交:谁做的、什么时候做的,以及具体改动了哪些行。删除的行以减号开头,新增的行以加号开头。这是回答"这次提交到底做了什么"最快的方式。
Juno查看单个提交git show <hash> 会给你一次提交的元信息和完整的 diff。你也可以指定具体路径,git show <hash> -- path/to/file,当一次提交动了好几个文件、而你只需要其中一个文件那部分改动时就用它。
Juno查看单个提交git show 做的事情是把某次提交保存的快照和它父提交的快照做对比,只打印出不同的那些行。等你习惯了用这种方式读 diff,一次提交就不再是个黑盒,而是两个快照之间的差异。

git diff 展示的内容,以及几乎所有人都会踩的坑 ​

git log 和 git show 读取的是已经提交的历史。git diff 读取的是还没提交的内容:也就是当前工作目录里的改动,和你上一次提交相比的结果。

bash
$ git diff
diff --git a/README.md b/README.md
index 1c2d3e4..9f8a7b6 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,4 @@
 # weather-app
+A five-day weather dashboard built with vanilla JavaScript.

这就是你对 README.md 做的改动,在你还没有 stage 或提交之前展示出来。接下来这部分几乎会让每个人在入门第一周就踩坑:运行 git add . 把这个改动 stage 起来,再运行一次 git diff,结果什么都没显示。

bash
$ git add .
$ git diff

什么都没打印出来。改动并没有消失,也没有出什么问题。普通的 git diff 比较的是你的工作目录和暂存区,一旦你把所有改动都 stage 了,这两者就一致了,自然也就没有差异可以显示。要查看暂存区里等待提交的改动,应该用 git diff --staged。

bash
$ git diff --staged
diff --git a/README.md b/README.md
index 1c2d3e4..9f8a7b6 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,4 @@
 # weather-app
+A five-day weather dashboard built with vanilla JavaScript.

同一个改动,现在显示出来了,因为 --staged 比较的是暂存区和你上一次提交。

普通的 git diff 和 git diff --staged 回答的是两个不同的问题,把它们分别说清楚会很有帮助。普通的 git diff 比较的是你的工作目录和暂存区:它显示的是你做了但还没 stage 的改动。git diff --staged 比较的是暂存区和你上一次提交:它显示的是如果你现在运行 git commit,会真正被提交进去的内容。这两个命令都不会同时展示两种差异,这正是为什么一次完全被 stage 的改动会让普通的 git diff 变得什么都不显示。你有时会在比较老的教程或文档里看到 --cached,它和 --staged 是一个意思。

在提交之前把两个命令都跑一遍是个好习惯:普通的 git diff 能确认没有重要的改动被漏掉没 stage,git diff --staged 能确认即将保存的内容正是你想要的。

Juno查看改动了什么git diff 显示的是你工作目录里还没被 stage 的改动。一旦你用 git add 把所有改动都 stage 了,普通的 git diff 就会什么都不显示。这是正常的,你的改动仍然在那里。想看等待提交的内容,用 git diff --staged。
Juno查看改动了什么 普通的 git diff 比较的是工作目录和暂存区,git diff --staged 比较的是暂存区和你上一次提交,是两种不同的比较。提交之前把两个都跑一遍,就能知道哪些还没 stage,哪些即将被保存。老一点的资料里有时会写成 --cached,是同一个选项的不同名字。
Juno查看改动了什么 这两种 diff 其实是同一种操作,只是应用在不同的快照对上:工作树对暂存区,或者暂存区对上一次提交。只要你能说清楚自己在比较哪两样东西,就再也不会被一个空空如也的 git diff 弄糊涂了。

为什么 Git 的历史是一张图 ​

到目前为止你见到的每一次提交都只指向唯一一个父提交,从上往下读 git log 一直感觉像在读一条直线。这在一个人独自开发、只有一条工作线的项目里确实如此。但一旦分支和合并登场,这条直线就不成立了,这部分内容会在下一章 Branches 里讲到,因为一个项目真实的历史可以分叉成不同的线,再重新汇合到一起。

这正是前面提到的 --graph 参数真正的用途。目前在 weather-app 上,它画出的只是一列,因为只有一条提交线可以画。一旦某条分支分叉出去、后来又合并回来,--graph 就会把分叉和汇合画成两条会交汇的列,这比试着从一堆哈希值的平铺列表里去想象要容易理解得多。

在一个合并提交上运行 git show,默认情况下它只会打印元信息,不会显示任何 diff。加上 -m,它就会依次给每个父提交打印一份单独的 diff。这两种行为其实都源自同一个结构:Git 的历史是一个 DAG,也就是有向无环图(directed acyclic graph)。"有向"意味着每条链接都只指向一个方向,从提交指向它的父提交,永远不会反过来。"无环"意味着这些链接永远不会绕成一个圈,所以沿着它们走,永远是朝项目起点的方向前进,不会绕回你已经走过的某次提交。一次普通的提交只有一个父提交,这也是为什么一个人独自开发的项目日志读起来像一条直线。合并提交是个例外:它带着两个父提交的哈希值,而不是一个,有了两个父提交,普通的 git show 就没法确定该打印"哪一个" diff,直到你用 -m 告诉它分别和每个父提交比较。分支和合并,也就是接下来的两章,正是建立在这同一张图之上的。

Juno作为一张图的历史 目前你见到的每一次提交都只指向一个父提交,所以日志读起来像一条直线。等分支分叉出去又合并回来,情况就会变化,这是你接下来要学的内容。而底层这种结构——提交链接到它前面的提交——正是让这一切成为可能的基础。
Juno作为一张图的历史 一个人独自开发的一条提交线看起来是直的,因为每次提交只有一个父提交。--graph 参数就是让这种结构在分支出现之后变得看得见的工具,它画出的是分叉和合并,而不是一份平铺的列表。就算现在还不需要,也值得先记在手上。
Juno作为一张图的历史 Git 的历史是一个有向无环图:父指针永远只指向过去,而且永远不会绕成圈。合并提交是唯一一个会有两个父提交而不是一个的地方,这也是为什么普通的 git show 在那里不会打印 diff,除非你加上 -m 去选定一个父提交。分支和合并会从不同的角度,教你理解这同一张图剩下的部分。