mirror of
https://github.com/iLoveElysia/openbilibili.git
synced 2026-09-02 23:58:04 -05:00
471 lines
14 KiB
Go
471 lines
14 KiB
Go
|
|
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
|
|||
|
|
}
|