text diff compare developer-tools

文本对比:瞬间比较并找出文本之间的差异

使用我们的在线文本对比工具轻松比较两段文本、文档或代码片段。瞬间高亮显示差异、新增和删除的内容。

简介

每当你打开一个 pull request、审阅文档修订或解决 merge conflict 时,你都在与文本 diff 打交道。diff(difference 的缩写)是两个版本文本之间变化的结构化表示——它展示了哪些内容被添加、哪些被删除、哪些保持不变。

文本 diff 工具是现代软件开发的基础。它们驱动着版本控制系统、代码审查平台、协作工具和部署流水线。理解 diff 的工作原理——不仅仅是如何阅读,还包括其背后算法的运作机制——能让你成为更高效的开发者和更细致的协作者。

本文将带你从 1974 年 Unix diff 的起源,一路探索到当今 git 所使用的现代算法,并详细解释 diff 格式、三路合并、可视化策略以及实用的最佳实践。


diff 的简史

1974 年——diff 的诞生

Doug McIlroy 于 1974 年在贝尔实验室为 Unix 编写了最初的 diff 工具。这是一个划时代的发明:开发者首次可以自动比较两个文本文件,并生成结构化的差异描述。这对于发布软件补丁和追踪源代码变更非常有用。

1984 年——GNU diff

自由软件基金会将 GNU diff 作为 GNU diffutils 的一部分发布,使所有人都能使用这个可移植的改进版本。GNU diff 引入了额外的输出格式——context diff 和 unified diff——这些格式成为了行业标准。

1986 年——Myers 算法

Eugene Myers 于 1986 年发表了他的里程碑论文《An O(ND) Difference Algorithm and Its Applications》。该算法——用于查找最短编辑脚本(Shortest Edit Script,SES)——成为大多数现代 diff 实现(包括 git)的理论基础。

1990 年代——diff/patch 成为通用补丁格式

diffpatch 的组合成为发布软件更新的事实标准。开源项目传播 .patch 文件,贡献者通过邮件列表发送 diff,Linux 内核几乎完全通过邮件的 diff/patch 工作流开发和维护。

2005 年——Git 与 Myers Diff

Linus Torvalds 于 2005 年创建了 git,并采用 Myers 算法作为默认 diff 引擎。Git 的 diff 子系统成为历史上使用最广泛的 diff 实现之一,每天在 GitHub 和 GitLab 等平台上处理数十亿次比较。

2010 年代——Histogram Diff 与网页可视化

Git 引入了 histogram diff 算法作为许多操作的首选默认算法。与此同时,基于网页的 diff 可视化蓬勃发展——GitHub 的分屏和内联 PR diff、GitLab 的审查工具,以及 Gerrit 的变更追踪,都将 diff 输出带给了大量开发者。


Diff 算法详解

LCS——最长公共子序列

计算 diff 的经典方法基于两个序列的最长公共子序列(Longest Common Subsequence,LCS)。LCS 是在两个输入中以相同相对顺序出现的最长元素序列(不一定连续)。

示例:

  • 字符串 A = "ABCBDAB"
  • 字符串 B = "BDCAB"
  • LCS = "BCAB"(长度为 4)

diff 由不属于 LCS 的内容推导:仅在 A 中出现的元素为删除内容,仅在 B 中出现的元素为插入内容。计算 LCS 需要 O(M×N) 的时间和空间,对小文件可接受,但对大文件较慢。

Myers 算法——最短编辑脚本

Eugene Myers 的 1986 年算法寻找最短编辑脚本(SES):将序列 A 转换为序列 B 所需的最少插入和删除次数。这等价于求 LCS,但 Myers 的方法在实践中效率更高。

关键特性:

  • 时间复杂度: O(ND),其中 N = len(A) + len(B),D = 编辑距离(变更数量)
  • 空间复杂度: O(N)(使用线性空间改进版本)
  • 使用**"蛇形"图遍历**——在编辑图中沿对角线路径前进,每条"蛇"代表一段匹配字符序列
  • git、GNU diff 和大多数现代 diff 工具使用

当变更相对于文件大小较小时(低 D 值),Myers 算法表现出色——这正是版本控制中的常见情况:大多数提交只改变文件的一小部分。

Patience Diff——更适合代码结构

Patience diff 采用不同的方法:首先找出唯一行(在两个文件中各只出现一次的行),将其作为锚点,然后递归地对各段之间的内容进行 diff。

这对包含大量相同结构行的代码——如 }{return 或空行——效果显著更好。Myers 可能匹配到错误的右花括号,而 patience diff 会锚定在独特的、有意义的行上,产生更易理解的 diff。

Patience diff 被 BazaarMercurial 使用,在 git 中可通过 git diff --diff-algorithm=patience 使用。

Histogram Diff——Git 的现代默认算法

Histogram diff 是 patience diff 的演进版本。它构建行频率直方图,并利用这些频率信息做出更智能的匹配决策。出现次数多的行不太可能成为有意义的锚点;罕见的行才是更好的候选。

Git 引入了 histogram diff,自 2012 年前后在许多场景中成为推荐的默认算法。可通过以下命令全局配置:

git config --global diff.algorithm histogram

Diff 输出格式

普通 diff 格式(原始 Unix 格式)

diff file1.txt file2.txt 生成的原始输出格式:

2d1
< 仅在 file1 中的行
5,7c4,6
< 旧行 A
---
> 新行 A

2d1(从 file1 删除第 2 行)和 5,7c4,6(将 file1 的第 5-7 行改为 file2 的第 4-6 行)等命令使该格式适合机器读取,但对人类来说难以理解。

Context Diff(-c 标志)

GNU diff 引入的 context diff 通过添加周围行来增强可读性,用 ! 标记更改行,用 *** / --- 分隔新旧块。

Unified Diff 格式(-u 标志)——标准格式

Unified diff 格式是现代标准,被 git 和所有主要的补丁工作流使用。它将新旧内容合并在单个块中,使用 +- 标记更改,并包含标识每个更改位置的 hunk 头部(hunk header)

git diff 输出

Git 的输出是带有附加元数据的 unified diff——包括文件模式、索引哈希和标识仓库路径的 diff --git 头部。


深入理解 Unified Diff 格式

让我们详细解码 unified diff 格式:

--- a/config.py
+++ b/config.py
@@ -10,7 +10,8 @@
 DATABASE_HOST = 'localhost'
 DATABASE_PORT = 5432
-DATABASE_NAME = 'myapp_dev'
+DATABASE_NAME = 'myapp_production'
+DATABASE_SSL = True
 
 # Cache settings
 CACHE_TTL = 300

文件头部

--- 标记原始文件(版本 A);+++ 标记文件(版本 B)。在 git 中,a/b/ 是约定的前缀。

Hunk 头部

@@ -10,7 +10,8 @@

这是 hunk 头部,精确告诉你这段变更在文件中的位置:

  • -10,7 → 在原始文件中,此 hunk 从第 10 行开始,跨越 7 行
  • +10,8 → 在文件中,此 hunk 从第 10 行开始,跨越 8 行(新增了一行)

格式始终为 @@ -起始行,行数 +起始行,行数 @@

行标记

hunk 正文中的每一行以以下三种字符之一为前缀:

  • (空格)——上下文行:未更改,为可读性而显示
  • -(减号)——删除行:存在于原始文件,不存在于新文件
  • +(加号)——添加行:不存在于原始文件,存在于新文件

在示例中,DATABASE_NAME = 'myapp_dev' 被删除并替换为生产环境名称,DATABASE_SSL = True 是全新添加的行。该 hunk 在原始文件中跨越 7 行(1 个删除 + 6 个上下文),在新文件中跨越 8 行(2 个添加 + 6 个上下文)。


行级 vs 词级 vs 字符级 Diff

标准 diff 在行级别运作——每行被视为原子单元。这对源代码很理想,因为行是自然的变更单元。

词级 Diff

对于散文、文档或配置文件,词级 diff 更具信息量。考虑以下变更:

变更前: The quick brown fox jumps over the lazy dog
变更后: The quick red fox leaps over the sleeping cat

行级 diff 会将整行显示为已更改。词级 diff 精确高亮显示发生变化的部分:

The quick brown red fox jumps leaps over the lazy dog sleeping cat

Git 通过 git diff --word-diff 支持词级 diff。

字符级 Diff

字符级 diff(使用 Levenshtein 距离等算法)在单个字符级别工作。最适合短字符串——密码、标识符、配置值——即使单个字符的变化也很重要的场景。

比较表格

方法 粒度 最适用于 工具示例
Line diff 源代码 git diff
Word diff 散文/文档 git diff --word-diff
Char diff 字符 短字符串 Levenshtein 算法
Semantic diff AST 节点 代码重构 difftastic

Semantic diff 工具(如 difftastic)将源代码解析为抽象语法树(AST),对树结构进行 diff,而不是原始文本,从而生成理解语言语法、忽略空白等表面变化的 diff。


三路合并与合并冲突

三路合并模型

当两个人独立修改同一文件时,简单的双向 diff 无法确定应该采用哪一方的变更。Git 使用三路合并(three-way merge)

  1. Base——共同祖先提交
  2. Ours——当前分支的版本
  3. Theirs——传入分支的版本

算法将 ourstheirs 都与 base 进行比较:

  • ours 更改了某区域 → 使用 ours
  • theirs 更改了某区域 → 使用 theirs
  • 两者都对同一区域进行了不同的更改 → 冲突(conflict)

合并冲突标记

当 git 无法自动解决冲突时,它会在文件中插入标记:

<<<<<<< HEAD
DATABASE_NAME = 'myapp_production'
=======
DATABASE_NAME = 'myapp_staging'
>>>>>>> feature/staging-config
  • <<<<<<<======= 之间的内容是你的版本(HEAD)
  • =======>>>>>>> 之间的内容是传入的版本
  • 你必须手动编辑文件以解决冲突,然后执行 git add

使用场景

代码审查

Diff 是代码审查的语言。GitHub、GitLab 和 Bitbucket 上的 pull request 都以 diff 形式呈现变更,让审阅者能够逐行了解确切的变化。小型、聚焦的 diff 显著提高审查质量和速度。

文档比较

法律团队使用 diff 工具比较合同修订版本。技术写作者使用它们审查文档变更。任何涉及版本化文档的工作流都能从结构化 diff 输出中受益。

日志分析

系统管理员比较日志文件以识别运行之间的变化——新错误、缺失条目、配置漂移。diffcolordiff 等工具是系统管理员工具箱的标准配置。

法律与合规

监管提交、审计跟踪和合规文件通常需要版本之间变更的正式记录。Diff 工具提供了关于何时、如何以及具体发生了什么变化的客观、可重现记录。

安全分析

安全研究人员通过对比配置快照和系统状态来检测未经授权的更改。文件完整性监控系统就是建立在 diff 原理之上的。


可视化方式

并排视图(分屏视图)

两个面板并排显示旧版和新版,在对应行中高亮显示变更。最适合大型变更,在两侧都需要上下文的情况下很有帮助。

内联视图(统一视图)

删除和添加以单一流显示,与上下文行交错。这是大多数命令行工具和 GitHub PR 视图的默认方式。最适合密集的小型变更。

GitHub PR 视图

GitHub 通过语法高亮、可展开上下文、行内审查评论、分屏切换以及每文件"已查看"追踪来增强 unified diff,使大型 pull request 对审阅者易于导航。

词级差异高亮

git diff --word-diff=color 等工具在行内高亮显示变更的词汇,使字符级变化在行级 diff 上下文中可见,对配置文件和散文文档特别有用。


最佳实践

  1. 保持提交小而专注。 只改变一件逻辑事项的 diff 比触及数十个文件的 diff 更容易审查。

  2. 编写有意义的提交信息。 Diff 展示变更了什么;提交信息解释为什么变更。

  3. 使用正确的 diff 算法。 对于代码,histogram 或 patience diff 通常比 Myers 产生更可读的输出。全局配置:git config --global diff.algorithm histogram

  4. 提交前审查 diff。 git diff --staged 显示将要提交的确切内容。在运行 git commit 之前始终阅读它。

  5. 散文使用词级 diff。 编写文档或 README 文件时,git diff --word-diff 比行级 diff 更易读。

  6. 理解 hunk 上下文。 每个 hunk 周围的三行上下文是有原因的——它们帮助你在上下文中理解变更。审查时不要跳过上下文行。

  7. 谨慎解决冲突。 在不理解对方变更的情况下,不要接受冲突的任何一方。两个变更可能都很重要。

  8. 对二进制文件使用 .gitattributes 告诉 git 如何处理二进制和特殊文件,以避免无意义的 diff。


常见问题

问:diffpatch 有什么区别?
答:diff 比较两个文件并生成 diff 输出。patch 接收该 diff 输出并将其应用于文件以重现变更。它们是互补的工具,设计为协同工作。

问:git 默认使用哪种 diff 算法?
答:Git 默认使用 Myers 算法,但推荐使用 histogram diff:git config --global diff.algorithm histogram

问:@@ -10,7 +10,8 @@ 是什么意思?
答:hunk 在两个文件中都从第 10 行开始。在旧文件中覆盖 7 行;在新文件中覆盖 8 行(新增了一行)。

问:可以对二进制文件进行 diff 吗?
答:标准 diff 工具对文本进行操作。对于二进制文件,存在专门的工具(如 bsdiff)。大多数 diff 工具只会报告"Binary files differ"(二进制文件不同)。

问:什么是"hunk"?
答:hunk 是 diff 中连续的变更块,包括周围的上下文行。如果变更分布在文件的各处,单个 diff 可以包含多个 hunk。

问:为什么 git 有时会对移动的代码产生令人困惑的 diff?
答:标准行级 diff 没有"移动"的概念——它只看到添加和删除。被移动的代码会在一处显示为已删除,在另一处显示为已添加。difftastic 等理解 AST 结构的工具可以检测到移动操作。

问:什么是三路合并?
答:一种使用共同祖先(base)以及两个变更版本来智能合并变更的策略,自动解决非冲突编辑,并标记真正的冲突。


理解文本 diff 不仅仅是技术上的好奇心——对于任何长期处理文本、代码或文档的人来说,这都是一项基本技能。从 Unix diff 命令的优雅简洁,到驱动现代代码审查平台的复杂算法,这个不起眼的 diff 工具已经塑造了软件构建、审查和维护的方式长达五十余年。