git diff 从入门到精通
从三个区域模型出发,拆解 git diff 的默认行为、区间语义、输出格式,以及那些让人困惑的设计选择。
前置知识:三个区域
理解git diff之前,必须先理解 Git 的三个状态区域:
工作区 暂存区 本地仓库 (Working Directory) (Staging / Index) (HEAD) ↓ ↓ ↓ 你编辑文件的地方 git add 进来的地方 git commit 进来的地方| 区域 | 是什么 | 怎么进 | 怎么出 |
|---|---|---|---|
| 工作区 | 磁盘上的实际文件 | 你保存编辑器 | git add复制到暂存区 |
| 暂存区 | 下次提交的"待提交清单" | git add | git commit变成新 HEAD |
| HEAD | 当前分支最后一次提交的快照 | git commit | 只能被下一次 commit 覆盖 |
有了这个模型,三个核心 diff 命令就一目了然:
gitdiff# 工作区 ←→ 暂存区gitdiff--staged# 暂存区 ←→ HEADgitdiffHEAD# 工作区 ←→ HEAD为什么git diff默认比较工作区和暂存区
这个默认行为让很多人困惑。直觉上你会觉得默认应该比较「工作区和上一次提交」,因为那才是"我改了什么"。
但 Git 的设计哲学不同:Git 假设你git add时已经检查过那些改动了,“确认没问题,准备提交”。所以git diff默认展示的是"你还没确认的那部分"——add 之后又改了什么,帮你决定要不要再 add 一次。
真正常用的提交预览其实是git diff --staged:
# "看看我即将提交什么"——这才是你最常用的gitdiff--staged# 设个别名省事gitconfig--globalalias.ds'diff --staged'一句话:git add= 勾选,git diff= 还没勾的,git diff --staged= 已经勾了的。习惯了这个心理模型就不别扭了。
工作区、暂存区、HEAD 状态演练
假设你创建一个新文件333.txt,内容333,然后git add:
# 修改文件 333.txt → git add 333.txtgitdiff# 空:工作区和暂存区内容一致gitdiff--staged# 有输出:暂存区有 333.txt,HEAD 没有gitdiffHEAD# 有输出:工作区有这个文件,HEAD 没有三步的状态图:
工作区 暂存区 HEAD 333.txt ──(相同)──▶ 333.txt ──(有差异)──▶ 无此文件git diff为空是因为 add 之后你没再改过文件——工作区和暂存区完全一致。git diff --staged展示的是「暂存了但还没提交」,这才是你下一步的动作预览。
未跟踪文件:diff 看不到
git diff只比较已跟踪内容,未跟踪文件不会出现在任何 diff 输出中。想看未跟踪的文件只能用git status。
如果一定要让 diff 包含未跟踪文件,可以用--intent-to-add假装暂存一个空版本:
gitadd-Nuntracked-file.txt# 暂存一个空占位gitdiff# 现在能看到差异了这不是日常操作——只是让你知道有这条路。
区间比较:两点 vs 三点
基础语法
gitdiffA..B# 从 A 到 B(A 之后的变化)gitdiffA...B# 从共同祖先到 B(B 分支独有的变化)gitdiffA# 省略第二个参数,默认对比工作区:A vs 当前工作区闭区间还是开区间
A..B是左开右闭(A, B]:以 A 为基准线,A 自己的改动不包含,A 之后到 B 的改动包含。
A 的改动 commit C commit D B 的改动 ✗ ✓ ✓ ✓想包含 A 的改动,基准线往前挪一位:
gitdiffA~1..B# A 的改动也被算进去了两点 vs 三点在有分支时才有区别
线性历史上..和...结果相同。分支场景下...才有特殊含义:
# 从 main 分叉出去后,feature 分支上独有的变化gitdiffmain...feature-branch# feature 分支从分叉点到现在的全部变化(含 main 合进来的)gitdiffmain..feature-branch...常用于 PR review:“别人在这个分支上到底改了啥,去掉 main 上混入的”。
实用输出格式
快速浏览
# 只看改了哪些文件、改了多少行gitdiff--stat# 额外标注新增/删除/重命名gitdiff--stat--compact-summary# 只看文件名gitdiff--name-only控制上下文行数
# 默认 3 行上下文gitdiff# 精简到 1 行gitdiff--unified=1# 或 -U1# 只显示改动行,完全不要上下文gitdiff-U0其他有用选项
# 只看新增的行(过滤掉整个 diff 的元信息)gitdiff|grep'^+'|grep-v'^+++'# 忽略空白变化gitdiff-w# 单词级别的 diff(改动浓缩到一行内)gitdiff--word-diff=plain# 按文件类型过滤gitdiff--'*.py'gitdiff-- src/高级用法
查看最近 N 个提交的累计差异
# 最近 3 个提交 + 当前未暂存修改gitdiffHEAD~3# 只看最近 3 个提交(不含未暂存)gitdiffHEAD~3..HEAD指定文件在某区间内的变化
gitdiffmain..HEAD -- path/to/file.py查看某次提交本身做了什么
gitshow<commit-hash># 等价于gitdiff<commit-hash>~1..<commit-hash>查看暂存区中某个文件的改动
gitdiff--staged-- path/to/file.py分支合并前的预览
# 合入 main 会带进去什么gitdiffmain...feature-branch# 如果有冲突,只看冲突文件gitdiff--name-only --diff-filter=U比较两个分支的文件差异(不看内容)
gitdiff--name-status main..feature# 输出每行:A/M/D + 文件名检查是否有改动(脚本中常用)
gitdiff--quiet# 工作区干净 → exit 0;有改动 → exit 1gitdiff--quiet--staged# 同上但检查暂存区常见场景速查
| 你想看什么 | 命令 |
|---|---|
| 改了还没 add 的 | git diff |
| add 了还没 commit 的 | git diff --staged |
| 所有还没 commit 的 | git diff HEAD |
| 上一次 commit 改了什么 | git show HEAD |
| 最近 3 个 commit 总共改了啥 | git diff HEAD~3 |
| 某两个 commit 之间的差异 | git diff A..B |
| PR 里这个分支独有改动 | git diff main...feature |
| 只列文件名和统计 | git diff --stat --compact-summary |
| 只看 .py 文件的改动 | git diff -- '*.py' |
总结
git diff的复杂性来自 Git 的三区域模型——工作区、暂存区、HEAD 各司其职。一旦理解了这个模型,各种 diff 变体只是"选两个点做比较"而已。
日常只用三个命令就够了:
gitdiff--staged# 提交前最后看一眼gitdiffHEAD~3# 回顾最近的改动gitdiffmain...HEAD# 分支合并前确认剩下的--stat、-U0、--word-diff是调味料,按需加。
