No Longer My Favorite Git Commit

Michael Lynch

不再是我最愛的 Git Commit

六年前,David Thompson(大衛・湯普森)寫了一篇名為「My favourite Git 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

現在測試可以正常運作了!真是浪費了我一小時的人生,再也回不來了⋯⋯

這個「笑點」在於,經過這麼長的前言後,湯普森才展示實際的 diff:

沒錯,這則提交訊息包含了六個段落和五個程式碼片段,卻只為了解釋一個字元的空白字元變更。

最愛不代表最好

很容易看出這則提交為何如此吸引人。

大多數開發者只會將這項變更簡單記錄為「Fix whitespace character」,因此有人如此大費周章地解釋自己調查與修正錯誤的過程,確實令人感到驚喜。

就湯普森所提出的各項理由而言,這確實是一則不錯的提交訊息:它創造了一個可被搜尋的紀錄,並分享了關於開發者工具與流程的實用見解。

這並非對湯普森,甚至對這則提交原始作者的攻擊。湯普森從未宣稱這是「最好」的提交訊息,只是他個人最喜歡的。

儘管如此,我現在看到了一些缺陷,讓我無法再將它作為提交訊息的範例。

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

當湯普森最初發表這篇部落格文章時,最常見的批評之一是提交訊息過於冗長。我認為這種批評是誤導的。

只要內容相關,提交訊息中的詳盡細節就是有用的,而湯普森的範例正是如此。它能幫助經驗較少的隊友學習作者的除錯流程與工具集,也能讓經驗較豐富的隊友有機會檢視開發者是否忽略了什麼,或是否不知道有相關工具可用。

人們之所以覺得湯普森的範例過於冗長,原因在於它把最重要的資訊深埋在提交訊息的後段。

重讀第一段:

我在一個功能分支上新增了一些測試,以比對 /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

讀到這則提交訊息的三個句子加一個程式碼片段後,讀者對於這項變更實際上做了什麼,仍然一無所知。

提交訊息應該將最重要的資訊放在最前面,再逐漸過渡到更細節的內容。新聞工作者稱這種寫作風格為倒金字塔式寫作。

倒金字塔

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

如果我在瀏覽提交歷史,我會想快速判斷每則提交是否與我相關。提交訊息應該在一開始就提供變更的高層次摘要。

它始終沒有真正解釋問題所在

讀完湯普森範例提交訊息的結尾,你真的理解這項變更了嗎?

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

該範本似乎是我們程式碼庫中唯一被識別為 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 編碼,卻從未解釋原因。幸好這個專案是開源的,所以我可以自行調查。

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

$ 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 編碼來解讀它,這是一種較新且對國際化更友善的文字編碼方案。

湯普森的程式碼庫使用的是Ruby 1.9.3,該版本支援 UTF-8 編碼,但若檔案未明確宣告,則預設會使用 US-ASCII。

翻查原始碼歷史後,我發現 commit 5a8607 最初引入了這個 UTF-8 字元。該提交訊息並未提及引入 UTF-8 字元的任何原因,因此很可能是不小心造成的。

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

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

-messe 於 Hacker News

它提及程式碼卻未提供連結

湯普森的範例提交以提及外部程式碼作為開頭:

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

但這則提交訊息從未指明分支名稱或提供提交雜湊值,因此讀者無從重現開發者的發現。

稍後,提交訊息又寫道:

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

若沒有提交雜湊值或連結,讀者無從得知開發者指的是哪些比對器或哪個 spec 檔案。

如果提交訊息提及外部程式碼,就應該明確地連結到該程式碼,讓程式碼審查者與未來的維護者能看到變更的確切脈絡。

我的重寫版本

以下是我對湯普森最愛的 Git 提交所提出的修訂版本:

將 routes.conf.erb 範本轉換為 US-ASCII

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

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

這項變更將該 UTF-8 字元替換為等效的 US-ASCII 字元,以避免 rake 中的測試失敗。

多餘的 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 $ 簡化為 $,因為前者大多只是雜訊。

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

定義自身原則的價值

重溫湯普森的文章讓我再次體會到,為自己定義軟體工程原則是多麼有價值。

我曾因為認同湯普森對其優點的看法,而接受這則提交作為良好的範例。直到我坐下來定義自己認為提交訊息中最重要的特質時,才看見湯普森範例中的不足之處。

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

延伸閱讀


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

原文由 Michael Lynch 發布

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