看得懂 @@ -12,7 +12,9 @@,diff 就不再可怕

git diff 的輸出中,多數人會直接略過這一行:

@@ -12,7 +12,9 @@ export function calculateTotal(items) {

+- 的意思人人都懂,但能說清楚這四個數字代表什麼的人並不多。看得懂之後,程式碼審查會更精準,套用修補檔失敗或合併衝突的原因也更容易定位。

這篇參考文章把 unified diff 格式逐塊拆開來看。

整體結構

典型的輸出分成三層:

diff --git a/src/cart.js b/src/cart.js     ← 這是哪個檔案
index 83db48f..bf269f4 100644              ← 變更前後的物件 ID 與檔案權限
--- a/src/cart.js                          ← 變更前(a)
+++ b/src/cart.js                          ← 變更後(b)
@@ -12,7 +12,9 @@ export function calculateTotal(items) {   ← hunk 標頭
   const subtotal = items.reduce(...)       ← 上下文行(開頭是空白)
-  return subtotal;                         ← 被刪除的行
+  const tax = subtotal * 0.1;              ← 新增的行
+  return subtotal + tax;
 }

a/b/ 是代表「變更前」「變更後」的慣例前綴,並非真實存在的目錄。新增檔案會顯示 --- /dev/null,刪除則是 +++ /dev/null

hunk 標頭的四個數字

@@ -12,7 +12,9 @@
   │  │  │  └── 變更後:從該處起 9 行
   │  │  └───── 變更後:從第 12 行開始
   │  └──────── 變更前:從該處起 7 行
   └─────────── 變更前:從第 12 行開始

也就是 -起始行,行數 +起始行,行數。關鍵在於:行數是這個 hunk 涵蓋範圍的大小,而不是被修改的行數——上下文行也算在內。上例代表 7 行的區塊變成 9 行,淨增 2 行。

行數為 1 時可以省略,寫成 @@ -1 +1 @@。結尾 @@ 之後的文字,是 git 往回尋找縮排較淺的行所推測出的區段標題,屬於給人看的脈絡,而不是變更內容本身。

每行開頭的第一個字元決定一切

前綴意義
(空白)上下文行,未變更
-只存在於舊版本(刪除)
+只存在於新版本(新增)
\關於檔案本身的註記(見下文)

沒有「修改某一行」這個概念。 只改了一個字元的行,同樣以刪除加新增的組合來表示。這是閱讀 diff 時最大的困惑來源:滿屏的紅綠,實際上可能只差一個分號。

若您想知道「行內」到底改了什麼,就必須以字詞為單位比較。git 可以用 git diff --word-diff,瀏覽器則可貼進 文字比對(Diff)工具

上下文預設 3 行

變更行前後顯示的未變更行稱為「上下文」,預設 3 行。套用修補檔的一方會依據這些周圍內容尋找「該貼在哪裡」。這正是行號位移後修補檔仍能套用的原因;反過來說,周圍內容也被改動時,修補就會失敗。

git diff -U0     # 不顯示上下文,只有變更行
git diff -U10    # 前後各 10 行

\ No newline at end of file

-const config = {};
\ No newline at end of file
+const config = {};

這個註記表示檔案結尾沒有換行。若兩個版本看起來一模一樣卻仍有差異,就是結尾換行被加上或移除了。編輯器與 .editorconfiginsert_final_newline)很容易在無意間改動它,成為審查時的雜訊。

為什麼只改空白會看起來像整行重寫

把縮排從 Tab 換成空白、或刪掉行尾空白,都會讓該行的內容被判定為改變,因此即使畫面上看不出差別,那些行仍會成對出現 -/+

git diff -w                    # 忽略空白數量的差異
git diff --ignore-blank-lines  # 忽略空白行的增減

換行字元(LF 與 CRLF)也會造成同樣效果。若 Windows 與 Linux 混用的儲存庫在您一行都沒動的情況下顯示整份檔案都變了,請先從這裡查起。

合併提交出現的 @@@

對合併提交執行 git show 時,可能看到三個 @ 的標頭:

@@@ -12,7 -12,8 +12,9 @@@

這是組合式 diff(combined diff),同時顯示與兩個親代提交的差異。前綴欄位也變成兩欄,第一欄對應第一個親代、第二欄對應第二個親代。只有與兩個親代都不同的行才會顯示,也就是解決合併時實際動手改過的部分。

更名是推斷出來的,不是被記錄的

diff --git a/src/utils.js b/src/helpers.js
similarity index 95%
rename from src/utils.js
rename to src/helpers.js

git 並不記錄更名,而是在計算 diff 時依內容相似度推斷,預設相似度達 50% 以上就視為更名,並以 similarity index 顯示該比例。若在同一個提交中一邊改檔名一邊大幅改寫內容,git 就不會判定為更名,而會顯示成「刪除 + 新增檔案」。

在本機比較

要比較尚未提交的文字,或 git 之外的日誌、設定檔,把兩份內容貼進 文字比對(Diff)工具,變更處就會以顏色對齊呈現。處理全在瀏覽器內完成,正式環境的設定檔或日誌片段也能直接比對,不會離開您的電腦。

比較前先格式化能減少雜訊:JSON 用 JSON 格式化工具、SQL 用 SQL 格式化工具 整理過後再比,留下的就只有有意義的差異。diff 的基礎(行層級與字詞層級的差別、空白的處理)整理在 文字差異的閱讀與應用

總結

  • @@ -12,7 +12,9 @@-起始行,行數 +起始行,行數;行數涵蓋包含上下文的整個範圍,不是變更行數
  • 開頭字元決定一切(空白/-+)。沒有「修改」,只有刪除加新增
  • 上下文預設 3 行,修補檔靠它定位
  • \ No newline at end of file 講的是結尾換行,是幽靈差異的常見來源
  • 只改空白或換行字元會看起來像整份重寫,用 git diff -w 區分
  • @@@ 代表合併提交的組合式 diff,同時比對兩個親代
  • 更名是依相似度推斷而非記錄(similarity index

常見問題

@@ -12,7 +12,9 @@ 的數字代表什麼?

- 側描述舊版本、+ 側描述新版本,各自是「起始行號, 行數」。在此例中,舊檔案從第 12 行起提供 7 行,新檔案從第 12 行起提供 9 行。行數包含未變更的上下文行,所以並不是被編輯的行數。

為什麼只改一個字元卻顯示成整行變更?

因為 unified diff 格式無法表達「這一行被修改了」,所有變更都以刪除(-)加新增(+)表示。想看行內究竟改了什麼,請使用 git diff --word-diff 或以字詞為單位比較的工具。

我什麼都沒改,卻顯示所有行都變了

幾乎都是換行字元(LF 與 CRLF)或縮排空白造成的。若 git diff -w 之後差異消失,原因就在這裡。要在整個儲存庫統一,可用 .gitattributes 固定換行字元。

貼上的內容會被傳送出去嗎?

不會。文字比對(Diff)工具 完全在瀏覽器內處理,貼上的內容不會傳送到伺服器,因此正式環境的設定檔與日誌也能安心比對。