借助svnchecker 和 gitchecker,实现 禅道与svn,以及禅道与git 集成的逻辑复用。即同一份代码,完成两份工作。双赢,赢两次!


0. 目录

  • 1. 前言
  • 2. 优势
  • 3. 实现
  • 3.1 配置文件`gitcheckerconfig.ini`
  • 3.2 核心文件`PushCommitMsgToZentao.py`
  • 3.3 zentao扩展
  • 4. 参考


1. 前言

在一些规模比较大的研发团队,内部由历史等诸如因素,可能存在多种SCM并存的情况,而且短时间内还不太容易完全迁移以实现统一。在这种情况下,诸如代码提交检查,代码提交后置操作等往往就需要团队进行分别维护。

在禅道的官方文档中,分别就"禅道与SVN",以及"禅道与GIT"的集成进行了介绍,根据文档本身以及下方的评论来看,集成的过程不仅比较复杂,还存在着一定的束缚(例如实现git集成的前提是git服务端和禅道服务端要在同一台服务器上)。

鉴于以上情况,我们可以通过借鉴svnchecker思路,实现具有相同功能的Gitee - gitChecker,最终达到同一份代码,既能实现"禅道与svn",也能完成"禅道与git 集成"。

2. 优势

本方案的优势:

  1. svn / git Hook 共用一套代码逻辑,无需分别开发。
  2. 在svn / git 源生Hook基础上进行封装,向上提供一些方便理解和调用的API,快速实现所需功能,降低维护成本。

效果图:

  1. SVN:
  2. 禅道中绑定gitlab提示token非root权限 禅道集成git_devops

  3. GIT:
  4. 禅道中绑定gitlab提示token非root权限 禅道集成git_devops_02

3. 实现

本文所使用的Gitee - gitChecker已经开源,有需要的读者可以直接下载使用。本文接下来的部分主要关注如何将SCM提交的日志同步到禅道。

关于如何进行gitchecker的配置,可以参考笔者之前写的【DevOps】SVN集成Checkstyle实现代码自动静态检查,gitchecker直接复用了svnchecker的代码和思路,只是将其适配到了git上。

3.1 配置文件gitcheckerconfig.ini

为了语义上的统一,我们将继承自svnchecker的配置文件改名为gitcheckerconfig.ini。具体的配置格式和配置方式保持不变。针对本文中将要实现的功能,相关配置如下图:

[Default]

#This property tells Subversionchecker about all checks
#(UnitTests, AccessRights, XMLValidator etc) it should execute.
#Separated with comma (",")
Main.PostCommitChecks=SqlChecker, PushCommitMsgToZentao

PushCommitMsgToZentao.FailureHandlers=Console
PushCommitMsgToZentao.SuccessHandlers=Console
3.2 核心文件PushCommitMsgToZentao.py

实现svn/git同步信息到禅道的关键文件就是本小节介绍的PushCommitMsgToZentao.py了。

import datetime
import time
import re
from urllib import urlopen
import json
from os import path
import base64
import hashlib
import os

// 实现"第三方应用调用zentao接口"所必备的两个参数
ZENTAO_CODE="robot123"
ZENTAO_KEY="c9c8246523323217278c123040b326e9a9"
// 禅道和svn的访问根路径
ZENTAO_BASE_URL="http://xxx.57.ccc.41:zzz/zentao"
SVN_BASE_URL="https://xxx.57.ccc.41:zzz/svn/"


import sys
reload(sys)
sys.setdefaultencoding('utf8')

def run(transaction, config):
    if not "joint3.0/trans" in os.getenv("GL_PROJECT_PATH"):
      return("", 0)

    commitMsg = transaction.getCommitMsgWithLinesep()  # getCommitMsg
    userId = transaction.getUserID()
    reversion = transaction.getRevision()

    # call zentao
    time1 = int(time.time())
    token = hashlib.md5((ZENTAO_CODE+ZENTAO_KEY+str(time1)).encode(encoding='UTF-8')).hexdigest()
    commitMsgBylines = commitMsg.splitlines(False)
    firstLine = commitMsgBylines[0].strip()
    patt = re.compile("(story|bug|task)\:(\d+)\s.+")
    result = re.search(patt,firstLine)
    if(result == None):
      return("", 0)

    objecType = result.group(1)
    objectID = result.group(2)
    commentMsg = commitMsg
    commentMsg = commentMsg + os.linesep + "#GIT: " + os.getenv("GL_PROJECT_PATH")
    # 将 + 作为url参数的会导致 Bad Request错误
    commentMsg = base64.b64encode(commentMsg).replace("+", "___")
    # 注意这里对于actionType参数的设置,这一点很重要,直接关系到我们在禅道端看到的效果
    visitUrl = '{}/api.php?m=action&f=comment2&objectType={}&objectID={}&actionType=gitcommited&comment={}&actor={}&extra={}&code={}&time={}&token={}'.format(ZENTAO_BASE_URL, objecType, objectID,commentMsg, userId,reversion, ZENTAO_CODE, time1, token)
    response = urlopen(visitUrl)
    if(response.read().decode("UTF-8").isdigit()):
      return ("", 0)

    return (response.read().decode("UTF-8"), 1)

好吧,其实上面这段代码,笔者已经上传到了Gitee - PushCommitMsgToZentao.py ,为了保持文章的完整性,这里还是得再贴一次。

另外为佐证笔者前面所说的"复用同步逻辑",这里贴出笔者所使用的SVN版本Hook:

def run(transaction, config):
    commitMsg = transaction.getCommitMsgWithLinesep()  # getCommitMsg
    userId = transaction.getUserID()
    reversion = transaction.getRevision()

    # fullUrlPath --->  https://ccc.57.mmm.41:804/svn/Dz/trunk/zzz_bdc
    # filename ---> trunk/zzz_bdc/XXX.txt
    filename = transaction.getFiles().items()[0][0]

    # transaction.getReposPath()  ---> /home/DJ/
    fullRespoPath = path.join(SVN_BASE_URL, transaction.getReposPath().replace("/home/", ""), path.dirname(filename))
    # call zentao
    time1 = int(time.time())
    token = hashlib.md5((ZENTAO_CODE+ZENTAO_KEY+str(time1)).encode(encoding='UTF-8')).hexdigest()
    bbbb = bytes(fullRespoPath)
    aaaa = base64.b64encode(bbbb)
    svnReposUrl = str(aaaa).replace("+", "___")
    #response = urlopen('{}/api.php?m=svn&f=syncLog&svnReposUrl={}&code={}&time={}&token={}'.format(ZENTAO_BASE_URL, svnReposUrl, ZENTAO_CODE, time1, token))


    #
    commitMsgBylines = commitMsg.splitlines(False)
    firstLine = commitMsgBylines[0].strip()
    patt = re.compile("(story|bug|task)\:(\d+)\s.+")
    result = re.search(patt,firstLine)
    if(result == None):
      return("", 0)

    objecType = result.group(1)
    objectID = result.group(2)
    commentMsg = commitMsg
    # 这里的替换是因为svn路径中存在中文
    commentMsg = commentMsg.replace("{U+","\\u").replace("}", "").encode('utf8').decode('unicode_escape')
    commentMsg = commentMsg + os.linesep + "#SVN: " + fullRespoPath
    commentMsg = base64.b64encode(commentMsg).replace("+", "___")
    # 注意这里对于actionType参数的设置,这一点很重要,直接关系到我们在禅道端看到的效果
    visitUrl = '{}/api.php?m=action&f=comment2&objectType={}&objectID={}&actionType=svncommited&comment={}&actor={}&extra={}&code={}&time={}&token={}'.format(ZENTAO_BASE_URL, objecType, objectID,commentMsg, userId,reversion, ZENTAO_CODE, time1, token)
    response = urlopen(visitUrl)
    #return (visitUrl, 1)
    if(response.read().decode("UTF-8").isdigit()):
      return (fullRespoPath, 0)

    return (response.read().decode("UTF-8"), 1)

通过对比,可以看到确实没有完全一致,但这只是因为我们当下并没有代码进行优化,而且python代码本身的即时编译功能也可以让我们只需要少量工作就能完成这些调整。

3.3 zentao扩展

上面的hook中,我们发现了svn/git Hook会向禅道发起一个http接口调用,但默认禅道是没有提供这个服务接口的,所以这其实是一个我们自己扩展的接口 —— 其主要目的是为了让我们灵活地控制推送到禅道的信息格式,这样就能更为真实地模拟禅道内置的svn/git提交日志格式了。

// 本文件存放路径
// {appRootPath}\module\action\ext\control\comment2.php
<?php

include '../../control.php'; 
class myAction extends action
{
    /**
     * Comment. 
     * 
     * @param  string $objectType 
     * @param  int    $objectID 
     * @access public
     * @return void
     */
    public function comment2($objectType, $objectID, $actionType='Commented', $comment="", $actor = '', $extra = '')
    {// , $actionType, $comment = '', $extra = '', $actor = '', $autoDelete = true
        $actionID = $this->action->create($objectType, $objectID, $actionType, base64_decode(str_replace("___", "+", $comment)), $extra, $actor);
        if(defined('RUN_MODE') && RUN_MODE == 'api')
        {
        	/ 这里直接返回所新增zt_action记录的id值; 方便调用端进行判断
            die($actionID);
            //die(array('status' => 'success', 'data' => $actionID));
        }
        else
        {
            die(js::reload('parent'));
        }
    }
}