原作者:阮一峰。原文发表于 2012 年 8 月 29 日,见《读懂diff》。本文核对整理,保留原例,补充了范围记法和 Git 对象的编辑校注。
diff 是 Unix 中比较文本文件差异的工具,也是版本管理的重要基础。按“旧文件在前、新文件在后”的顺序执行,它就会描述怎样把前一个文件变成后一个文件:
diff f1 f2
读 diff,关键是分清三件事:比较的是哪两个文件,变动发生在哪些行,哪些内容被删除或添加。普通格式、上下文格式和统一格式采用的标记不同,但都在回答这些问题。

先准备两个容易比较的文件
原文的第一个文件叫 f1,共有七行,每行都是字母 a:
a
a
a
a
a
a
a
第二个文件 f2 只把第 4 行改成 b:
a
a
a
b
a
a
a
下面的所有输出都在描述这一次修改。代码块中的输出取自原文并修整了显示空格,不代表本次运行了命令。
普通格式:先看变动指令
diff f1 f2 的普通格式输出是:
4c4
< a
---
> b
4c4 可以拆成三部分:左边的 4 是旧文件的行号;c 表示 change,即修改;右边的 4 是新文件的行号。因此,这里是在说“旧文件第 4 行变成新文件第 4 行”。其他指令还有 a(添加)和 d(删除)。
< a 表示旧文件里被移除的那行内容是 a;中间的 --- 分隔两侧;> b 表示新文件增加的内容是 b。尖括号是输出标记,不属于文件本身。
上下文格式:把附近几行一起显示
普通格式能准确描述差异,却不容易看出修改所在的上下文。加上 -c 后,diff 会把附近未改变的行也列出来:
diff -c f1 f2
*** f1 2012-08-29 16:45:41.000000000 +0800
--- f2 2012-08-29 16:45:51.000000000 +0800
***************
*** 1,7 ****
a
a
a
! a
a
a
a
--- 1,7 ----
a
a
a
! b
a
a
a
前两行是文件头:*** 标记旧文件,--- 标记新文件,后面跟文件名和修改时间。原文中的时间只是示例元数据,不影响差异的含义。
接下来的一行星号开始一个差异区块,通常称作 hunk。*** 1,7 **** 引出旧文件的第 1 至第 7 行;--- 1,7 ---- 引出新文件的第 1 至第 7 行。默认上下文是修改位置的前后三行,所以本例七行都显示出来。
编辑校注:上下文格式这里的 1,7 是“起始行、结束行”,不是通用的“起始行、行数”。原例恰好从第 1 行开始,两种说法数值相同,容易造成误会。后面统一格式的 1,7 才表示“从第 1 行起,共 7 行”。
内容行的前缀也有区别:两个空格表示未变的上下文,! 表示发生修改的行,- 表示从旧文件删除,+ 表示在新文件添加。前缀后的内容才是真正的文件文本。
统一格式:同一份上下文只显示一次
两个文件很相似时,上下文格式会把大量相同的内容显示两遍。原文称 unified diff 为“合并格式”;这里采用更常见的“统一格式”,它并不意味着执行了分支合并。
diff -u f1 f2
--- f1 2012-08-29 16:45:41.000000000 +0800
+++ f2 2012-08-29 16:45:51.000000000 +0800
@@ -1,7 +1,7 @@
a
a
a
-a
+b
a
a
a
文件头用 --- 表示旧文件,用 +++ 表示新文件。区块头 @@ -1,7 +1,7 @@ 说明,这个区块在旧文件中从第 1 行开始、覆盖 7 行,在新文件中也从第 1 行开始、覆盖 7 行。
正文只保留一份上下文:首列空格表示两边都有这一行,减号表示旧文件有、新文件没有,加号表示新文件有、旧文件没有。本例把 a 改成 b,就是先显示 -a,再显示 +b。
读取行数时,不要把 diff 的显示行数直接当作文件行数。旧文件的 7 行等于 6 行上下文加 1 行删除;新文件的 7 行等于同样的 6 行上下文加 1 行新增。文件头的 ---、+++ 也不是删掉或增加了三行。
Git diff:在统一格式前加上对象信息
Git 使用统一格式的一种扩展。原文示例执行 git diff,得到:
diff --git a/f1 b/f1
index 6f8a38c..449b072 100644
--- a/f1
+++ b/f1
@@ -1,7 +1,7 @@
a
a
a
-a
+b
a
a
a
第一行表明这是一份 Git 补丁,比较的是旧侧 a/f1 和新侧 b/f1。a/ 与 b/ 是区分两侧的路径前缀,并不要求磁盘上真的存在两个同名目录。
index 6f8a38c..449b072 100644 给出两侧文件内容对象的缩写标识,以及未改变的文件模式。100644 表示普通非可执行文件;它不是文件内容,也不是本次执行的权限修改命令。
编辑校注:这里的哈希是文件内容对应的 blob 对象标识,不能泛称为两个提交的 commit ID。没有参数的 git diff 通常比较工作区与暂存区;换成 git diff --cached 或指定提交后,比较两侧会随命令变化。原文关于“index 区域与工作目录”的解释只适用于它展示的命令。
后续的文件头、区块头以及加减号,与上一节统一格式的读法相同。先定位文件,再读区块范围,最后核对增删行,就能把一份补丁拆成可检查的小部分。
本文的边界与检查记录
这篇文章讲的是文本差异的表示法,不覆盖二进制补丁、合并冲突,也不能仅凭一份 diff 判断补丁可否安全应用。本次只做文本与命令的静态检查,没有执行 diff、git diff 或应用任何补丁。示例没有硬编码秘密、下载执行或删除文件操作;未发现这些问题不等于补丁内容没有漏洞。
补充核对依据为 GNU Diffutils 手册和 Git diff 格式文档。原文还列出了 Wikipedia 的 diff 条目、How to read a patch or diff 和 Stack Overflow 相关讨论作为延伸阅读。
版权与归属:原文署名阮一峰,原站声明为CC BY-NC-ND 3.0。本整理版的编辑性改写经另行授权;原站公开许可证不因此改变。示意图由未完纪绘制。
原文补充的历史背景:正常格式用于早期AT&T Unix;20世纪80年代初BSD加入上下文格式;1990年GNU diff推出统一格式,以避免重复显示相同上下文。












暂无评论内容