简介
git diff 是最常用的 Git 命令之一,但也是最容易被误解的。许多开发者认为它只是显示“改了什么”,但这忽略了关键点:git diff 比较的是两个特定的对象。如果你不清楚是哪两个对象在比较,输出结果可能会误导你。
本文澄清了六种最常见的比较模式,解释了如何解读 diff 输出,并提供了日常开发中的实用技巧。读完本文,你将知道在什么场景下使用哪个变体,以及为什么。

核心概念:关键在于比较基线
官方 git-diff 文档指出,git diff 可以比较:
- 工作区 vs 暂存区
- 暂存区 vs 某个树对象(通常是 HEAD)
- 工作区 vs 某个提交
- 两个任意提交
- 合并结果
- 两个 blob 对象
- 磁盘上的两个文件
在运行任何 git diff 命令之前,问自己:“我在比较哪两个东西?” 答案决定了使用哪个变体,并确保你得到预期的输出。
六种最常见的比较模式
1. 默认 git diff:工作区 vs 暂存区
这是最常见的误解。许多人认为它比较工作区和 HEAD,但实际上它显示的是工作区和暂存区之间的差异。
git diff
这显示了你尚未暂存的更改。如果你已经用 git add 暂存了一些更改,这些更改不会出现在这里。
何时使用:在决定下一步暂存什么之前,审查未暂存的更改。
2. git diff --cached:暂存区 vs HEAD
这将暂存区与最近一次提交(默认是 HEAD)进行比较。它精确显示了如果你现在运行 git commit 将会提交的内容。
git diff --cached
# 或
git diff --staged
何时使用:审查即将提交的精确补丁。
3. git diff HEAD:工作区 + 暂存区 vs HEAD
这结合了未暂存和已暂存的更改,显示自上次提交以来的所有差异。
git diff HEAD
何时使用:获取自上次提交以来所有更改的完整概览。
4. git diff A B 或 git diff A..B:比较两个提交
两种形式都比较两个任意提交的状态。这里的 .. 语法不同于 git log 中的语义,它只是一个分隔符。
git diff main feature
git diff main..feature # 同上
何时使用:查看两个分支或标签之间的差异。
5. git diff A...B:基于共同祖先的比较
这将 A 和 B 的合并基础与 B 进行比较。它显示了 B 从与 A 分叉以来引入了哪些更改。
git diff main...feature
这等价于:
git diff $(git merge-base main feature) feature
何时使用:代码审查时,只看特性分支上的更改,忽略 main 上同时发生的更改。
6. git diff --merge-base:显式指定共同祖先
为了清晰,你可以使用 --merge-base 标志:
git diff --merge-base A B
这在脚本和 CI 流水线中很有用,显式性提高了可读性。
完整示例:比较两个分支
让我们演练一个真实场景。你有一个 main 分支和一个 feature 分支。你想看看 feature 引入了什么。
设置
# 创建两个分支
git init
echo "Initial content" > file.txt
git add file.txt && git commit -m "Initial commit"
# 创建 feature 分支
git checkout -b feature
echo "Feature work" >> file.txt
git add file.txt && git commit -m "Feature commit"
# 回到 main 并添加一个提交
git checkout main
echo "Main work" >> file.txt
git add file.txt && git commit -m "Main commit"
错误方式
git diff main feature
这显示两个分支最新提交之间的差异。它既包含特性工作也包含 main 工作,使得难以隔离特性分支实际添加的内容。
正确方式
git diff main...feature
这只显示 feature 从 main 分叉以来所做的更改。输出干净且聚焦。
输出模式:选择合适的格式
git diff 提供多种输出格式,每种适用于不同需求。
| 模式 | 命令 | 使用场景 |
|---|---|---|
| 补丁(默认) | git diff |
详细审查更改 |
| 统计 | git diff --stat |
快速概览哪些文件更改及行数 |
| 仅文件名 | git diff --name-only |
更改的文件列表(机器友好) |
| 文件名+状态 | git diff --name-status |
文件列表及更改类型(M, A, D, R) |
| 数字统计 | git diff --numstat |
机器友好的统计(每个文件增删行数) |
| 词级差异 | git diff --word-diff |
高亮单个词的变化(适用于文档、配置文件) |
示例:--word-diff
git diff --word-diff
这在审查文档、配置文件或长字符串的更改时特别有用,因为行级差异过于粗糙。
常见陷阱
- 认为默认
git diff与 HEAD 比较:它比较工作区与暂存区。使用git diff HEAD查看总更改。 - 误以为
git diff A..B与git log A..B语义相同:在git diff中,..只是分隔符,结果与git diff A B相同。 - 误解
git diff A...B:它比较共同祖先,而不是直接比较两个最新提交。 - 预测基于时间的令牌时忽略时区:始终使用服务器的时区,而不是本地假设。
- 天真地解析
--name-only输出:使用-z处理包含空格或特殊字符的文件名。
常见问题
git diff 和 git diff HEAD 有什么区别?
git diff 比较工作区与暂存区。git diff HEAD 比较工作区+暂存区与最近一次提交。前者显示未暂存的更改,后者显示自上次提交以来的所有更改。
何时使用 git diff --cached 与 git diff --staged?
它们是同义词。使用你觉得更清晰的即可。--staged 对许多开发者更直观。
如何比较 Git 仓库外的两个文件?
使用 git diff --no-index file1 file2。即使文件不在 Git 仓库中也能工作。
为什么 git diff 显示没有更改,但我明明做了修改?
你可能已经用 git add 暂存了这些更改。使用 git diff --cached 查看已暂存的更改,或 git diff HEAD 查看所有更改。
如何只查看更改的文件列表?
使用 git diff --name-only。为了机器安全,添加 -z 使用 NUL 作为分隔符。
结论
git diff 不是单个命令——它是一个比较工具家族。关键在于知道你在和什么比较。一旦掌握了这里描述的六种模式,你将避免常见陷阱,并自信地使用 git diff。
试试我们的文本差异工具,交互式地可视化两个文本片段之间的差异。