任何軟體專案都是由至少兩個以上的開發者所共同合作開發的。

這句話非常有意思,哪來的一定有兩個以上的開發者?其實就是:

A. 原始開發者 以及 B. 原始開發者經過了幾週或幾個月後

我想任何一位程式設計師都對這個概念再熟悉不過了,當時間一過,腦中用於暫存當初某段程式碼撰寫的時空背景前因後果等各種思路的記憶體早己清空殆盡,在能夠開始動手修改或撰寫新程式碼前,我們需要在腦中重新建立對某段程式碼的環境背景 (Context),而這通常會花上大筆成本(時間就是成本),尤其是當一個開發者需要接手另外一位開發者的工作時,或是進行 pull request 的代碼審查。

我們當然可以透過良好易懂的程式碼,或是各種撰寫風格與規範來減少建立思路所花費的時間,但這都是必須在看到程式碼以後才能在腦中進行重建,這就是為什麼需要 Git 提交訊息(Git Commit Message,以下簡稱 GCM)的原因 ,GCM 就是提交版本更動的摘要,開發者原則上在閱讀完 GCM 後,就能夠對此提交內容有大概的想像。

而為什麼我們會寫出非有效的 GCM,其實大部分原因是因為我們習慣把 GCM 閱讀的假想對象設定為/自己/,一旦你知道你寫的 GCM 是給別人閱讀和檢核的時候,你自然就會改變撰寫的內容,也會注意資訊是否有傳遞清楚,也就是說要寫一個有效的 GCM,最重要的就是要先有一個正確的閱讀對象設定。

A commit message shows whether a developer is a good collaborator. Peter Hutterer

要成為一位好的合作開發夥伴,撰寫有效的 GCM 就是你不可偷懶的步驟之一。

有效 Git Commit Message 的必要元素

一個有效的 GCM 要能夠幫助閱讀者理解提交版本的三件事情:What、Why 以及 How

1. 這個提交版本做了什麼事情(What):

這個版本修正了什麼 bug 、新增了什麼 feature、或是優化了什麼效能,也可能只是簡單的文字修正。這是一個 GCM 最重要的一部分,必須要清楚直白地告訴開發者這個提交版本的目的,例如:

Fix the crash issue of SOMETHING

或是

Refactor the SOME_METHOD of SOME_CLASS

這部分寫得好的話,這個 GCM 就已經成功一半了,在大部分的情況下開發者也只會閱讀到這個部分,其他兩個部分都是屬於細節與補充,換句話說如果你在一個提交版本裡面一次做了很多不同屬性的事情時,你就應該將這些變動透過 Git 重新整理分開來提交。

2. 為什麼要做這件事情(Why)?

一個常見的錯誤是我們只記錄了提交版本做了什麼事情,但卻沒有說明為什麼要做這件事情,當時間一久回來追朔問題的時候,常常已經不記得當初更動的動機與原因是什麼了,所以盡可能地提供提交版本的 Why 資訊可以有效幫助我們理解其背後原因。Chris Beams 提供了一個解釋 Why 的例子如下:

commit eb0b56b19017ab5c16c745e6da39c53126924ed6
Author: Pieter Wuille 
Date:   Fri Aug 1 22:57:55 2014 +0200

   Simplify serialize.h's exception handling

   Remove the 'state' and 'exceptmask' from serialize.h's stream
   implementations, as well as related methods.

   As exceptmask always included 'failbit', and setstate was always
   called with bits = failbit, all it did was immediately raise an
   exception. Get rid of those variables, and replace the setstate
   with direct exception throwing (which also removes some dead
   code).

   As a result, good() is never reached after a failure (there are
   only 2 calls, one of which is in tests), and can just be replaced
   by !eof().

   fail(), clear(n) and exceptions() are just never called. Delete
   them.

但並不是每個提交都一定需要這麼漏漏長的敘述才能表達其修改原因與動機,針對一些直覺的更動與優化,其實可以直接合併在 GCM 的第一行摘要中如下如可閱讀性(readability)、穩定性(stability)、效能(performance)等,然後再針對需要詳加解釋的原因例如造成效能緩慢或閃退的確切成因寫在段落中。

Refactor the crash issue of SOMETHING for stability
Refactor the SOME_METHOD of SOME_CLASS for readability

3. 用什麼方法做到的(How)?

In most cases, you can leave out details about how a change has been made. Code is generally self-explanatory in this regard (and if the code is so complex that it needs to be explained in prose, that’s what source comments are for). Chris Beams

至於用什麼方法做到提交版本的目的,在大部分簡單單純的提交版本中是不需要特別詳註的,建議是有需要特別補充的才紀錄在訊息中,並且應該是提供高層級(High-level)的方法與概念的敘述(例如用了什麼演算法,設計模式等),而不是方法的實際細節。方法細節應當透過更動的程式碼本身以及程式碼註解來自行解釋。

Refactor the SOME_CLASS for performance by Factory pattern

Additional information of WHY ...

Additional information of HOW ...

撰寫 GCM 的規範與準則

知道了一個 GCM 需要提供什麼樣的訊息後,接下來就是一些 GCM 常見的規範與準則,其中大部分都是針對英文撰寫的慣例:

將訊息透過空白斷行區分主旨與本文

原則上 GCM 的第一行就是這個 GCM 的主旨,透過一行空白斷行才區隔,之後的部分就可以當作是 GCM 的本文,如以下範例:

$ git log
commit 42e769bdf4894310333942ffc5a15151222a87be
Author: Kevin Flynn 
Date:   Fri Jan 01 00:00:00 1982 -0200

 Derezz the master control program

 MCP turned out to be evil and had become intent on world domination.
 This commit throws Tron's disc into MCP (causing its deresolution)
 and turns it back into a chess game.

GCM 主旨則是:

Derezz the master control program

GCM 本文:

MCP turned out to be evil and had become intent on world domination. This commit throws Tron's disc into MCP (causing its deresolution) and turns it back into a chess game.

Git-format-patch 則會將 GCM 的主旨與本文自動轉換至 Email 中的主旨與本文,也可以透過 git log --oneline 只印出主旨的部分:

$ git log --oneline
42e769 Derezz the master control program

或是 git shortlog 將提交訊息以作者群組起來:

$ git shortlog
Kevin Flynn (1):
      Derezz the master control program

Alan Bradley (1):
      Introduce security program "Tron"

Ed Dillinger (3):
      Rename chess program to "MCP"
      Modify chess program
      Upgrade chess program

Walter Gibbs (1):
      Introduce protoype chess program

另外還有很多 Git 指令都是透過空白斷行才分出訊息主旨與本文。

限制主旨在 50 字元

關於 50 字元的數目建議只是一個經驗值,這部分可根據需求做調整,最主要目的是配合 Git 相關服務的介面限制如 Github(可到 72 個字元), 如果超過字數限制的話就無法完整的顯示在介面當中,如果想要閱讀完整訊息,則必須要點擊到詳細的頁面,會增加閱讀的複雜度和效率。

如果你發現你很難在 50 個字元裡摘要你所提交所做的更動,很有可能其實是你在這個提交版本中包含了太多不同屬性的變動,這時候代表你需要做的其實是重新整理你的提交內容,將他們切格成許多獨立的小提交項目。

Tip: If you’re having a hard time summarizing, you might be committing too many changes at once. Strive for atomic commits (a topic for a separate post).

關於提交的相關討論可以參考 Atomic Commit

本文在 72 字元內需要自行斷行

這部分也是考量到閱讀性,若沒有自行斷行的話,在 terminal 或是 Github 介面中印出後會導致訊息過長難以閱讀,所以建議維持在 72 個字元內,這部分可以透過其他文字編輯器的幫忙。

訊息主旨只需要以大寫開頭

我想既然稱作主旨,英文開頭要大寫應該就是一種很稀疏平常的慣例了,當然如果你寫中文就沒有這個問題拉,但如果全部提交訊息一字排開,開頭大小寫穿插就會覺得阿雜..

Spring-boot 範例
Git Commit Message Spring-boot

不要在訊息主旨結束加上句點

如果你把主旨想像成標題的話,我們通常不會在標題結束加上句點,而很多寫作規範上也明文規定在標題上英文句點只能用在簡寫,不過如果你把它想成是 Email 主旨的話就是另外一回事情,對於 Email 主旨是否要加上結束句點有各種討論和研究,並且會實際影響到信件的開啟率,這是挺有趣的課題,有興趣的話可以再搜尋相關文獻與資料。

但在 GCM 中是沒有特別必要的,並且在有字元限制的前提下。

訊息主旨使用祈使語氣

Git 中的預設訊息也都是使用祈使語氣,如 git mergegit revert 指令的訊息:

 Merge branch 'myfeature'
Revert "Add the thing with the stuff"
This reverts commit cc87791524aedd593cff5a74532befe7ab69ce9d.

或是透過 Github 介面 Merge pull request:

Merge pull request #123 from someuser/somebranch

關於 GCM 到底要使用祈使語氣還是過去式其實也是有滿多的討論(Git Commits: Past or Imperative? – Dan Clarke),使用祈使語氣的概念其實是在於,你其實是在命令 Git 來完成你要的動作,這跟版本發佈通知(Release notes)使用過去式的概念不太一樣,需要特別注意,但這裡只限於在 GCM 的主旨部分,內文還是可以類似像版本發佈通知一樣使用過去式。

定義和統一訊息中使用的詞彙

為了避免和減少訊息的誤導和模糊不清,統一的詞彙使用是基本的和必要的,而根據不同的語言和專案性質,可能需要不同的詞彙,這些最好都在一開始就定義清楚,可以幫助搜尋與找查的效率。

有的甚至還會結合 Emoji 來表達提交的型態:
GitHub – kazupon/git-commit-message-convention: Extend git commit message from angular style

結語

最後我想關於 GCM 寫作慣例與規範,還是要針對專案和內部的需求來制定適合的格式,只能要能夠清楚傳達提交的訊息,重點都在於減少溝通障礙和誤會。

參考文獻

[1] Who-T: On commit messages
[2] How to Write a Git Commit Message
[3] https://wiki.openstack.org/wiki/GitCommitMessages
[4] tbaggery – A Note About Git Commit Messages
[5] Developer Tip: Keep Your Commits “Atomic” – Fresh Consulting
[6] Understanding the Git Workflow
[7] Git Commits: Past or Imperative? – Dan Clarke
[8] Image via steve_l, CC Licensed.