利用gitlab的web hook和钉钉做代码review工作

     还记得在我读大学的时候有一门课叫做计算机安全,我们的教材《信息安全原理与应用(第四版)》中提到代码检查工作每千行代码发现的错误数量是10个,而其他的程序系统检查方法:需求复查为2.5个/1000行、设计复查为5个/1000行、集成测试为3个/1000行,接受测试为2个/1000行。
     那时候看完这个研究结果之后,代码检查的观念就根深蒂固地植入我的脑海里。工作了这么多年,一直不忘记代码review。团队也制定了很多的代码review工作的要求,但总是因为各种各样的原因做不好。最大的问题就是业务工作太忙,没有时间review。
     还记得上高三的时候,准备高考可谓是争分夺秒,我们都随身携带小笔记本,把重要的知识点记录在里面,蹲厕所、排队吃饭的时候掏出来复习,将细碎的时候利用起来也是一笔不小的财富。那我们就可以利用零碎的时间来做代码review工作,将bug扼杀在摇篮之中!
     webhook功能的作用是当开发者向git服务器push代码完成时,git会触发webhook里配置的url,即向我们配置的url用post的方式发送一个json格式的内容,这个内容里包含本次push的所有信息。众所周知,github和oschina这些代码管理的平台的项目设置里都提供webhook功能,我们公司使用的是gitlab来管理我们的代码,gitlab也拥有webhook功能。webhook功能给了我们无限的想象空间,可以用来自动部署代码,可以自动集成测试,当然,还有本文的主题,review提交的代码。
     我在公司里独自一人维护了一个叫做superb的thinkphp项目,这个系统提供项目管理、日报周报、接口文档、运维信息等功能,我觉得这个功能做到superb里是最合适不过了!
实现步骤:
第一步:根据gitlab的文档中描述的web hook post的json格式编写一个接受post内容的http接口。
第二步:接口处理完post的内容之后发送钉钉消息给指定人员。
具体实现:

1.接口代码(thinkphp框架):

<?php
namespace Home\Controller;

use Think\Controller;
use Think\Log;

class GitController extends Controller
{
    public function index()
    {
        $requestBody = file_get_contents('php://input');
        if (empty($requestBody)) {
            echo '发送失败';
            return false;
        }
        $content = json_decode($requestBody, true);

        $message = "";
        if ($content['total_commits_count'] > 0) {
            $message .= $content['user_name'] . '向' . $content['repository']['name'] . '项目的' . $content['ref'] . '分支push了' . $content['total_commits_count'] . '个commit:' . "\n";
            $count = 0;
            foreach ($content['commits'] as $commit) {
                $count++;
                $message .= $count . '. ' . $commit['author']['name'] . '在' . date('Y年m月d日 h:i:s', strtotime($commit['timestamp'])) . ' 提交的:' . $commit['message'] . "\n";
                $message .= '点击 ' . $commit['url'] . ' 查看本次commit diff' . "\n";
            }
            echo $message;
            Log::write($message);
        } else {
            $message .= $content['user_name'] . '在' . $content['repository']['name'] . '项目创建或者删除了一个分支:' . $content['ref'] . "\n";
        }
        echo D('Dingding', 'Service')->sendTextMsg('@all', $message);
    }
}
2.发送钉钉的代码:
<?php
namespace Home\Service;

use \Think\Model;
use \Think\Log;

class DingdingService extends Model
{
    private $_url = 'https://oapi.dingtalk.com/gettoken?corpid=你的&corpsecret=你的';

    public function sendTextMsg($touser, $text, $agentid = 你的)
    {
        $accessToken = $this->_getAccessToken();
        if ($accessToken == null) {
            return '获取accessToken失败';
        }
        $content = array(
            'touser' => $touser,
            'agentid' => $agentid,
            'msgtype' => 'text',
            'text' => array('content' => $text)
        );
        $url = 'https://oapi.dingtalk.com/message/send?access_token=' . $accessToken;
        $result = $this->_sendPost($url, $content);
        Log::write($result);
        return $result;
    }

    private function _getAccessToken()
    {
        $content = $this->_sendGet($this->_url);
        $json = json_decode($content, true);
        if ($json['errcode'] == 0) {
            $accessToken = $json['access_token'];
            return $accessToken;
        } else {
            Log::write('获取accessToken失败');
            return null;
        }
    }

    private function _sendPost($url, $data)
    {
        $ch = curl_init($url);
        $payload = json_encode($data);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
        curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type:application/json'));
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        $result = curl_exec($ch);
        curl_close($ch);
        return $result;
    }

    private function _sendGet($url)
    {
        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        $result = curl_exec($ch);
        curl_close($ch);
        return $result;
    }
}
完成之后,把接口的url配置到gitlab的web hook上就可以坐等你的push消息了。结果是这样的:
HMHAWQ5%}[I]L(${HK6LNI2
点击最下方的diff url就可以打开gitlab的代码diff页面进行代码review工作。
其实这是个非常简单的功能,但是它给我带来的是完全不同的体验:
  1. 团队成员在push代码之后我第一时间收到钉钉的通知,我可以打开diff页面,立刻查看或者等会儿有时间了再看这个网页,我完全可以利用编译代码的零碎时间来做这项工作。记得这个功能上线第一天我就快速review出两个组员的代码bug,他们都万万没想到。
  2. 让团队中的初级程序员养成提交代码的时候自己先diff的习惯,并且写好comment。以前初级程序员常常乱提交代码,对代码分支造成严重的破坏,现在我告诉他们,只要他们提交代码boss就会看到,他们都非常小心谨慎了。
  3. 和其他团队的协作开发变得更加透明,其他团队的同学提交什么代码,功能说明我就可以看到。一起联调代码,他提交后我第一时间知道他接口搞定了;要求对方修改的bug对方提交了代码我就知道代码修复了。这两个体验不是我随口说的,现实中我真的遇到,感觉很爽!
这个功能的未来:
我希望将来在superb里面增加一个订阅功能,所有开发人员可以订阅自己关注的项目,订阅自己关心的人,这样对团队的代码管理将带来非常大的帮助。

Git分支代码回滚流程

在大型系统开发中难免会遇到一些分支的错误合并导致代码需要回滚到合并前的样子,以下是操作流程:
  1. 将代码的指针指向你所要回滚到的那个版本的分支:

    git reset --hard [分支的Hash值]
  2. 将本地的当前代码的指针push到指定的远程分支:

    git push origin [远程分支的名字] --force
  3. 如果在其他客户端或者服务器上有pull过错误的代码的,每个客户端或者服务器都需要执行

    git reset --hard [分支的Hash值]

    否则你之前做的回滚操作这些客户端和服务器无法生效

git切换分支保存修改的代码的方法

最近在一个原有的项目上做一次非常大的改版,底层的数据库做了很大的变化,跟现在的版本无法兼容。现在的工作除了开发最新的版本之外还要对原来的版本做例行的维护,修修补补。于是有了在两个分支之间游走切换的问题,最新改版的代码在分支new上,旧版本的代码在分支old上,我在new上开发了一半,忽然有人给了我一个改进的需求,于是我要切换回old去修改代码。在这个场景下,我摸索了三种方法:

及时commit代码

在new分支上把已经开发完成的部分代码commit掉,不push,然后切换到old分支修改代码,做完了commit,所有分支互不影响,这是一个理想的方法。

 

使用git stash

有时候写了一半的JAVA代码,都还不能编译通过的,就被叫去改另一个分支的bug了。

在new分支上的时候在命令行输入:

git stash

或者

git stash save “修改的信息"

这样以后你的代码就回到自己上一个commit了,直接git stash的话git stash的栈会直接给你一个hash值作为版本的说明,如果用git stash save “修改的信息”,git stash的栈会把你填写的“修改的信息”作为版本的说明。

接下来你回到old分支修改代码完成,你又再回到new分支,输入:

git stash pop

或者

git stash list
git stash apply stash@{0}

就可以回到保存的版本了。git stash pop的作用是将git stash栈中最后一个版本取出来,git stash apply stash@{0}的作用是可以指定栈中的一个版本,通过git stash list可以看到所有的版本信息:

stash@{0}: On order-master-bugfix: 22222
stash@{1}: On order-master-bugfix: 22222

然后你可以选择一个你需要的版本执行:

git stash apply stash@{0}

这时候你搁置的代码就回来了。

 

用IDE工具的shelve的功能

有一些IDE工具提供了shelve的功能,shelve的意思是“将…搁在一边”,即把还没写完的代码先搁在一边。我开发都是使用jetbrains公司的IDEA和PhpStorm,它们就提供了shelve的功能,方法:

首先在IDE的底部找到“Changes”,点开会有local的选项卡,选中你要搁置的代码,点击右键,选择“Shelve Changes”,在提交的输入框中输入你的注释,以便回来的时候识别你需要的版本,点击“Shelve Changes”键即可。这时选项卡上会多一个“Shelf”的选项卡,里面就有你搁置的代码。

这时候你可以去old分支修改代码,改完了之后回到new分支,到“Shelf”选项卡下选择你要恢复的代码或者版本,点击右键选择“Unshelve Changes”,你的搁置的代码就回来了。

mybatis generator自动生成代码报The error occurred while processing mapper_resultMap[BaseResultMap]错误的解决方法

今天在用mybatis自动生成代码之后遇到了一个错误:

org.apache.ibatis.exceptions.PersistenceException :
### Error building SqlSession.
### The error may exist in com/erp/webservice/mapper/UsersMapper.xml
### The error occurred while processing mapper_resultMap[BaseResultMap]
### Cause: org.apache.ibatis.builder.BuilderException: Error parsing SQL Mapper Configuration. Cause: java.lang.RuntimeException: Error parsing Mapper XML. Cause: java.lang.IllegalArgumentException : Result Maps collection already contains value for com.erp.webservice.inter.UsersMapper.BaseResultMap
     at org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java:23)
     at org.apache.ibatis.session.SqlSessionFactoryBuilder.build(SqlSessionFactoryBuilder.java:51)
     at org.apache.ibatis.session.SqlSessionFactoryBuilder.build(SqlSessionFactoryBuilder.java:35)
     at com.erp.webservice.test.Test.<clinit>( Test.java:22)
Caused by: org.apache.ibatis.builder.BuilderException: Error parsing SQL Mapper Configuration. Cause: java.lang.RuntimeException: Error parsing Mapper XML. Cause: java.lang.IllegalArgumentException : Result Maps collection already contains value for com.erp.webservice.inter.UsersMapper.BaseResultMap
     at org.apache.ibatis.builder.xml.XMLConfigBuilder.parseConfiguration(XMLConfigBuilder.java:107)
     at org.apache.ibatis.builder.xml.XMLConfigBuilder.parse(XMLConfigBuilder.java:89)
     at org.apache.ibatis.session.SqlSessionFactoryBuilder.build(SqlSessionFactoryBuilder.java:49)
     ... 2 more
Caused by: java.lang.RuntimeException: Error parsing Mapper XML. Cause: java.lang.IllegalArgumentException : Result Maps collection already contains value for com.erp.webservice.inter.UsersMapper.BaseResultMap
     at org.apache.ibatis.builder.xml.XMLMapperBuilder.configurationElement(XMLMapperBuilder.java:113)
     at org.apache.ibatis.builder.xml.XMLMapperBuilder.parse(XMLMapperBuilder.java:88)
     at org.apache.ibatis.builder.xml.XMLConfigBuilder.mapperElement(XMLConfigBuilder.java:325)
     at org.apache.ibatis.builder.xml.XMLConfigBuilder.parseConfiguration(XMLConfigBuilder.java:105)
     ... 4 more
Caused by: java.lang.IllegalArgumentException: Result Maps collection already contains value for com.erp.webservice.inter.UsersMapper.BaseResultMap
     at org.apache.ibatis.session.Configuration$StrictMap.put(Configuration.java:710)
     at org.apache.ibatis.session.Configuration$StrictMap.put(Configuration.java:682)
     at org.apache.ibatis.session.Configuration.addResultMap(Configuration.java:473)
     at org.apache.ibatis.builder.MapperBuilderAssistant.addResultMap(MapperBuilderAssistant.java:204)
     at org.apache.ibatis.builder.ResultMapResolver.resolve(ResultMapResolver.java:44)
     at org.apache.ibatis.builder.xml.XMLMapperBuilder.resultMapElement(XMLMapperBuilder.java:277)
     at org.apache.ibatis.builder.xml.XMLMapperBuilder.resultMapElement(XMLMapperBuilder.java:244)
     at org.apache.ibatis.builder.xml.XMLMapperBuilder.resultMapElements(XMLMapperBuilder.java:236)
     at org.apache.ibatis.builder.xml.XMLMapperBuilder.configurationElement(XMLMapperBuilder.java:109)
     ... 7 more
Exception in thread "main" java.lang.NullPointerException
     at com.erp.webservice.test.Test.main( Test.java:29)

后来发现是我运行了多次自动生成代码的程序,导致了com/erp/webservice/mapper/UsersMapper.xml的内容多出来很多,原来自动生成代码的时候生成的XML代码不是覆盖原来的,而是附加的XML的尾部的。

第一次运行自动生成代码程序:

mapper1

第二次运行自动生成代码程序:

mapper2

看看,XML文件明显大了很多,只要删掉原来的XML然后重新运行自动生成代码的程序即可。

使用Underscore.js的template将Backbone.js的js代码和html代码分离

这段时间在学习Require.js和Backbone.js的过程中,发现有些项目里的HTML代码都是写在View的js代码里面的,渲染的时候需要对Collection进行循环,再将HTML代码拼接上去,这似乎不是一件非常好的事情,因为将js代码和html代码融合到一起会增加代码的维护难度,而且这个过程中考虑到性能的因素,需要将HTML代码放到一个数组中,最后进行拼接,代码写起来比较麻烦。我看到他们的代码之后就在考虑是否有一种类似php模板引擎的东西可以将Collection传递进去然后渲染。

我查阅了Backbone.js的手册http://backbonejs.org/#View-template ,里面有一段文字:

However, we suggest choosing a nice JavaScript templating library. Mustache.js, Haml-js, and Eco are all fine alternatives. Because Underscore.js is already on the page, _.template is available, and is an excellent choice if you prefer simple interpolated-JavaScript style templates.

Whatever templating strategy you end up with, it’s nice if you never have to put strings of HTML in your JavaScript.

它建议我们使用js的模板库,而刚好Backbone.js强依赖于Underscore.js所以Underscore.js已经被引入了,它提供了一个_.template方法,这个方法支持使用内嵌js代码的html模板代码,在js代码里没有出现HTML代码是一件非常nice的事情!这正符合了我们MVC的思想,前端的HTML代码也便于维护,要不然就真的成为意大利面条式代码了!

关于Underscore.js的template的说明在http://underscorejs.org/#template ,这里有教你怎么使用。

Template functions can both interpolate variables, using <%= … %>, as well as execute arbitrary JavaScript code, with <% … %>. If you wish to interpolate a value, and have it be HTML-escaped, use <%- … %>

上面这段文字告诉我们在这个模板的代码里面js内嵌代码的标签如何使用,接下来我举一个例子:

我们先建一个template,位于:template/album/index.html

<%
var title = 'My albums';
document.title = title;
%>
<h1><%= title %></h1>
<p>
    <a href="album-rest/add">Add new album</a>
</p>
<table class="table">
<thead>
    <tr>
        <th>Title</th>
        <th>Artist</th>
        <th> </th>
    </tr>
</thead>
<tbody id="album-list">
<% _.each(albums, function(album) { %>
<tr class="album-row">
    <td><%= album.get('title') %></td>
    <td><%= album.get('artist') %></td>
    <td>
        <a href="album-rest/edit/<%= album.get('id') %>">Edit</a>
        <a href="album-rest/delete/<%= album.get('id') %>">Delete</a>
    </td>
</tr>
<% }); %>
</tbody>
</table>

下面的这个代码片段是定义了一个Backbone的View,sync属性会去请求服务端获取获取所有album的数据,最后将数据存放到albumList这个Collection里面。随后执行render方法,在render里面this.template = _.template(AlbumTpl, albums);这句代码就是用来完成数据和模板混合的工作的,AlbumTpl来自template/album/index.html,另外必须要将Collection中的所有的model以数组的形式获取到赋给albums,除非你在模板里面又进行了对Collection的解析操作,否则不能只传入一个Collection,因为Underscore.js的template是无法识别Backbone.js的Collection的对象结构的。

define(["model/album", "collection/album-list", "text", 'text!template/album/index.html'], function(Album, AlbumList, text, AlbumTpl) {
	var IndexView = Backbone.View.extend({
		model : Album,
		initialize: function() {
		},
		sync : function (render) {
			var albumList = new AlbumList;
			var view = this;
			Backbone.sync('read', albumList, {
				success : function (result) {
					albumList.add(result.ret);
					view.collection = albumList;
					view.render();
				}
			});
		},
		render: function() {
			albumList = this.collection;
			albums = albumList.models;
			console.log(_.template(AlbumTpl, albums));
			this.template = _.template(AlbumTpl, albums);
			$("#page-wrapper").html(this.template);
		}
	});
	return IndexView;
});

通过上面的操作,就可以实现js代码和html代码分离了。