本文由《Git 權威指南》的作者蔣鑫老師撰寫(全文以第一人稱分享),介紹了代碼提交的最佳實踐建議。
—— 問:“能夠寫出正確代碼的程序員就是有品味的程序員么?”
—— 答:“還不夠。”
品味來自于每一個細節,有品位的程序員會把每一次提交做小、做對、做好,讓人看懂,且無可挑剔,這樣才夠專業,才可以稱為有品位。
熟練使用 Git,會讓您更有品味。
1. 提交做小
小提交就是將問題解耦:“Do one thing and do it well”。開源項目的提交通常都很小,每個提交只修改一個到幾個文件,每次只修改幾行到幾十行。
找一個您熟悉的開源項目,在倉庫中執行下面的命令,可以很容易地統計出來每個提交的修改量。
$ git log --no-merges --pretty= --shortstat
2 files changed, 25 insertions(+), 4 deletions(-)
1 file changed, 4 insertions(+), 12 deletions(-)
2 files changed, 30 insertions(+), 1 deletion(-)
3 files changed, 15 insertions(+), 5 deletions(-)
而很多公司內的項目則不是這樣,一個提交動輒修改成百上千的文件,涉及成千上萬行的源代碼。試問這樣的提交能Show出來給人看么?
可是在開發過程中,程序員一旦進入狀態,往往才思如泉涌,不經意間就寫出一個大提交。比如我又一次向Git貢獻代碼時,提交還不算太大,就被Git的維護者Junio吐槽,要我拆分提交,便于評審:
“I think this patch should be in at least two parts:
Introduce the two-phase "collect in del_list, remove in a separate loop at the end" restructuring.
(optional, if you are feeling ambitious) Change the path that is stored in del_list relative to the prefix, so that all functions that operate on the string in the del_list do not have to do *_relative() thing. Some functions may instead have to prepend prefix but if they are minority compared to the users of *_relative(), it may be an overall win from the readability's point of view.
Add the "interactively allow you to reduce the del_list" bit between the two phases.”
那么如何將提交拆分為若干個小提交呢?
1.1 拆分當前提交(松耦合)
先以拆分最新的提交為例,可以如下操作:
將當前提交撤銷,重置到上一次提交。撤銷提交的改動保留在工作區中。
$ git reset HEAD^
通過補丁塊揀選方式選擇要提交的修改。Git會逐一顯示工作區更改,如果確認此處改動要提交,輸入“y“。
$ git add -p
以撤銷提交的提交說明為藍本,撰寫新的提交。
$ git commit -e -C HEAD@{1}
對于工作區剩余的修改進行提交。這樣就完成一個提交拆分為兩個、或者多個的操作。
$ git add -u $ git commit
1.2 拆分當前提交(緊耦合)
如果要拆分的提交,不同的實現邏輯耦合在一起,難以通過補丁塊揀選(git add -p
)的方式修改提交,怎么辦?這時可以直接編輯文件,刪除要剝離出此次提交的修改,然后執行:
$ git commit --amend
然后執行下面的命令,還原原有的文件修改,然后再提交。如下:
$ git checkout HEAD@{1} -- .
$ git commit
同樣完成了緊耦合時的一個提交拆分為多個提交的操作。
1.3 拆分歷史某個提交
如果要拆分的是歷史提交(如提交 54321),而非當前提交,則可以執行交互式變基(git rebase -i
),如下:
$ git rebase -i 54321
Git會自動將參與變基的提交寫在一個動作文件中,還會自動打開編輯器(比如 vi 編輯器)。
在編輯器中顯示內容示例如下:
pick 54321 要拆分的提交
pick ... 其他參與變基的提交
將要拆分的提交54321前面的關鍵字 pick
修改為 edit
,保存并退出。變基操作隨即開始執行。
首先會在提交54321處停下來,這時要拆分的提交成為了當前提交,參照前面“拆分當前提交”的方法對提交54321進行拆分。拆分結束再執行
git rebase --continue
完成整個變基操作。
2. 提交做對
好的文章不是寫出來的,而是改出來的, 代碼提交也是如此。
2.1 git commit --amend
程序員寫完代碼,往往迫不及待地敲下:git commit
,然后發現提交中少了一個文件,或者提交了多余的文件,或者發現提交中包含錯誤無法編譯,或者提交說明中出現了錯別字。
Git 提供了一個修改當前提交的快捷命令:git commit --amend
,相信很多人都用過,不再贅述。
2.2 git commit --fixup 和 git rebase -i
如果您發現錯誤出現在上一個提交或其他歷史提交中怎么辦呢?我有一個小竅門,在《Git權威指南》里我沒有寫到哦。
比如發現歷史提交 a1234567
中包含錯誤,直接在當前工作區中針對這個錯誤進行修改,然后執行下面命令。
$ git commit --fixup a1234567
您會發現使用了 --fixup
參數的提交命令,不再詢問您提交說明怎么寫,而是直接把錯誤提交 a1234567
的提交說明的第一行拿來,在前面增加一個前綴“fixup!”,如下:
fixup! 原提交說明
如果一次沒有改對,還可以再接著改,甚至您還可以針對這個修正提交進行fixup,產生如下格式的提交說明:
fixup! fixup! 原提交說明
當開發工作完成后,待推送/評審的提交中出現大量的包含“fixup!”前綴的提交該如何處理呢?
如果您執行過一次下面的命令,即針對錯誤提交 a1234567
及其后面所有提交執行交互式變基(注意其中的 --autosquash
參數),您就會驚嘆Git設計的是這么巧妙:
$ git rebase -i --autosquash a1234567^
交互式變基彈出的編輯器內自動對提交進行排序,將提交 a1234567 連同它的所有修正提交壓縮為一個提交。
執行 git config --global rebase.autoSquash true
命令設置配置變量 rebase.autosquash
,執行 git rebase -i
命令會自動帶上 --autosquash
參數。
2.3 TAP(test anything protocol)和 Git 測試框架
對于“提交做對”,很多開源項目還通過單元測試、集成測試用例提供保障。對于這樣的項目,在提交代碼時往往要求提供相應的測試用例。
Git項目本身就對測試用例有著很高的要求,其測試框架就是由Git的maintainer Junio寫的,基于Perl的 TAP(test anything protocol)寫的一個測試框架。用起來非常順手。
示例:
我曾經針對Git的單元測試框架寫過博客,參見復用 git.git 測試框架。
2.4 sharness
Git的測試框架代碼經過重構,已經成為一個單獨的項目:Sharness,更加方便重用了。
3. 提交做好
僅僅做到提交做小、提交做對,往往還不夠,還要通過撰寫詳細的提交說明讓評審者信服,這樣才能夠讓提交盡快通過評審合入項目倉庫中。
3.1 提交說明的結構
幾年前,發現Git服務器上的一個異常,最終將問題定位到Git工具本身,整個代碼改動只有區區一行,提交為例:
receive-pack: crash when checking with non-exist HEAD
您能猜到提交說明寫了多少么?寫了20多行!
01 receive-pack: crash when checking with non-exist HEAD
02
03 If HEAD of a repository points to a conflict reference, such as:
04
05 * There exist a reference named 'refs/heads/jx/feature1', but HEAD
06 points to 'refs/heads/jx', or
07
08 * There exist a reference named 'refs/heads/feature', but HEAD points
09 to 'refs/heads/feature/bad'.
10
11 When we push to delete a reference for this repo, such as:
12
13 git push /path/to/bad-head-repo.git :some/good/reference
14
15 The git-receive-pack process will crash.
16
17 This is because if HEAD points to a conflict reference, the function
18 `resolve_refdup("HEAD", ...)` does not return a valid reference name,
19 but a null buffer. Later matching the delete reference against the null
20 buffer will cause git-receive-pack crash.
21
22 Signed-off-by: Jiang Xin <worldhello***.net@gmail.com>
23 Signed-off-by: Junio C Hamano <gitster***@pobox.com>
這一端提交分成幾個部分: 1. 第 01 行是提交標題。英文、可包含前綴、長度小于50(建議)、結尾不要有句號。
2. 第 02 行要有一個空行,分隔提交標題和提交主體。 3. 第 03 到 20 行是提交的 Body,說明為什么要做這個提交。 4. 第 03 到 15 行說的是要解決的問題,什么情況下會引發這個Bug。 5. 第 17 到 20 行是原因分析和解決方案。 6. 第 22 行開始是簽名區。有作者的簽名(我的簽名),和maintainer將我的郵件補丁合入Git代碼倉時添加簽名(Junio),說明這個提交是經過Junio認可的提交。
下面再詳細介紹一下Git對于提交說明的這些約定俗成的規定。
3.2 提交標題(Subject,即提交說明的第一行)
提交說明第一行是提交標題,是整個提交的概要性描述。
提交標題的長度要求:盡量不要超過 50 個字符。 這是因為對于像Linux、Git這樣的開源項目,是以郵件列表作為代碼評審的平臺,提交標題要作為郵件的標題,而郵件標題本身有長度上的限制。
提交標題使用英文,不要出現中文。這是因為一些Git工具如
git format-patch
講提交轉換補丁,或以郵件方式交換提交補丁的時候,會丟失中文(中文轉換為空白字符)。建議在提交標題中添加前綴,對改動范圍進行區分(例如用模塊名做前綴:
receive-pack
)。不要在提交標題后面添加標點符號(如句號),一個原因是提交標題的長度要求,不要浪費;另外一個是提交標題在郵件發送補丁時,作為郵件的標題,您見過郵件標題要添加結尾句號的么?
3.3 提交標題后的空行
必須要在提交說明的第一行(subject)和后續的提交說明(body)中間留一個空行。如果沒有這個空行,很多Git客戶端會將連續幾行的提交說明合在一起作為提交的簡要描述(git log --oneline
),這樣顯然太糟了。
3.4 提交說明主體(body)
提交標題之外的提交說明也有長度的限制,最好以72字節為限,超過則斷行。
提交說明主體中要寫什么內容呢? 例如:本次提交要解決什么問題?如何解決的?為什么這么做是最合理的。
因為GitHub等代碼托管平臺支持Markdown語法,所以作為一個有品位的程序員學些Markdown的語法,讓您的提交說明的可讀性變得更強吧。關于 Markdown和其他文本標記語言的語法說明,我寫過一個速記卡片輕量級標記語言語法參考,可供參考。
3.5 簽名區
git commit
命令有一個-s
參數,自動在提交說明最后添加 "sob" 簽名(不是您想的那個縮寫)。為什么要在提交后面添加簽名呢?因為一個提交的元信息中只有作者(author)、提交者(committer)兩個字段,而一段代碼的誕生,參與的人往往不止于此,還可能有問題報告者(Reported-by)、代碼評審者(Reviewed-by)、上游Committer 的簽名(Signed-off-by)。為此一些開源項目(如 Git、Linux)的一個約定俗成的習慣,是在提交的最后加上簽名,每個貢獻者一行,從上到下可以看到這段代碼誕生的過程。
對幫助過您的人致謝吧,加上您的代碼簽名。
3.6 示例
最新在Git社區寫了幾個提交:
pack-redundant: new algorithm to find min packs,這個提交的說明寫得足夠詳細,幾乎不會有人來challenge。如果不是Junio感冒加搬家,可能會更早合入社區。
pack-redundant: consistent sort method,這個提交的簽名區,就出現了
Reported-by
的簽名,是我手動敲進去的,對貢獻者致謝。
正確的代碼評審方式
代碼評審要關注過程,要由遠及近地看每一個提交,不能只看前后兩個版本之間的差異。
有人認為這樣的代碼評審多此一舉,認為這樣可能是浪費時間。有的時候,給一個提交不規范的開發者做代碼評審,的確頭疼又浪費時間:看到一個提交中的代碼問題,花了幾分鐘寫評論,然后發現下一個提交中這個問題被修正(fixup)了。
如果評審的代碼來自提交規范的開發者,逐提交評審可能是一件賞心悅目的事情:
一些重構操作(修改方法名、變量名;代碼塊在文件之間移動;文件改名),單獨作為一個提交,評審起來工作量很小,對后續提交評審的干擾也小。
一個提交干一件事,由遠及近的評審的過程,能夠看到開發者工作的邏輯性和思路。
因為有
git rebase --interactive
等神器,不會出現后一個提交修改前一個提交中錯誤的實現,讓每個提交把事情一次做對。因為每個提交能夠把事情一次做對,在代碼調試過程中
git bisect
神器就可以派上用場。提交cherry-pick或者rebase到新的基線(如定制開發型項目中,遷移到上游新的版本),工作量小。
最后,讓我們一起學習成為一名有品位的程序員,依靠您對代碼的品味和高質量嚴要求,持續守護您的項目質量!