No Longer My Favorite Git Commit

Michael Lynch

不再是我最喜愛的 Git Commit

原文由 Michael Lynch 發布,訂閱此部落格

六年前,David Thompson 寫了一篇名為「My favourite Git commit」的熱門部落格文章,盛讚他同事寫的一則細節豐富、帶點幽默感的 commit 訊息。當時我很喜歡這篇文章,還曾把它傳給好幾位隊友,當作撰寫良好 commit 訊息的範例。

最近,我在撰寫自己的如何寫出實用 commit 訊息指南時,重讀了 Thompson 的文章。當被要求解釋 Thompson 的文章為何能成為如此有效的範例時,我驚訝地發現自己竟然答不上來。以旁觀者的角度來看,那篇文章讀起來很有趣,但我卻無法將它當成良好軟體工程的典範來合理化。

Thompson 最愛的那個 commit

以下是當時讓 Thompson 以及包括我在內的許多人著迷的那則 commit 訊息

將樣板轉換為 US-ASCII 以修復錯誤

我在一個功能分支上新增了一些測試,用來比對 /etc/nginx/router_routes.conf 的內容。用 bundle exec rake specbundle exec rspec modules/router/spec 執行時都正常,但用 bundle exec rake 執行時,每個 should 區塊都失敗並顯示:

ArgumentError:
 invalid byte sequence in US-ASCII

我最後發現,移除 .with_content(//) 比對後錯誤就消失了。spec 檔案裡並沒有什麼奇怪的字元。而且只要在同一個直譯器中載入 Puppet,就能重現這個問題:

rake -E 'require "puppet"' spec

那個樣板似乎是我們程式碼庫中唯一被判定為 utf-8 編碼的檔案,其他全都是 us-ascii

dcarley-MBA:puppet dcarley$ find modules -type f -exec file --mime {} \+ | grep utf
modules/router/templates/routes.conf.erb:  text/plain; charset=utf-8

嘗試將該檔案轉回 US-ASCII 時,發現造成問題的字元看起來像是空白字元:

dcarley-MBA:puppet dcarley$ iconv -f UTF8 -t US-ASCII modules/router/templates/routes.conf.erb 2>&1 | tail -n5
  proxy_intercept_errors off;

  # Set proxy timeout to 50 seconds as a quick fix for problems
  #
iconv: modules/router/templates/routes.conf.erb:458:3: cannot convert

(手動)替換掉它之後,該檔案又被判定為 us-ascii

dcarley-MBA:puppet dcarley$ file --mime modules/router/templates/routes.conf.erb
modules/router/templates/routes.conf.erb: text/plain; charset=us-ascii

現在測試可以跑了!真是浪費了我一小時的生命,再也回不來了……

這個故事的「爆點」在於,在如此冗長的鋪陳之後,Thompson 展示了實際的 diff:

沒錯,整則 commit 訊息包含了六個段落和五段程式碼片段,結果只為了說明一個字元的空白字元修改。

最愛 ≠ 最好

不難理解這則 commit 為何如此吸引人。

多數開發者大概只會用一句「Fix whitespace character」帶過這個修改,所以看到有人如此詳盡地解釋自己排查與修復錯誤的過程,確實讓人驚喜。

就 Thompson 提出的那些理由而言,它確實是一則不錯的 commit 訊息:它創造了一份可供搜尋的紀錄,也分享了關於開發者工具與流程的有用見解。

這並不是對 Thompson 甚至原作者的攻擊。Thompson 從來沒有宣稱這是「最好」的 commit 訊息,只是說這是他最喜愛的一則。

話雖如此,我現在看到了一些缺陷,讓我無法再把它當作 commit 訊息的範本。

它把最重要的資訊埋在了最後

Thompson 當初發表這篇文章時,最常見的批評是這則 commit 訊息過於冗長。我當時覺得這種批評是誤導的。

只要細節與主題相關,commit 訊息寫得詳盡是有用的,而 Thompson 範例中的細節確實相關。這些細節能幫助經驗較少的隊友學習作者的除錯過程與工具,也能讓經驗豐富的隊友有機會檢視開發者是否忽略了什麼,或是否不知道某個相關工具。

人們之所以覺得 Thompson 的範例過於冗長,原因在於它把最重要的資訊深深埋在了 commit 訊息的後段。

重讀第一段:

我在一個功能分支上新增了一些測試,用來比對 /etc/nginx/router_routes.conf 的內容。用 bundle exec rake specbundle exec rspec modules/router/spec 執行時都正常,但用 bundle exec rake 執行時,每個 should 區塊都失敗並顯示:

ArgumentError:
 invalid byte sequence in US-ASCII

讀到這則 commit 訊息的三句話加上一段程式碼之後,讀者仍然對這次修改實際做了什麼一無所知。

Commit 訊息應該先呈現最重要的資訊,再逐漸過渡到細節。新聞工作者稱這種寫法為倒金字塔結構。

倒金字塔

新聞工作者以倒金字塔結構撰寫報導,將與最多人相關的資訊放在最上方。

當我在瀏覽 commit 歷史時,我希望能快速判斷每一則 commit 是否與自己相關。Commit 訊息應該一開始就提供這次修改的高層次摘要。

它始終沒有真正解釋清楚問題

讀完 Thompson 範例 commit 訊息的最後,你真的搞懂這次修改是什麼嗎?

以下是它最接近解釋問題的部分:

那個樣板似乎是我們程式碼庫中唯一被判定為 utf-8 編碼的檔案,其他全都是 us-ascii:

dcarley-MBA:puppet dcarley$ find modules -type f -exec file --mime {} \+ | grep utf
modules/router/templates/routes.conf.erb:  text/plain; charset=utf-8

這則訊息說 routes.conf.erb 具有 UTF-8 編碼,卻從來沒解釋原因。幸好這個專案是開源的,所以我可以自己調查。

問題出在第 463 行routes.conf.erb

$ cat modules/router/templates/routes.conf.erb | head -n 463 | tail -n 1
  # where civica QueryPayments calls are taking too long.

用一般的文字編輯器或瀏覽器看不出問題,但如果用 xxd 這類工具傾印檔案的原始位元組,就能看出端倪:

$ cat modules/router/templates/routes.conf.erb \
  | head -n 463 | tail -n 1 \
  | xxd | head -n 1
00000000: 2020 23c2 a077 6865 7265 2063 6976 6963    #..where civic
                 ^^ ^^

如果你沒有背下 US-ASCII 和 UTF-8 的對照表,以下是那一行最前面幾個字元的對照:

位元組表示文字表示
0x20' ' (空白)
0x20' ' (空白)
0x23'#'
0xC2 0xA0' 'UTF-8 不換行空白

所以,這個檔案含有位元組序列 0xC2 0xA0,這表示它不可能是 US-ASCII 檔案,因為 0xC20xA0 都超出了US-ASCII 位元組範圍

0xC2 0xA0 這個序列意味著任何讀取 routes.conf.erb 的應用程式都必須以 UTF-8 編碼來解讀它,這是一種更新、對國際化更友善的文字編碼方式。

Thompson 的程式碼庫使用的是Ruby 1.9.3,它支援 UTF-8 編碼,但如果檔案沒有明確宣告,就會預設為 US-ASCII。

翻查原始碼歷史後,我發現是commit 5a8607 最早引入了這個 UTF-8 字元。該 commit 訊息完全沒有提到引入 UTF-8 字元的理由,所以很可能只是個意外。

一位 Hacker News 的留言者提出了一個合理的推測,解釋為何 routes.conf.erb 會出現這個 stray UTF-8 字元:

這個無效字元最可能的來源,是有人使用愛爾蘭/英國的 Apple 鍵盤配置,其中 # 是 Option-3(AltGr-3),而不換行空白是 Option-Space(AltGr-Space)。

-messe on Hacker News

它引用了程式碼卻沒有附上連結

Thompson 範例 commit 的開頭就引用了一段外部程式碼:

我在一個功能分支上新增了一些測試,用來比對 /etc/nginx/router_routes.conf 的內容。用 bundle exec rake specbundle exec rspec modules/router/spec 執行時都正常。

但這則 commit 訊息既沒有指出分支名稱,也沒有提供 commit hash,讀者根本無從重現開發者的發現。

稍後,commit 訊息又寫道:

我最後發現,移除 .with_content(//) 比對後錯誤就消失了。我在 spec 檔案中並未看到任何奇怪的字元。

沒有 commit hash 或連結,讀者根本不知道開發者指的是哪些 matcher 或哪個 spec 檔案。

如果 commit 訊息引用了外部程式碼,就應該明確附上連結,讓程式碼審查者與未來的維護者能看到這次修改的確切脈絡。

我的重寫版本

以下是我對 Thompson 最愛的 Git commit 的重寫提案:

將 routes.conf.erb 樣板轉換為 US-ASCII

routes.conf.erb 有一個 stray UTF-8 字元,似乎是在5a8607 中意外引入的。

rake 預期的是 US-ASCII 格式,因此 routes.conf.erb 中這個單一的 UTF-8 字元會導致 rake 中的測試失敗。

這次修改將該 UTF-8 字元替換為等效的 US-ASCII 字元,以避免 rake 的測試失敗。

Stray UTF-8 字元

問題出在 modules/router/templates/routes.conf.erb 的第 463 行:

$ cat modules/router/templates/routes.conf.erb \
  | head -n 463 | tail -n 1 \
  | xxd | head -n 1
00000000: 2020 23c2 a077 6865 7265 2063 6976 6963    #..where civic
                 ^^ ^^

0xC2 0xA0 並非有效的 US-ASCII 位元組序列,而是UTF-8 不換行空白字元。任何以 US-ASCII 編碼讀取該檔案的工具都會失敗。

我是如何發現這個問題的

我在一個功能分支上新增了一些測試來比對 /etc/nginx/router_routes.conf 的內容(見abcd123)。用 bundle exec rake specbundle exec rspec modules/router/spec 執行時都正常,但用 bundle exec rake 執行測試時,每個 should 區塊都失敗並顯示:

ArgumentError:
 invalid byte sequence in US-ASCII

我最後發現移除 .with_content(//) 比對後錯誤就消失了。我在 spec 檔案中並未看到任何奇怪的字元。我可以透過在同一個直譯器中載入 Puppet 來重現錯誤:

rake -E 'require "puppet"' spec

那個樣板似乎是我們程式碼庫中唯一被 file 判定為 utf-8 的檔案,其他全都是 us-ascii

$ find modules -type f -exec file --mime {} \+ | grep utf
modules/router/templates/routes.conf.erb:  text/plain; charset=utf-8

嘗試將該檔案轉回 US-ASCII 時,發現造成問題的字元看起來像是空白字元:

$ iconv -f UTF8 -t US-ASCII modules/router/templates/routes.conf.erb 2>&1 \
   | tail -n5

  proxy_intercept_errors off;

  # Set proxy timeout to 50 seconds as a quick fix for problems
  #
iconv: modules/router/templates/routes.conf.erb:458:3: cannot convert

(手動)替換掉該 UTF-8 字元後,file 又將 routes.conf.erb 判定為 us-ascii

$ file --mime modules/router/templates/routes.conf.erb
modules/router/templates/routes.conf.erb: text/plain; charset=us-ascii

現在測試可以跑了!真是浪費了我一小時的生命,再也回不來了……

以下是我的修改重點:

  • 我在訊息開頭加入了高層次的摘要。
  • 我對 UTF-8 字元及其來源加入了更明確的說明。
  • 我將原作者的大部分內容移到「我是如何發現這個問題的」一節,明確標示這部分屬於額外參考閱讀
  • 我做了輕微的文法修正。
  • 移除了被動語態以減少歧義。
  • 我將終端機提示字元從 dcarley-MBA:puppet dcarley $ 簡化為 $,因為前者大多只是雜訊。

值得注意的是,我並沒有刪除細節,因為問題不在於冗長,而在於開發者如何組織與呈現資訊。

定義個人原則的價值

重讀 Thompson 的文章讓我再次意識到,為自己定義軟體工程原則是多麼有價值。

我曾因為認同 Thompson 所說的優點,而接受這則 commit 是個好範例。直到我坐下來定義自己認為 commit 訊息最重要的特質時,才看見 Thompson 範例的缺點。

這些年來,我曾針對幾種不同的軟體工程實踐闡述過自己的觀點,而每一次這麼做,都讓我成為更好的開發者。這迫使我批判性地思考那些被視為理所當然的想法,並幫助我記住理想的樣貌,即使我未必每次都能達成。

延伸閱讀


摘自 govuk-puppet 專案的內容版權歸 Crown Government Digital Service 所有,依MIT 授權條款使用。

本文章由 muse-spark-1.2-contributor 進行翻譯

留言