Skip to content

Instantly share code, notes, and snippets.

@xmapst
Last active February 22, 2023 00:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save xmapst/84893b46c299c596f05d76c3f8414e3a to your computer and use it in GitHub Desktop.
Save xmapst/84893b46c299c596f05d76c3f8414e3a to your computer and use it in GitHub Desktop.
GitLab 使用 Server Hooks 校验 Commit 用户名与邮箱及commit message样式
// +build linux
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"log/syslog"
"net/http"
"os"
"os/exec"
"regexp"
"strconv"
"strings"
)
const (
FEAT = "feat"
FIX = "fix"
DOCS = "docs"
STYLE = "style"
REFACTOR = "refactor"
TEST = "test"
CHORE = "chore"
PERF = "perf"
HOTFIX = "hotfix"
)
// 错误提示模板
const checkFailedMessage = `##############################################################################
##
## 提交消息样式检查失败
##
## 提交消息样式必须满足此常规:
## ^(?:fixup!\s*)?(\w*)(\(([\w\$\.\*/-].*)\))?\: (. *)|^Merge\ branch(.*)
##
## 示例:
## feat(web): test commit style check.
##
##############################################################################`
const checkFailedUser = `##############################################
##
## 提交用户名与gitlab登录用户名不匹配, 请修改!
## git config --global user.name "%s"
##
## 修改后, 先操作撤销commit, 以下示例只是撤销一个
## git reset --soft HEAD~1
##
##############################################`
const checkFailedEmail = `##############################################
##
## 提交邮箱与gitlab登录邮箱不匹配, 请修改!
## git config --global user.email "%s"
##
## 修改后, 先操作撤销commit, 以下示例只是撤销一个
## git reset --soft HEAD~1
##
##############################################`
const checkFailedMsgLen = `##############################################
## 先撤销commit, 然后再进行commit提交。以下示例只是撤销一个
## git reset --soft HEAD~1
##
##############################################`
// 定义gitlab user info api 返回json结构体
type Commits struct {
Commit string `json:"commit,omitempty"`
AbbreviatedCommit string `json:"abbreviated_commit,omitempty"`
Subject string `json:"subject,omitempty"`
SanitizedSubjectLine string `json:"sanitized_subject_line,omitempty"`
Author User `json:"author,omitempty"`
Committer User `json:"committer,omitempty"`
}
type User struct {
Name string `json:"name,omitempty"`
Email string `json:"email,omitempty"`
Date string `json:"date,omitempty"`
Timestamp string `json:"timestamp,omitempty"`
RelativeDate string `json:"relative_date,omitempty"`
}
type GitlabUserInfo struct {
ID int `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Username string `json:"username,omitempty"`
State string `json:"state,omitempty"`
AvatarURL string `json:"avatar_url,omitempty"`
WebURL string `json:"web_url,omitempty"`
CreatedAt string `json:"created_at,omitempty"`
Bio string `json:"bio,omitempty"`
BioHTML string `json:"bio_html,omitempty"`
Location interface{} `json:"location,omitempty"`
PublicEmail string `json:"public_email,omitempty"`
Skype string `json:"skype,omitempty"`
Linkedin string `json:"linkedin,omitempty"`
Twitter string `json:"twitter,omitempty"`
WebsiteURL string `json:"website_url,omitempty"`
Organization interface{} `json:"organization,omitempty"`
JobTitle string `json:"job_title,omitempty"`
WorkInformation interface{} `json:"work_information,omitempty"`
LastSignInAt string `json:"last_sign_in_at,omitempty"`
ConfirmedAt string `json:"confirmed_at,omitempty"`
LastActivityOn string `json:"last_activity_on,omitempty"`
Email string `json:"email,omitempty"`
ThemeID int `json:"theme_id,omitempty"`
ColorSchemeID int `json:"color_scheme_id,omitempty"`
ProjectsLimit int `json:"projects_limit,omitempty"`
CurrentSignInAt string `json:"current_sign_in_at,omitempty"`
Identities []interface{} `json:"identities,omitempty"`
CanCreateGroup bool `json:"can_create_group,omitempty"`
CanCreateProject bool `json:"can_create_project,omitempty"`
TwoFactorEnabled bool `json:"two_factor_enabled,omitempty"`
External bool `json:"external,omitempty"`
PrivateProfile bool `json:"private_profile,omitempty"`
SharedRunnersMinutesLimit interface{} `json:"shared_runners_minutes_limit,omitempty"`
ExtraSharedRunnersMinutesLimit interface{} `json:"extra_shared_runners_minutes_limit,omitempty"`
IsAdmin bool `json:"is_admin,omitempty"`
Note interface{} `json:"note,omitempty"`
UsingLicenseSeat bool `json:"using_license_seat,omitempty"`
}
const (
// 是否开启严格模式,严格模式下将校验所有的提交信息格式(多 commit 下)
strictMode = true
// commit log format
CommitLogFormat = `{"commit":"%H","sanitized_subject_line":"%f","author":{"name":"%an","email":"%ae","timestamp":"%at"},"committer":{"name":"%cn","email":"%ce","timestamp":"%ct"}}`
// commit message pattern
CommitMessagePattern = `^(?:fixup!\s*)?(\w*)(\(([\w\$\.\*/-].*)\))?\: (.*)|^Merge\ branch(.*)`
// zero_commit
ZeroCommit = `0000000000000000000000000000000000000000`
)
// Gitlab API
const (
GITLAB_URL = "http://127.0.0.1"
GITLAB_TOKEN = "{your gitlab server admin api token}"
)
var (
commitMsgReg = regexp.MustCompile(CommitMessagePattern)
)
var logger *log.Logger
// 日志输出到syslog
func init() {
sysLog, err := syslog.Dial("", "", syslog.LOG_ERR, "Saturday")
if err != nil {
log.Fatal(err)
}
logger = log.New(sysLog, "", 0777)
}
func main() {
input, _ := ioutil.ReadAll(os.Stdin)
param := strings.Fields(string(input))
// allow branch/tag delete
if param[1] == ZeroCommit {
os.Exit(0)
}
// Check for new branch or tag
var span string
if param[0] == ZeroCommit {
span = param[1]
} else {
span = fmt.Sprintf("%s...%s", param[0], param[1])
}
commitRevList, err := getCommitRevList(span)
if err != nil || len(commitRevList) == 0 {
logger.Println(err)
os.Exit(0)
}
userName := os.Getenv("GL_USERNAME")
projectID := strings.Split(os.Getenv("GL_REPOSITORY"), "-")[1]
// get user email for gitlab api
userInfo := getGitlabUserInfo(userName)
for _, commit := range commitRevList {
commitInfo, err := getCommitInfo(commit)
if err != nil || len(commitInfo) == 0 {
logger.Println(err)
_, _ = fmt.Fprintln(os.Stderr, "GL-HOOK-ERR: exit code 444, can't get commit user info", commit, err)
os.Exit(444)
}
logger.Println(string(commitInfo))
var commits = Commits{}
if err := json.Unmarshal(commitInfo, &commits); err != nil {
logger.Println(err)
logger.Printf("error decoding sakura response: %v", err)
if e, ok := err.(*json.SyntaxError); ok {
logger.Printf("syntax error at byte offset %d", e.Offset)
}
logger.Printf("sakura response: %q", commitInfo)
logger.Printf("sakura response: %q", commitInfo)
_, _ = fmt.Fprintln(os.Stderr, "GL-HOOK-ERR: exit code 555 未知错误, 请联系管理员")
os.Exit(555)
}
// continue old commit
_t, err := strconv.Atoi(commits.Committer.Timestamp)
if err != nil {
logger.Println(err)
_, _ = fmt.Fprintln(os.Stderr, "GL-HOOK-ERR: exit code 777, 知错误, 请联系管理员")
os.Exit(777)
}
// 忽略此时间段前的提交
if _t < 1607936839 {
continue
}
logger.Println(userName, projectID, commits.Commit, commits.Author.Name, userName)
// check auth user name
//if commits.Author.Name != userName && commits.Author.Name != userInfo.Name {
// _, _ = fmt.Fprintln(os.Stderr, "GL-HOOK-ERR: 用户名不对: ", commits.Author.Name, ", 应为: ", userName)
// _, _ = fmt.Fprintln(os.Stderr, fmt.Sprintf(checkFailedUser, userName))
// os.Exit(999)
//}
logger.Println(userName, projectID, commits.Commit, commits.Author.Email, userInfo.Email)
//check auth user email
//if commits.Author.Email != userInfo.Email {
// _, _ = fmt.Fprintln(os.Stderr, "GL-HOOK-ERR: 邮箱不对: ", commits.Author.Email, ", 应为: ", userInfo.Email)
// _, _ = fmt.Fprintln(os.Stderr, fmt.Sprintf(checkFailedEmail, userInfo.Email))
// os.Exit(999)
//}
logger.Println(userName, projectID, commits.Commit, commits.Committer.Name, userName)
// check committer user name
if commits.Committer.Name != userName && commits.Committer.Name != userInfo.Name {
_, _ = fmt.Fprintln(os.Stderr, "GL-HOOK-ERR: 用户名不对: ", commits.Committer.Name, ", 应为: ", userName)
_, _ = fmt.Fprintln(os.Stderr, fmt.Sprintf(checkFailedUser, userName))
os.Exit(999)
}
logger.Println(userName, projectID, commits.Commit, commits.Committer.Email, userInfo.Email)
// check committer user email
if commits.Committer.Email != userInfo.Email {
_, _ = fmt.Fprintln(os.Stderr, "GL-HOOK-ERR: 邮箱不对: ", commits.Committer.Email, ", 应为: ", userInfo.Email)
_, _ = fmt.Fprintln(os.Stderr, fmt.Sprintf(checkFailedEmail, userInfo.Email))
os.Exit(999)
}
commits.Subject, err = getCommitMsg(commits.Commit)
if err != nil {
logger.Println(err)
logger.Println("con't get commit message")
os.Exit(0)
}
// check commit message style
commitTypes := commitMsgReg.FindAllStringSubmatch(commits.Subject, -1)
if len(commitTypes) != 1 {
checkFailed()
} else {
switch commitTypes[0][1] {
case FEAT:
case FIX:
case DOCS:
case STYLE:
case REFACTOR:
case TEST:
case CHORE:
case PERF:
case HOTFIX:
default:
if !strings.HasPrefix(commits.Subject, "Merge branch") {
checkFailed()
}
}
// check message length
if len(commitTypes[0][4]) < 8 {
_, _ = fmt.Fprintln(os.Stderr, "GL-HOOK-ERR: Commit message太短, 没有任何意义")
os.Exit(666)
}
}
if !strictMode {
os.Exit(0)
}
}
}
func getGitlabUserInfo(userName string) GitlabUserInfo {
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v4/users?username=%s", GITLAB_URL, userName), nil)
if err != nil {
logger.Println(err)
_, _ = fmt.Fprintln(os.Stderr, "GL-HOOK-ERR: exit code 9-1, 未知错误, 请联系管理员")
os.Exit(9)
}
req.Header.Set("Private-Token", GITLAB_TOKEN)
resp, err := http.DefaultClient.Do(req)
if err != nil {
logger.Println(err)
_, _ = fmt.Fprintln(os.Stderr, "GL-HOOK-ERR: exit code 9-2, 未知错误, 请联系管理员")
os.Exit(9)
}
defer resp.Body.Close()
bodyByte, err := ioutil.ReadAll(resp.Body)
if err != nil {
logger.Println(err)
_, _ = fmt.Fprintln(os.Stderr, "GL-HOOK-ERR: exit code 9-3, 未知错误, 请联系管理员")
os.Exit(9)
}
logger.Println(string(bodyByte))
var gitlabUserInfo []GitlabUserInfo
if err := json.Unmarshal(bodyByte, &gitlabUserInfo); err != nil {
logger.Println(err)
_, _ = fmt.Fprintln(os.Stderr, "GL-HOOK-ERR: exit code 9-4, 未知错误, 请联系管理员")
os.Exit(9)
}
if len(gitlabUserInfo) == 0 {
_, _ = fmt.Fprintln(os.Stderr, fmt.Sprintf("GL-HOOK-ERR: exit code 9-5, 未找到%s用户", userName))
os.Exit(9)
}
return gitlabUserInfo[0]
}
func getCommitRevList(commit string) ([]string, error) {
getCommitMsgCmd := exec.Command("git", "rev-list", commit, "--not", "--all")
getCommitMsgCmd.Stdin = os.Stdin
getCommitMsgCmd.Stderr = os.Stderr
b, err := getCommitMsgCmd.Output()
if err != nil {
return nil, err
}
commitList := strings.Split(string(b), "\n")
var res []string
for _, commits := range commitList {
if strings.ReplaceAll(commits, " ", "") == "" {
continue
}
res = append(res, commits)
}
return res, nil
}
func getCommitInfo(commit string) ([]byte, error) {
getCommitMsgCmd := exec.Command("git", "log", "-n", "1", commit, fmt.Sprintf(`--pretty=format:%s`, CommitLogFormat))
getCommitMsgCmd.Stdin = os.Stdin
getCommitMsgCmd.Stderr = os.Stderr
res, err := getCommitMsgCmd.Output()
if err != nil {
return nil, err
}
return res, nil
}
func getCommitMsg(commit string) (string, error) {
getCommitMsgCmd := exec.Command("git", "log", "-n", "1", commit, "--pretty=format:%s")
getCommitMsgCmd.Stdin = os.Stdin
getCommitMsgCmd.Stderr = os.Stderr
byteMsg, err := getCommitMsgCmd.Output()
if err != nil {
return "", err
}
res := strings.ReplaceAll(string(byteMsg), " ", "")
res = strings.ReplaceAll(string(byteMsg), " ", "")
res = strings.ReplaceAll(string(byteMsg), "\n", "")
return res, nil
}
func checkFailed() {
_, _ = fmt.Fprintln(os.Stderr, checkFailedMessage)
os.Exit(2)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment