// Linux · 2025-06-09

Diff 与 Patch:从格式详解到工程实践

一、引言

在软件开发、系统运维和版本控制中,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/>&#64;&#64; -l,s +l,s &#64;&#64;"]
        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