Tools mentioned in this article
Open the browser-based tool while you read and try the workflow immediately.
看得懂 @@ -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 = {};
這個註記表示檔案結尾沒有換行。若兩個版本看起來一模一樣卻仍有差異,就是結尾換行被加上或移除了。編輯器與 .editorconfig(insert_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)工具 完全在瀏覽器內處理,貼上的內容不會傳送到伺服器,因此正式環境的設定檔與日誌也能安心比對。