不再是我最喜愛的 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 範例的缺點。
這些年來,我曾針對幾種不同的軟體工程實踐闡述過自己的觀點,而每一次這麼做,都讓我成為更好的開發者。這迫使我批判性地思考那些被視為理所當然的想法,並幫助我記住理想的樣貌,即使我未必每次都能達成。
延伸閱讀
- 「如何撰寫實用的 Commit 訊息」 — 關於我認為什麼構成一則好的 commit 訊息,更詳細的說明。
摘自 govuk-puppet 專案的內容版權歸 Crown Government Digital Service 所有,依MIT 授權條款使用。
隨機一篇部落格

留言
登入後參與討論