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,却想不起周五发布的是什么。或者一位队友问你为什么上周改了温度转换公式,你想要真实答案而不是猜测。提交循环涵盖了如何保存提交。本章涵盖的是读取历史:发生了什么、谁做的,以及何时发生的。执行这个操作的命令是 git log,以下是它为一个小项目打印的所有内容:

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

    修复摄氏度到华氏度转换公式中的温度转换错误

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

    在仪表盘中添加五天预报

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

    设置项目结构

一条命令,整个项目的故事就摆在眼前:三个提交,最新的在前,每个都显示了谁写的、什么时候写的、以及为什么写的。

使用 git log 读取日志

git log 遍历从你当前位置可以到达的每个提交,最新的首先出现。每条条目显示四件事:一个哈希值(标识该确切提交的长字母数字串)、作者、日期和其作者编写的消息。

一个**提交是将这四件事捆绑在一起:该时刻每个被追踪文件的快照、描述更改的消息、谁做的、以及到它之前那个提交的链接,即它的父提交**。这个父链接是将一堆单独的快照转变为真实时间线的原因。从上到下读 git log 就是从现在向项目开始倒读这条时间线。

哈希值一开始看起来很吓人,但你很少需要整个东西。4f2a1c9d8e7b6a5c4d3e2f1a0b9c8d7e6f5a4b3c4f2a1c9 指向同一个提交。Git 只需要足够的哈希来把它与仓库中的其他提交区分开,前七八个字符几乎总是足够的。你会在各个地方看到短形式,包括本章剩余部分的命令。

默认的 git log 输出很详细但很长:三个提交已经占满了你的整个终端。几种格式让它在日常使用中更实用。

bash
$ git log --oneline
4f2a1c9 修复摄氏度到华氏度转换公式中的温度转换错误
5f3d8b2 在仪表盘中添加五天预报
1a2b3c4 设置项目结构

--oneline 将每个提交折叠成它的短哈希和消息,每个提交一行,这是你最常使用的格式。--graph 在日志旁边画出历史的形状,以线和点的形式。在还没有分支的项目上,它会画一条直线,但要早点习惯它:分支出现的那一刻(下一章)正是这个图开始有用的时候。

你也可以筛选日志而不是滚动浏览。git log --author="王" 只显示由王刚写的提交,当你想看共享项目中某个人的工作时很有用。git log -- src/index.js 只显示接触过那个特定文件的提交,忽略从未接触过它的一切:

bash
$ git log --oneline -- src/index.js
4f2a1c9 修复摄氏度到华氏度转换公式中的温度转换错误
1a2b3c4 设置项目结构

第二个提交,关于预报的那个,从未接触过 src/index.js,所以它没有出现。两种筛选都能与 --oneline 完美结合。

上面的筛选回答了"哪些提交接触过这个文件"。真正让你在糟糕的日子里深入历史的问题更加具体:这段代码是何时出现或消失的?这就是 git log -S 的用处,也被称为**pickaxe**。给它一个字符串,它只保留那些该字符串出现次数改变的提交,意味着添加或删除它的提交:

bash
$ git log --oneline -S "showLoadingSpinner"
7d1e8c3 在加载预报数据时添加加载微调器

整个历史中就一个提交:这个调用进入代码库的时刻。这是快速回答"这个有问题的代码何时被引入的"而无需检出任何东西的最快方法。计数规则有一个值得了解的后果:只在文件内移动该行的提交会使出现次数不变,所以 -S 会跳过它。当你想要每个差异都接触文本的提交时,移动包括在内,改用 git log -G 和正则表达式。首先选择 -S;它在你通常拥有的问题上是更锐利的工具。

Juno读取日志git log 列出每个提交,最新的首先,带有它的哈希、作者、日期和消息。提交是那个快照加上它的消息加上到它之前提交的链接。你几乎从不需要完整的哈希,前几个字符对 Git 来说足以确切地知道你指的是哪个提交。
Juno读取日志git log --oneline 是你每天都会使用的格式,每个提交一行。添加 --author 或末尾的 -- path 来筛选到一个人的工作或一个文件的历史,一旦分支出现,就选择 --graph
Juno读取日志 每个提交的哈希是它自己的内容加上它父提交的哈希的指纹,所以两个提交永远不会意外碰撞,短前缀对输入是安全的。当问题是"这行何时出现或消失"时,git log -S "text" 为你遍历整个历史,只保留添加或删除它的提交。除此之外还有父链接,这一章就是我调试工具箱的大部分。

使用 git show 检查单个提交

git log 告诉你提交发生了。git show <hash> 告诉你它确切做了什么:

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

    修复摄氏度到华氏度转换公式中的温度转换错误

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 分组在括号内,所以它在乘法之前就被添加了。这个修复重新将括号分组在 celsius * 9 周围,这样 32 最后被添加,就是李明的消息描述的运算符优先级错误。

添加一个路径只查看较大提交的一部分:git show 4f2a1c9 -- src/index.js 只显示那个文件的更改部分,当一个提交接触多个文件而你只关心其中一个时很重要。

Juno检查单个提交git show <hash> 完整显示一个提交:谁做的、什么时候做的,以及它更改的实际行。删除的行以减号开头,添加的行以加号开头。这是回答"这个提交实际上做了什么"的最快方式。
Juno检查单个提交git show <hash> 为一个提交给出元数据和完整的差异。当提交接触多个文件而你只需要一个文件的部分时,也可以指向一个特定路径,git show <hash> -- path/to/file
Juno检查单个提交git show 是比较提交的保存快照与其父提交的快照,只打印不同的行。一旦你开始以这种方式读取差异,提交就从一个神秘的黑匣子变成两个快照及其之间的差异。

git diff 显示什么,以及几乎每个人都会犯的误区

git loggit 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
+用原生 JavaScript 构建的五天天气仪表盘。

这是你对 README.md 所做的编辑,在你暂存或提交任何东西之前显示。这里是让几乎每个人在第一周陷入困境的部分:运行 git add . 来暂存相同的更改,然后再运行 git diff,它什么都不显示。

bash
$ git add .
$ git diff

什么都没有打印。编辑没有消失,也没有什么地方出错。普通的 git diff 比较你的工作目录与暂存区,一旦你暂存了所有东西,这两个现在匹配,所以那里没有左差异了。要查看等待在暂存区中的更改,改用 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
+用原生 JavaScript 构建的五天天气仪表盘。

相同的更改,现在可见,因为 --staged 比较暂存区与你的最后一次提交。

普通的 git diffgit diff --staged 在回答两个不同的问题,精确命名它们很有帮助。普通的 git diff 比较你的工作目录与暂存区:它显示你已做但尚未暂存的编辑。git diff --staged 比较暂存区与你的最后一次提交:它显示如果你现在运行 git commit 会进入你下一个提交的确切内容。两者都不同时显示,这正是为什么完全暂存的更改让普通的 git diff 保持沉默。你有时会在较旧的教程和文档中看到 --cached;它的意思与 --staged 相同。

在提交前同时运行两者是个好习惯:普通的 git diff 确认没有重要的东西遗漏暂存,git diff --staged 确认确切什么即将被保存。

Juno查看什么改变了git diff 显示你工作目录中尚未暂存的编辑。一旦你使用 git add 暂存了所有东西,普通的 git diff 就会保持沉默。这是预期的,你的更改仍在那里。使用 git diff --staged 来查看等待提交的内容。
Juno查看什么改变了 普通的 git diff 是工作目录对暂存区,git diff --staged 是暂存区对你的最后一次提交,两个不同的比较。在提交前运行两者告诉你什么是未暂存的以及什么即将被保存。较旧的材料有时会说 --cached,相同的标志,不同的名称。
Juno查看什么改变了 两个差异是相同的操作应用于不同的快照对:工作树对暂存区,或暂存区对最后一次提交。命名你在比较的两个东西是永远不再被空的 git diff 搞迷糊的整个技巧。

为什么 Git 历史形成一个图

你到目前为止看到的每个提交都指向确切的一个父提交,从上到下读 git log 感觉像在读一条直线。这对于在单一工作线上进行的单独项目是成立的。一旦分支和合并出现,这就停止成立了,下一章的分支中涵盖,因为项目的真实历史可以分裂成单独的线并重新汇合。

这就是早前的 --graph 标志真正的用处。在 weather-app 上现在它会画一个单列,因为只有一行提交要画。一旦分支分叉开然后稍后合并回来,--graph 开始将分叉和重新加入作为单独的列来画,它们汇合,这比试图从平面的哈希列表来描绘它要容易得多。

在合并提交上运行 git show 然后,默认情况下,它只打印元数据:根本没有差异。添加 -m 它会依次为每个父打印一个单独的差异。两种行为都可以追溯到相同的形状:Git 的历史是一个**DAG**,有向无环图。"有向"意味着每个链接指向一个方向,一个提交回到它的父提交,永远不向前。"无环"意味着那些链接永不循环,所以遍历它们总是向项目的开始移动,永不回到你已经经过的提交。一个普通提交有确切的一个父,这就是为什么单独项目的日志读起来像一条直线。合并提交是例外:它携带两个父哈希而不是一个,有两个父,普通的 git show 没有单一的"差异"要打印直到 -m 告诉它分别与每个父比较。分支和合并,接下来的两章,从相同的图的不同角度直接构建。

Juno历史作为图 现在你看到的每个提交都指向一个父提交,所以日志读起来像一条直线。一旦分支分裂开然后合并回来,这就改变了,你将在下一步遇到。形状的底层,提交链接到它们之前的提交,是使任何这一切成为可能的原因。
Juno历史作为图 单行的提交看起来是直的,因为每个都有一个单一父。--graph 标志是一旦分支开始就使形状可见的东西,画分叉和合并而不是平面列表。即使在你需要它之前,也要把它放在你的工具箱中。
Juno历史作为图 Git 的历史是一个有向无环图:父指针只能指向后方,它们永不循环。合并提交是一个提交得到两个父而不是一个的唯一地方,这就是为什么普通的 git show 在那里打印不出差异直到你添加 -m 来选择一个父。分支和合并从不同的角度教你这个相同图的其余部分。