Last active
February 22, 2023 00:51
-
-
Save xmapst/84893b46c299c596f05d76c3f8414e3a to your computer and use it in GitHub Desktop.
GitLab 使用 Server Hooks 校验 Commit 用户名与邮箱及commit message样式
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// +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