一、引言
在软件开发、系统运维和版本控制中,Diff 和 Patch 是两个最基础也最强大的工具组合。Diff 负责“找出差异”,生成一份机器可读、人类可理解的变更描述;Patch 则负责“应用差异”,将这份描述精确地还原到目标文件上。无论是 Linux 内核的邮件列表补丁、Git 的代码审查,还是传统软件分发中的 hotfix,背后都是这对工具在支撑。
本文将从底层格式出发,逐层拆解 Diff 的三种输出格式,重点剖析现代工程中最常用的 Unified Diff,并配合 Patch 命令的实战用法,帮助读者建立完整的知识体系。
二、Diff 的三种输出格式
GNU Diffutils 提供了三种主要的输出格式,分别适用于不同的场景和工具链。
2.1 Normal Diff(普通格式)
最原始的输出,不带上下文,仅通过行号和操作指令描述变更。
2c2
< This is a test.
---
> This is a new test.
3a4
> Added this line.
2c2:第2行被修改(c= change)3a4:在第3行后追加(a= append),新内容位于第4行<:旧文件内容>:新文件内容---:分隔符
缺点:没有上下文,定位困难,现代工作流中已极少使用。
2.2 Context Diff(上下文格式)
通过 -c 选项生成,提供显式的旧/新文件分离视图,兼容老旧的 Patch 工具。
*** old.txt
--- new.txt
***************
*** 1,4 ****
Hello, world.
! This is a test.
Line three here.
Goodbye.
--- 1,5 ----
Hello, world.
! This is a new test.
Line three here.
+ Added this line.
Goodbye.
***开头的块代表旧文件---开头的块代表新文件!:修改的行+:新增的行- 空格:未变更的上下文行
缺点:冗余度高,同一处变更需要展示两次,体积较大。
2.3 Unified Diff(统一格式)
通过 -u 选项生成,是现代版本控制(Git、SVN)和代码审查平台(GitHub、GitLab)的事实标准。它将旧文件和新文件合并到同一个 hunk 中,用前缀符号区分操作,极大提升了可读性和紧凑性。
--- old.txt
+++ new.txt
@@ -1,4 +1,5 @@
Hello, world.
-This is a test.
+This is a new test.
Line three here.
+Added this line.
Goodbye.
优点:紧凑、可读性强、上下文丰富、被 patch 和 git apply 原生支持。
三、Unified Diff 格式深度解析
Unified Diff 由 文件头(Header) 和 差异块(Hunk) 组成。理解每一行的含义,是阅读和手工编写 Patch 的基础。
3.1 文件头
--- from-file 2024-01-15 10:00:00.000000000 +0800
+++ to-file 2024-01-15 11:30:00.000000000 +0800
---:标识旧文件(原始版本)+++:标识新文件(修改后版本)- 后面通常跟随文件名和时间戳,可通过
-L选项自定义标签
3.2 Hunk 头(Chunk Header)
@@ -start_old,count_old +start_new,count_new @@
例如 @@ -1,4 +1,5 @@ 表示:
- 旧文件从第1行开始,共涉及4行
- 新文件从第1行开始,共涉及5行
特殊规则:
- 如果行数为1,可省略计数,如
@@ -3 +3 @@表示仅涉及第3行 - 如果行数为0,显示为
,0,表示此处被完全删除或从无到有新增
3.3 行前缀语义
| 前缀 | 含义 | 来源文件 |
|---|---|---|
| 空格 | 上下文行(未变更) | 双方共有 |
- |
删除行 | 仅存在于旧文件 |
+ |
新增行 | 仅存在于新文件 |
\ |
无换行符标记 | 文件末尾无 \n |
3.4 多 Hunk 示例
当文件存在多处不相邻的变更时,Diff 会生成多个独立的 Hunk:
--- config.py
+++ config.py
@@ -10,7 +10,7 @@
"host": "localhost",
- "port": 8080,
+ "port": 3000,
"debug": True,
}
@@ -45,6 +45,8 @@
def init_db():
conn = create_connection()
+ setup_migrations()
+ verify_schema()
return conn
每个 Hunk 都是自包含的,包含足够的上下文(默认3行)供 Patch 工具定位。
四、Patch 命令详解
Patch 是 Diff 的互补工具,负责将 Diff 输出(Patch 文件)应用到目标文件。它不仅能精确匹配,还具备模糊匹配能力,允许在目标文件行号发生偏移时依然正确应用。
4.1 创建 Patch 文件
# 单文件
diff -u old.txt new.txt > fix.patch
# 整个目录(递归、统一格式、包含新文件)
diff -ruN original_dir/ modified_dir/ > project.patch
4.2 应用 Patch
# 方式一:标准输入
patch < fix.patch
# 方式二:指定文件
patch -p0 -i fix.patch
# 方式三:应用到指定输出文件
patch original.txt -i fix.patch -o updated.txt
关键选项 -p#:控制路径剥离层级。Patch 文件头中通常包含完整路径(如 --- a/src/main.c),-p1 会剥离第一层目录(a/),使 Patch 工具在当前目录的 src/main.c 上操作。Git 生成的 Patch 通常需要 -p1。
4.3 撤销 Patch(反向应用)
patch -p0 -R -i fix.patch
-R 选项将新增行视为删除、删除行视为新增,完美回滚。
4.4 目录级 Patch
# 创建
diff -ruN project_v1/ project_v2/ > v1_to_v2.patch
# 应用(在项目根目录执行)
patch -p1 < v1_to_v2.patch
4.5 常用选项速查
| 选项 | 作用 |
|---|---|
-b |
应用前备份原文件(生成 .orig) |
-i |
从指定文件读取 Patch,而非 stdin |
-pN |
剥离路径前 N 层目录 |
-R |
反向应用(撤销) |
-s |
静默模式,仅报错时输出 |
-N |
忽略已存在的文件(配合目录级 Patch) |
--dry-run |
模拟应用,不实际修改文件 |
五、Diff/Patch 工作流程与格式结构
以下 Mermaid 图表展示了 Diff 与 Patch 的协作流程,以及 Unified Diff 的内部结构。
5.1 Diff 与 Patch 协作流程
flowchart TD
A[原始文件 v1] -->|diff -u| B[生成 Patch 文件]
C[修改后文件 v2] -->|diff -u| B
B -->|传输/存储| D[Patch 文件]
E[目标环境<br/>文件 v1'] -->|patch -p1| F[应用 Patch]
D --> F
F -->|成功| G[目标文件变为 v2']
F -->|失败/冲突| H[生成 .rej 文件<br/>手动解决]
H -->|修复后| I[git am --resolved<br/>或重新 patch]
5.2 Unified Diff 内部结构
graph TB
subgraph "Patch File"
H1["文件头<br/>--- old_file<br/>+++ new_file"]
H2["Hunk 头<br/>@@ -l,s +l,s @@"]
H3["上下文行<br/> unchanged"]
H4["删除行<br/>- deleted"]
H5["新增行<br/>+ inserted"]
H6["上下文行<br/> unchanged"]
H7["下一个 Hunk..."]
end
H1 --> H2
H2 --> H3
H3 --> H4
H4 --> H5
H5 --> H6
H6 -->|更多变更| H7
H7 --> H2
5.3 三种 Diff 格式对比
graph BT
subgraph Normal["Normal Diff"]
N1["2c2"] --> N2["old ←"]
N2 --> N3["---"]
N3 --> N4["→ new"]
end
subgraph Context["Context Diff"]
C1["*** old"] --> C2["--- new"]
C2 --> C3["*** l,l ****"]
C3 --> C4["! changed"]
C4 --> C5["--- l,l ----"]
C5 --> C6["! changed"]
end
subgraph Unified["Unified Diff"]
U1["--- old"] --> U2["+++ new"]
U2 --> U3["@@ -l,s +l,s @@"]
U3 --> U4["context"]
U4 --> U5["- removed"]
U5 --> U6["+ added"]
U6 --> U7["context"]
end
style Unified fill:#e6f3ff,stroke:#333,stroke-width:2px
六、实际应用场景
6.1 开源社区的邮件补丁
Linux 内核和许多 GNU 项目仍通过邮件列表传递 Patch。开发者使用 git format-patch 生成 Unified Diff 格式的 .patch 文件,邮件正文直接粘贴,维护者通过 git am 或 patch -p1 应用。
6.2 代码审查与 Git 差异
Git 默认使用 Unified Diff 的变体。git diff 的输出可直接作为 Patch 文件使用:
git diff HEAD~1 > my_changes.patch
git apply my_changes.patch
6.3 软件分发与热修复
在无法使用完整版本控制系统的环境中(如嵌入式设备、生产服务器),运维人员通过 diff -ruN 生成两个软件包之间的 Patch,在目标机器上通过 patch 命令升级,无需重新部署整个包。
6.4 冲突解决
当 git am 或 patch 应用失败时,可使用以下策略:
# 应用不冲突部分,生成 .rej 文件记录冲突
git apply --reject fix.patch
# 手动解决 .rej 文件中的冲突
# 删除 .rej 文件后
git add -A
git am --resolved
七、总结
Diff 和 Patch 是 Unix 哲学中“只做一件事,并把它做好”的典范。理解 Unified Diff 的格式细节——从文件头到 Hunk 头,从行前缀到上下文机制——不仅能帮助开发者更高效地阅读代码变更,还能在自动化脚本、CI/CD 流程和系统运维中游刃有余地处理文件差异。
掌握 diff -u 的生成与 patch -pN 的应用,是每一位工程师从“会用 Git”迈向“理解版本控制底层”的关键一步。
原文 https://blog.csdn.net/2301_79518550/article/details/148478729