golang实现gitlab commit注释校验hook

最近和项目成员约定了git commit规则,但是约定归约定,要保证大家都执行,还是需要程序来做些校验工作。

大致的约定如下:

comment 格式:<start|do|end>:#69 fix something bug

其中的start为在redmine版本管理中指定的关键字,具体参见redmine的”配置“ -> "版本库" -> "在提交信息中引用和解决问题" 中的配置。

废话不多说,直接上代码:

package main

import (

"encoding/json"

"fmt"

"io/ioutil"

"net/http"

"os"

"os/exec"

"regexp"

"strconv"

"strings"

)

type COMMIT_TYPEstring

const (

OK    COMMIT_TYPE ="ok"    //issue 完成

  START COMMIT_TYPE ="start" //issue 开始

  DOING COMMIT_TYPE ="doing" //issue 进行中

)

// 是否开启严格模式,严格模式下将校验所有的提交信息格式(多 commit 下)

var commitMsgReg = regexp.MustCompile(COMMIT_MESSAGE_PATTERN)

var USER2EMAIL =map[string]string{

"developer1's name":"developer1's email",

}

var WHITE_LIST = []string{

//"",

}

const (

ISSUE_STATUS_NEW      ="1"

  ISSUE_STATUS_DOING    ="2"

  ISSUE_STATUS_END      ="3"

  ISSUE_STATUS_FEEDBACK ="4"

  ISSUE_STATUS_CLOSE    ="5"

  ISSUE_STATUS_REJECTED ="6"

)

var ISSUE_STATUS_STR =map[string]string{

"1":"NEW",

  "2":"DOING",

  "3":"RESOLVED",

  "4":"FEEDBACK",

  "5":"CLOSED",

  "6":"REJECTED",

}

const (

COMMENT_PREFIX_BEGIN ="begin:"

  COMMENT_PREFIX_END  ="end:"

  COMMENT_PREFIX_DO    ="do:"

)

func main() {

input, _ := ioutil.ReadAll(os.Stdin)

//write2Log(string(input))

  param := strings.Fields(string(input))

// allow branch/tag delete

  if param[0] =="0000000000000000000000000000000000000000" ||

param[1] =="0000000000000000000000000000000000000000" {

os.Exit(0)

}

//write2Log(fmt.Sprintf("%v\n", param))

//commitMsg := getCommitMsg(param[0], param[1])

  commitDetails := getCommitDetail(param[0], param[1])

checkMsgFormat(commitDetails)

}

//提交的详细信息

type CommitDetailstruct {

messagestring

  commitEmailstring

  hashstring

}

//获取提交的详细信息

func getCommitDetail(oldCommitID, commitIDstring) (details []*CommitDetail) {

details =make([]*CommitDetail, 0, 10)

getCommitMsgCmd := exec.Command("git", "log", oldCommitID+".."+commitID, "--pretty=format:%s::%ce::%H")

getCommitMsgCmd.Stdin = os.Stdin

getCommitMsgCmd.Stderr = os.Stderr

b, err := getCommitMsgCmd.Output()

if err != nil {

write2Log(MSG_TYPE_ERROR, fmt.Sprintf("cmd %v execute error : %v", getCommitMsgCmd, err))

//checkFailed()

      return

  }

write2Log(MSG_TYPE_INFO, fmt.Sprintf("%v", getCommitMsgCmd.Args))

//write2Log(string(b))

  //先按照"\n"来分割,因为可能会存在多个commit同时push的情况

  commits := strings.Split(string(b), "\n")

if len(commits) <=0 {

write2Log(MSG_TYPE_ERROR, fmt.Sprintf("get commits failed from %s !", string(b)))

//checkFailed()

      return

  }

for _, commit :=range commits {

infos := strings.Split(commit, "::")

//write2Log(fmt.Sprintf("len(infos) : %d", len(infos)))

      if len(infos) !=3 {

write2Log(MSG_TYPE_ERROR, "get commit info failed !")

//checkFailed()

        return

      }

details = append(details, &CommitDetail{

message:    infos[0],

        commitEmail: infos[1],

        hash:        infos[2],

      })

}

return

}

//校验注释格式是否正确

func checkMsgFormat(details []*CommitDetail) {

for _, d :=range details {

//查找"#"

      pos0 := strings.Index(d.message, "#")

if pos0 == -1 {

write2Log(MSG_TYPE_ERROR, d.hash+" '#' no found in comment")

//checkSucceed()

        continue

      }

//获取前缀

      prefix := d.message[:pos0]

if len(prefix) <=0 {

write2Log(MSG_TYPE_ERROR, d.hash+"WARNING: no any prefix , pls check as follow : begin|end|do:# .")

//checkSucceed()

        continue

      }

//查找空格

      pos1 := strings.Index(d.message[pos0+1:], " ")

if pos1 == -1 {

write2Log(MSG_TYPE_ERROR, d.hash+"WARNING: no any blankspace , pls check as follow : begin|end|do:# .")

continue

        //checkFailed()

      }

//write2Log(fmt.Sprintf("pos0 %d, pos1 %d of %v", pos0, pos1, d))

      //是否是正确的issue序列号

      issueId, err := strconv.ParseUint(d.message[pos0+1:pos0+1+pos1], 0, 64)

if err != nil {

write2LogErr(err)

//checkFailed()

        continue

      }

reqStr := fmt.Sprintf("http://<你的gitlab服务器>/issues/%d.json?key=<你的api key>", issueId)

resp, err := http.Get(reqStr)

if err != nil {

write2LogErr(err)

//checkFailed()

        continue

      }

if resp.StatusCode !=200 {

write2Log(MSG_TYPE_ERROR, d.hash+fmt.Sprintf("get issue info from redmine failed ! resp.StatusCode %d, please check if redmine service is valid !", resp.StatusCode))

//checkFailed()

        continue

      }

contents, err := ioutil.ReadAll(resp.Body)

resp.Body.Close()

items :=make(map[string]interface{}, 0)

err = json.Unmarshal(contents, &items)

if err != nil {

write2LogErr(err)

continue

        //checkFailed()

      }

//write2Log(d.hash + fmt.Sprintf("issue detail : %v\n", items))

      //issue当前责任人是否是提交人

      if issue, ok := items[ISSUE]; !ok {

write2Log(MSG_TYPE_ERROR, d.hash+fmt.Sprintf("issue %d no found !", issueId))

//checkFailed()

        continue

      }else {

if v, ok := issue.(map[string]interface{}); !ok {

write2Log(MSG_TYPE_ERROR, d.hash+fmt.Sprintf("issue info error. %v", issue))

//checkFailed()

            continue

        }else {

//获取责任人

            ok, assignedTo := getIssueItemName(d, ISSUE_ASSIGNED_TO, v)

if !ok {

write2Log(MSG_TYPE_ERROR, d.hash+fmt.Sprintf("get %s failed ! %v", v))

//checkFailed()

              continue

            }

email, ok := USER2EMAIL[assignedTo]

if !ok {

write2Log(MSG_TYPE_ERROR, d.hash+fmt.Sprintf("unkown user.name %s !\nkown users : %v", assignedTo, USER2EMAIL))

//checkFailed()

              continue

            }

//当前问题的责任人不是提交人

            if 0 != strings.Compare(email, d.commitEmail) {

write2Log(MSG_TYPE_ERROR, d.hash+fmt.Sprintf(" issue#%d's owner is %s but not you.", issueId, assignedTo))

//checkFailed()

              continue

            }

//问题状态校验

            ok, status := getIssueItemId(d, ISSUE_STATUS, v)

if !ok {

write2Log(MSG_TYPE_ERROR, d.hash+fmt.Sprintf("get %s failed ! %v", ISSUE_STATUS, v))

//checkFailed()

              continue

            }

if status !=ISSUE_STATUS_NEW &&

status !=ISSUE_STATUS_DOING &&

status !=ISSUE_STATUS_FEEDBACK {

write2Log(MSG_TYPE_ERROR, d.hash+fmt.Sprintf("error issue #%d status : %s", issueId, ISSUE_STATUS_STR[status]))

//checkFailed()

              continue

            }

//switch prefix {

//case COMMENT_PREFIX_BEGIN:

// {

//    if status != ISSUE_STATUS_NEW &&

//      status != ISSUE_STATUS_FEEDBACK {

//      write2Log(fmt.Sprintf("issue#%d should be new or feedback !", issueId))

//      checkFailed()

//    }

// }

//case COMMENT_PREFIX_END:

// {

//    if status != ISSUE_STATUS_NEW &&

//      status != ISSUE_STATUS_FEEDBACK &&

//      status != ISSUE_STATUS_DOING {

//      write2Log(fmt.Sprintf("issue#%d should be new or feedback or doing !", issueId))

//      checkFailed()

//    }

// }

//case COMMENT_PREFIX_DO:

// {

//    if status != ISSUE_STATUS_DOING {

//      write2Log(fmt.Sprintf("issue#%d should be doing !", issueId))

//      checkFailed()

//    }

// }

//default:

// {

//    write2Log(fmt.Sprintf("unkown prefix %s !", prefix))

//    checkFailed()

// }

//}

            write2Log(MSG_TYPE_INFO, d.hash+" check succeed!")

}

}

}

}

//属性结构体字段索引

const (

INDEX_ID =iota

INDEX_NAME

)

func getIssueItemName(d *CommitDetail, namestring, vmap[string]interface{}) (okbool, valuestring) {

return getIssueItemStr(d, name, v, INDEX_NAME)

}

func getIssueItemId(d *CommitDetail, namestring, vmap[string]interface{}) (okbool, valuestring) {

return getIssueItemStr(d, name, v, INDEX_ID)

}

//获取issue子信息

func getIssueItemStr(d *CommitDetail, namestring, vmap[string]interface{}, indexint) (okbool, valuestring) {

//获取责任人

  var vTmpinterface{}

if vTmp, ok = v[name]; !ok {

write2Log(MSG_TYPE_ERROR, d.hash+fmt.Sprintf("%s no found!!! from %v", name, v))

checkFailed()

}else {

valueMap, ok := vTmp.(map[string]interface{})

if !ok {

write2Log(MSG_TYPE_ERROR, d.hash+fmt.Sprintf("convert %s map failed. %v", name, vTmp))

checkFailed()

}

var keystring

      switch index {

case INDEX_ID:

key =ID

      case INDEX_NAME:

key =NAME

      default:

write2Log(MSG_TYPE_ERROR, d.hash+fmt.Sprintf("unkown index %v", index))

checkFailed()

}

valueTmp, ok := valueMap[key]

if !ok {

write2Log(MSG_TYPE_ERROR, d.hash+fmt.Sprintf("get %s.%s failed ! %v", name, key, vTmp))

checkFailed()

}

value = fmt.Sprintf("%v", valueTmp)

}

return

}

const (

ISSUE            ="issue"

  ID                ="id"

  NAME              ="name"

  ISSUE_STATUS      ="status"

  ISSUE_ASSIGNED_TO ="assigned_to"

  ISSUE_SUBJECT    ="subject"

)

func checkFailed() {

os.Exit(1)

}

func checkSucceed() {

os.Exit(0)

}

type MSG_TYPEint

const (

MSG_TYPE_ERROR MSG_TYPE =iota

MSG_TYPE_WARNING

MSG_TYPE_INFO

)

func write2LogErr(errerror) {

write2Log(MSG_TYPE_ERROR, fmt.Sprintf("%v", err))

}

func write2Log(tMSG_TYPE, sstring) {

var msg_prefixstring

  switch t {

case MSG_TYPE_ERROR:

msg_prefix ="ERROR"

  case MSG_TYPE_WARNING:

msg_prefix ="WARNING"

  default:

msg_prefix ="INFO"

  }

fmt.Fprintln(os.Stderr, msg_prefix+": "+s)

}

如上代码将gitlab url地址和api key替换成自己的就可以直接使用。

为了方便项目组成员过度,在校验不通过的时候,暂时只返回ERROR提示信息,不阻塞提交。等实施了一段时间后,把打印修改为阻塞,强制执行约定。

希望对大家有用。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,634评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,951评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,427评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,770评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,835评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,799评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,768评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,544评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,979评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,271评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,427评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,121评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,756评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,375评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,579评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,410评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,315评论 2 352

推荐阅读更多精彩内容