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 spec 或 bundle 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 spec 或 bundle 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 檔案,因為 0xC2 和 0xA0 都超出了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 spec 或 bundle 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 spec 或 bundle 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 進行翻譯

留言