如何写好提交,做一个有品位的开发者

本文由《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 拆分当前提交(松耦合)

先以拆分最新的提交为例,可以如下操作:

  1. 将当前提交撤销,重置到上一次提交。撤销提交的改动保留在工作区中。

    $ git reset HEAD^
  2. 通过补丁块拣选方式选择要提交的修改。Git会逐一显示工作区更改,如果确认此处改动要提交,输入“y“。

    $ git add -p

  3. 以撤销提交的提交说明为蓝本,撰写新的提交。

    $ git commit -e -C HEAD@{1}
  4. 对于工作区剩余的修改进行提交。这样就完成一个提交拆分为两个、或者多个的操作。

    $ 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社区写了几个提交:

  1. pack-redundant: new algorithm to find min packs,这个提交的说明写得足够详细,几乎不会有人来challenge。如果不是Junio感冒加搬家,可能会更早合入社区。

  2. pack-redundant: consistent sort method,这个提交的签名区,就出现了 Reported-by的签名,是我手动敲进去的,对贡献者致谢。

正确的代码评审方式

代码评审要关注过程,要由远及近地看每一个提交,不能只看前后两个版本之间的差异。

有人认为这样的代码评审多此一举,认为这样可能是浪费时间。有的时候,给一个提交不规范的开发者做代码评审,的确头疼又浪费时间:看到一个提交中的代码问题,花了几分钟写评论,然后发现下一个提交中这个问题被修正(fixup)了。

如果评审的代码来自提交规范的开发者,逐提交评审可能是一件赏心悦目的事情:

  1. 一些重构操作(修改方法名、变量名;代码块在文件之间移动;文件改名),单独作为一个提交,评审起来工作量很小,对后续提交评审的干扰也小。

  2. 一个提交干一件事,由远及近的评审的过程,能够看到开发者工作的逻辑性和思路。

  3. 因为有 git rebase --interactive等神器,不会出现后一个提交修改前一个提交中错误的实现,让每个提交把事情一次做对。

  4. 因为每个提交能够把事情一次做对,在代码调试过程中 git bisect神器就可以派上用场。

  5. 提交cherry-pick或者rebase到新的基线(如定制开发型项目中,迁移到上游新的版本),工作量小。

最后,让我们一起学习成为一名有品位的程序员,依靠您对代码的品味和高质量严要求,持续守护您的项目质量!