57

golang实现gitlab commit注释校验hook

 6 years ago
source link: https://studygolang.com/articles/14237?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

最近和项目成员约定了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提示信息,不阻塞提交。等实施了一段时间后,把打印修改为阻塞,强制执行约定。

希望对大家有用。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK