差异文件(diff)和补丁文件(patch)简介

差异文件(diff)和补丁文件(patch)简介

这篇文章介绍差异文件diff和补丁文件patch,以及它们如何在开源项目中使用的例子。

如果你曾有机会在一个使用分布式开发模型的大型代码库上工作过,你就应该听说过类似下面的话,“Sue 刚发过来一个补丁patch”,“Rajiv 正在签出checking out差异diff”, 可能这些词(补丁、差异文件)对你而言很陌生,而你确定很想搞懂他们到底指什么。开源软件对上述提到的名词有很大的贡献,作为大型项目从 Apache web 服务器到 Linux 内核的开发模型,“基于补丁文件的开发” 这一模式贯穿了上述项目的始终。实际上,你可能不知道 Apache 的名字就来自“一系列的代码补丁”(LCTT 译注:Apache 英文发音和补丁的英文 patch 相似),它们被一一收集起来并针对原来的 NCSA HTTPd server source code 进行了修订。

你可能认为这只不过是些逸闻,但是一份早期的 Apache 网站的存档中 声称 Apache 的名字就是来自于最早的“补丁”集合;即“打了补丁的APAtCHy”服务器,简化为 Apache。

好了,言归正传,程序员嘴里说的“差异”和“补丁”到底是什么?

首先,在这篇文章里,我们可以认为这两个术语都指向同一个概念。“diff” 是 ”difference“ 的简写;Unix 下的同名工具程序 diff剖析了一个或多个文件之间的“差异”。下面我们会看到 diff 的例子:

一个“补丁”指的是文件之间一系列差异,这些差异能被 Unix 的 diff 程序应用在源代码树上。我们能使用 diff 工具来创建“差异”(或“补丁”),然后使用该工具将它们 “打” 在一个没有这个补丁的同样的源代码版本上。此外,(我又要开始跑题说些历史轶事了……),“补丁” 这个词真的指在计算机的早期使用打卡机的时候,用来覆盖在打孔纸带上来对软件进行修改的覆盖纸,那个时代打孔纸带就是在计算机处理器上运行的程序。下面来自 维基页面 的这张图真切的描绘了最初的“打补丁”这个词的出处:

差异文件(diff)和补丁文件(patch)简介

现在你对补丁和差异就了一个基本的概念,让我们来看看软件开发者是怎么使用这些工具的。如果你还没有使用过类似于 Gitsubversion 这样的源代码版本控制工具的话,我将会一步步展示最流行的软件项目是怎么使用它们的。如果你将一个软件的生命周期看成是一条时间线的话,你就能看见这个软件的点滴变化,比如在何时源代码加上了一个功能,在何时源代码修复了一个功能缺陷。我们称这些改变的点为“提交commit”,“提交”这个词被当今最流行的源代码版本管理工具 Git 所使用,当你想检查在一个提交前后的代码变化的话,(或者在许多个提交之间的代码变化),你都可以使用工具来观察文件差异。

如果你同样在使用 Git 开发软件的话,你可以在你的本地开发环境做些希望交给别的开发者的提交,以添加到他们的源代码树中。为了给别的开发者你的提交,一个方法就是创建一个你本地文件的差异文件,然后将这个“补丁”发送给和你工作在同一个源代码树的别的开发者。别的开发者在“打”了你的补丁之后,就能看到在你的代码变树上的变化。

Linux、Git 和 GitHub

这种分享补丁的开发模型正是现今 Linux 内核社区如何处理内核修改提议而采用的模型。如果你有机会浏览任何一个主流的 Linux 内核邮件列表 —— 主要是 LKML,也包括 linux-containersfs-develNetdev 等等,你能看到很多开发者会贴出他们想让其他内核开发者审核、测试或者合入 Linux 官方 Git 代码树某个位置的补丁。当然,讨论 Git 不在这篇文章范围之内(Git 是由 Linus Torvalds 开发的源代码控制系统,它支持分布式开发模型以及允许独立于主要代码仓库的补丁包,这些补丁包能被推送或拉取到不同的源代码树上,并遵守这些代码树各自的开发流程。)

在继续我们的话题之前,我们当然不能忽略和补丁和差异这个概念相关的最流行的服务:GitHub。从它的名字就能猜想出 GitHub 是基于 Git 的,而且它还围绕着 Git 对分布式开源代码开发模型提供了基于 Web 和 API 的工作流管理。(LCTT 译注:即拉取请求Pull Request)。在 GitHub 上,分享补丁的方式不是像 Linux 内核社区那样通过邮件列表,而是通过创建一个 拉取请求 。当你提交你自己的源代码树的改动时,你能通过创建一个针对软件项目的共享仓库的“拉取请求”来分享你的代码改动(LCTT 译注:即核心开发者维护一个主仓库,开发者去“复刻fork”这个仓库,待各自的提交后再创建针对这个主仓库的拉取请求,所有的拉取请求由主仓库的核心开发者批准后才能合入主代码库。)GitHub 被当今很多活跃的开源社区所采用,如 KubernetesDocker容器网络接口 (CNI)Istio 等等。在 GitHub 的世界里,用户会倾向于使用基于 Web 页面的方式来审核一个拉取请求里的补丁或差异,你也可以直接访问原始的补丁并在命令行上直接使用它们。

该说点干货了

我们前面已经讲了在流行的开源社区里是怎么应用补丁和差异的,现在看看一些例子。

第一个例子包括一个源代码树的两个不同副本,其中一个有代码改动,我们想用 diff 来看看这些改动是什么。这个例子里,我们想看的是“合并格式unified”的补丁,这是现在软件开发世界里最通用的格式。如果想知道更详细参数的用法以及如何生成差异文件,请参考 diff 手册。原始的代码在 sources-orig 目录,而改动后的代码在 sources-fixed 目录。如果要在你的命令行上用“合并格式”来展示补丁,请运行如下命令。(LCTT 译注:参数 -N 代表如果比较的文件不存在,则认为是个空文件, -a 代表将所有文件都作为文本文件对待,-u 代表使用合并格式并输出上下文,-r 代表递归比较目录)

$ diff -Naur sources-orig/ sources-fixed/

……下面是 diff 命令的输出:

diff -Naur sources-orig/officespace/interest.go sources-fixed/officespace/interest.go


--- sources-orig/officespace/interest.go        2018-08-10 16:39:11.000000000 -0400


+++ sources-fixed/officespace/interest.go       2018-08-10 16:39:40.000000000 -0400


@@ -11,15 +11,13 @@


   InterestRate float64


 }


 


+// compute the rounded interest for a transaction


 func computeInterest(acct *Account, t Transaction) float64 {


 


   interest := t.Amount * t.InterestRate


   roundedInterest := math.Floor(interest*100) / 100.0


   remainingInterest := interest - roundedInterest


 


-  // a little extra..


-  remainingInterest *= 1000


-


   // Save the remaining interest into an account we control:


   acct.Balance = acct.Balance + remainingInterest

最开始几行 diff 命令的输出可以这样解释:三个 --- 显示了原来文件的名字;任何在原文件(LCTT 译注:不是源文件)里存在而在新文件里不存在的行将会用前缀 -,用来表示这些行被从源代码里“减去”了。而 +++ 表示的则相反:在新文件里被加上的行会被放上前缀 +,表示这是在新文件里被“加上”的行。补丁文件中的每一个补丁“块”(用 @@ 作为前缀的的部分)都有上下文的行号,这能帮助补丁工具(或其它处理器)知道在代码的哪里应用这个补丁块。你能看到我们已经修改了“Office Space”这部电影里提到的那个函数(移除了三行并加上了一行代码注释),电影里那个有点贪心的工程师可是偷偷的在计算利息的函数里加了点“料”哦。(LCTT译注:剧情详情请见电影 https://movie.douban.com/subject/1296424/)

如果你想找人来测试你的代码改动,你可以将差异保存到一个补丁里:

$ diff -Naur sources-orig/ sources-fixed/ >myfixes.patch

现在你有补丁 myfixes.patch 了,你能把它分享给别的开发者,他们可以将这个补丁打在他们自己的源代码树上从而得到和你一样的代码并测试他们。如果一个开发者的当前工作目录就是他的源代码树的根的话,他可以用下面的命令来打补丁:

$ patch -p1 < ../myfixes.patch


patching file officespace/interest.go

现在这个开发者的源代码树已经打好补丁并准备好构建和测试文件的修改了。那么如果这个开发者在打补丁之前已经改动过了怎么办?只要这些改动没有直接冲突(LCTT 译注:比如改在同一行上),补丁工具就能自动的合并代码的改动。例如下面的interest.go 文件,它有其它几处改动,然后它想打上 myfixes.patch 这个补丁:

$ patch -p1 < ../myfixes.patch


patching file officespace/interest.go


Hunk #1 succeeded at 26 (offset 15 lines).

在这个例子中,补丁警告说代码改动并不在文件原来的地方而是偏移了 15 行。如果你文件改动的很厉害,补丁可能干脆说找不到要应用的地方,还好补丁程序提供了提供了打开“模糊”匹配的选项(这个选项在文档里有预置的警告信息,对其讲解已经超出了本文的范围)。

相关推荐