不再是我最愛的 Git Commit
六年前,David Thompson(大衛・湯普森)寫了一篇名為「My favourite Git 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現在測試可以正常運作了!真是浪費了我一小時的人生,再也回不來了⋯⋯
這個「笑點」在於,經過這麼長的前言後,湯普森才展示實際的 diff:
沒錯,這則提交訊息包含了六個段落和五個程式碼片段,卻只為了解釋一個字元的空白字元變更。
最愛不代表最好
很容易看出這則提交為何如此吸引人。
大多數開發者只會將這項變更簡單記錄為「Fix whitespace character」,因此有人如此大費周章地解釋自己調查與修正錯誤的過程,確實令人感到驚喜。
就湯普森所提出的各項理由而言,這確實是一則不錯的提交訊息:它創造了一個可被搜尋的紀錄,並分享了關於開發者工具與流程的實用見解。
這並非對湯普森,甚至對這則提交原始作者的攻擊。湯普森從未宣稱這是「最好」的提交訊息,只是他個人最喜歡的。
儘管如此,我現在看到了一些缺陷,讓我無法再將它作為提交訊息的範例。
它把最重要的資訊埋在了最後
當湯普森最初發表這篇部落格文章時,最常見的批評之一是提交訊息過於冗長。我認為這種批評是誤導的。
只要內容相關,提交訊息中的詳盡細節就是有用的,而湯普森的範例正是如此。它能幫助經驗較少的隊友學習作者的除錯流程與工具集,也能讓經驗較豐富的隊友有機會檢視開發者是否忽略了什麼,或是否不知道有相關工具可用。
人們之所以覺得湯普森的範例過於冗長,原因在於它把最重要的資訊深埋在提交訊息的後段。
重讀第一段:
我在一個功能分支上新增了一些測試,以比對
/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
讀到這則提交訊息的三個句子加一個程式碼片段後,讀者對於這項變更實際上做了什麼,仍然一無所知。
提交訊息應該將最重要的資訊放在最前面,再逐漸過渡到更細節的內容。新聞工作者稱這種寫作風格為倒金字塔式寫作。
新聞工作者以倒金字塔結構來撰寫新聞報導,將與最多人相關的資訊置於最上方。
如果我在瀏覽提交歷史,我會想快速判斷每則提交是否與我相關。提交訊息應該在一開始就提供變更的高層次摘要。
它始終沒有真正解釋問題所在
讀完湯普森範例提交訊息的結尾,你真的理解這項變更了嗎?
以下是它最接近解釋問題的部分:
該範本似乎是我們程式碼庫中唯一被識別為
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 檔案,因為 0xC2 與 0xA0 都落在 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 spec或bundle 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 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 $簡化為$,因為前者大多只是雜訊。
值得注意的是,我並未刪減細節,因為問題不在於冗長,而在於開發者如何組織與呈現資訊。
定義自身原則的價值
重溫湯普森的文章讓我再次體會到,為自己定義軟體工程原則是多麼有價值。
我曾因為認同湯普森對其優點的看法,而接受這則提交作為良好的範例。直到我坐下來定義自己認為提交訊息中最重要的特質時,才看見湯普森範例中的不足之處。
這些年來,我已針對多項不同的軟體工程實踐闡述過自己的觀點,而每一次這麼做,都讓我成為更優秀的開發者。這迫使我批判性地思考那些被視為理所當然的想法,並幫助我記住理想的樣貌,即使我並非總能達成。
延伸閱讀
- 「How to Write Useful Commit Messages」 - 我對於何謂好的提交訊息更詳細的說明。
摘自 govuk-puppet 專案的內容版權歸 Crown Government Digital Service 所有,依 MIT License 使用。
隨機一篇部落格
