最近和项目成员约定了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提示信息,不阻塞提交。等实施了一段时间后,把打印修改为阻塞,强制执行约定。
希望对大家有用。
有疑问加站长微信联系(非本文作者)