Create & Init Project...

This commit is contained in:
2019-04-22 18:49:16 +08:00
commit fc4fa37393
25440 changed files with 4054998 additions and 0 deletions

View File

@@ -0,0 +1,73 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_library(
name = "go_default_library",
srcs = [
"comment.go",
"contributors.go",
"mr.go",
"pipeline.go",
"service.go",
"wechat.go",
],
importpath = "go-common/app/tool/saga/service",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/tool/saga/conf:go_default_library",
"//app/tool/saga/dao:go_default_library",
"//app/tool/saga/model:go_default_library",
"//app/tool/saga/service/command:go_default_library",
"//app/tool/saga/service/gitlab:go_default_library",
"//app/tool/saga/service/mail:go_default_library",
"//app/tool/saga/service/notification:go_default_library",
"//app/tool/saga/service/wechat:go_default_library",
"//library/log:go_default_library",
"//vendor/github.com/pkg/errors:go_default_library",
"//vendor/github.com/robfig/cron:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [
":package-srcs",
"//app/tool/saga/service/command:all-srcs",
"//app/tool/saga/service/gitlab:all-srcs",
"//app/tool/saga/service/mail:all-srcs",
"//app/tool/saga/service/notification:all-srcs",
"//app/tool/saga/service/wechat:all-srcs",
],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
go_test(
name = "go_default_test",
srcs = [
"comment_test.go",
"contributors_test.go",
"service_test.go",
],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = [
"//app/tool/saga/conf:go_default_library",
"//app/tool/saga/model:go_default_library",
"//vendor/github.com/smartystreets/goconvey/convey:go_default_library",
],
)

View File

@@ -0,0 +1,62 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_library(
name = "go_default_library",
srcs = [
"command.go",
"contributors.go",
"merge.go",
"review.go",
],
importpath = "go-common/app/tool/saga/service/command",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/tool/saga/conf:go_default_library",
"//app/tool/saga/dao:go_default_library",
"//app/tool/saga/model:go_default_library",
"//app/tool/saga/service/gitlab:go_default_library",
"//app/tool/saga/service/notification:go_default_library",
"//library/log:go_default_library",
"//vendor/github.com/xanzy/go-gitlab:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
go_test(
name = "go_default_test",
srcs = [
"command_test.go",
"contributors_test.go",
"review_test.go",
],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = [
"//app/tool/saga/conf:go_default_library",
"//app/tool/saga/dao:go_default_library",
"//app/tool/saga/model:go_default_library",
"//app/tool/saga/service/gitlab:go_default_library",
"//vendor/github.com/smartystreets/goconvey/convey:go_default_library",
],
)

View File

@@ -0,0 +1,113 @@
package command
import (
"context"
"fmt"
"runtime/debug"
"time"
"go-common/app/tool/saga/conf"
"go-common/app/tool/saga/dao"
"go-common/app/tool/saga/model"
"go-common/app/tool/saga/service/gitlab"
"go-common/library/log"
)
// Command Command def.
type Command struct {
dao *dao.Dao
gitlab *gitlab.Gitlab
cmds map[string]cmdFunc
}
type cmdFunc func(ctx context.Context, event *model.HookComment, repo *model.Repo) (err error)
// New ...
func New(dao *dao.Dao, gitlab *gitlab.Gitlab) (c *Command) {
c = &Command{
dao: dao,
gitlab: gitlab,
cmds: make(map[string]cmdFunc),
}
return
}
// Exec ...
func (c *Command) Exec(ctx context.Context, cmd string, event *model.HookComment, repo *model.Repo) (err error) {
var (
f cmdFunc
ok bool
projID = int(event.MergeRequest.SourceProjectID)
mrIID = int(event.MergeRequest.IID)
)
if f, ok = c.cmds[cmd]; !ok {
return
}
if err = f(ctx, event, repo); err != nil {
c.gitlab.CreateMRNote(projID, mrIID, fmt.Sprintf("<pre>SAGA 异常:%+v %s</pre>", err, debug.Stack()))
return
}
return
}
func (c *Command) register(cmd string, f cmdFunc) {
c.cmds[cmd] = f
}
// Registers ...
func (c *Command) Registers() {
c.register(model.SagaCommandPlusOne, c.runPlusOne)
c.register(model.SagaCommandMerge, c.runTryMerge)
c.register(model.SagaCommandMerge1, c.runTryMerge)
c.register(model.SagaCommandPlusOne1, c.runPlusOne)
}
// ListenTask ...
func (c *Command) ListenTask() {
var (
ctx = context.TODO()
err error
t *time.Timer
ok bool
taskInfos []*model.TaskInfo
)
defer func() {
if x := recover(); x != nil {
log.Error("ListenTask: %+v %s", x, debug.Stack())
}
}()
t = time.NewTimer(time.Duration(conf.Conf.Property.TaskInterval))
for range t.C {
if _, taskInfos, err = c.dao.MergeTasks(ctx, model.TaskStatusWaiting); err != nil {
log.Error("request MergeTasks: %+v", err)
continue
}
for _, taskInfo := range taskInfos {
if ok, err = c.dao.TryLock(ctx, fmt.Sprintf(model.SagaRepoLockKey, int(taskInfo.Event.ProjectID)), model.SagaLockValue, int(taskInfo.Repo.Config.LockTimeout)); err != nil {
log.Error("TryLock ProjectID: %d, MRIID: %d, err: %+v", taskInfo.Event.ProjectID, taskInfo.Event.MergeRequest.IID, err)
continue
}
if !ok {
log.Info("TryLock ok: %t, ProjectID: %d, MRIID: %d", ok, taskInfo.Event.ProjectID, taskInfo.Event.MergeRequest.IID)
continue
}
log.Info("request MRIID: %d, MergeTasks:%+v", taskInfo.Event.MergeRequest.IID, taskInfo)
go func(taskInfo *model.TaskInfo) {
var (
projID = int(taskInfo.Event.MergeRequest.SourceProjectID)
mrIID = int(taskInfo.Event.MergeRequest.IID)
branch = taskInfo.Event.MergeRequest.SourceBranch
)
if err = c.execMergeTask(taskInfo); err != nil {
c.gitlab.UpdateMRNote(projID, mrIID, taskInfo.NoteID, fmt.Sprintf("<pre>SAGA 异常:%+v</pre>", err))
if err = c.resetMergeStatus(projID, mrIID, branch, false); err != nil {
log.Error("resetMergeStatus error: %+v", err)
}
log.Error("execMergeTask: %+v", err)
}
}(taskInfo)
}
t.Reset(time.Duration(conf.Conf.Property.TaskInterval))
}
}

View File

@@ -0,0 +1,209 @@
package command
import (
"context"
"encoding/json"
"flag"
"path/filepath"
"testing"
"go-common/app/tool/saga/conf"
"go-common/app/tool/saga/dao"
"go-common/app/tool/saga/model"
"go-common/app/tool/saga/service/gitlab"
"github.com/smartystreets/goconvey/convey"
)
var (
c *Command
gitlabHookCommentTest = []byte(`{
"object_kind":"note",
"event_type":"note",
"user":{
"name":"changhengyuan",
"username":"changhengyuan",
"avatar_url":"https://www.gravatar.com/avatar/d3218d34473c6fb4d18a770f14e59a89?s=80\u0026d=identicon"
},
"project_id":35,
"project":{
"id":35,
"name":"test-saga",
"description":"",
"web_url":"http://gitlab.bilibili.co/changhengyuan/test-saga",
"avatar_url":null,
"git_ssh_url":"git@gitlab.bilibili.co:changhengyuan/test-saga.git",
"git_http_url":"http://gitlab.bilibili.co/changhengyuan/test-saga.git",
"namespace":"changhengyuan",
"visibility_level":20,
"path_with_namespace":"changhengyuan/test-saga",
"default_branch":"master",
"ci_config_path":null,
"homepage":"http://gitlab.bilibili.co/changhengyuan/test-saga",
"url":"git@gitlab.bilibili.co:changhengyuan/test-saga.git",
"ssh_url":"git@gitlab.bilibili.co:changhengyuan/test-saga.git",
"http_url":"http://gitlab.bilibili.co/changhengyuan/test-saga.git"},
"object_attributes":{
"id":3040,
"note":"test",
"noteable_type":"MergeRequest",
"author_id":15,
"created_at":"2018-09-26 06:55:13 UTC",
"updated_at":"2018-09-26 06:55:13 UTC",
"project_id":35,
"attachment":null,
"line_code":null,
"commit_id":"",
"noteable_id":390,
"system":false,
"st_diff":null,
"updated_by_id":null,
"type":null,
"position":null,
"original_position":null,
"resolved_at":null,
"resolved_by_id":null,
"discussion_id":"450c34e4c0f9e925bdc6a24c2ae4920d7a394ebc",
"change_position":null,
"resolved_by_push":null,
"url":"http://gitlab.bilibili.co/changhengyuan/test-saga/merge_requests/52#note_3040"
},
"repository":{
"name":"test-saga",
"url":"git@gitlab.bilibili.co:changhengyuan/test-saga.git",
"description":"",
"homepage":"http://gitlab.bilibili.co/changhengyuan/test-saga"
},
"merge_request":{
"assignee_id":null,
"author_id":15,
"created_at":"2018-09-26 06:41:55 UTC",
"description":"",
"head_pipeline_id":4510,
"id":390,
"iid":52,
"last_edited_at":null,
"last_edited_by_id":null,
"merge_commit_sha":null,
"merge_error":null,
"merge_params":{
"force_remove_source_branch":"0"
},
"merge_status":"cannot_be_merged",
"merge_user_id":null,
"merge_when_pipeline_succeeds":false,
"milestone_id":null,
"source_branch":"test-branch",
"source_project_id":35,
"state":"opened",
"target_branch":"master",
"target_project_id":35,
"time_estimate":0,
"title":"Test branch",
"updated_at":"2018-09-26 06:54:33 UTC",
"updated_by_id":null,
"url":"http://gitlab.bilibili.co/changhengyuan/test-saga/merge_requests/52",
"source":{
"id":35,
"name":"test-saga",
"description":"",
"web_url":"http://gitlab.bilibili.co/changhengyuan/test-saga",
"avatar_url":null,
"git_ssh_url":"git@gitlab.bilibili.co:changhengyuan/test-saga.git",
"git_http_url":"http://gitlab.bilibili.co/changhengyuan/test-saga.git",
"namespace":"changhengyuan",
"visibility_level":20,
"path_with_namespace":"changhengyuan/test-saga",
"default_branch":"master",
"ci_config_path":null,
"homepage":"http://gitlab.bilibili.co/changhengyuan/test-saga",
"url":"git@gitlab.bilibili.co:changhengyuan/test-saga.git",
"ssh_url":"git@gitlab.bilibili.co:changhengyuan/test-saga.git",
"http_url":"http://gitlab.bilibili.co/changhengyuan/test-saga.git"
},
"target":{
"id":35,
"name":"test-saga",
"description":"",
"web_url":"http://gitlab.bilibili.co/changhengyuan/test-saga",
"avatar_url":null,
"git_ssh_url":"git@gitlab.bilibili.co:changhengyuan/test-saga.git",
"git_http_url":"http://gitlab.bilibili.co/changhengyuan/test-saga.git",
"namespace":"changhengyuan",
"visibility_level":20,
"path_with_namespace":"changhengyuan/test-saga",
"default_branch":"master",
"ci_config_path":null,
"homepage":"http://gitlab.bilibili.co/changhengyuan/test-saga",
"url":"git@gitlab.bilibili.co:changhengyuan/test-saga.git",
"ssh_url":"git@gitlab.bilibili.co:changhengyuan/test-saga.git",
"http_url":"http://gitlab.bilibili.co/changhengyuan/test-saga.git"
},
"last_commit":{
"id":"51e9c3ba2ceac496dbaf55f0db564ab6a15e20eb",
"message":"add CONTRIBUTORS.md\n",
"timestamp":"2018-09-17T18:02:13+08:00",
"url":"http://gitlab.bilibili.co/changhengyuan/test-saga/commit/51e9c3ba2ceac496dbaf55f0db564ab6a15e20eb",
"author":{
"name":"哔哩哔哩",
"email":"bilibili@bilibilideMac-mini.local"
}
},
"work_in_progress":false,
"total_time_spent":0,
"human_total_time_spent":null,
"human_time_estimate":null}}`)
)
func init() {
dir, _ := filepath.Abs("../../cmd/saga-test.toml")
flag.Set("conf", dir)
conf.Init()
c = New(&dao.Dao{}, &gitlab.Gitlab{})
}
func TestCommandNew(t *testing.T) {
convey.Convey("New", t, func(ctx convey.C) {
ctx.Convey("When everything goes positive", func(ctx convey.C) {
ctx.Convey("Then c should not be nil.", func(ctx convey.C) {
ctx.So(c, convey.ShouldNotBeNil)
})
})
})
}
func TestCommandExec(t *testing.T) {
convey.Convey("Exec", t, func(ctx convey.C) {
var (
ct = context.Background()
cmd = "+1"
event = &model.HookComment{}
repo = &model.Repo{}
c = &Command{}
)
_ = json.Unmarshal(gitlabHookCommentTest, event)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
err := c.Exec(ct, cmd, event, repo)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
})
}
func TestCommandRegister(t *testing.T) {
convey.Convey("register", t, func(ctx convey.C) {
var (
cmd = "test_cmd"
f cmdFunc
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
c.register(cmd, f)
ctx.Convey("No return values", func(ctx convey.C) {
cmd, ok := c.cmds["test_cmd"]
ctx.So(ok, convey.ShouldEqual, true)
ctx.So(cmd, convey.ShouldEqual, f)
})
})
})
}

View File

@@ -0,0 +1,470 @@
package command
import (
"context"
"fmt"
"path/filepath"
"strings"
"go-common/app/tool/saga/model"
"go-common/app/tool/saga/service/notification"
"go-common/library/log"
)
type contributor struct {
Owner []string
Author []string
Reviewer []string
}
func readContributor(content []byte) (c *contributor) {
var (
lines []string
lineStr string
curSection string
)
c = &contributor{}
lines = strings.Split(string(content), "\n")
for _, lineStr = range lines {
if lineStr == "" {
continue
}
if strings.Contains(strings.ToLower(lineStr), "owner") {
curSection = "owner"
continue
}
if strings.Contains(strings.ToLower(lineStr), "author") {
curSection = "author"
continue
}
if strings.Contains(strings.ToLower(lineStr), "reviewer") {
curSection = "reviewer"
continue
}
switch curSection {
case "owner":
c.Owner = append(c.Owner, strings.TrimSpace(lineStr))
case "author":
c.Author = append(c.Author, strings.TrimSpace(lineStr))
case "reviewer":
c.Reviewer = append(c.Reviewer, strings.TrimSpace(lineStr))
}
}
return
}
// BuildContributor ...
func (c *Command) BuildContributor(repo *model.RepoInfo) (err error) {
var (
host string
token string
files []string
projID int
branch = repo.Branch
)
if host, token, err = c.gitlab.HostToken(); err != nil {
return
}
if files, err = c.dao.RepoFiles(context.TODO(), host, token, repo); err != nil {
return
}
if projID, err = c.gitlab.ProjectID(fmt.Sprintf("git@%s:%s/%s.git", host, repo.Group, repo.Name)); err != nil {
return
}
if err = c.SaveContributor(projID, branch, files, false); err != nil {
return
}
return
}
func hasbranch(branch string, branchs []string) bool {
for _, b := range branchs {
if strings.EqualFold(b, branch) {
return true
}
}
return false
}
// UpdateContributor ...
func (c *Command) UpdateContributor(projID int, mrIID int, sourceBranch string, targetBranch string, authBranches []string) (err error) {
var changeFiles []string
var deleteFiles []string
if !hasbranch(targetBranch, authBranches) {
log.Info("UpdateContributor not authBranches: %d, %s, %+v", projID, targetBranch, authBranches)
return
}
if changeFiles, deleteFiles, err = c.gitlab.MRChanges(projID, mrIID); err != nil {
return
}
//log.Info("UpdateContributor: projID: %d, changeFiles: %+v, deleteFiles: %+v", projID, changeFiles, deleteFiles)
if err = c.SaveContributor(projID, targetBranch, changeFiles, false); err != nil {
return
}
if err = c.SaveContributor(projID, targetBranch, deleteFiles, true); err != nil {
return
}
return
}
// SaveContributor ...
func (c *Command) SaveContributor(projID int, branch string, files []string, clean bool) (err error) {
var (
ctx = context.TODO()
raw []byte
cb *contributor
)
for _, file := range files {
if strings.EqualFold(filepath.Base(file), model.SagaContributorsName) {
path := filepath.Dir(file)
if clean {
//delete redis key
if err = c.dao.DeletePathAuthR(ctx, projID, branch, path); err != nil {
log.Error("delete Auth Redis key error(%+v) project ID:%d, branch:%s, path:%s", err, projID, branch, path)
err = nil
}
//delete hbase key
if err = c.dao.DeletePathAuthH(ctx, projID, branch, path); err != nil {
log.Error("delete Auth hbase key error(%+v) project ID:%d, branch:%s, path:%s", err, projID, branch, path)
err = nil
}
} else {
if raw, err = c.gitlab.RepoRawFile(projID, branch, file); err != nil {
return
}
cb = readContributor(raw)
log.Info("SaveContributor projectID:%d, branch:%s, path:%s, owner:%+v, reviewer:%+v", projID, branch, path, cb.Owner, cb.Reviewer)
//add to redis
authUser := &model.AuthUsers{
Owners: cb.Owner,
Reviewers: cb.Reviewer,
}
if err = c.dao.SetPathAuthR(ctx, projID, branch, path, authUser); err != nil {
log.Error("Set Auth to Redis error(%+v) project ID:%d, path:%s, owner:%+v, reviewer:%+v", err, projID, path, cb.Owner, cb.Reviewer)
err = nil
}
//add to hbase
if err = c.dao.SetPathAuthH(ctx, projID, branch, path, cb.Owner, cb.Reviewer); err != nil {
log.Error("Set Auth to Hbase error(%+v) projectID:%d, branch:%s, path:%s, owner:%+v, reviewer:%+v", err, projID, branch, path, cb.Owner, cb.Reviewer)
err = nil
}
}
}
}
return
}
// checkSuperAuth ...
func checkSuperAuth(username string, reviewedUsers []string, superUsers []string) (ok bool) {
for _, super := range superUsers {
if strings.EqualFold(super, username) {
return true
}
}
for _, r := range reviewedUsers {
for _, s := range superUsers {
if strings.EqualFold(r, s) {
return true
}
}
}
return false
}
// checkAllPathAuth ...
func (c *Command) checkAllPathAuth(taskInfo *model.TaskInfo) (ok bool, err error) {
var (
comment string
files []string
folders map[string]string
owners []string
reviewers []string
requireOwners []string
requireReviewers []string
reviewedUsers []string
requireReviewFolders []*model.RequireReviewFolder
username = taskInfo.Event.User.UserName
projID = int(taskInfo.Event.MergeRequest.SourceProjectID)
mrIID = int(taskInfo.Event.MergeRequest.IID)
sourceBranch = taskInfo.Event.MergeRequest.SourceBranch
targetBranch = taskInfo.Event.MergeRequest.TargetBranch
authBranch = c.GetAuthBranch(targetBranch, taskInfo.Repo.Config.AuthBranches)
url = taskInfo.Event.ObjectAttributes.URL
minReviewer = taskInfo.Repo.Config.MinReviewer
limitAuth = taskInfo.Repo.Config.LimitAuth
minReviewTip bool
isOwner bool
)
log.Info("checkAllPathAuth start ... MRIID: %d", mrIID)
if reviewedUsers, err = c.reviewedUsers(projID, mrIID); err != nil {
return
}
if len(taskInfo.Repo.Config.SuperAuthUsers) > 0 {
log.Info("checkAllPathAuth MRIID: %d, reviewedUsers: %v, SuperAuthUsers: %v", mrIID, reviewedUsers, taskInfo.Repo.Config.SuperAuthUsers)
ok = checkSuperAuth(username, reviewedUsers, taskInfo.Repo.Config.SuperAuthUsers)
if !ok {
comment, err = c.showRequireSuperAuthComment(taskInfo)
go notification.WechatAuthor(c.dao, username, url, sourceBranch, targetBranch, comment)
}
return
}
if files, err = c.gitlab.CompareDiff(projID, targetBranch, sourceBranch); err != nil {
return
}
log.Info("checkAllPathAuth projID:%d, MRIID:%d, reviewedUsers:%+v, files:%+v, targetBranch:%s, sourceBranch:%s", projID, mrIID, reviewedUsers, files, targetBranch, sourceBranch)
// 去重目录,校验目录权限
folders = make(map[string]string)
for _, file := range files {
folder := filepath.Dir(file)
if _, has := folders[folder]; !has {
folders[folder] = folder
authEnough := false
ownerPathReviewed := false
ownerReviewedStat := false
reviewedCount := 0
requireOwners = []string{}
requireReviewers = []string{}
dir := folder
for {
if owners, reviewers, err = c.GetPathAuth(projID, authBranch, dir); err != nil {
return
}
isOwner, ownerPathReviewed = reviewedOwner(owners, reviewedUsers, username)
num := reviewedNum(reviewers, reviewedUsers)
reviewedCount = reviewedCount + num
if isOwner {
log.Info("checkAllPathAuth user is owner, MRIID: %d, user: %s, owners %v, path: %s", mrIID, username, owners, dir)
authEnough = true
break
}
if ownerPathReviewed {
ownerReviewedStat = true
if reviewedCount >= minReviewer {
log.Info("checkAllPathAuth owner reviewed and reach minReviewer, user: %s, owners %+v, path: %s, reviewedUsers: %+v, reviewedCount: %d, minReviewer: %d", username, owners, dir, reviewedUsers, reviewedCount, minReviewer)
authEnough = true
break
}
}
if (len(requireOwners) == 0) && (len(owners) > 0) {
log.Info("checkAllPathAuth required owners %v, path: %s, MRIID: %d", owners, dir, mrIID)
requireOwners = owners
}
if (len(requireReviewers) == 0) && (len(reviewers) > 0) {
log.Info("checkAllPathAuth required reviewers %v, path: %s, MRIID: %d", reviewers, dir, mrIID)
requireReviewers = reviewers
}
if dir == "." {
break
}
if (len(owners) > 0) && limitAuth {
break
}
dir = filepath.Dir(dir)
}
if !authEnough {
requireAuth := &model.RequireReviewFolder{}
requireAuth.Folder = folder
if !ownerReviewedStat {
requireAuth.Owners = requireOwners
}
if reviewedCount < minReviewer {
requireAuth.Reviewers = requireReviewers
minReviewTip = true
}
requireReviewFolders = append(requireReviewFolders, requireAuth)
}
}
}
if len(requireReviewFolders) > 0 {
comment, err = c.showRequireAuthComment(taskInfo, requireReviewFolders, minReviewTip)
go notification.WechatAuthor(c.dao, username, url, sourceBranch, targetBranch, comment)
return
}
ok = true
return
}
// GetAllPathAuth ...
func (c *Command) GetAllPathAuth(projID int, mrIID int, authBranch string) (pathOwners []model.RequireReviewFolder, err error) {
var (
files []string
deleteFiles []string
folders map[string]string
pathOwner []string
pathReviewer []string
)
if files, deleteFiles, err = c.gitlab.MRChanges(projID, mrIID); err != nil {
return
}
files = append(files, deleteFiles...)
// 去重目录,校验目录权限
folders = make(map[string]string)
for _, file := range files {
folder := filepath.Dir(file)
if _, has := folders[folder]; !has {
folders[folder] = folder
dir := folder
for {
if pathOwner, pathReviewer, err = c.GetPathAuth(projID, authBranch, dir); err != nil {
return
}
if len(pathOwner) > 0 {
exist := false
for _, os := range pathOwners {
if os.Folder == dir {
exist = true
break
}
}
if exist {
break
}
requireOwner := model.RequireReviewFolder{}
requireOwner.Folder = dir
requireOwner.Owners = pathOwner
if len(pathReviewer) > 0 {
requireOwner.Reviewers = pathReviewer
}
pathOwners = append(pathOwners, requireOwner)
break
}
if dir == "." {
break
}
dir = filepath.Dir(dir)
}
}
}
return
}
// GetPathAuth ...
func (c *Command) GetPathAuth(projID int, branch string, path string) (owners []string, reviewers []string, err error) {
var (
ctx = context.TODO()
authUser *model.AuthUsers
)
if authUser, err = c.dao.PathAuthR(ctx, projID, branch, path); err != nil || authUser == nil {
if err != nil {
log.Error("GetPathAuthInfo error project ID:%d, branch:%s, path: %s (err: %+v) ", projID, branch, path, err)
}
if owners, reviewers, err = c.dao.PathAuthH(ctx, projID, branch, path); err != nil {
return
}
if len(owners) <= 0 && len(reviewers) <= 0 {
return
}
if authUser == nil {
authUser = new(model.AuthUsers)
}
authUser.Owners = owners
authUser.Reviewers = reviewers
if err1 := c.dao.SetPathAuthR(ctx, projID, branch, path, authUser); err1 != nil {
log.Error("SetPathAuthR error project ID:%d, branch:%s, path: %s (err1: %+v) ", projID, branch, path, err1)
}
return
}
if authUser != nil {
owners = authUser.Owners
reviewers = authUser.Reviewers
}
return
}
// GetAuthBranch ...
func (c *Command) GetAuthBranch(targetBranch string, authBranches []string) (authBranch string) {
for _, r := range authBranches {
if r == targetBranch {
return r
}
}
return authBranches[0]
}
// showRequireAuthComment ...
func (c *Command) showRequireAuthComment(taskInfo *model.TaskInfo, requireReviewFolders []*model.RequireReviewFolder, minReviewTip bool) (comment string, err error) {
var (
username = taskInfo.Event.User.UserName
mrIID = int(taskInfo.Event.MergeRequest.IID)
projID = int(taskInfo.Event.MergeRequest.SourceProjectID)
noteID = taskInfo.NoteID
minReviewer = taskInfo.Repo.Config.MinReviewer
)
if minReviewTip {
comment = fmt.Sprintf("<pre>[%s]尝试合并失败无权限的文件目录如下并且最少需要review人数 [%d] 个,请寻找对应的大佬 +1 后再 +merge</pre>", username, minReviewer)
} else {
comment = fmt.Sprintf("<pre>[%s]尝试合并失败,无权限的文件目录如下,请寻找对应的大佬 +1 后再 +merge</pre>", username)
}
comment += "\n"
for _, reviewFolder := range requireReviewFolders {
comment += fmt.Sprintf("+ %s ", reviewFolder.Folder)
if len(reviewFolder.Owners) > 0 {
comment += "OWNER: "
for _, o := range reviewFolder.Owners {
if o == "all" {
comment += "所有人" + " 或 "
} else {
comment += o + " 或 "
}
}
comment = comment[:len(comment)-len(" 或 ")]
}
if len(reviewFolder.Reviewers) > 0 {
comment += "REVIEWER: "
for _, o := range reviewFolder.Reviewers {
if o == "all" {
comment += "所有人" + " 与 "
} else {
comment += o + " 与 "
}
}
comment = comment[:len(comment)-len(" 与 ")]
//comment += fmt.Sprintf("<pre>; 已review人数 [%d] 个</pre>", minReviewer)
}
comment += "\n\n"
}
err = c.gitlab.UpdateMRNote(projID, mrIID, noteID, comment)
return
}
// showRequireSuperAuthComment ...
func (c *Command) showRequireSuperAuthComment(taskInfo *model.TaskInfo) (comment string, err error) {
var (
username = taskInfo.Event.User.UserName
mrIID = int(taskInfo.Event.MergeRequest.IID)
projID = int(taskInfo.Event.MergeRequest.SourceProjectID)
noteID = taskInfo.NoteID
)
comment = fmt.Sprintf("<pre>[%s]尝试合并失败,已配置超级权限用户,请寻找其中至少一位大佬 +1 后再 +merge</pre>", username)
comment += "\n"
comment += fmt.Sprintf("+ SUPERMAN: ")
for _, user := range taskInfo.Repo.Config.SuperAuthUsers {
comment += fmt.Sprintf("%s 或 ", user)
}
comment = comment[:len(comment)-len(" 或 ")]
err = c.gitlab.UpdateMRNote(projID, mrIID, noteID, comment)
return
}

View File

@@ -0,0 +1,38 @@
package command
import (
"fmt"
"io/ioutil"
"testing"
"github.com/smartystreets/goconvey/convey"
)
func TestCommandReadContributor(t *testing.T) {
convey.Convey("readContributor", t, func(ctx convey.C) {
content, _ := ioutil.ReadFile("../../CONTRIBUTORS.md")
ctx.Convey("When everything goes positive", func(ctx convey.C) {
cc := readContributor(content)
ctx.Convey("Then c should not be nil.", func(ctx convey.C) {
ctx.So(fmt.Sprint(cc.Author), convey.ShouldEqual, `[muyang yubaihai wangweizhen wuwei]`)
ctx.So(fmt.Sprint(cc.Owner), convey.ShouldEqual, `[muyang zhanglin]`)
ctx.So(fmt.Sprint(cc.Reviewer), convey.ShouldEqual, `[muyang]`)
})
})
})
}
func TestCommandHasBranch(t *testing.T) {
convey.Convey("hasbranch", t, func(ctx convey.C) {
var (
branch = "test"
branchs = []string{"master", "test"}
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
p1 := hasbranch(branch, branchs)
ctx.Convey("Then p1 should not be nil.", func(ctx convey.C) {
ctx.So(p1, convey.ShouldEqual, true)
})
})
})
}

View File

@@ -0,0 +1,529 @@
package command
import (
"context"
"fmt"
"runtime/debug"
"go-common/app/tool/saga/model"
"go-common/app/tool/saga/service/notification"
"go-common/library/log"
ggitlab "github.com/xanzy/go-gitlab"
)
func (c *Command) runTryMerge(ctx context.Context, event *model.HookComment, repo *model.Repo) (err error) {
var (
ok bool
canMerge bool
projID = int(event.MergeRequest.SourceProjectID)
mrIID = int(event.MergeRequest.IID)
wip = event.MergeRequest.WorkInProgress
noteID int
taskInfo = &model.TaskInfo{
Event: event,
Repo: repo,
}
)
log.Info("runTryMerge start ... MRIID: %d, Repo Config: %+v", mrIID, repo.Config)
if ok, err = c.dao.ExistMRIID(ctx, mrIID); err != nil || ok {
return
}
if noteID, err = c.gitlab.CreateMRNote(projID, mrIID, "<pre>SAGA 开始执行,请大佬稍后......</pre>"); err != nil {
return
}
taskInfo.NoteID = noteID
// 1, check wip
if wip {
c.gitlab.UpdateMRNote(projID, mrIID, noteID, "<pre>警告当前MR处于WIP状态请待开发结束后再merge</pre>")
return
}
// 2, check labels
if ok, err = c.checkLabels(projID, mrIID, noteID, repo); err != nil || !ok {
return
}
// 3, check merge status
if canMerge, err = c.checkMergeStatus(projID, mrIID, noteID); err != nil || !canMerge {
return
}
// 4, check pipeline status
if repo.Config.RelatePipeline {
if repo.Config.DelayMerge {
if ok, _, err = c.checkPipeline(projID, mrIID, noteID, 0, model.QueryProcessing); err != nil || !ok {
return
}
} else {
if ok, _, err = c.checkPipeline(projID, mrIID, noteID, 0, model.QuerySuccess); err != nil || !ok {
return
}
}
}
// 5, check path auth
if ok, err = c.checkAllPathAuth(taskInfo); err != nil || !ok {
return
}
// 6, show current mr queue info
c.showMRQueueInfo(ctx, taskInfo)
if err = c.dao.PushMergeTask(ctx, model.TaskStatusWaiting, taskInfo); err != nil {
return
}
if err = c.dao.AddMRIID(ctx, mrIID, int(repo.Config.LockTimeout)); err != nil {
return
}
log.Info("runTryMerge merge task 已加入 waiting 任务列队中... MRIID: %d", mrIID)
return
}
func (c *Command) execMergeTask(taskInfo *model.TaskInfo) (err error) {
var (
ctx = context.TODO()
projID = int(taskInfo.Event.MergeRequest.SourceProjectID)
mrIID = int(taskInfo.Event.MergeRequest.IID)
sourceBranch = taskInfo.Event.MergeRequest.SourceBranch
pipeline = &ggitlab.Pipeline{}
noteID = taskInfo.NoteID
mergeInfo = &model.MergeInfo{
ProjID: projID,
MRIID: mrIID,
URL: taskInfo.Event.ObjectAttributes.URL,
AuthBranches: taskInfo.Repo.Config.AuthBranches,
SourceBranch: taskInfo.Event.MergeRequest.SourceBranch,
TargetBranch: taskInfo.Event.MergeRequest.TargetBranch,
AuthorID: int(taskInfo.Event.MergeRequest.AuthorID),
UserName: taskInfo.Event.User.UserName,
MinReviewer: taskInfo.Repo.Config.MinReviewer,
LockTimeout: taskInfo.Repo.Config.LockTimeout,
Title: taskInfo.Event.MergeRequest.Title,
Description: taskInfo.Event.MergeRequest.Description,
}
)
mergeInfo.NoteID = noteID
// 从等待任务列队移除
if err = c.dao.DeleteMergeTask(ctx, model.TaskStatusWaiting, taskInfo); err != nil {
return
}
// 加入到正在执行任务列队
if err = c.dao.PushMergeTask(ctx, model.TaskStatusRunning, taskInfo); err != nil {
return
}
if taskInfo.Repo.Config.RelatePipeline {
if taskInfo.Repo.Config.DelayMerge {
if err = c.HookDelayMerge(projID, sourceBranch, mergeInfo); err != nil {
return
}
return
}
if err = c.gitlab.UpdateMRNote(projID, mrIID, noteID, "<pre>SAGA 提示为了保证合进主干后能正常编译正在重跑pipeline等待时间取决于pipeline运行时间请耐心等待</pre>"); err != nil {
return
}
if pipeline, err = c.retryPipeline(taskInfo.Event); err != nil {
return
}
mergeInfo.PipelineID = pipeline.ID
if err = c.dao.SetMergeInfo(ctx, projID, sourceBranch, mergeInfo); err != nil {
return
}
} else {
if err = c.HookMerge(projID, sourceBranch, mergeInfo); err != nil {
return
}
}
return
}
func (c *Command) retryPipeline(event *model.HookComment) (pipeline *ggitlab.Pipeline, err error) {
var (
trigger *ggitlab.PipelineTrigger
triggers []*ggitlab.PipelineTrigger
projID = int(event.MergeRequest.SourceProjectID)
sourceBranch = event.MergeRequest.SourceBranch
)
if triggers, err = c.gitlab.Triggers(projID); err != nil {
return
}
if len(triggers) == 0 {
log.Info("No triggers were found for project %d, try to create it now.", projID)
if trigger, err = c.gitlab.CreateTrigger(projID); err != nil {
return
}
triggers = []*ggitlab.PipelineTrigger{trigger}
}
trigger = triggers[0]
if trigger.Owner == nil || trigger.Owner.ID == 0 {
log.Info("Legacy trigger (without owner), take ownership now.")
if trigger, err = c.gitlab.TakeOwnership(projID, trigger.ID); err != nil {
return
}
}
if pipeline, err = c.gitlab.TriggerPipeline(projID, sourceBranch, trigger.Token); err != nil {
return
}
return
}
// HookPipeline ...
func (c *Command) HookPipeline(projID int, branch string, pipelineID int) (err error) {
var (
ok bool
canMerge bool
mergeInfo *model.MergeInfo
)
defer func() {
if x := recover(); x != nil {
log.Error("HookPipeline: %+v %s", x, debug.Stack())
}
}()
if ok, mergeInfo, err = c.dao.MergeInfo(context.TODO(), projID, branch); err != nil || !ok {
return
}
log.Info("HookPipeline projID: %d, MRIID: %d, branch: %s, pipelineId: %d", projID, mergeInfo.MRIID, branch, mergeInfo.PipelineID)
if pipelineID < mergeInfo.PipelineID {
return
}
defer func() {
if err = c.resetMergeStatus(projID, mergeInfo.MRIID, branch, true); err != nil {
log.Error("resetMergeStatus MRIID: %d, error: %+v", mergeInfo.MRIID, err)
}
}()
// 1, check pipeline id
if ok, _, err = c.checkPipeline(projID, mergeInfo.MRIID, mergeInfo.NoteID, mergeInfo.PipelineID, model.QueryID); err != nil || !ok {
return
}
// 2, check pipeline status
if ok, _, err = c.checkPipeline(projID, mergeInfo.MRIID, mergeInfo.NoteID, 0, model.QuerySuccess); err != nil || !ok {
return
}
// 3, check merge status
if canMerge, err = c.checkMergeStatus(projID, mergeInfo.MRIID, mergeInfo.NoteID); err != nil || !canMerge {
return
}
log.Info("HookPipeline acceptMerge ... MRIID: %d", mergeInfo.MRIID)
if ok, err = c.acceptMerge(mergeInfo); err != nil || !ok {
return
}
return
}
// HookMerge ...
func (c *Command) HookMerge(projID int, branch string, mergeInfo *model.MergeInfo) (err error) {
var (
ok bool
canMerge bool
)
defer func() {
if x := recover(); x != nil {
log.Error("HookMerge: %+v %s", x, debug.Stack())
}
}()
defer func() {
if err = c.resetMergeStatus(projID, mergeInfo.MRIID, branch, true); err != nil {
log.Error("resetMergeStatus MRIID: %d, error: %+v", mergeInfo.MRIID, err)
}
}()
log.Info("HookMerge projID: %d, MRIID: %d, branch: %s", projID, mergeInfo.MRIID, branch)
if canMerge, err = c.checkMergeStatus(projID, mergeInfo.MRIID, mergeInfo.NoteID); err != nil || !canMerge {
return
}
log.Info("HookMerge acceptMerge ... MRIID: %d", mergeInfo.MRIID)
if ok, err = c.acceptMerge(mergeInfo); err != nil || !ok {
return
}
return
}
// HookDelayMerge ...
func (c *Command) HookDelayMerge(projID int, branch string, mergeInfo *model.MergeInfo) (err error) {
var (
ctx = context.TODO()
ok bool
noteID = mergeInfo.NoteID
mrIID = mergeInfo.MRIID
pipelineID int
status string
)
defer func() {
if x := recover(); x != nil {
log.Error("HookDelayMerge: %+v %s", x, debug.Stack())
}
}()
//if ok, pipelineID, err = c.checkPipeline(projID, mrIID, noteID, 0, model.QuerySuccessRmNote); err != nil {
//return
//}
if pipelineID, status, err = c.gitlab.MRPipelineStatus(projID, mrIID); err != nil {
return
}
if status == model.PipelineSuccess || status == model.PipelineSkipped {
ok = true
} else if status != model.PipelineRunning && status != model.PipelinePending {
comment := fmt.Sprintf("<pre>警告pipeline状态异常请确保pipeline状态正常后再执行merge操作</pre>")
err = c.gitlab.UpdateMRNote(projID, mrIID, noteID, comment)
return
}
log.Info("HookDelayMerge projID: %d, MRIID: %d, branch: %s, pipeline status: %t", projID, mergeInfo.MRIID, branch, ok)
if ok {
if err = c.HookMerge(projID, branch, mergeInfo); err != nil {
return
}
} else {
mergeInfo.PipelineID = pipelineID
if err = c.dao.SetMergeInfo(ctx, projID, branch, mergeInfo); err != nil {
return
}
}
return
}
func (c *Command) acceptMerge(mergeInfo *model.MergeInfo) (ok bool, err error) {
var (
comment string
author string
canMerge bool
state string
authorID = mergeInfo.AuthorID
username = mergeInfo.UserName
projID = mergeInfo.ProjID
mrIID = mergeInfo.MRIID
url = mergeInfo.URL
sourceBranch = mergeInfo.SourceBranch
targetBranch = mergeInfo.TargetBranch
noteID = mergeInfo.NoteID
content = mergeInfo.Title
)
if author, err = c.gitlab.UserName(authorID); err != nil {
return
}
if canMerge, err = c.checkMergeStatus(projID, mrIID, noteID); err != nil {
return
}
if !canMerge {
go notification.WechatAuthor(c.dao, author, url, sourceBranch, targetBranch, comment)
return
}
if len(mergeInfo.Description) > 0 {
content = content + "\n\n" + mergeInfo.Description
}
mergeMSG := fmt.Sprintf("Merge branch [%s] into [%s] by [%s]\n%s", sourceBranch, targetBranch, username, content)
if state, err = c.gitlab.AcceptMR(projID, mrIID, mergeMSG); err != nil || state != model.MRStateMerged {
if err != nil {
comment = fmt.Sprintf("<pre>[%s]尝试合并失败当前状态不允许合并请查看上方merge按钮旁的提示</pre>", username)
} else {
comment = fmt.Sprintf("<pre>[%s]尝试合并失败,请检查当前状态或同步目标分支代码后再试!</pre>", username)
}
go notification.WechatAuthor(c.dao, author, url, sourceBranch, targetBranch, comment)
c.gitlab.UpdateMRNote(projID, mrIID, noteID, comment)
return
}
ok = true
comment = fmt.Sprintf("<pre>[%s]尝试合并成功!</pre>", username)
go notification.WechatAuthor(c.dao, author, url, sourceBranch, targetBranch, comment)
c.gitlab.UpdateMRNote(projID, mrIID, noteID, comment)
return
}
func (c *Command) resetMergeStatus(projID int, MRIID int, branch string, taskRunning bool) (err error) {
var (
ctx = context.TODO()
)
log.Info("resetMergeStatus projID: %d, MRIID: %d start", projID, MRIID)
if err = c.dao.UnLock(ctx, fmt.Sprintf(model.SagaRepoLockKey, projID)); err != nil {
log.Error("UnLock error: %+v", err)
}
if err = c.dao.DeleteMergeInfo(ctx, projID, branch); err != nil {
log.Error("DeleteMergeInfo error: %+v", err)
}
if err = c.dao.DeleteMRIID(ctx, MRIID); err != nil {
log.Error("Delete MRIID :%d, error: %+v", MRIID, err)
}
if taskRunning {
if err = c.DeleteRunningTask(projID, MRIID); err != nil {
log.Error("DeleteRunningTask: %+v", err)
}
}
log.Info("resetMergeStatus projID: %d, MRIID: %d end!", projID, MRIID)
return
}
// DeleteRunningTask ...
func (c *Command) DeleteRunningTask(projID int, mrID int) (err error) {
var (
ctx = context.TODO()
taskInfos []*model.TaskInfo
)
if _, taskInfos, err = c.dao.MergeTasks(ctx, model.TaskStatusRunning); err != nil {
return
}
for _, taskInfo := range taskInfos {
pID := int(taskInfo.Event.MergeRequest.SourceProjectID)
mID := int(taskInfo.Event.MergeRequest.IID)
if pID == projID && mID == mrID {
// 从正在运行的任务列队中移除
err = c.dao.DeleteMergeTask(ctx, model.TaskStatusRunning, taskInfo)
return
}
}
return
}
func (c *Command) checkMergeStatus(projID int, mrIID int, noteID int) (canMerge bool, err error) {
var (
wip bool
state string
status string
comment string
)
if wip, state, status, err = c.gitlab.MergeStatus(projID, mrIID); err != nil {
return
}
if wip {
comment = "<pre>SAGA 尝试合并失败当前MR是一项正在进行的工作若已完成请先点击“Resolve WIP status”按钮处理后再+merge</pre>"
} else if state != model.MergeStateOpened {
comment = "<pre>SAGA 尝试合并失败当前MR已经关闭或者已经合并</pre>"
} else if status != model.MergeStatusOk {
comment = "<pre>SAGA 尝试合并失败,请先解决合并冲突!</pre>"
} else {
canMerge = true
}
if len(comment) > 0 {
c.gitlab.UpdateMRNote(projID, mrIID, noteID, comment)
}
return
}
// checkLabels ios or android need checkout label when release app stage
func (c *Command) checkLabels(projID int, mrIID int, noteID int, repo *model.Repo) (ok bool, err error) {
var (
labels []string
comment = fmt.Sprintf("<pre>警告SAGA 无法执行+merge发版阶段只允许合入指定label的MR</pre>")
)
if len(repo.Config.AllowLabel) <= 0 {
ok = true
return
}
if labels, err = c.gitlab.MergeLabels(projID, mrIID); err != nil {
return
}
log.Info("checkMrLabels MRIID: %d, labels: %+v", mrIID, labels)
for _, label := range labels {
if label == repo.Config.AllowLabel {
ok = true
return
}
}
if err = c.gitlab.UpdateMRNote(projID, mrIID, noteID, comment); err != nil {
return
}
return
}
func (c *Command) checkPipeline(projID int, mrIID int, noteID int, lastPipelineID int, queryStatus model.QueryStatus) (ok bool, pipelineID int, err error) {
var status string
if pipelineID, status, err = c.gitlab.MRPipelineStatus(projID, mrIID); err != nil {
return
}
log.Info("checkPipeline MRIID: %d, queryStatus: %d, pipeline status: %s", mrIID, queryStatus, status)
// query pipeline id index
if queryStatus == model.QueryID {
if pipelineID > lastPipelineID {
comment := fmt.Sprintf("<pre>警告SAGA 检测到重新提交代码了,+merge中断请重新review代码</pre>")
err = c.gitlab.UpdateMRNote(projID, mrIID, noteID, comment)
return
}
ok = true
return
}
// query process status
if queryStatus == model.QueryProcessing {
if status == model.PipelineRunning || status == model.PipelinePending {
comment := fmt.Sprintf("<pre>警告pipeline正在运行中暂不能立即merge待pipeline运行通过后会自动执行merge操作</pre>")
err = c.gitlab.UpdateMRNote(projID, mrIID, noteID, comment)
ok = true
return
} else if status == model.PipelineSuccess || status == model.PipelineSkipped {
ok = true
return
}
comment := fmt.Sprintf("<pre>警告pipeline状态异常请确保pipeline状态正常后再执行merge操作</pre>")
err = c.gitlab.UpdateMRNote(projID, mrIID, noteID, comment)
return
}
// query success status
if queryStatus == model.QuerySuccess {
if status != model.PipelineSuccess {
comment := fmt.Sprintf("<pre>警告SAGA 无法执行+mergepipeline还未成功请大佬先让pipeline执行通过</pre>")
err = c.gitlab.UpdateMRNote(projID, mrIID, noteID, comment)
ok = false
return
}
}
ok = true
return
}
// showMRQueueInfo ...
func (c *Command) showMRQueueInfo(ctx context.Context, taskInfo *model.TaskInfo) (err error) {
var (
mrIID = int(taskInfo.Event.MergeRequest.IID)
projID = int(taskInfo.Event.MergeRequest.SourceProjectID)
noteID = taskInfo.NoteID
taskInfos []*model.TaskInfo
comment string
waitNum int
runningNum int
)
if _, taskInfos, err = c.dao.MergeTasks(ctx, model.TaskStatusWaiting); err != nil {
return
}
for _, waitTaskInfo := range taskInfos {
if waitTaskInfo.Event.ProjectID == taskInfo.Event.ProjectID {
waitNum++
}
}
if _, taskInfos, err = c.dao.MergeTasks(ctx, model.TaskStatusRunning); err != nil {
return
}
for _, runningTaskInfo := range taskInfos {
if runningTaskInfo.Event.ProjectID == taskInfo.Event.ProjectID {
runningNum++
}
}
if waitNum > 0 {
comment = fmt.Sprintf("<pre>SAGA 提示:当前还有 [%d] 个 MR 等待合并,请大佬耐心等待!</pre>", waitNum)
c.gitlab.UpdateMRNote(projID, mrIID, noteID, comment)
} else if runningNum > 0 {
comment = fmt.Sprintf("<pre>SAGA 提示:当前还有 [%d] 个 MR 正在执行,请大佬耐心等待!</pre>", runningNum)
c.gitlab.UpdateMRNote(projID, mrIID, noteID, comment)
}
return
}

View File

@@ -0,0 +1,91 @@
package command
import (
"context"
"fmt"
"strings"
"go-common/app/tool/saga/model"
"go-common/app/tool/saga/service/notification"
"go-common/library/log"
)
func (c *Command) runPlusOne(ctx context.Context, event *model.HookComment, repo *model.Repo) (err error) {
var (
author string
url = event.ObjectAttributes.URL
commit = event.MergeRequest.LastCommit.ID
reviewer = event.User.UserName
authorID = int(event.MergeRequest.AuthorID)
sourceBranch = event.MergeRequest.SourceBranch
targetBranch = event.MergeRequest.TargetBranch
wip = event.MergeRequest.WorkInProgress
)
log.Info("runPlusOne start ...")
if wip {
c.gitlab.CreateMRNote(event.Project.ID, int(event.MergeRequest.IID), fmt.Sprintf("<pre>警告当前MR处于WIP状态请待开发结束后再review</pre>"))
return
}
if author, err = c.gitlab.UserName(authorID); err != nil {
log.Error("%+v", err)
return
}
log.Info("runPlusOne notification author: %s", author)
if author != "" {
go func() {
notification.MailPlusOne(author, reviewer, url, commit, sourceBranch, targetBranch)
}()
go func() {
notification.WechatPlusOne(c.dao, author, reviewer, url, commit, sourceBranch, targetBranch)
}()
}
return
}
func reviewedOwner(owners []string, reviewedUsers []string, username string) (isowner bool, reviewed bool) {
for _, owner := range owners {
if strings.EqualFold(owner, username) || strings.EqualFold(owner, "all") {
return true, true
}
}
for _, owner := range owners {
for _, user := range reviewedUsers {
if strings.EqualFold(user, owner) {
return false, true
}
}
}
return false, false
}
func (c *Command) reviewedUsers(projID int, mrIID int) (reviewedUsers []string, err error) {
var awardUsers []string
if reviewedUsers, err = c.gitlab.PlusUsernames(projID, mrIID); err != nil {
return
}
if awardUsers, err = c.gitlab.AwardEmojiUsernames(projID, mrIID); err != nil {
return
}
OUTER:
for _, au := range awardUsers {
for _, mu := range reviewedUsers {
if au == mu {
continue OUTER
}
}
reviewedUsers = append(reviewedUsers, au)
}
return
}
func reviewedNum(reviewers []string, reviewedUsers []string) (num int) {
for _, reviewer := range reviewers {
for _, user := range reviewedUsers {
if strings.EqualFold(user, reviewer) || strings.EqualFold(reviewer, "all") {
num++
}
}
}
return
}

View File

@@ -0,0 +1,53 @@
package command
import (
"fmt"
"testing"
"github.com/smartystreets/goconvey/convey"
)
func TestCommandReviewedOwner(t *testing.T) {
convey.Convey("ownerReviewed", t, func(ctx convey.C) {
var (
owners = []string{"a", "b"}
reviewedUsers = []string{"a"}
username = "d"
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
isowner, reviewed := reviewedOwner(owners, reviewedUsers, username)
fmt.Println(isowner, reviewed)
ctx.Convey("Then isowner,reviewed should not be nil.", func(ctx convey.C) {
ctx.So(reviewed, convey.ShouldBeTrue)
ctx.So(isowner, convey.ShouldBeFalse)
})
})
})
}
func TestCommandReviewedNum(t *testing.T) {
convey.Convey("reviewedNum", t, func(ctx convey.C) {
var (
reviewers = []string{"a", "b"}
reviewedUsers = []string{"c"}
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
num := reviewedNum(reviewers, reviewedUsers)
ctx.Convey("Then num should not be nil.", func(ctx convey.C) {
ctx.So(num, convey.ShouldEqual, 0)
})
})
})
convey.Convey("reviewedNum", t, func(ctx convey.C) {
var (
reviewers = []string{"a", "b"}
reviewedUsers = []string{"a"}
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
num := reviewedNum(reviewers, reviewedUsers)
ctx.Convey("Then num should not be nil.", func(ctx convey.C) {
ctx.So(num, convey.ShouldEqual, 1)
})
})
})
}

View File

@@ -0,0 +1,43 @@
package service
import (
"context"
"fmt"
"strings"
"go-common/app/tool/saga/model"
"go-common/library/log"
)
// HandleGitlabComment handle comment webhook
func (s *Service) HandleGitlabComment(c context.Context, event *model.HookComment) (err error) {
var (
comment = strings.TrimSpace(event.ObjectAttributes.Note)
projName = event.MergeRequest.Source.Name
targetBranch = event.MergeRequest.TargetBranch
gitRepo *model.Repo
ok bool
)
if event.ObjectAttributes.NoteableType != model.HookCommentTypeMR {
log.Info("Comment hook noteableType [%s] ignore", event.ObjectAttributes.NoteableType)
return
}
if event.MergeRequest.State == model.MRStateMerged {
log.Info("Gitlab MR [%d] has been merged", event.MergeRequest.ID)
return
}
if gitRepo, ok = s.gitRepoMap[projName]; !ok {
log.Info("Gitlab MR (%d) unknown projName (%s)", event.MergeRequest.ID, projName)
return
}
if !s.validTargetBranch(targetBranch, gitRepo) {
log.Info("Target branch (%s) is not in white list, won't serve comment!", targetBranch)
return
}
fmt.Println("HandleGitlabComment:", event)
if err = s.cmd.Exec(c, comment, event, gitRepo); err != nil {
log.Error("Command Exec cmd: %s err: (%+v)", comment, err)
return
}
return
}

View File

@@ -0,0 +1,30 @@
package service
import (
"context"
"encoding/json"
"testing"
"go-common/app/tool/saga/model"
"github.com/smartystreets/goconvey/convey"
)
func TestServiceHandleGitlabComment(t *testing.T) {
convey.Convey("HandleGitlabComment", t, func(ctx convey.C) {
var (
c = context.Background()
event = &model.HookComment{}
err error
s Service
)
err = json.Unmarshal(GitlabHookCommentTest, event)
convey.So(err, convey.ShouldBeNil)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
err := s.HandleGitlabComment(c, event)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
})
}

View File

@@ -0,0 +1,63 @@
package service
import (
"context"
"runtime/debug"
"strings"
"go-common/app/tool/saga/model"
"go-common/library/log"
"github.com/pkg/errors"
)
// HandleBuildContributors ...
func (s *Service) HandleBuildContributors(c context.Context, repo *model.RepoInfo) (err error) {
if strings.TrimSpace(repo.Group) == "" {
err = errors.Errorf("repo Group is not valid")
return
}
if strings.TrimSpace(repo.Name) == "" {
err = errors.Errorf("repo Name is not valid")
return
}
if strings.TrimSpace(repo.Branch) == "" {
err = errors.Errorf("repo Branch is not valid")
return
}
go func() {
defer func() {
if x := recover(); x != nil {
log.Error("BuildContributor: %+v %s", x, debug.Stack())
}
}()
if err = s.cmd.BuildContributor(repo); err != nil {
log.Error("BuildContributor %+v", err)
}
}()
return
}
// BuildContributors ...
func (s *Service) BuildContributors(repos []*model.Repo) (err error) {
var (
repo *model.Repo
branch string
)
log.Info("BuildContributors start ...")
for _, repo = range repos {
for _, branch = range repo.Config.AuthBranches {
repoInfo := &model.RepoInfo{
Group: repo.Config.Group,
Name: repo.Config.Name,
Branch: branch,
}
log.Info("BuildContributors project [%s], group [%s], Name [%s], branch [%s]", repo.Config.URL, repo.Config.Group, repo.Config.Name, branch)
if err = s.HandleBuildContributors(context.TODO(), repoInfo); err != nil {
log.Error("BuildContributors err (%+v)", err)
}
}
}
return
}

View File

@@ -0,0 +1,22 @@
package service
import (
"context"
"testing"
"github.com/smartystreets/goconvey/convey"
)
func TestServiceHandleBuildContributors(t *testing.T) {
convey.Convey("HandleBuildContributors", t, func(ctx convey.C) {
var (
c = context.Background()
)
ctx.Convey("When everything goes positive", func(ctx convey.C) {
err := s.HandleBuildContributors(c, repoTest)
ctx.Convey("Then err should be nil.", func(ctx convey.C) {
ctx.So(err, convey.ShouldBeNil)
})
})
})
}

View File

@@ -0,0 +1,45 @@
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_library(
name = "go_default_library",
srcs = ["gitlab.go"],
importpath = "go-common/app/tool/saga/service/gitlab",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/tool/saga/model:go_default_library",
"//library/log:go_default_library",
"//vendor/github.com/pkg/errors:go_default_library",
"//vendor/github.com/xanzy/go-gitlab:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
go_test(
name = "go_default_test",
srcs = ["gitlab_test.go"],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = [
"//app/tool/saga/conf:go_default_library",
"//vendor/github.com/smartystreets/goconvey/convey:go_default_library",
],
)

View File

@@ -0,0 +1,545 @@
package gitlab
import (
"net/url"
"strings"
"go-common/app/tool/saga/model"
"go-common/library/log"
"github.com/pkg/errors"
"github.com/xanzy/go-gitlab"
)
// Gitlab def
type Gitlab struct {
url string
token string
client *gitlab.Client
}
// New new gitlab structure
func New(url string, token string) (g *Gitlab) {
g = &Gitlab{
url: url,
token: token,
client: gitlab.NewClient(nil, token),
}
g.client.SetBaseURL(url)
return
}
// HostToken ...
func (g *Gitlab) HostToken() (host string, token string, err error) {
var u *url.URL
if u, err = url.Parse(g.url); err != nil {
return
}
host = u.Host
token = g.token
return
}
// LastGreenCommit get last green pipeline commit
func (g *Gitlab) LastGreenCommit(ciProjectID string, ciCommitRefName string) (commit string, err error) {
var (
status = gitlab.Success
pipelines gitlab.PipelineList
)
opt := &gitlab.ListProjectPipelinesOptions{
Status: &status,
Ref: gitlab.String(ciCommitRefName),
}
if pipelines, _, err = g.client.Pipelines.ListProjectPipelines(ciProjectID, opt); err != nil {
return
}
if len(pipelines) == 0 {
return
}
commit = pipelines[0].Sha
return
}
// AcceptMR accept Merge request
func (g *Gitlab) AcceptMR(projID int, mrIID int, msg string) (state string, err error) {
var (
opt = &gitlab.AcceptMergeRequestOptions{
ShouldRemoveSourceBranch: gitlab.Bool(true),
MergeCommitMessage: gitlab.String(msg),
}
mergeRequest *gitlab.MergeRequest
)
if mergeRequest, _, err = g.client.MergeRequests.AcceptMergeRequest(projID, mrIID, opt); err != nil {
err = errors.Wrapf(err, "AcceptMergeRequest(%d,%d)", projID, mrIID)
return
}
state = mergeRequest.State
return
}
// CloseMR close merge request
func (g *Gitlab) CloseMR(projID int, mrIID int) (err error) {
var (
opt = &gitlab.UpdateMergeRequestOptions{
StateEvent: gitlab.String("close"),
}
)
if _, _, err = g.client.MergeRequests.UpdateMergeRequest(projID, mrIID, opt); err != nil {
err = errors.Wrapf(err, "CloseMR(%d,%d)", projID, mrIID)
}
return
}
// CreateMRNote create note to merge request
func (g *Gitlab) CreateMRNote(projID int, mrIID int, content string) (noteID int, err error) {
var (
opt = &gitlab.CreateMergeRequestNoteOptions{
Body: gitlab.String(content),
}
note *gitlab.Note
)
if note, _, err = g.client.Notes.CreateMergeRequestNote(projID, mrIID, opt); err != nil {
err = errors.Wrapf(err, "CreateMergeRequestNote(%d,%d)", projID, mrIID)
return
}
noteID = note.ID
return
}
// UpdateMRNote update the specified merge request note
func (g *Gitlab) UpdateMRNote(projID int, mrIID int, noteID int, content string) (err error) {
var (
opt = &gitlab.UpdateMergeRequestNoteOptions{
Body: gitlab.String(content),
}
)
if _, _, err = g.client.Notes.UpdateMergeRequestNote(projID, mrIID, noteID, opt); err != nil {
err = errors.Wrapf(err, "UpdateMergeRequestNote(%d,%d,%d)", projID, mrIID, noteID)
return
}
return
}
// DeleteMRNote delete the specified note for merge request
func (g *Gitlab) DeleteMRNote(projID int, mrIID int, noteID int) (err error) {
if _, err = g.client.Notes.DeleteMergeRequestNote(projID, mrIID, noteID); err != nil {
err = errors.Wrapf(err, "DeleteMergeRequestNote(%d,%d,%d)", projID, mrIID, noteID)
}
return
}
// UserName get user name for user id
func (g *Gitlab) UserName(userID int) (userName string, err error) {
var (
user *gitlab.User
)
if user, _, err = g.client.Users.GetUser(userID); err != nil {
err = errors.WithStack(err)
return
}
userName = user.Username
return
}
// Triggers get pipeline triggers
func (g *Gitlab) Triggers(projectID int) (triggers []*gitlab.PipelineTrigger, err error) {
var (
opt = &gitlab.ListPipelineTriggersOptions{}
)
if triggers, _, err = g.client.PipelineTriggers.ListPipelineTriggers(projectID, opt); err != nil {
err = errors.Wrapf(err, "ListPipelineTriggers (projectID: %d)", projectID)
return nil, err
}
return triggers, nil
}
// CreateTrigger create the trigger to trigger pipeline
func (g *Gitlab) CreateTrigger(projectID int) (trigger *gitlab.PipelineTrigger, err error) {
var (
opt = &gitlab.AddPipelineTriggerOptions{
Description: gitlab.String("gitlab-ci-build-on-merge-request"),
}
)
if trigger, _, err = g.client.PipelineTriggers.AddPipelineTrigger(projectID, opt); err != nil {
err = errors.Wrapf(err, "CreateTrigger (projectID: %d)", projectID)
return nil, err
}
return trigger, nil
}
// TakeOwnership take ownership of the trigger
func (g *Gitlab) TakeOwnership(projectID int, triggerID int) (trigger *gitlab.PipelineTrigger, err error) {
if trigger, _, err = g.client.PipelineTriggers.TakeOwnershipOfPipelineTrigger(projectID, triggerID); err != nil {
err = errors.Wrapf(err, "TakeOwnershipOfPipelineTrigger (projectID: %d, triggerID: %d)", projectID, triggerID)
return nil, err
}
return trigger, nil
}
// TriggerPipeline trigger the pipeline
func (g *Gitlab) TriggerPipeline(projectID int, ref string, token string) (pipeline *gitlab.Pipeline, err error) {
var (
opt = &gitlab.RunPipelineTriggerOptions{
Ref: gitlab.String(ref),
Token: gitlab.String(token),
}
)
if pipeline, _, err = g.client.PipelineTriggers.RunPipelineTrigger(projectID, opt); err != nil {
err = errors.Wrapf(err, "RunPipelineTrigger (projectID: %d, ref: %s, token: %s)", projectID, ref, token)
return nil, err
}
return pipeline, nil
}
// AwardEmojiUsernames get the username who gave the emoji
func (g *Gitlab) AwardEmojiUsernames(projID int, mrIID int) (usernames []string, err error) {
var (
opt = &gitlab.ListAwardEmojiOptions{
Page: 1,
PerPage: 100,
}
awards []*gitlab.AwardEmoji
)
if awards, _, err = g.client.AwardEmoji.ListMergeRequestAwardEmoji(projID, mrIID, opt); err != nil {
return
}
for _, a := range awards {
if a.Name == "thumbsup" {
usernames = append(usernames, a.User.Username)
}
}
return
}
// AuditProjects audit all the given projects
func (g *Gitlab) AuditProjects(repos []*model.Repo, hooks []*model.WebHook) (err error) {
var (
repo *model.Repo
projID int
)
log.Info("AuditProjects start")
for _, repo = range repos {
log.Info("AuditProjects project [%s]", repo.Config.URL)
if projID, err = g.ProjectID(repo.Config.URL); err != nil {
log.Error("Failed to get project ID [%s], err: %+v", repo.Config.URL, err)
continue
}
if err = g.AuditProjectHooks(projID, hooks); err != nil {
log.Error("Failed to get hooks [%s], err: %+v", repo.Config.URL, err)
continue
}
log.Info("AuditProjects project [%s]", repo.Config.URL)
}
log.Info("AuditProjects end")
return
}
// AuditProjectHooks check and add or edit the project webhooks
func (g *Gitlab) AuditProjectHooks(projID int, hooks []*model.WebHook) (err error) {
var (
webHooks []*gitlab.ProjectHook
h *model.WebHook
)
if webHooks, err = g.ListProjectHook(projID); err != nil {
return
}
// 配置文件中的webhook是否配置在项目中
inWebHooks := func(h *model.WebHook) (in bool, ph *gitlab.ProjectHook) {
for _, ph = range webHooks {
if h.URL == ph.URL {
in = true
return
}
}
in = false
return
}
// 找到的webhook是否和配置文件中webhook配置相等
equalHook := func(h *model.WebHook, ph *gitlab.ProjectHook) (equal bool) {
if h.PushEvents == ph.PushEvents && h.IssuesEvents == ph.IssuesEvents &&
h.ConfidentialIssuesEvents == ph.ConfidentialIssuesEvents && h.MergeRequestsEvents == ph.MergeRequestsEvents &&
h.TagPushEvents == ph.TagPushEvents && h.NoteEvents == ph.NoteEvents &&
h.JobEvents == ph.JobEvents && h.PipelineEvents == ph.PipelineEvents &&
h.WikiPageEvents == ph.WikiPageEvents && !ph.EnableSSLVerification {
return true
}
return false
}
// 配置的webhook与查询出来的对比如果存在则比较属性属性不同则修改不存在则创建
for _, h = range hooks {
if in, ph := inWebHooks(h); in {
if !equalHook(h, ph) {
if err = g.EditProjectHook(projID, ph.ID, h); err != nil {
return
}
}
} else {
if err = g.AddProjectHook(projID, h); err != nil {
return
}
}
}
return
}
// AddProjectHook add project hook for the project
func (g *Gitlab) AddProjectHook(projID int, hook *model.WebHook) (err error) {
var (
opt = &gitlab.AddProjectHookOptions{
URL: &hook.URL,
PushEvents: &hook.PushEvents,
IssuesEvents: &hook.IssuesEvents,
ConfidentialIssuesEvents: &hook.ConfidentialIssuesEvents,
MergeRequestsEvents: &hook.MergeRequestsEvents,
TagPushEvents: &hook.TagPushEvents,
NoteEvents: &hook.NoteEvents,
JobEvents: &hook.JobEvents,
PipelineEvents: &hook.PipelineEvents,
WikiPageEvents: &hook.WikiPageEvents,
EnableSSLVerification: gitlab.Bool(false),
}
)
if _, _, err = g.client.Projects.AddProjectHook(projID, opt); err != nil {
return
}
return
}
// ListProjectHook list all the webhook for the project
func (g *Gitlab) ListProjectHook(projID int) (hooks []*gitlab.ProjectHook, err error) {
var (
opt = &gitlab.ListProjectHooksOptions{
Page: 1,
PerPage: 100,
}
)
if hooks, _, err = g.client.Projects.ListProjectHooks(projID, opt); err != nil {
return
}
return
}
// EditProjectHook edit the specified webhook for the project
func (g *Gitlab) EditProjectHook(projID int, hookID int, hook *model.WebHook) (err error) {
var (
opt = &gitlab.EditProjectHookOptions{
URL: &hook.URL,
PushEvents: &hook.PushEvents,
IssuesEvents: &hook.IssuesEvents,
ConfidentialIssuesEvents: &hook.ConfidentialIssuesEvents,
MergeRequestsEvents: &hook.MergeRequestsEvents,
TagPushEvents: &hook.TagPushEvents,
NoteEvents: &hook.NoteEvents,
JobEvents: &hook.JobEvents,
PipelineEvents: &hook.PipelineEvents,
WikiPageEvents: &hook.WikiPageEvents,
EnableSSLVerification: gitlab.Bool(false),
}
)
if _, _, err = g.client.Projects.EditProjectHook(projID, hookID, opt); err != nil {
return
}
return
}
// DeletePojectHook delete the specified hook for the project
func (g *Gitlab) DeletePojectHook(projID int, hookID int) (err error) {
if _, err = g.client.Projects.DeleteProjectHook(projID, hookID); err != nil {
return
}
return
}
// ProjectID get project id by project URL-encoded name
func (g *Gitlab) ProjectID(url string) (projID int, err error) {
var (
projectName = url[strings.LastIndex(url, ":")+1 : strings.LastIndex(url, ".git")]
project *gitlab.Project
)
if project, _, err = g.client.Projects.GetProject(projectName); err != nil {
return
}
projID = project.ID
return
}
// MRPipelineStatus query PipelineState for mr
func (g *Gitlab) MRPipelineStatus(projID int, mrIID int) (id int, status string, err error) {
var pipelineList gitlab.PipelineList
if pipelineList, _, err = g.client.MergeRequests.ListMergeRequestPipelines(projID, mrIID); err != nil {
return
}
if len(pipelineList) == 0 {
status = model.PipelineSuccess
return
}
id = pipelineList[0].ID
status = pipelineList[0].Status
return
}
// LastPipeLineState query Last PipeLineState
func (g *Gitlab) LastPipeLineState(projID int, ciCommitRefName string) (state bool, err error) {
var pipelines gitlab.PipelineList
opt := &gitlab.ListProjectPipelinesOptions{
Ref: gitlab.String(ciCommitRefName),
}
state = true
if pipelines, _, err = g.client.Pipelines.ListProjectPipelines(projID, opt); err != nil {
return
}
if len(pipelines) < 2 {
return
}
state = pipelines[1].Status == model.PipelineSuccess
return
}
// MergeStatus query MergeStatus
func (g *Gitlab) MergeStatus(projID int, mrIID int) (wip bool, state string, status string, err error) {
var mergerequest *gitlab.MergeRequest
if mergerequest, _, err = g.client.MergeRequests.GetMergeRequest(projID, mrIID); err != nil {
return
}
state = mergerequest.State
status = mergerequest.MergeStatus
wip = mergerequest.WorkInProgress
return
}
// MergeLabels get Merge request labels
func (g *Gitlab) MergeLabels(projID int, mrIID int) (labels []string, err error) {
var mergerequest *gitlab.MergeRequest
if mergerequest, _, err = g.client.MergeRequests.GetMergeRequest(projID, mrIID); err != nil {
return
}
labels = mergerequest.Labels
return
}
// PlusUsernames get the username who +1
func (g *Gitlab) PlusUsernames(projID int, mrIID int) (usernames []string, err error) {
var (
opt = &gitlab.ListMergeRequestNotesOptions{
Page: 1,
PerPage: 100,
}
notes []*gitlab.Note
exist bool
)
if notes, _, err = g.client.Notes.ListMergeRequestNotes(projID, mrIID, opt); err != nil {
return
}
for _, note := range notes {
if (strings.TrimSpace(note.Body) == model.SagaCommandPlusOne) || (strings.TrimSpace(note.Body) == model.SagaCommandPlusOne1) {
exist = false
for _, user := range usernames {
if user == note.Author.Username {
exist = true
break
}
}
if !exist {
usernames = append(usernames, note.Author.Username)
}
}
}
return
}
// CompareDiff ...
func (g *Gitlab) CompareDiff(projID int, from string, to string) (files []string, err error) {
var (
opt = &gitlab.CompareOptions{
From: &from,
To: &to,
}
compare *gitlab.Compare
)
if compare, _, err = g.client.Repositories.Compare(projID, opt); err != nil {
return
}
for _, diff := range compare.Diffs {
if diff.NewFile {
files = append(files, diff.NewPath)
} else {
files = append(files, diff.OldPath)
}
}
return
}
// CommitDiff ...
func (g *Gitlab) CommitDiff(projID int, sha string) (files []string, err error) {
var (
diffs []*gitlab.Diff
opt = &gitlab.GetCommitDiffOptions{
Page: 1,
PerPage: 100,
}
)
if diffs, _, err = g.client.Commits.GetCommitDiff(projID, sha, opt); err != nil {
return
}
for _, diff := range diffs {
if diff.NewFile {
files = append(files, diff.NewPath)
} else {
files = append(files, diff.OldPath)
}
}
return
}
// MRChanges ...
func (g *Gitlab) MRChanges(projID, mrIID int) (changeFiles []string, deleteFiles []string, err error) {
var (
mr *gitlab.MergeRequest
)
if mr, _, err = g.client.MergeRequests.GetMergeRequestChanges(projID, mrIID); err != nil {
return
}
for _, c := range mr.Changes {
if c.RenamedFile {
deleteFiles = append(deleteFiles, c.OldPath)
changeFiles = append(changeFiles, c.NewPath)
} else if c.DeletedFile {
deleteFiles = append(deleteFiles, c.OldPath)
} else if c.NewFile {
changeFiles = append(changeFiles, c.NewPath)
} else {
changeFiles = append(changeFiles, c.OldPath)
}
}
return
}
// RepoRawFile ...
func (g *Gitlab) RepoRawFile(projID int, branch string, filename string) (content []byte, err error) {
var (
opt = &gitlab.GetRawFileOptions{
Ref: &branch,
}
)
if content, _, err = g.client.RepositoryFiles.GetRawFile(projID, filename, opt); err != nil {
return
}
return
}

View File

@@ -0,0 +1,141 @@
package gitlab
import (
"flag"
"testing"
"go-common/app/tool/saga/conf"
. "github.com/smartystreets/goconvey/convey"
)
var (
g *Gitlab
)
func init() {
var (
err error
)
g = New("http://gitlab.bilibili.co/api/v4", "z3nN4s4BVX5oNYXKbEPL")
flag.Set("conf", "/Users/bilibili/go/src/go-common/app/tool/saga/cmd/saga-test.toml")
if err = conf.Init(); err != nil {
panic(err)
}
}
// Test Function HostToken
func TestHostToken(t *testing.T) {
Convey("Test HostToken", t, func() {
var (
host string
token string
err error
)
host, token, err = g.HostToken()
So(err, ShouldBeNil)
So(host, ShouldEqual, "gitlab.bilibili.co")
So(token, ShouldEqual, "z3nN4s4BVX5oNYXKbEPL")
})
}
// TODO AutoPush
// Test MR Create/Close
// Test MRNote Create/Update/Delete
//func TestCreateMRNote(t *testing.T) {
// Convey("Test CreateMRNote", t, func() {
// var (
// err error
// noteID int
//
// mr *gitlab.MergeRequest
// //res *gitlab.Response
//
// // MR CreateMergeRequestOptions Info
// title = "test"
// description = "test"
// sourceBranch = "chy-saga-test"
// targetBranch = "saga-test-1"
// assigneeID = 15
// projectID = 23
//
// //MR Info
// state string
// status string
// )
//
// // Create CreateMergeRequestOptions Instance
// opt := new(gitlab.CreateMergeRequestOptions)
// opt.Title = &title
// opt.Description = &description
// opt.SourceBranch = &sourceBranch
// opt.TargetBranch = &targetBranch
// opt.AssigneeID = &assigneeID
// opt.TargetProjectID = &projectID
//
// // Create MergeRequest
// mr, _, err = g.client.MergeRequests.CreateMergeRequest(35, opt)
// So(err, ShouldBeNil)
//
// // Query MR Status
// state = mr.State
// status = mr.MergeStatus
// So(state, ShouldEqual, "opened")
// So(status, ShouldEqual, "cannot_be_merged")
//
// // Create MRNote
// noteID, err = g.CreateMRNote(projectID, mr.IID, "CreateMRNote: PASS!")
// So(err, ShouldBeNil)
// So(noteID, ShouldNotBeNil)
//
// // Update MRNote
// err = g.UpdateMRNote(projectID, mr.IID, noteID, "CreateMRNote: PASS!\nUpdateMRNote: PASS!")
// So(err, ShouldBeNil)
//
// // Delete MRNote
// err = g.DeleteMRNote(projectID, mr.IID, noteID)
// So(err, ShouldBeNil)
//
// // Accept MR
// err = g.AcceptMR(projectID, mr.IID, "Accept MR: PASS!")
// So(err, ShouldBeNil)
//
// //Close MR
// //err = g.CloseMR(projectID, mr.IID)
// //So(err, ShouldBeNil)
//
// // Report
// noteID, err = g.CreateMRNote(projectID, mr.IID, "- MR Create Successfully!\n"+
// "- MRNote Create Successfully\n"+
// "- MRNote Update Successfully\n"+
// "- MRNote Delete Successfully\n"+
// "- MR Accept Successfully\n"+
// "- MR Close Successfully")
// So(err, ShouldBeNil)
// })
//}
func TestProjectID(t *testing.T) {
Convey("Test ProjectID", t, func() {
var (
projID int
err error
)
projID, err = g.ProjectID("git@gitlab.bilibili.co:platform/go-common.git")
So(err, ShouldBeNil)
So(projID, ShouldEqual, 23)
})
}
func TestCommitDiff(t *testing.T) {
Convey("Test CommitDiff", t, func() {
var (
err error
files []string
)
files, err = g.CommitDiff(23, "f5c9bfa037771b7f8179db7a245a25695c90be9b")
So(err, ShouldBeNil)
So(files[0], ShouldEqual, ".gitlab-ci.yml")
})
}

View File

@@ -0,0 +1,50 @@
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_library(
name = "go_default_library",
srcs = [
"mail.go",
"tpl.go",
],
importpath = "go-common/app/tool/saga/service/mail",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/tool/saga/conf:go_default_library",
"//app/tool/saga/model:go_default_library",
"//library/log:go_default_library",
"//vendor/github.com/pkg/errors:go_default_library",
"//vendor/gopkg.in/gomail.v2:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = ["mail_test.go"],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = [
"//app/tool/saga/conf:go_default_library",
"//app/tool/saga/model:go_default_library",
"//vendor/github.com/smartystreets/goconvey/convey:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,111 @@
package mail
import (
"bytes"
"fmt"
"text/template"
"go-common/app/tool/saga/conf"
"go-common/app/tool/saga/model"
"go-common/library/log"
"github.com/pkg/errors"
gomail "gopkg.in/gomail.v2"
)
// SendMail2 ...
func SendMail2(addr *model.MailAddress, subject string, data string) (err error) {
var (
msg = gomail.NewMessage()
)
msg.SetAddressHeader("From", conf.Conf.Property.Mail.Address, conf.Conf.Property.Mail.Name)
msg.SetHeader("To", msg.FormatAddress(addr.Address, addr.Name))
msg.SetHeader("Subject", subject)
msg.SetBody("text/plain", data)
d := gomail.NewDialer(
conf.Conf.Property.Mail.Host,
conf.Conf.Property.Mail.Port,
conf.Conf.Property.Mail.Address,
conf.Conf.Property.Mail.Pwd,
)
if err = d.DialAndSend(msg); err != nil {
err = errors.WithMessage(err, fmt.Sprintf("Send mail (%+v,%s,%s) failed", addr, subject, data))
return
}
log.Info("Send mail (%+v,%s,%s) success", addr, subject, data)
return
}
// SendMail send mail
func SendMail(m *model.Mail, data *model.MailData) (err error) {
var (
toUsers []string
msg = gomail.NewMessage()
buf = &bytes.Buffer{}
)
msg.SetAddressHeader("From", conf.Conf.Property.Mail.Address, conf.Conf.Property.Mail.Name) // 发件人
for _, ads := range m.ToAddress {
toUsers = append(toUsers, msg.FormatAddress(ads.Address, ads.Name))
}
t := template.New("MR Mail")
if t, err = t.Parse(mailTPL); err != nil {
log.Error("tpl.Parse(%s) error(%+v)", mailTPL, errors.WithStack(err))
return
}
err = t.Execute(buf, data)
if err != nil {
log.Error("t.Execute error(%+v)", errors.WithStack(err))
return
}
msg.SetHeader("To", toUsers...)
msg.SetHeader("Subject", m.Subject) // 主题
msg.SetBody("text/html", buf.String()) // 正文
d := gomail.NewDialer(
conf.Conf.Property.Mail.Host,
conf.Conf.Property.Mail.Port,
conf.Conf.Property.Mail.Address,
conf.Conf.Property.Mail.Pwd,
)
if err = d.DialAndSend(msg); err != nil {
log.Error("Send mail Fail(%v) diff(%s)", msg, err)
return
}
return
}
// SendMail3 SendMail all parameter
func SendMail3(from string, sender string, senderPwd string, m *model.Mail, data *model.MailData) (err error) {
var (
toUsers []string
msg = gomail.NewMessage()
buf = &bytes.Buffer{}
)
msg.SetAddressHeader("From", from, sender) // 发件人
for _, ads := range m.ToAddress {
toUsers = append(toUsers, msg.FormatAddress(ads.Address, ads.Name))
}
t := template.New("MR Mail")
if t, err = t.Parse(mailTPL3); err != nil {
log.Error("tpl.Parse(%s) error(%+v)", mailTPL3, errors.WithStack(err))
return
}
err = t.Execute(buf, data)
if err != nil {
log.Error("t.Execute error(%+v)", errors.WithStack(err))
return
}
msg.SetHeader("To", toUsers...)
msg.SetHeader("Subject", m.Subject) // 主题
msg.SetBody("text/html", buf.String()) // 正文
d := gomail.NewDialer(
"smtp.exmail.qq.com",
465,
from,
senderPwd,
)
if err = d.DialAndSend(msg); err != nil {
log.Error("Send mail Fail(%v) diff(%s)", msg, err)
return
}
return
}

View File

@@ -0,0 +1,46 @@
package mail
import (
"flag"
"fmt"
"testing"
"go-common/app/tool/saga/conf"
"go-common/app/tool/saga/model"
. "github.com/smartystreets/goconvey/convey"
)
func init() {
var err error
flag.Set("conf", "../../cmd/saga-test.toml")
if err = conf.Init(); err != nil {
panic(err)
}
}
// go test -test.v -test.run TestMail
func TestMail(t *testing.T) {
Convey("Test mail", t, func() {
m := &model.Mail{
ToAddress: []*model.MailAddress{{Name: "baihai", Address: "changhengyuan@bilibili.com"},
{Name: "muyan", Address: "changhengyuan@bilibili.com"}},
Subject: fmt.Sprintf("【Sage 提醒】%s项目发生Merge Request事件", "test-mail"),
}
mergeOut := " Merge made by the 'recursive' strategy.\n" +
"tools/saga/CHANGELOG.md | 4 ++++\n" +
"business/interface/app-show/service/rank/rank.go | 28 +++++++++++------------\n" +
"business/interface/app-show/service/show/cache.go | 6 ++---\n" +
"3 files changed, 21 insertions(+), 17 deletions(-)"
err := SendMail(m, &model.MailData{
UserName: "baihai",
SourceBranch: "featre_answer",
TargetBranch: "master",
Title: "修改变量A",
Description: "内容就是",
URL: "http://www.baidu.com",
Info: mergeOut,
})
So(err, ShouldBeNil)
})
}

View File

@@ -0,0 +1,130 @@
package mail
var (
mailTPL = `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>saga page</title>
</head>
<body>
<form action="" method="post" class="basic-grey" style="margin-left:auto;
margin-right:auto;
max-width: 500px;
background: #F7F7F7;
padding: 25px 15px 25px 10px;
font: 12px Georgia, 'Times New Roman', Times, serif;
color: #888;
text-shadow: 1px 1px 1px #FFF;
border:1px solid #E4E4E4;">
<h1 style="font-size: 25px;
padding: 0px 0px 10px 40px;
display: block;
border-bottom:1px solid #E4E4E4;
margin: -10px -15px 30px -10px;;
color: #888;">
Saga
<span style="display: block;font-size: 11px;">
Merge Request 事件通知</span>
</h1>
<label style="display: block;margin: 0px;">
<span style="float: left;
width:100%;
text-align: left;
padding-right: 10px;
padding-left: 30px;
margin-top: 10px;
color: #888;">
申请人 : {{.UserName}}
<br />
来源分支 : {{.SourceBranch}}
<br />
目标分支 : {{.TargetBranch}}
<br />
修改标题 : {{.Title}}
<br />
修改说明 : {{.Description}}
<br />
<a href="{{.URL}}">点击查看..</a>
<br />
<br />
<br />
<h3>额外信息: </h3>
{{.Info}}
<br />
<br />
<br />
</span>
</label>
</form>
</body>
</html>`
mailTPL3 = `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>saga page</title>
</head>
<body>
<form action="" method="post" class="basic-grey" style="
margin-right:auto;
max-width: 500px;
font: 12px Georgia, 'Times New Roman', Times, serif;
color: #888;
text-shadow: 1px 1px 1px #FFF;">
<h1 style="font-size: 25px;
padding: 10px 0px 10px 40px;
display: block;
border-bottom:1px solid #E4E4E4;
margin: -10px -15px 5px -10px;;
color: #03A9F4;">
Saga
<span style="display: block;font-size: 11px;">
事件通知</span>
</h1>
</form>
<label style="display: block;margin: 0px;">
<span style="float: left;
width:100%;
text-align: left;
padding-right: 10px;
padding-left: 30px;
margin-top: 10px;
color: #888;">
执行状态 :
<font class="{{.PipelineStatus}}" >
{{.PipeStatus}}
</font>
<br />
Pipeline信息:
<font class="{{.PipelineStatus}}" >
<a href="{{.URL}}">{{.URL}}</a>
</font>
<br />
来源分支 : {{.SourceBranch}}
<br />
修改说明 : {{.Description}}
<br />
额外信息: {{.Info}}
<br />
<br />
<br />
</span>
</label>
</body>
<style type="text/css">
.failed {
color: #f21303;
}
.failed a{
color: #f21303;
}
.success {
color: #1aaa55;
}
.success a{
color: #1aaa55;
}
</style>
</html>`
)

155
app/tool/saga/service/mr.go Normal file
View File

@@ -0,0 +1,155 @@
package service
import (
"context"
"fmt"
"go-common/app/tool/saga/model"
"go-common/library/log"
)
// MergeRequest ...
func (s *Service) MergeRequest(c context.Context, event *model.HookMR) (err error) {
var (
ok bool
repo *model.Repo
url = event.Project.GitSSHURL
projID = event.Project.ID
mrIID = int(event.ObjectAttributes.IID)
sourceBranch = event.ObjectAttributes.SourceBranch
targetBranch = event.ObjectAttributes.TargetBranch
wip = event.ObjectAttributes.WorkInProgress
)
log.Info("Hook MergeRequest state: %s, projID: %d, mrid: %d", event.ObjectAttributes.State, projID, mrIID)
if ok, repo = s.findRepo(url); !ok {
return
}
if event.ObjectAttributes.State == model.MRStateOpened {
if wip {
log.Info("MergeRequest mr is wip, project ID: [%d], branch: [%s]", event.Project.ID, sourceBranch)
return
}
if !s.validTargetBranch(targetBranch, repo) {
s.gitlab.CreateMRNote(projID, mrIID, fmt.Sprintf("<pre>警告:目标分支 %s 不在SAGA白名单中此MR不会触发SAGA行为</pre>", targetBranch))
return
}
authBranch := s.cmd.GetAuthBranch(targetBranch, repo.Config.AuthBranches)
if err = s.ShowMRRoleInfo(c, authBranch, repo, event); err != nil {
return
}
} else {
if event.ObjectAttributes.State == model.MRStateMerged {
log.Info("MergeRequest.UpdateContributor:%d,%s,%s,%+v", projID, sourceBranch, targetBranch, repo.Config.AuthBranches)
if err = s.cmd.UpdateContributor(projID, mrIID, sourceBranch, targetBranch, repo.Config.AuthBranches); err != nil {
return
}
}
if err = s.d.DeleteReportStatus(c, projID, mrIID); err != nil {
return
}
}
return
}
// ShowMRRoleInfo ...
func (s *Service) ShowMRRoleInfo(c context.Context, authBranch string, repo *model.Repo, event *model.HookMR) (err error) {
var (
projID = event.Project.ID
mrIID = int(event.ObjectAttributes.IID)
result bool
)
if result, err = s.d.ReportStatus(c, projID, mrIID); err != nil {
return
}
log.Info("MergeRequest whether create note auth info: %v", result)
if !result {
if len(repo.Config.SuperAuthUsers) > 0 {
if err = s.ReportSuperRoleInfo(c, repo.Config.SuperAuthUsers, event); err != nil {
return
}
} else {
if err = s.ReportMRRoleInfo(c, authBranch, event); err != nil {
return
}
}
if err = s.d.SetReportStatus(c, projID, mrIID, true); err != nil {
return
}
}
return
}
// ReportMRRoleInfo ...
func (s *Service) ReportMRRoleInfo(c context.Context, authBranch string, event *model.HookMR) (err error) {
var (
projID = event.Project.ID
mrIID = int(event.ObjectAttributes.IID)
pathOwners []model.RequireReviewFolder
)
if pathOwners, err = s.cmd.GetAllPathAuth(projID, mrIID, authBranch); err != nil {
return
}
authInfo := ""
authInfo = fmt.Sprintf("<pre>SAGA权限信息提示请review: %s</pre>", event.ObjectAttributes.Title)
authInfo += "\n"
for _, os := range pathOwners {
if len(os.Owners) > 0 {
//authInfo += "----- PATH: " + os.Folder + "OWNER: "
authInfo += fmt.Sprintf("+ PATH: %sOWNER: ", os.Folder)
for _, o := range os.Owners {
if o == "all" {
authInfo += "所有人" + " 或 "
} else {
authInfo += "@" + o + " 或 "
}
}
authInfo = authInfo[:len(authInfo)-len(" 或 ")]
}
if len(os.Reviewers) > 0 {
authInfo += "REVIEWER: "
for _, o := range os.Reviewers {
if o == "all" {
authInfo += "所有人" + " 与 "
} else {
authInfo += "@" + o + " 与 "
}
}
authInfo = authInfo[:len(authInfo)-len(" 与 ")]
}
authInfo += "\n\n"
}
if _, err = s.gitlab.CreateMRNote(projID, mrIID, authInfo); err != nil {
return
}
return
}
// ReportSuperRoleInfo ...
func (s *Service) ReportSuperRoleInfo(c context.Context, superUsers []string, event *model.HookMR) (err error) {
var (
projID = event.Project.ID
mrIID = int(event.ObjectAttributes.IID)
)
authInfo := fmt.Sprintf("<pre>SAGA权限信息提示已配置超级权限用户请review: %s</pre>", event.ObjectAttributes.Title)
authInfo += "\n"
authInfo += fmt.Sprintf("+ SUPERMAN: ")
for _, user := range superUsers {
authInfo += "@" + user + " 或 "
}
authInfo = authInfo[:len(authInfo)-len(" 或 ")]
if _, err = s.gitlab.CreateMRNote(projID, mrIID, authInfo); err != nil {
return
}
return
}

View File

@@ -0,0 +1,35 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
go_library(
name = "go_default_library",
srcs = ["notification.go"],
importpath = "go-common/app/tool/saga/service/notification",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/tool/saga/dao:go_default_library",
"//app/tool/saga/model:go_default_library",
"//app/tool/saga/service/mail:go_default_library",
"//app/tool/saga/service/wechat:go_default_library",
"//library/log:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,202 @@
package notification
import (
"context"
"fmt"
"runtime/debug"
"strconv"
"strings"
"go-common/app/tool/saga/dao"
"go-common/app/tool/saga/model"
"go-common/app/tool/saga/service/mail"
"go-common/app/tool/saga/service/wechat"
"go-common/library/log"
)
// WechatAuthor send wechat message to original author
func WechatAuthor(dao *dao.Dao, authorName string, url, sourceBranch, targetBranch string, comment string) (err error) {
defer func() {
if x := recover(); x != nil {
log.Error("wechatAuthor: %+v %s", x, debug.Stack())
}
}()
var (
subject = fmt.Sprintf("[SAGA] MR ( %s ) merge 通知", sourceBranch)
data = fmt.Sprintf("MR : %s \n ( %s -> %s ) 状态 %s", url, sourceBranch, targetBranch, comment)
wct = wechat.New(dao)
ctx = context.Background()
)
return wct.PushMsg(ctx, []string{authorName}, fmt.Sprintf("%s\n\n%s", subject, data))
}
// MailPipeline ...
func MailPipeline(event *model.HookPipeline) (err error) {
var (
author = event.User.UserName
branch = event.ObjectAttributes.Ref
commit = event.ObjectAttributes.Sha
title = ""
url = ""
status = "失败"
)
if event.Commit != nil {
commitIndex := strings.LastIndex(event.Commit.URL, "commit")
url = event.Commit.URL[:commitIndex] + "pipelines/" + strconv.FormatInt(event.ObjectAttributes.ID, 10)
}
if strings.Contains(event.Commit.Message, "\n") {
title = event.Commit.Message[:strings.Index(event.Commit.Message, "\n")]
} else {
title = event.Commit.Message
}
if event.ObjectAttributes.Status == model.PipelineSuccess {
status = "成功"
}
subject := fmt.Sprintf("[SAGA] Pipeline ( %s ) %s 通知", branch, status)
data := fmt.Sprintf("Pipeline : %s \nCommit : %s\nCommitID : %s\n状态: 运行%s !", url, title, commit, status)
addr := &model.MailAddress{
Address: author + "@bilibili.com",
Name: author,
}
if err = mail.SendMail2(addr, subject, data); err != nil {
log.Error("%+v", err)
}
return
}
// WechatPipeline ...
func WechatPipeline(dao *dao.Dao, event *model.HookPipeline) (err error) {
var (
wct = wechat.New(dao)
ctx = context.Background()
author = event.User.UserName
branch = event.ObjectAttributes.Ref
commit = event.ObjectAttributes.Sha
title = ""
url = ""
status = "失败"
)
if event.Commit != nil {
commitIndex := strings.LastIndex(event.Commit.URL, "commit")
url = event.Commit.URL[:commitIndex] + "pipelines/" + strconv.FormatInt(event.ObjectAttributes.ID, 10)
}
if strings.Contains(event.Commit.Message, "\n") {
title = event.Commit.Message[:strings.Index(event.Commit.Message, "\n")]
} else {
title = event.Commit.Message
}
if event.ObjectAttributes.Status == model.PipelineSuccess {
status = "成功"
}
subject := fmt.Sprintf("[SAGA] Pipeline ( %s ) %s 通知", branch, status)
data := fmt.Sprintf("Pipeline : %s \nCommit : %s\nCommitID : %s\n状态: 运行%s !", url, title, commit, status)
if err = wct.PushMsg(ctx, []string{author}, fmt.Sprintf("%s\n\n%s", subject, data)); err != nil {
log.Error("%+v", err)
}
return
}
// MailPlusOne ...
func MailPlusOne(author, reviewer, url, commit, sourceBranch, targetBranch string) (err error) {
var (
subject = fmt.Sprintf("[SAGA] MR ( %s ) review 通知", sourceBranch)
data = fmt.Sprintf("MR : %s \n ( %s -> %s ) commitID ( %s ) 已被 ( %s ) review!", url, sourceBranch, targetBranch, commit, reviewer)
addr = &model.MailAddress{
Address: author + "@bilibili.com",
Name: author,
}
)
if err = mail.SendMail2(addr, subject, data); err != nil {
log.Error("%+v", err)
}
return
}
// WechatPlusOne ...
func WechatPlusOne(dao *dao.Dao, author, reviewer, url, commit, sourceBranch, targetBranch string) (err error) {
defer func() {
if x := recover(); x != nil {
log.Error("wechatPlusOne: %+v %s", x, debug.Stack())
}
}()
var (
subject = fmt.Sprintf("[SAGA] MR ( %s ) review 通知", sourceBranch)
data = fmt.Sprintf("MR : %s \n ( %s -> %s ) commitID ( %s ) 已被 ( %s ) review!", url, sourceBranch, targetBranch, commit, reviewer)
wct = wechat.New(dao)
ctx = context.Background()
)
if err = wct.PushMsg(ctx, []string{author}, fmt.Sprintf("%s\n\n%s", subject, data)); err != nil {
log.Error("%+v", err)
}
return
}
// func notifyAssign(assign string, authorName string, url, sourceBranch, targetBranch string) (err error) {
// var (
// subject = fmt.Sprintf("[SAGA] MR ( %s ) assign 通知", sourceBranch)
// data = fmt.Sprintf("MR : %s \n ( %s -> %s ) 开发者 ( %s ) 已指派给您 !", url, sourceBranch, targetBranch, authorName)
// addr = &model.MailAddress{
// Address: assign + "@bilibili.com",
// Name: assign,
// }
// )
// if err = mail.SendMail2(addr, subject, data); err != nil {
// log.Error("%+v", err)
// }
// return
// }
// func wechatAssign(dao *dao.Dao, assign string, authorName string, url, sourceBranch, targetBranch string) (err error) {
// var (
// subject = fmt.Sprintf("[SAGA] MR ( %s ) assign 通知", sourceBranch)
// data = fmt.Sprintf("MR : %s \n ( %s -> %s ) 开发者 ( %s ) 已指派给您 !", url, sourceBranch, targetBranch, authorName)
// wct = wechat.New(dao)
// ctx = context.Background()
// )
// if err = wct.PushMsg(ctx, []string{assign}, fmt.Sprintf("%s\n\n%s", subject, data)); err != nil {
// log.Error("wechatAssign failed, err (%s)", err.Error())
// }
// return
// }
// func notifyReviewer(reviewers []string, authorName, url, sourceBranch, targetBranch string) (err error) {
// var (
// subject = fmt.Sprintf("[SAGA] MR ( %s ) review 请求", sourceBranch)
// data = fmt.Sprintf("MR : %s \n ( %s -> %s ) 开发者 ( %s ) 需要您的 review !\n请在 review 后留言 +1 !", url, sourceBranch, targetBranch, authorName)
// )
// for _, reviewer := range reviewers {
// addr := &model.MailAddress{
// Address: reviewer + "@bilibili.com",
// Name: reviewer,
// }
// if err = mail.SendMail2(addr, subject, data); err != nil {
// log.Error("%+v", err)
// }
// }
// return
// }
// func wechatReviewer(dao *dao.Dao, reviewers []string, authorName, url, sourceBranch, targetBranch string) (err error) {
// var (
// subject = fmt.Sprintf("[SAGA] MR ( %s ) review 请求", sourceBranch)
// data = fmt.Sprintf("MR : %s \n ( %s -> %s ) 开发者 ( %s ) 需要您的 review !\n请在 review 后留言 +1 !", url, sourceBranch, targetBranch, authorName)
// wct = wechat.New(dao)
// ctx = context.Background()
// )
// if err = wct.PushMsg(ctx, reviewers, fmt.Sprintf("%s\n\n%s", subject, data)); err != nil {
// log.Error("%+v", err)
// }
// return
// }

View File

@@ -0,0 +1,73 @@
package service
import (
"context"
"go-common/app/tool/saga/model"
"go-common/app/tool/saga/service/notification"
"go-common/library/log"
)
// PipelineChanged handle pipeline changed webhook
func (s *Service) PipelineChanged(c context.Context, event *model.HookPipeline) (err error) {
var (
ok bool
repo *model.Repo
state bool
lastPipeLineState bool
wip bool
)
//get associated pipeline mr and check it's wip status, if wip is true and return
if wip, err = s.checkMrStatus(c, event.Project.ID, event.ObjectAttributes.Ref); wip {
log.Info("Pipeline associated mr is wip, project ID: [%d], branch: [%s]", event.Project.ID, event.ObjectAttributes.Ref)
return
}
if ok, repo = s.findRepo(event.Project.GitSSHURL); !ok || !repo.Config.RelatePipeline {
log.Info("PipelineChanged return repo: %s, ok: %t", event.Project.GitSSHURL, ok)
return
}
if event.ObjectKind != model.HookPipelineType {
log.Info("Pipeline hook object kind [%s] ignore", event.ObjectKind)
return
}
if (event.ObjectAttributes.Status != model.PipelineFailed) && (event.ObjectAttributes.Status != model.PipelineSuccess) && (event.ObjectAttributes.Status != model.PipelineCanceled) {
log.Info("Pipeline status [%s] ignore for project [%s]", event.ObjectAttributes.Status, event.Project.Name)
return
}
go func() {
if err = s.cmd.HookPipeline(event.Project.ID, event.ObjectAttributes.Ref, int(event.ObjectAttributes.ID)); err != nil {
log.Error("CheckPipeline: %d %s %+v", event.Project.ID, event.ObjectAttributes.Ref, err)
}
}()
// 查询上次pipeline状态状态变化才发通知
if lastPipeLineState, err = s.gitlab.LastPipeLineState(event.Project.ID, event.ObjectAttributes.Ref); err != nil {
return
}
state = event.ObjectAttributes.Status == model.PipelineSuccess
log.Info("status:%t, lastPipeLineState: %t", state, lastPipeLineState)
if lastPipeLineState != state {
go notification.MailPipeline(event)
go notification.WechatPipeline(s.d, event)
}
return
}
func (s *Service) checkMrStatus(c context.Context, projectID int, branch string) (wip bool, err error) {
var (
ok bool
mergeInfo *model.MergeInfo
)
if ok, mergeInfo, err = s.d.MergeInfo(c, projectID, branch); err != nil || !ok {
return
}
if wip, _, _, err = s.gitlab.MergeStatus(projectID, mergeInfo.MRIID); err != nil {
return
}
return
}

View File

@@ -0,0 +1,146 @@
package service
import (
"context"
"fmt"
"strings"
"go-common/app/tool/saga/conf"
"go-common/app/tool/saga/dao"
"go-common/app/tool/saga/model"
"go-common/app/tool/saga/service/command"
"go-common/app/tool/saga/service/gitlab"
"go-common/library/log"
"github.com/pkg/errors"
"github.com/robfig/cron"
)
// Service biz service def.
type Service struct {
missch chan func()
d *dao.Dao
gitlab *gitlab.Gitlab
gitRepoMap map[string]*model.Repo // map[repoName]*repo
cmd *command.Command
cron *cron.Cron
}
// New a DirService and return.
func New() (s *Service) {
s = &Service{
d: dao.New(),
missch: make(chan func(), 10240),
}
// init gitlab client
s.gitlab = gitlab.New(conf.Conf.Property.Gitlab.API, conf.Conf.Property.Gitlab.Token)
s.cmd = command.New(s.d, s.gitlab)
s.cmd.Registers()
go s.cmd.ListenTask()
s.loadRepos(false)
go s.updateproc()
// start cron
s.cron = cron.New()
if err := s.cron.AddFunc(conf.Conf.Property.SyncContact.CheckCron, s.synccontactsproc); err != nil {
panic(err)
}
s.cron.Start()
//
return
}
func (s *Service) validTargetBranch(targetBranch string, gitRepo *model.Repo) bool {
for _, r := range gitRepo.Config.TargetBranchRegexes {
if r.MatchString(targetBranch) {
return true
}
}
/*for _, r := range gitRepo.Config.TargetBranches {
if r == targetBranch {
return true
}
}*/
return false
}
func (s *Service) loadRepos(reload bool) {
var (
repo *model.Repo
ok bool
)
// init code repo
if s.gitRepoMap == nil {
s.gitRepoMap = make(map[string]*model.Repo)
}
webHookRepos := make([]*model.Repo, 0)
authRepos := make([]*model.Repo, 0)
for _, r := range conf.Conf.Property.Repos {
if repo, ok = s.gitRepoMap[r.GName]; !ok {
repo = &model.Repo{
Config: r,
}
s.gitRepoMap[r.GName] = repo
webHookRepos = append(webHookRepos, repo)
authRepos = append(authRepos, repo)
} else {
if repo.AuthUpdate(r) {
authRepos = append(authRepos, repo)
}
if repo.WebHookUpdate(r) {
webHookRepos = append(webHookRepos, repo)
}
if repo.Update(r) {
s.gitRepoMap[r.GName] = repo
}
}
}
if reload {
s.BuildContributors(authRepos)
}
if err := s.gitlab.AuditProjects(webHookRepos, conf.Conf.Property.WebHooks); err != nil {
log.Error("loadRepos err (%+v)", err)
}
}
func (s *Service) findRepo(gitURL string) (ok bool, repo *model.Repo) {
for _, r := range conf.Conf.Property.Repos {
if strings.EqualFold(r.URL, gitURL) {
repo = &model.Repo{
Config: r,
}
ok = true
return
}
}
return
}
// Ping check dao health.
func (s *Service) Ping(c context.Context) (err error) {
return s.d.Ping(c)
}
// Wait wait all closed.
func (s *Service) Wait() {
}
// Close close all dao.
func (s *Service) Close() {
s.d.Close()
}
func (s *Service) updateproc() {
defer func() {
if x := recover(); x != nil {
log.Error("updateproc panic(%v)", errors.WithStack(fmt.Errorf("%v", x)))
go s.updateproc()
log.Info("updateproc recover")
}
}()
for range conf.ReloadEvents() {
log.Info("DirService reload")
s.loadRepos(true)
}
}

View File

@@ -0,0 +1,196 @@
package service
import (
"flag"
"testing"
"go-common/app/tool/saga/conf"
"go-common/app/tool/saga/model"
. "github.com/smartystreets/goconvey/convey"
)
var (
s *Service
repoTest = &model.RepoInfo{Group: "changhengyuan", Name: "test-saga", Branch: "master"}
GitlabHookCommentTest = []byte(`{
"object_kind":"note",
"event_type":"note",
"user":{
"name":"changhengyuan",
"username":"changhengyuan",
"avatar_url":"https://www.gravatar.com/avatar/d3218d34473c6fb4d18a770f14e59a89?s=80\u0026d=identicon"
},
"project_id":35,
"project":{
"id":35,
"name":"test-saga",
"description":"",
"web_url":"http://gitlab.bilibili.co/changhengyuan/test-saga",
"avatar_url":null,
"git_ssh_url":"git@gitlab.bilibili.co:changhengyuan/test-saga.git",
"git_http_url":"http://gitlab.bilibili.co/changhengyuan/test-saga.git",
"namespace":"changhengyuan",
"visibility_level":20,
"path_with_namespace":"changhengyuan/test-saga",
"default_branch":"master",
"ci_config_path":null,
"homepage":"http://gitlab.bilibili.co/changhengyuan/test-saga",
"url":"git@gitlab.bilibili.co:changhengyuan/test-saga.git",
"ssh_url":"git@gitlab.bilibili.co:changhengyuan/test-saga.git",
"http_url":"http://gitlab.bilibili.co/changhengyuan/test-saga.git"},
"object_attributes":{
"id":3040,
"note":"test",
"noteable_type":"MergeRequest",
"author_id":15,
"created_at":"2018-09-26 06:55:13 UTC",
"updated_at":"2018-09-26 06:55:13 UTC",
"project_id":35,
"attachment":null,
"line_code":null,
"commit_id":"",
"noteable_id":390,
"system":false,
"st_diff":null,
"updated_by_id":null,
"type":null,
"position":null,
"original_position":null,
"resolved_at":null,
"resolved_by_id":null,
"discussion_id":"450c34e4c0f9e925bdc6a24c2ae4920d7a394ebc",
"change_position":null,
"resolved_by_push":null,
"url":"http://gitlab.bilibili.co/changhengyuan/test-saga/merge_requests/52#note_3040"
},
"repository":{
"name":"test-saga",
"url":"git@gitlab.bilibili.co:changhengyuan/test-saga.git",
"description":"",
"homepage":"http://gitlab.bilibili.co/changhengyuan/test-saga"
},
"merge_request":{
"assignee_id":null,
"author_id":15,
"created_at":"2018-09-26 06:41:55 UTC",
"description":"",
"head_pipeline_id":4510,
"id":390,
"iid":52,
"last_edited_at":null,
"last_edited_by_id":null,
"merge_commit_sha":null,
"merge_error":null,
"merge_params":{
"force_remove_source_branch":"0"
},
"merge_status":"cannot_be_merged",
"merge_user_id":null,
"merge_when_pipeline_succeeds":false,
"milestone_id":null,
"source_branch":"test-branch",
"source_project_id":35,
"state":"opened",
"target_branch":"master",
"target_project_id":35,
"time_estimate":0,
"title":"Test branch",
"updated_at":"2018-09-26 06:54:33 UTC",
"updated_by_id":null,
"url":"http://gitlab.bilibili.co/changhengyuan/test-saga/merge_requests/52",
"source":{
"id":35,
"name":"test-saga",
"description":"",
"web_url":"http://gitlab.bilibili.co/changhengyuan/test-saga",
"avatar_url":null,
"git_ssh_url":"git@gitlab.bilibili.co:changhengyuan/test-saga.git",
"git_http_url":"http://gitlab.bilibili.co/changhengyuan/test-saga.git",
"namespace":"changhengyuan",
"visibility_level":20,
"path_with_namespace":"changhengyuan/test-saga",
"default_branch":"master",
"ci_config_path":null,
"homepage":"http://gitlab.bilibili.co/changhengyuan/test-saga",
"url":"git@gitlab.bilibili.co:changhengyuan/test-saga.git",
"ssh_url":"git@gitlab.bilibili.co:changhengyuan/test-saga.git",
"http_url":"http://gitlab.bilibili.co/changhengyuan/test-saga.git"
},
"target":{
"id":35,
"name":"test-saga",
"description":"",
"web_url":"http://gitlab.bilibili.co/changhengyuan/test-saga",
"avatar_url":null,
"git_ssh_url":"git@gitlab.bilibili.co:changhengyuan/test-saga.git",
"git_http_url":"http://gitlab.bilibili.co/changhengyuan/test-saga.git",
"namespace":"changhengyuan",
"visibility_level":20,
"path_with_namespace":"changhengyuan/test-saga",
"default_branch":"master",
"ci_config_path":null,
"homepage":"http://gitlab.bilibili.co/changhengyuan/test-saga",
"url":"git@gitlab.bilibili.co:changhengyuan/test-saga.git",
"ssh_url":"git@gitlab.bilibili.co:changhengyuan/test-saga.git",
"http_url":"http://gitlab.bilibili.co/changhengyuan/test-saga.git"
},
"last_commit":{
"id":"51e9c3ba2ceac496dbaf55f0db564ab6a15e20eb",
"message":"add CONTRIBUTORS.md\n",
"timestamp":"2018-09-17T18:02:13+08:00",
"url":"http://gitlab.bilibili.co/changhengyuan/test-saga/commit/51e9c3ba2ceac496dbaf55f0db564ab6a15e20eb",
"author":{
"name":"哔哩哔哩",
"email":"bilibili@bilibilideMac-mini.local"
}
},
"work_in_progress":false,
"total_time_spent":0,
"human_total_time_spent":null,
"human_time_estimate":null}}`)
)
func init() {
var err error
flag.Set("conf", "../cmd/saga-test.toml")
if err = conf.Init(); err != nil {
panic(err)
}
}
func TestContributor(t *testing.T) {
var (
err error
repo = &model.RepoInfo{
Group: "platform",
Name: "go-common",
Branch: "master",
}
)
Convey("TEST BuildContributor", t, func() {
s = New()
err = s.cmd.BuildContributor(repo)
So(err, ShouldBeNil)
})
}
func TestMergeRequest(t *testing.T) {
var (
err error
ok bool
repo *model.Repo
url = "git@gitlab.bilibili.co:platform/go-common.git"
projID = 23
mrIID = 130
sourceBranch = "ci/test-7"
targetBranch = "master"
)
Convey("TEST ParseContributor", t, func() {
s = New()
ok, repo = s.findRepo(url)
So(ok, ShouldBeTrue)
err = s.cmd.UpdateContributor(projID, mrIID, sourceBranch, targetBranch, repo.Config.AuthBranches)
So(err, ShouldBeNil)
})
}

View File

@@ -0,0 +1,77 @@
package service
import (
"context"
"fmt"
"go-common/app/tool/saga/conf"
"go-common/app/tool/saga/model"
"go-common/app/tool/saga/service/mail"
"go-common/app/tool/saga/service/wechat"
"go-common/library/log"
"github.com/pkg/errors"
)
// CollectWachatUsers send required wechat visible users stored in memcache by email
func (s *Service) CollectWachatUsers(c context.Context) (err error) {
var (
contactInfo *model.ContactInfo
userMap = make(map[string]model.RequireVisibleUser)
user string
)
if err = s.d.RequireVisibleUsers(c, &userMap); err != nil {
log.Error("get require visible user error(%v)", err)
return
}
for k, v := range userMap {
if contactInfo, err = s.d.QueryUserByID(k); err == nil {
if contactInfo.VisibleSaga {
continue
}
}
user += v.UserName + " , " + v.NickName + "\n"
}
content := fmt.Sprintf("\n\n邮箱前缀 昵称\n\n%s", user)
for _, addr := range conf.Conf.Property.ReportRequiredVisible.AlertAddrs {
if err = mail.SendMail2(addr, "需添加的企业微信名单", content); err != nil {
return
}
}
if err = s.d.DeleteRequireVisibleUsers(c); err != nil {
log.Error("Delete require visible user error(%v)", err)
return
}
return
}
// SyncContacts sync the wechat contacts
func (s *Service) SyncContacts(c context.Context) (err error) {
var (
w = wechat.New(s.d)
)
if err = w.SyncContacts(c); err != nil {
return
}
return
}
// synccontactsproc sync wechat contact procedure
func (s *Service) synccontactsproc() {
defer func() {
if x := recover(); x != nil {
log.Error("synccontactsproc panic(%v)", errors.WithStack(fmt.Errorf("%v", x)))
go s.synccontactsproc()
log.Info("synccontactsproc recover")
}
}()
var err error
if err = s.SyncContacts(context.TODO()); err != nil {
log.Error("s.SyncContacts err (%+v)", err)
}
}

View File

@@ -0,0 +1,53 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_library(
name = "go_default_library",
srcs = [
"contact.go",
"wechat.go",
],
importpath = "go-common/app/tool/saga/service/wechat",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//app/tool/saga/conf:go_default_library",
"//app/tool/saga/dao:go_default_library",
"//app/tool/saga/model:go_default_library",
"//library/log:go_default_library",
"//vendor/github.com/pkg/errors:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
go_test(
name = "go_default_test",
srcs = ["wechat_test.go"],
embed = [":go_default_library"],
rundir = ".",
tags = ["automanaged"],
deps = [
"//app/tool/saga/conf:go_default_library",
"//app/tool/saga/dao:go_default_library",
"//app/tool/saga/model:go_default_library",
"//vendor/github.com/smartystreets/goconvey/convey:go_default_library",
],
)

View File

@@ -0,0 +1,180 @@
package wechat
import (
"context"
"go-common/app/tool/saga/model"
"go-common/library/log"
)
// Changes changes structure
type Changes struct {
Adds []*model.ContactInfo
Upts []*model.ContactInfo
Dels []*model.ContactInfo
}
// SyncContacts sync the contacts from wechat work
func (w *Wechat) SyncContacts(c context.Context) (err error) {
if err = w.AnalysisContacts(c); err != nil {
return
}
/*if err = w.UpdateVisible(c); err != nil {
return
}*/
return
}
// AnalysisContacts analysis the contact difference and save them
func (w *Wechat) AnalysisContacts(c context.Context) (err error) {
var (
contactsInDB []*model.ContactInfo
wechatContacts []*model.ContactInfo
changes = &Changes{}
)
if contactsInDB, err = w.dao.ContactInfos(); err != nil {
return
}
if wechatContacts, err = w.QueryWechatContacts(c); err != nil {
return
}
if changes, err = w.diffChanges(wechatContacts, contactsInDB); err != nil {
return
}
if err = w.saveChanges(changes); err != nil {
return
}
return
}
// QueryWechatContacts query wechat contacts with access token
func (w *Wechat) QueryWechatContacts(c context.Context) (contacts []*model.ContactInfo, err error) {
var (
token string
)
if token, err = w.AccessToken(c, w.contact); err != nil {
return
}
if contacts, err = w.dao.WechatContacts(c, token); err != nil {
return
}
return
}
func (w *Wechat) saveChanges(changes *Changes) (err error) {
var (
contact *model.ContactInfo
)
log.Info("saveChanges add(%d), upt(%d), del(%d)", len(changes.Adds), len(changes.Upts), len(changes.Dels))
for _, contact = range changes.Adds {
if err = w.dao.CreateContact(contact); err != nil {
return
}
log.Info("saveChanges add: %v", contact)
}
for _, contact = range changes.Upts {
if err = w.dao.UptContact(contact); err != nil {
return
}
log.Info("saveChanges upt: %v", contact)
}
for _, contact = range changes.Dels {
if err = w.dao.DelContact(contact); err != nil {
return
}
log.Info("saveChanges del: %v", contact)
}
return
}
func (w *Wechat) diffChanges(wechatContacts, contactsInDB []*model.ContactInfo) (changes *Changes, err error) {
var (
contact *model.ContactInfo
wechatContactsMap = make(map[string]*model.ContactInfo)
contactsInDBMap = make(map[string]*model.ContactInfo)
wechatContactIDs []string
dbContactsIDs []string
userID string
)
changes = new(Changes)
for _, contact = range wechatContacts {
wechatContactsMap[contact.UserID] = contact
wechatContactIDs = append(wechatContactIDs, contact.UserID)
}
for _, contact = range contactsInDB {
contactsInDBMap[contact.UserID] = contact
dbContactsIDs = append(dbContactsIDs, contact.UserID)
}
// 分析变化
for _, userID = range wechatContactIDs {
contact = wechatContactsMap[userID]
if w.inSlice(dbContactsIDs, userID) { // 企业微信联系人ID在数据库中能找到
if !contact.AlmostEqual(contactsInDBMap[userID]) { // 但是域不同
contact.ID = contactsInDBMap[userID].ID
changes.Upts = append(changes.Upts, contact)
}
} else {
changes.Adds = append(changes.Adds, contact) // 这个联系人是新增的
}
}
for _, userID = range dbContactsIDs {
if !w.inSlice(wechatContactIDs, userID) {
changes.Dels = append(changes.Dels, contactsInDBMap[userID])
}
}
return
}
func (w *Wechat) inSlice(slice []string, target string) bool {
for _, v := range slice {
if v == target {
return true
}
}
return false
}
// UpdateVisible update the visible property
func (w *Wechat) UpdateVisible(c context.Context) (err error) {
var (
user *model.UserInfo
users []*model.UserInfo
contact *model.ContactInfo
)
if users, err = w.querySagaVisible(c); err != nil {
return
}
for _, user = range users {
contact = &model.ContactInfo{UserID: user.UserID, VisibleSaga: true}
if err = w.dao.UptContact(contact); err != nil {
return
}
}
return
}
func (w *Wechat) querySagaVisible(c context.Context) (users []*model.UserInfo, err error) {
var (
token string
)
if token, err = w.AccessToken(c, w.saga); err != nil {
return
}
if users, err = w.dao.WechatSagaVisible(c, token, w.saga.AppID); err != nil {
return
}
return
}

View File

@@ -0,0 +1,165 @@
package wechat
import (
"context"
"fmt"
"strings"
"go-common/app/tool/saga/conf"
"go-common/app/tool/saga/dao"
"go-common/app/tool/saga/model"
"go-common/library/log"
"github.com/pkg/errors"
)
// Wechat 企业微信应用
type Wechat struct {
dao *dao.Dao
saga *model.AppConfig
contact *model.AppConfig
}
// New create an new wechat work
func New(d *dao.Dao) (w *Wechat) {
w = &Wechat{
dao: d,
saga: conf.Conf.Property.Wechat,
contact: conf.Conf.Property.Contact,
}
return w
}
// NewTxtNotify create wechat format text notification
func (w *Wechat) NewTxtNotify(content string) (txtMsg *model.TxtNotification) {
return &model.TxtNotification{
Notification: model.Notification{
MsgType: "text",
AgentID: w.saga.AppID,
},
Body: model.Text{
Content: content,
},
Safe: 0,
}
}
// AccessToken get access_token from cache first, if not found, get it via wechat api.
func (w *Wechat) AccessToken(c context.Context, app *model.AppConfig) (token string, err error) {
var (
key string
expire int32
)
key = fmt.Sprintf("appid_%d", app.AppID)
if token, err = w.dao.AccessToken(c, key); err != nil {
log.Warn("AccessToken: failed to get access_token from cache, appId (%d), error (%s)", app.AppID, err.Error())
if token, expire, err = w.dao.WechatAccessToken(c, app.AppSecret); err != nil {
err = errors.Wrapf(err, "AccessToken: both mc and api can't provide access_token, appId(%d)", app.AppID)
return
}
// 通过API获取到了缓存一波
err = w.dao.SetAccessToken(c, key, token, expire)
return
}
if token == "" {
if token, expire, err = w.dao.WechatAccessToken(c, app.AppSecret); err != nil {
return
}
// 通过API获取到了缓存一波
err = w.dao.SetAccessToken(c, key, token, expire)
}
return
}
// PushMsg push text message via wechat notification api with access_token.
func (w *Wechat) PushMsg(c context.Context, userNames []string, content string) (err error) {
var (
token string
userIds string
invalidUser string
txtMsg = w.NewTxtNotify(content)
)
if token, err = w.AccessToken(c, w.saga); err != nil {
return
}
if token == "" {
err = errors.Errorf("PushMsg: get access token failed, it's empty. appid (%d), secret (%s)", w.saga.AppID, w.saga.AppSecret)
return
}
if userIds, err = w.UserIds(userNames); err != nil {
return
}
txtMsg.ToUser = userIds
if invalidUser, err = w.dao.WechatPushMsg(c, token, txtMsg); err != nil {
if err = w.addRequireVisible(c, invalidUser); err != nil {
log.Error("PushMsg add userID (%s) in cache, error(%s)", invalidUser, err.Error())
}
return
}
return
}
// UserIds query user ids for user name list
func (w *Wechat) UserIds(userNames []string) (ids string, err error) {
ids, err = w.dao.UserIds(userNames)
return
}
// addRequireVisible update wechat require visible users in memcache
func (w *Wechat) addRequireVisible(c context.Context, userIDs string) (err error) {
var (
contactInfo *model.ContactInfo
userID string
alreadyIn bool
)
users := strings.Split(userIDs, "|")
for _, userID = range users {
if alreadyIn, err = w.alreadyInCache(c, userID); err != nil || alreadyIn {
continue
}
if contactInfo, err = w.dao.QueryUserByID(userID); err != nil {
log.Error("no such userID (%s) in db, error(%s)", userID, err.Error())
return
}
if err = w.dao.SetRequireVisibleUsers(c, contactInfo); err != nil {
log.Error("failed set to cache userID (%s) username (%s), err (%s)", userID, contactInfo.UserName, err.Error())
return
}
}
return
}
// alreadyInCache check user is or not in the memcache
func (w *Wechat) alreadyInCache(c context.Context, userID string) (alreadyIn bool, err error) {
var (
userMap = make(map[string]model.RequireVisibleUser)
)
if err = w.dao.RequireVisibleUsers(c, &userMap); err != nil {
log.Error("get userID (%s) from cache error(%s)", userID, err.Error())
return
}
for k, v := range userMap {
if userID == k {
log.Info("(%s) is already exist in cache, value(%v)", k, v)
alreadyIn = true
return
}
}
return
}

View File

@@ -0,0 +1,126 @@
package wechat
import (
"context"
"flag"
"os"
"testing"
"go-common/app/tool/saga/conf"
"go-common/app/tool/saga/dao"
"go-common/app/tool/saga/model"
. "github.com/smartystreets/goconvey/convey"
)
var (
mydao *dao.Dao
wechat *Wechat
ctx = context.Background()
)
func TestMain(m *testing.M) {
var err error
flag.Set("conf", "../../cmd/saga-test.toml")
if err = conf.Init(); err != nil {
panic(err)
}
mydao = dao.New()
defer mydao.Close()
wechat = New(mydao)
os.Exit(m.Run())
}
func TestAddRequireVisible(t *testing.T) {
var (
err error
userMap = make(map[string]model.RequireVisibleUser)
)
Convey("TEST addRequireVisible", t, func() {
err = wechat.addRequireVisible(ctx, "000000")
So(err, ShouldNotBeNil)
err = wechat.addRequireVisible(ctx, "001134")
So(err, ShouldBeNil)
err = mydao.RequireVisibleUsers(ctx, &userMap)
So(err, ShouldBeNil)
So(userMap, ShouldContainKey, "001134")
})
}
func TestAlreadyInCache(t *testing.T) {
var (
err error
result bool
contactInfo model.ContactInfo
)
contactInfo = model.ContactInfo{
ID: "111",
UserName: "zhangsan",
UserID: "222",
NickName: "xiaolizi",
VisibleSaga: true,
}
Convey("TEST alreadyInCache", t, func() {
result, err = wechat.alreadyInCache(ctx, "000")
So(err, ShouldBeNil)
So(result, ShouldEqual, false)
So(mydao.SetRequireVisibleUsers(ctx, &contactInfo), ShouldBeNil)
result, err = wechat.alreadyInCache(ctx, "222")
So(err, ShouldBeNil)
So(result, ShouldEqual, true)
})
}
func TestSyncContacts(t *testing.T) {
var (
err error
contactInfo = &model.ContactInfo{
//UserID: "004273",
UserID: "E10021",
UserName: "eyotang",
NickName: "ben大神点C",
}
modify = &model.ContactInfo{
UserID: "000328",
UserName: "eyotang",
NickName: "ben大神点C",
VisibleSaga: false,
}
target *model.ContactInfo
almostEqual bool
)
Convey("TEST sync after add incorrect", t, func() {
err = wechat.dao.CreateContact(contactInfo)
So(err, ShouldBeNil)
target, err = wechat.dao.QueryUserByID(contactInfo.UserID)
So(err, ShouldBeNil)
almostEqual = contactInfo.AlmostEqual(target)
So(almostEqual, ShouldBeTrue)
err = wechat.SyncContacts(ctx)
So(err, ShouldBeNil)
target, err = wechat.dao.QueryUserByID(contactInfo.UserID)
So(err, ShouldNotBeNil)
})
Convey("TEST aync after change", t, func() {
contactInfo, err = wechat.dao.QueryUserByID(modify.UserID)
So(err, ShouldBeNil)
modify.ID = contactInfo.ID
err = wechat.dao.UptContact(contactInfo)
So(err, ShouldBeNil)
err = wechat.SyncContacts(ctx)
So(err, ShouldBeNil)
target, err = wechat.dao.QueryUserByID(modify.UserID)
So(err, ShouldBeNil)
//So(target.VisibleSaga, ShouldBeTrue)
So(target.UserName, ShouldNotEqual, "eyotang")
})
}