代码评审系统 ReviewBoard 和 Gerrit
不论是公司里还是公司外,正经的多人合作开发最好要经常做代码评审,其必要性不用我多说,但是如何做代码评审确实个头大的事情,我个人是非常反对拉一票人去会议室开投影仪一行行讲的,太浪费资源和时间了。
抛开评审的积极性不谈,我觉得代码评审应是可以随时发起随时结束的,邮件是个很不错的载体,这在开源界已经印证了。但是邮件里发补丁确实不够正式,需要众人极高的热情和自觉,另外邮件的时延比较大,纯文本diff 格式很多人接受不了。
我以前整了一套龌龊的脚本,解决临时库和正式库的自动提交问题:
auto fetch manually push 正式库 --------> 临时库 <--------+ | <-------- | | auto push | | | +---------------> 开发人员------+ manually fetch |
开发人员往临时库上自己的独立分支 push,触发 GIT hook 发送邮件通知大家,邮件里内嵌了提交信息,然后另外一个人回复通知邮件,在邮件开头添加[COMMIT],临时库所在机器上有个定时任务收取邮件,遇到标题以[COMMIT] 开头的邮件就去邮件内容里找提交信息,并自动 push 到正式库里。
这么搞的目的一是让正式库安全点,因为大家 GIT 用的不是很熟练,二是增加点提交延时,虽然大家并不真的评审,但至少每一个修改会有两个人“关注”(其实是牵涉而已,很少有人真的看修改内容)。这个东西太过于理想化了,被人抱怨的最大问题是提交延时(其实也就半小时左右)。当然,也有几次确实避免了错误的提交。
但在公司里邮件往往太多,处理很低效,还是有个 Web 界面更友好更实时点,这方面开源的独立可运行产品最为知名的该属 ReviewBoard 和Gerrit 了。还有个 Rietveld,算是 Gerrit 的前辈,Python 之父针对 Subversion 做的,使用了 Google App Engine 的服务,有人给它打了补丁以支持在本地单机运行,但终是在 Google 之外使用不广。
ReviewBoard 的界面简洁漂亮,我挺喜欢的,但我司用的 1.5 的版本频繁挂掉,不知道新的 1.6 好点没,界面上倒是又有改进,特别是可以在 comment 里开 issue 的做法很有创意。注意这个并不是真的在 BUG 管理系统里开一个 BUG,而是在 ReviewBoard 里做一个类似FIXME 的标记。
ReviewBoard 的安装很容易,官方文档做的挺好,我在邮件配置里卡壳了下,我的 Exim 4 只给配置了 GSSAPI 和 DIGEST-MD5 认证方式,前者给用户,后者给需要发邮件的服务,比如 RoundCube、ReviewBoard、Gerrit,但是悲催的是 ReviewBoard 使用的 Python smtplib 库只支持 CRAM-MD5、LOGIN,俺折腾了好几天,最后在qunshan@newsmth 的大力帮助下,终于搞出一个凑合可用的支持DIGEST-MD5 的 smtplib.py,代码见 https://gist.github.com/2679719 ,但最后我还是决定让 Exim 4 支持 CRAM-MD5 认证得了,反正有 SSL 保护。
这个过程中有个小笑话,折腾 ReviewBoard 邮件发送时,我发现有时候可以成功,很是惊讶,最后发现 Web 界面保存 Email 设置时 smtp 密码有时保存为空,也就是 smtp 密码没指定时可以发送成功,原来此时 ReviewBoard 压根不会向 Exim 发送 AUTH指令做认证,而 Exim4 居然也乐呵呵的同意发送了,惊了我半身冷汗!
Exim 这么做可能是 SMTP 协议历史上很开放,以及局域网内比较安全的缘故,但是我还是偏执的担心某个访客进公司后插个网线就可以利用Exim 狂发邮件,所以就做了限制,必需 STARTTLS 并且认证了才能发邮件:
$ cat /etc/exim4/conf.d/acl/30_exim4-config_check_mail ### acl/30_exim4-config_check_mail ################################# # This access control list is used for every MAIL command in an incoming # SMTP message. The tests are run in order until the address is either # accepted or denied. # acl_check_mail: require message = no AUTH given before MAIL command authenticated = * message = no STARTTLS given before MAIL command encrypted = * .ifdef CHECK_MAIL_HELO_ISSUED deny message = no HELO given before MAIL command condition = ${if def:sender_helo_name {no}{yes}} .endif accept |
ReviewBoard 另一个问题是不支持 HTTP 认证,原因是ReviewBoard 使用的 Django框架的旧版本对这个支持不力,新版 Django 貌似支持了,但 ReviewBoard 还需要做额外工作才能配合,比如自动创建 ReviewBoard 里的用户,比如去掉登录、注册、注销链接等等,有人提了补丁出来,还没收录,我也不清楚是不是改的完备,我不懂 Python,暂且不折腾了。
接下来是把玩下 Gerrit,这厮的文档写的也很赞(开源的东西文档写的好的真不多见),安装是很简单了,早期的 Gerrit 据说是用 Python 写的,在 GIT 主力开发者以及 jgit 项目发起人 Shawn O. Pearce 加入 Google 后就改用 Java 写了,编译好的 Gerrit 就是一个 war 包,可以放入 Servlet 容器里运行,也可以java -jar gerrit.war 直接用内置的 Jetty,太贴心了。Shawn 是个很勤奋的人,用 Java 重新实现了 GIT 核心功能,Gerrit 内置 Web server、SSH server,还有一个 Prolog 语言解释器。。。。
Gerrit 里评审流程分三个阶段,可以分别让不同角色执行:
review: 人肉扫描代码有无问题
verify: 编译、测试,可以用 Jenkins 的 Gerrit Trigger 插件自动出发
submit: 提交代码到正式分支上,貌似由人触发,Gerrit 来执行
在使用 Gerrit 时,不要忘记把 commit-msg hook 装上:http://gerrit-documentation.googlecode.com/svn/Documentation/2.3/user-changeid.html#_creation
由于我想把安装过程自动化,所以在执行 java -jar gerrit.war init 时磕绊了下,这个 init 命令有个 --batch 选项,表示在非交互状态下时 init会用默认配置创建一个 Gerrit site,比如使用 H2 数据库,这不是我期望的,我希望它用 PostgreSQL 数据库,虽然创建完 site 后可以修改SITE_DIR/etc/gerrit.config,但我担心 init 时会初始化数据库什么的,不是在 gerrit.config 简单改下配置就能切换数据库的。 于是我把 init 交互式运行时的答案写入文件里,想通过管道传给 java -jar gerrit.war init,没想到伊判断了输入是否终端,发现不是终端就直接走 --batch 模式了,真是自做聪明。。。折腾了会 empty (http://empty.sf.net)、expect、socat 后放弃了,还是老实交互式安装吧,反正不会频繁重装。 下面是我输入的问题答案,@@...@@ 标记处需要替换成真的密码,在执行 init 之前要先创建好gerrit 数据库以及 gerrit 系统账户、gerrit 邮件账户:
### Gerrit Code Review 2.3 # Create '/srv/gerrit/site' [Y/n]? y ### Git Repositories # Location of Git repositories [git]: git ### SQL Database # Database server type [H2/?]: postgresql # Server hostname [localhost]: localhost # Server port [(POSTGRESQL default)]: 5432 # Database name [reviewdb]: gerrit # Database username [gerrit]: gerrit # gerrit's password : @@GERRIT_DB_PASSWORD@@ # confirm password : @@GERRIT_DB_PASSWORD@@ ### User Authentication # Authentication method [OPENID/?]: http # Get username from custom HTTP header [y/N]? y # Username HTTP header [SM_USER]: X-Forwarded-User # SSO logout URL : https://sso.corp.example.com/logout ### Email Delivery # SMTP server hostname [localhost]: smtp.corp.example.com # SMTP server port [(default)]: 25 # SMTP encryption [NONE/?]: tls # SMTP username [gerrit]: [email protected] # gerrit's password : @@GERRIT_SMTP_PASSWORD@@ # confirm password : @@GERRIT_SMTP_PASSWORD@@ ### Container Process # Run as [gerrit]: gerrit # Java runtime [/usr/lib/jvm/java-6-openjdk-i386/jre]: /usr/lib/jvm/default-java/jre # Copy gerrit.war to /srv/gerrit/site/bin/gerrit.war [Y/n]? y ### SSH Daemon # Listen on address [*]: * # Listen on port [29418] 2022 # Download and install it now [Y/n]? y ### HTTP Daemon # Behind reverse proxy [y/N] y # Proxy uses SSL (https://) [y/N]? n # Subdirectory on proxy server [/]: / # Listen on address [*]: 127.0.0.1 # Listen on port [8081]: 2080 |
在这个回答里有几个地方是比较特殊的,一是用户认证方式,由于我是把Gerrit 放在 Apache 后面,Apache 使用 mod_auth_kerb 做用户认证,所以这里我给 Gerrit 选择了 http 认证方式,默认情况下 Gerrit http认证方式会使用前端 Web 服务器传过来的 Authorization HTTP 头部,比如 "Authorization: Basic xxxxxx" 或者 "Authorization: Digest xxxx",可惜的是 Gerrit 代码没有处理 "Authorization: Negotiate xxxx" 的情况,所以需要在 Apache 里用 mod_rewrite 把 REMOTE_USER 变量作为X-Forwarded-User 头部传给 Gerrit,这个名字可以随便取,但根据 Gerrit文档说法,不要重用 Authorization 头部。
第二个特殊的地方是 SMTP encryption,ssl 表示直接以 ssl 方式连接,tls 表示先以非 ssl 方式连接,然后用 STARTTLS 升级为 ssl 连接,后一种方式是现在 ssl 用法里推荐的。使用哪一种取决于你的 smtp 服务器配置,一般 ssl 会用独立端口,tls 的话直接用标准 SMTP 25 端口。
第三个特殊的地方是反向代理,因为我要配置 Kerberos 统一登录,所以Gerrit 前面有个 Apache 做反向代理,这俩我配置在同一台机器上,所以不用 https。
在 java -jar gerrit.war init -d /srv/gerrit/site 执行完之后,它会提示你访问 http://127.0.0.1:2080/#/admin/projects/,但你应该访问http://gerrit.corp.example.com/#/admin/projects/,这里gerrit.corp.example.com 是我给 gerrit 所在机器设置的 CNAME,这个请求会被 Apache 的 gerrit virtual host 截获,做完 HTTP Negotiate认证后转发给后台的 Gerrit,也就是 http://127.0.0.1:2080/...,直接请求 2080 端口这个地址的话,Gerrit 会报错说没有 Authorization 头部。
下面是我的 Apache gerrit 虚拟主机配置:
ServerName gerrit.corp.example.com ServerAdmin [email protected] DocumentRoot /nonexistent ErrorLog ${APACHE_LOG_DIR}/gerrit-error.log # Possible values include: debug, info, notice, warn, error, crit, # alert, emerg. LogLevel warn CustomLog ${APACHE_LOG_DIR}/gerrit-access.log combined ProxyRequests Off ProxyVia Off ProxyPreserveHost On Order deny,allow Allow from all AuthType Kerberos Require valid-user Order allow,deny Allow from all RewriteEngine On RewriteCond %{REMOTE_USER} (.+) RewriteRule .* - [E=RU:%1] RequestHeader set X-Forwarded-User %{RU}e ProxyPass / http://127.0.0.1:2080/ ProxyPassReverse / http://127.0.0.1:2080/ |
在请求 http://gerrit.corp.example.com/#/admin/projects/ 时,Gerrit可能报错说找不到 All-Projects,原因不明,解决办法是把 PostgreSQL里的 gerrit 数据库删除重建,再重新 java -jar gerrit.war init。
没出其它问题的话,Gerrit 的 Web 界面就展现在你面前,它要求为当前用户注册一个 email 帐号,第一个登录的用户自动成为管理员,后续登录的其它用户是普通权限的。
如果你用的 SMTP 服务器的 SSL 证书是自签名的,并且跟我一样 Gerrit使用 tls 方式连接 SMTP 服务器,到这里会卡壳一下。第一个问题是site/etc/gerrit.config 里默认没有 sendemail.sslverify,它的值默认是 true,这会导致 javax.net.ssl 检查 SMTP 服务器的 SSL 证书是否是 trusted 的,答案当然是否,于是 Gerrit 抛异常了:
sun.security.validator.ValidatorException: PKIX path building failed:
sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
解决办法有如下这些:
在 /srv/gerrit/site/etc/gerrit.config 里 [sendemail] 里添加 sslverify = false,虽然使用 openssl 的 client 大多是这个德行,但我觉得不大舒服,所以没用这个办法。
把 SMTP 服务器的 SSL 证书导入 Java 的 truststore 里供 javax.net.ssl 使用。
truststore 是只包含公钥的 keystore, keystore 是 Java 安全框架里用来保存证书、私钥等等的东西,最常用的是 JKS 格式的 keystore 文件,比如 $JAVA_HOME/jre/lib/security/cacerts, $HOME/.keystore。truststore 被 Java 类库里的 TrustManager 使用,keystore 被 Java 类库里的 KeyManager 使用,当然 TrustManager 也能用 keystore。
在这个证书验证问题上,需要给 TrustManager 指定一个 truststore 或者keystore,有三种办法:
如果 javax.net.ssl.trustStore 系统属性指定了,就使用这个系统属性指定的那个文件当作 truststore,truststore 的密码从javax.net.ssl.trustStorePassword系统属性获取。网上有不少文章还提到 javax.net.ssl.keyStore 和 javax.net.ssl.keyStorePassword,这个只在 ssl server 端或者 ssl client 端使用client cert 向 ssl server认证的情况下才需要。
如果 javax.net.ssl.trustStore 属性没指定或者文件没找到,则使用 $JAVA_HOME/jre/lib/security/jssecacerts
如果 jssecacerts 没找到,则使用 $JAVA_HOME/jre/lib/security/cacerts。
网上有不少文章都说把证书直接加入 jssecacerts 或者 cacerts 中,我是半安全偏执狂,觉得把一个自己玩的证书加进去不太靠谱,另外担心 Debian 的软件包升级会自动更新 cacerts(伊其实是符号链接到 /etc/ssl/certs/java/cacerts了,ca-certificates 包的 /usr/sbin/update-ca-certificates 会通过ca-certificates-java 包的 /etc/ca-certificates/update.d/jks-keystore更新它,文档有提到 local cert 会依然保留,我没试验)。
通过分析 /srv/gerrit/site/bin/gerrit.sh 启动脚本,伊使用了JAVA_OPTIONS 变量,并且读取 /etc/default/gerritcodereview 文件,所以可以在 /etc/default/gerritcodereview 里写入:
JAVA_OPTIONS="-Djavax.net.ssl.trustStore=/srv/gerrit/truststore \ -Djavax.net.ssl.trustStorePassword=changeit" |
然后 /srv/gerrit/site/bin/gerrit.sh stop 再 start 重启 gerrit。(也可以把这个选项放在 /srv/gerrit/site/etc/gerrit.config 的container.javaOptions 里:http://gerrit-documentation.googlecode.com/svn/Documentation/2.3/config-gerrit.html#_a_id_container_a_section_container)
/srv/gerrit/truststore 是这么生成的:
gerrit$ keytool -importcert -alias exim -file /etc/exim4/exim.crt \ -keystore /srv/gerrit/truststore -storepass changeit |
这个 truststore 的密码是无所谓的,因为它里头没有私钥。 exim.crt 是用/usr/share/doc/exim4/examples/exim-gencert 生成的。
重启 Gerrit 后,证书问题解决了,第一次登录要求注册邮箱的对话框也没了,这时可以点击右上角的 settings 链接,在 contact information 那一栏里。可惜的是问题没完,输入邮箱点击"Register New Email..."后,Gerrit 一直 Loading,/var/log/exim4/mainlog 以及 /srv/gerrit/site/logs/error_log 里没有错误信息,Gerrit Web 页面就那么一直挂着,直到 exim4 报告连接超时,把 Gerrit发起的 smtp 链接断掉。
花费了三百脑细胞后,俺终于找到原因,是 Gerrit 的 AuthSMTPClient.startTLS()实现跟 SMTP 服务器配合有问题,这是一个 SMTP STARTTLS 会话:
$ gsasl --smtp smtp.corp.example.com Trying `gold.corp.example.com'... 220 gold.corp.example.com ESMTP Exim 4.77 Mon, 21 May 2012 14:46:43 +0800 EHLO [127.0.0.1] 250-gold.corp.example.com Hello localhost [127.0.0.1] 250-SIZE 10485760 250-PIPELINING 250-AUTH GSSAPI 250-STARTTLS 250 HELP STARTTLS 220 TLS go ahead EHLO [127.0.0.1] 250-gold.corp.example.com Hello localhost [127.0.0.1] 250-SIZE 10485760 250-PIPELINING 250-AUTH GSSAPI DIGEST-MD5 CRAM-MD5 250 HELP AUTH GSSAPI .... |
可以看到在 STARTTLS 之后,Exim 不会再次发送 banner 了:220 gold.corp.example.com ESMTP Exim 4.77 Mon, 21 May 2012 14:46:43 +0800
下面是 Gerrit AuthSMTPClient 的代码,伊在 org.apache 的命名空间了插了个 AuthSMTPClient 类,试图给 apache commons-net 2.2 的 SMTPClient增加 STARTTLS 支持:http://code.google.com/p/gerrit/source/browse/gerrit-patch-commonsnet/src/main/java/org/apache/commons/net/smtp/AuthSMTPClient.java?name=stable-2.3
public boolean startTLS(final String hostname, final int port, final boolean verify) throws SocketException, IOException { if (sendCommand("STARTTLS") != 220) { return false; } _socket_ = sslFactory(verify).createSocket(_socket_, hostname, port, true); _connectAction_(); return true; } |
事情坏在 _connectAction_() 里,这个在 AuthSMTPClient 的父类 SMTPClient的父类 SMTP 里会在去读取 SMTP 服务器的 banner 信息,于是 startTLS() 就挂在这个地方傻等直到 SMTP 服务器踢开它。。。。人生不如意事十之八九啊。。。
http://www.rfc-editor.org/rfc/rfc2487.txt 5.2 Result of the STARTTLS Command Upon completion of the TLS handshake, the SMTP protocol is reset to the initial state (the state in SMTP after a server issues a 220 service ready greeting). |
从这个描述看,SMTP server 是不应该再发一次 banner 的。在 AuthSMTPClient.startTLS()搭个补丁后(http://code.google.com/p/gerrit/issues/detail?id=1397),STARTTLS 顺利完成,开始 SMTP 认证,注意 apache commons-net 2.2 只支持 CRAM-SHA1, CRAM-MD5, LOGIN, PLAIN这几种,不支持 DIGEST-MD5,还好我前面为了ReviewBoard打开了 Exim4 的 CRAM-MD5 认证支持。
这块代码感觉比较龌龊,不知道是不是实现有缺陷,所以 apache commons-net网站上把 2.x 系列从下载页面删除了,只有 1.x 和 3.x 系列。。。不知道Google 那帮人为什么没转向标准的 JavaMail API。
如果 Gitweb 已经安装了,那么 Gerrit 自动集成 Gitweb,标准安装情况下啥都不用配置,在 Gerrit Web UI 上每个补丁旁边有 gitweb 的链接,相当相当的好用。Gerrit 文档还声称能跟 cgit 集成,我没实验过。由于 Gerrit 会动态的在/srv/gerrit/.gerritcodereview/tmp/gerrit..../ 下生成 gitweb_config.perl,有这个文件后 /usr/lib/cgi-bin/gitweb.cgi 就不会读取 /etc/gitweb.conf 了。
Gerrit 还能通过 commentlink 和 trackingid 跟外部的 Bug 跟踪系统集成,参考:http://gerrit-documentation.googlecode.com/svn/Documentation/2.3/config-gerrit.html
Gerrit 直接使用 jgit 库直接管理代码库,如果有外部的 GIT 库,比如被Gitolite 管理的,有两个办法让提交到 Gerrit 的修改也散播到外部 GIT 库里:
使用 Gerrit 的 Git replication 特性,Gerrit 背地里把修改 git push 到外部库里,这个办法会有延迟。 参考:http://gerrit-documentation.googlecode.com/svn/Documentation/2.3/config-replication.html
创建外部库的符号链接到 /srv/gerrit/site/git/ 里,比如 ln -s /srv/git/repositories/testing.git /srv/gerrit/site/git/testing.git,需要注意文件权限。 参考:http://gerrit-documentation.googlecode.com/svn/Documentation/2.3/project-setup.html#_manual_creation,这个文档不是说创建符号链接的,我只是猜测可行。
一番配置、读文档下来,感觉 Gerrit 真是 GIT 用户居家办公必备良品,这么好的玩意居然是开源的,太赞了!