差異檔案(diff)和補丁檔案(patch)簡介

發表於2018-09-15

差異檔案(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 工具來建立“差異”(或“補丁”),然後使用該工具將它們 “打” 在一個沒有這個補丁的同樣的原始碼版本上。此外,(我又要開始跑題說些歷史軼事了……),“補丁” 這個詞真的指在計算機的早期使用打卡機的時候,用來覆蓋在打孔紙帶上來對軟體進行修改的覆蓋紙,那個時代打孔紙帶就是在計算機處理器上執行的程式。下面來自 維基頁面 的這張圖真切的描繪了最初的“打補丁”這個詞的出處:

現在你對補丁和差異就了一個基本的概念,讓我們來看看軟體開發者是怎麼使用這些工具的。如果你還沒有使用過類似於 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 命令的輸出:

最開始幾行 diff 命令的輸出可以這樣解釋:三個 --- 顯示了原來檔案的名字;任何在原檔案(LCTT 譯註:不是原始檔)裡存在而在新檔案裡不存在的行將會用字首 -,用來表示這些行被從原始碼裡“減去”了。而 +++ 表示的則相反:在新檔案裡被加上的行會被放上字首 +,表示這是在新檔案裡被“加上”的行。補丁檔案中的每一個補丁“塊”(用 @@ 作為字首的的部分)都有上下文的行號,這能幫助補丁工具(或其它處理器)知道在程式碼的哪裡應用這個補丁塊。你能看到我們已經修改了“Office Space”這部電影裡提到的那個函式(移除了三行並加上了一行程式碼註釋),電影裡那個有點貪心的工程師可是偷偷的在計算利息的函式里加了點“料”哦。(LCTT譯註:劇情詳情請見電影 https://movie.douban.com/subject/1296424/)

如果你想找人來測試你的程式碼改動,你可以將差異儲存到一個補丁裡:

現在你有補丁 myfixes.patch 了,你能把它分享給別的開發者,他們可以將這個補丁打在他們自己的原始碼樹上從而得到和你一樣的程式碼並測試他們。如果一個開發者的當前工作目錄就是他的原始碼樹的根的話,他可以用下面的命令來打補丁:

現在這個開發者的原始碼樹已經打好補丁並準備好構建和測試檔案的修改了。那麼如果這個開發者在打補丁之前已經改動過了怎麼辦?只要這些改動沒有直接衝突(LCTT 譯註:比如改在同一行上),補丁工具就能自動的合併程式碼的改動。例如下面的interest.go 檔案,它有其它幾處改動,然後它想打上 myfixes.patch 這個補丁:

在這個例子中,補丁警告說程式碼改動並不在檔案原來的地方而是偏移了 15 行。如果你檔案改動的很厲害,補丁可能乾脆說找不到要應用的地方,還好補丁程式提供了提供了開啟“模糊”匹配的選項(這個選項在文件裡有預置的警告資訊,對其講解已經超出了本文的範圍)。

如果你使用 Git 或者 GitHub 的話,你可能不會直接使用補丁或差異。Git 已經內建了這些功能,你能使用這些功能和共享一個原始碼樹的其他開發者互動,拉取或合併程式碼。Git 一個比較相近的功能是可以使用 git diff 來對你的原生程式碼樹生成全域性差異,又或者對你的任意兩次”引用“(可能是一個代表提交的數字,或一個標記或分支的名字,等等)做全域性補丁。你甚至能簡單的用管道將 git diff 的輸出到一個檔案裡(這個檔案必須嚴格符合將要被使用它的程式的輸入要求),然後將這個檔案交給一個並不使用 Git 的開發者應用到他的程式碼上。當然,GitHub 把這些功能放到了 Web 上,你能直接在 Web 頁面上檢視一個拉取請求的檔案變動。在 Web 上你能看到所展示的合併差異,GitHub 還允許你將這些程式碼改動下載為原始的補丁檔案。

總結

好了,你已經學到了”差異“和”補丁“是什麼,以及在 Unix/Linux 上怎麼使用命令列工具和它們互動。除非你還在像 Linux 核心開發這樣的專案中工作而使用完全基於補丁檔案的開發方式,你應該會主要通過你的原始碼控制系統(如 Git)來使用補丁。但熟悉像 GitHub 這樣的高階別工具的技術背景和技術底層對你的工作也是大有裨益的。誰知道會不會有一天你需要和一個來自 Linux 世界郵件列表的補丁包打交道呢?

相關文章