591 lines
16 KiB
Go
591 lines
16 KiB
Go
package service
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"regexp"
|
||
"strings"
|
||
"time"
|
||
|
||
"go-common/app/admin/ep/saga/model"
|
||
"go-common/app/admin/ep/saga/service/utils"
|
||
"go-common/library/log"
|
||
|
||
"github.com/xanzy/go-gitlab"
|
||
)
|
||
|
||
const _specialMrID = 10199
|
||
|
||
// QueryProjectMr query project commit info according to project id.
|
||
func (s *Service) QueryProjectMr(c context.Context, req *model.ProjectDataReq) (resp *model.ProjectDataResp, err error) {
|
||
|
||
if resp, err = s.QueryProject(c, model.ObjectMR, req); err != nil {
|
||
return
|
||
}
|
||
return
|
||
}
|
||
|
||
// QueryTeamMr query team commit info according to department and business
|
||
func (s *Service) QueryTeamMr(c context.Context, req *model.TeamDataRequest) (resp *model.TeamDataResp, err error) {
|
||
|
||
if resp, err = s.QueryTeam(c, model.ObjectMR, req); err != nil {
|
||
return
|
||
}
|
||
return
|
||
}
|
||
|
||
// QueryProjectMrReport query mr review
|
||
func (s *Service) QueryProjectMrReport(c context.Context, req *model.ProjectMrReportReq) (resp *model.ProjectMrReportResp, err error) {
|
||
var (
|
||
info []*model.MrInfo
|
||
changeAdd int
|
||
changeDel int
|
||
mrCount int
|
||
stateCount int
|
||
discussionCount int
|
||
discussionResolved int
|
||
mrTime int
|
||
spentTime time.Duration
|
||
avarageTime time.Duration
|
||
reviewers []string
|
||
reviews []string
|
||
reviewChangeAdd int
|
||
reviewChangeDel int
|
||
reviewTime int
|
||
reviewTotalTime time.Duration
|
||
)
|
||
if info, err = s.QueryAllMergeRequestInfo(c, req.ProjectID); err != nil {
|
||
return
|
||
}
|
||
|
||
for _, i := range info {
|
||
for _, r := range i.Reviewers {
|
||
if r.Name == req.Member {
|
||
reviews = append(reviews, i.Author)
|
||
reviewChangeAdd += i.ChangeAdd
|
||
reviewChangeDel += i.ChangeDel
|
||
reviewTime += i.SpentTime
|
||
}
|
||
}
|
||
if i.Author == req.Member {
|
||
mrCount++
|
||
if i.State == model.StatusMerged {
|
||
stateCount++
|
||
mrTime += i.SpentTime
|
||
changeAdd += i.ChangeAdd
|
||
changeDel += i.ChangeDel
|
||
discussionCount += i.TotalDiscussion
|
||
discussionResolved += i.SolvedDiscussion
|
||
for _, r := range i.Reviewers {
|
||
reviewers = append(reviewers, r.Name)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
// 判断mr为零的情况
|
||
if mrCount == 0 {
|
||
resp = &model.ProjectMrReportResp{}
|
||
return
|
||
}
|
||
|
||
if spentTime, err = time.ParseDuration(fmt.Sprintf("%ds", mrTime)); err != nil {
|
||
return
|
||
}
|
||
if avarageTime, err = time.ParseDuration(fmt.Sprintf("%ds", mrTime/mrCount)); err != nil {
|
||
return
|
||
}
|
||
if reviewTotalTime, err = time.ParseDuration(fmt.Sprintf("%ds", reviewTime)); err != nil {
|
||
return
|
||
}
|
||
|
||
resp = &model.ProjectMrReportResp{
|
||
ChangeAdd: changeAdd,
|
||
ChangeDel: changeDel,
|
||
MrCount: mrCount,
|
||
SpentTime: spentTime.String(),
|
||
StateCount: stateCount,
|
||
AverageMerge: avarageTime.String(),
|
||
Reviewers: reviewers,
|
||
Discussion: discussionCount,
|
||
Resolve: discussionResolved,
|
||
ReviewerOther: reviews,
|
||
ReviewChangeAdd: reviewChangeAdd,
|
||
ReviewChangeDel: reviewChangeDel,
|
||
ReviewTotalTime: reviewTotalTime.String(),
|
||
}
|
||
|
||
return
|
||
}
|
||
|
||
// QueryAllMergeRequestInfo ...
|
||
func (s *Service) QueryAllMergeRequestInfo(c context.Context, projID int) (info []*model.MrInfo, err error) {
|
||
var (
|
||
until = time.Now()
|
||
since = until.AddDate(0, -1, 0)
|
||
mrs []*gitlab.MergeRequest
|
||
resp *gitlab.Response
|
||
)
|
||
|
||
for page := 1; ; page++ {
|
||
if mrs, resp, err = s.gitlab.ListProjectMergeRequests(projID, &since, &until, page); err != nil {
|
||
return
|
||
}
|
||
for _, m := range mrs {
|
||
|
||
mr := &model.MrInfo{ProjectID: projID, MrID: m.IID, Author: m.Author.Name, State: m.State}
|
||
|
||
if m.State == model.StatusMerged {
|
||
spent := m.UpdatedAt.Sub(*m.CreatedAt)
|
||
mr.SpentTime = int(spent.Seconds())
|
||
}
|
||
|
||
if mr.ChangeAdd, mr.ChangeDel, err = s.QueryMergeRequestDiff(c, projID, m.IID); err != nil {
|
||
return
|
||
}
|
||
|
||
if mr.Reviewers, err = s.QueryMergeRequestReview(c, projID, m.IID); err != nil {
|
||
return
|
||
}
|
||
|
||
if mr.TotalDiscussion, mr.SolvedDiscussion, err = s.QueryMergeRequestDiscussion(c, projID, m.IID); err != nil {
|
||
return
|
||
}
|
||
|
||
info = append(info, mr)
|
||
}
|
||
if resp.NextPage == 0 {
|
||
break
|
||
}
|
||
}
|
||
|
||
return
|
||
}
|
||
|
||
// QueryMergeRequestReview 查询mr reviewer信息
|
||
func (s *Service) QueryMergeRequestReview(c context.Context, projectID, mrIID int) (reviewers []*model.MrReviewer, err error) {
|
||
var (
|
||
notes []*model.StatisticsNotes
|
||
emojis []*model.StatisticsMRAwardEmojis
|
||
owners []string
|
||
r *regexp.Regexp
|
||
)
|
||
|
||
//query note
|
||
if notes, err = s.dao.NoteByMRIID(c, projectID, mrIID); err != nil {
|
||
return
|
||
}
|
||
|
||
// query emoji
|
||
if emojis, err = s.dao.AwardEmojiByMRIID(c, projectID, mrIID); err != nil {
|
||
return
|
||
}
|
||
|
||
//评论中解析获取owner
|
||
if len(notes) == 0 {
|
||
return
|
||
}
|
||
if r, err = regexp.Compile("OWNER:.*?@(.*)(;|)"); err == nil {
|
||
matchResult := r.FindStringSubmatch(notes[len(notes)-1].Body)
|
||
if len(matchResult) > 0 {
|
||
owners = strings.Split(matchResult[1], " 或 @")
|
||
}
|
||
}
|
||
|
||
mrCreatedAt := notes[len(notes)-1].CreatedAt
|
||
|
||
// 从评论和表情中解析 reviewers
|
||
for _, note := range notes {
|
||
if note.Body == "+1" {
|
||
reviewers = append(reviewers, &model.MrReviewer{Name: note.AuthorName, FinishedAt: note.CreatedAt, SpentTime: int(note.CreatedAt.Sub(*mrCreatedAt))})
|
||
}
|
||
}
|
||
for _, emoji := range emojis {
|
||
if emoji.Name == "thumbsup" {
|
||
reviewers = append(reviewers, &model.MrReviewer{Name: emoji.UserName, FinishedAt: emoji.CreatedAt, SpentTime: int(emoji.CreatedAt.Sub(*mrCreatedAt))})
|
||
}
|
||
}
|
||
|
||
// 判断reviewer类型
|
||
for _, r := range reviewers {
|
||
for _, owner := range owners {
|
||
if owner == r.Name {
|
||
r.UserType = "owner"
|
||
break
|
||
}
|
||
}
|
||
if r.UserType == "" {
|
||
r.UserType = "other"
|
||
}
|
||
}
|
||
|
||
return
|
||
}
|
||
|
||
// QueryMergeRequestDiscussion 查询获取mr的discussion
|
||
func (s *Service) QueryMergeRequestDiscussion(c context.Context, projectID, mrIID int) (total, solved int, err error) {
|
||
var discussions []*model.StatisticsDiscussions
|
||
if discussions, err = s.dao.DiscussionsByMRIID(c, projectID, mrIID); err != nil {
|
||
return
|
||
}
|
||
for _, d := range discussions {
|
||
var notesArray []int
|
||
if err = json.Unmarshal([]byte(d.Notes), ¬esArray); err != nil {
|
||
return
|
||
}
|
||
for _, n := range notesArray {
|
||
var note *model.StatisticsNotes
|
||
if note, err = s.dao.NoteByID(c, projectID, mrIID, n); err != nil {
|
||
return
|
||
}
|
||
if note.Resolvable {
|
||
total++
|
||
if note.Resolved {
|
||
solved++
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return
|
||
}
|
||
|
||
// QueryMergeRequestDiff 查询获取到mr修改文件的总行数, 参数 p project_ID m MR_ID
|
||
func (s *Service) QueryMergeRequestDiff(c context.Context, p, m int) (ChangeAdd, ChangeDel int, err error) {
|
||
var mr *gitlab.MergeRequest
|
||
|
||
// 此MR数据量太大,曾导致过gitlab服务器崩溃,访问会返回502服务器错误,因此特殊处理。
|
||
if m == _specialMrID {
|
||
return 0, 0, nil
|
||
}
|
||
|
||
if mr, _, err = s.gitlab.GetMergeRequestDiff(p, m); err != nil {
|
||
return
|
||
}
|
||
for _, change := range mr.Changes {
|
||
rows := strings.Split(change.Diff, "\n")
|
||
if len(rows) < 3 {
|
||
// 处理diff为空
|
||
continue
|
||
}
|
||
|
||
for _, row := range rows[3:] {
|
||
if strings.HasPrefix(row, "+") {
|
||
ChangeAdd++
|
||
} else if strings.HasPrefix(row, "-") {
|
||
ChangeDel++
|
||
}
|
||
}
|
||
}
|
||
|
||
return
|
||
}
|
||
|
||
/*-------------------------------------- sync MR ----------------------------------------*/
|
||
|
||
// SyncProjectMR ...
|
||
func (s *Service) SyncProjectMR(c context.Context, projectID int) (result *model.SyncResult, err error) {
|
||
var (
|
||
//syncAllTime = conf.Conf.Property.SyncData.SyncAllTime
|
||
syncAllTime = false
|
||
mrs []*gitlab.MergeRequest
|
||
resp *gitlab.Response
|
||
since *time.Time
|
||
until *time.Time
|
||
projectInfo *model.ProjectInfo
|
||
)
|
||
result = &model.SyncResult{}
|
||
|
||
if projectInfo, err = s.dao.ProjectInfoByID(projectID); err != nil {
|
||
return
|
||
}
|
||
|
||
if !syncAllTime {
|
||
since, until = utils.CalSyncTime()
|
||
}
|
||
log.Info("sync project(%d) MR time since: %v, until: %v", projectID, since, until)
|
||
for page := 1; ; page++ {
|
||
result.TotalPage++
|
||
if mrs, resp, err = s.gitlab.ListProjectMergeRequests(projectID, since, until, page); err != nil {
|
||
return
|
||
}
|
||
|
||
for _, mr := range mrs {
|
||
if err = s.structureDatabaseMR(c, projectID, projectInfo.Name, mr); err != nil {
|
||
log.Error("mr Save Database err: projectID(%d), MRIID(%d)", projectID, mr.IID)
|
||
err = nil
|
||
|
||
errData := &model.FailData{
|
||
ChildID: mr.IID,
|
||
}
|
||
result.FailData = append(result.FailData, errData)
|
||
continue
|
||
}
|
||
result.TotalNum++
|
||
}
|
||
|
||
if resp.NextPage == 0 {
|
||
break
|
||
}
|
||
}
|
||
return
|
||
}
|
||
|
||
// structureDatabaseMR ...
|
||
func (s *Service) structureDatabaseMR(c context.Context, projectID int, projectName string, mr *gitlab.MergeRequest) (err error) {
|
||
var (
|
||
milestoneID int
|
||
mrLables string
|
||
mrChanges string
|
||
mrLablesByte []byte
|
||
mrChangesByte []byte
|
||
)
|
||
|
||
if mr.Milestone != nil {
|
||
milestoneID = mr.Milestone.ID
|
||
}
|
||
if mrLablesByte, err = json.Marshal(mr.Labels); err != nil {
|
||
mrLables = model.JsonMarshalErrorText
|
||
} else {
|
||
mrLables = string(mrLablesByte)
|
||
}
|
||
|
||
if mrChangesByte, err = json.Marshal(mr.Changes); err != nil {
|
||
mrChanges = model.JsonMarshalErrorText
|
||
} else {
|
||
mrChanges = string(mrChangesByte)
|
||
}
|
||
|
||
mrDB := &model.StatisticsMrs{
|
||
MRID: mr.ID,
|
||
MRIID: mr.IID,
|
||
TargetBranch: mr.TargetBranch,
|
||
SourceBranch: mr.SourceBranch,
|
||
ProjectID: mr.ProjectID,
|
||
ProjectName: projectName,
|
||
Title: mr.Title,
|
||
State: mr.State,
|
||
CreatedAt: mr.CreatedAt,
|
||
UpdatedAt: mr.UpdatedAt,
|
||
Upvotes: mr.Upvotes,
|
||
Downvotes: mr.Downvotes,
|
||
AuthorID: mr.Author.ID,
|
||
AuthorName: mr.Author.Name,
|
||
AssigneeID: mr.Assignee.ID,
|
||
AssigneeName: mr.Assignee.Name,
|
||
SourceProjectID: mr.SourceProjectID,
|
||
TargetProjectID: mr.TargetProjectID,
|
||
Labels: mrLables,
|
||
Description: mr.Description,
|
||
WorkInProgress: mr.WorkInProgress,
|
||
MilestoneID: milestoneID,
|
||
MergeWhenPipelineSucceeds: mr.MergeWhenPipelineSucceeds,
|
||
MergeStatus: mr.MergeStatus,
|
||
MergedByID: mr.MergedBy.ID,
|
||
MergedByName: mr.MergedBy.Name,
|
||
MergedAt: mr.MergedAt,
|
||
ClosedByID: mr.ClosedBy.ID,
|
||
ClosedAt: mr.ClosedAt,
|
||
Subscribed: mr.Subscribed,
|
||
SHA: mr.SHA,
|
||
MergeCommitSHA: mr.MergeCommitSHA,
|
||
UserNotesCount: mr.UserNotesCount,
|
||
ChangesCount: mr.ChangesCount,
|
||
ShouldRemoveSourceBranch: mr.ShouldRemoveSourceBranch,
|
||
ForceRemoveSourceBranch: mr.ForceRemoveSourceBranch,
|
||
WebURL: mr.WebURL,
|
||
DiscussionLocked: mr.DiscussionLocked,
|
||
Changes: mrChanges,
|
||
TimeStatsHumanTimeEstimate: mr.TimeStats.HumanTimeEstimate,
|
||
TimeStatsHumanTotalTimeSpent: mr.TimeStats.HumanTotalTimeSpent,
|
||
TimeStatsTimeEstimate: mr.TimeStats.TimeEstimate,
|
||
TimeStatsTotalTimeSpent: mr.TimeStats.TotalTimeSpent,
|
||
Squash: mr.Squash,
|
||
PipelineID: mr.Pipeline.ID,
|
||
}
|
||
if len(mrDB.Labels) > model.MessageMaxLen {
|
||
mrDB.Labels = mrDB.Labels[0 : model.MessageMaxLen-1]
|
||
}
|
||
if len(mrDB.Description) > model.MessageMaxLen {
|
||
mrDB.Description = mrDB.Description[0 : model.MessageMaxLen-1]
|
||
}
|
||
return s.SaveDatabaseMR(c, mrDB)
|
||
}
|
||
|
||
// SaveDatabaseMR ...
|
||
func (s *Service) SaveDatabaseMR(c context.Context, mrDB *model.StatisticsMrs) (err error) {
|
||
var total int
|
||
|
||
if total, err = s.dao.HasMR(c, mrDB.ProjectID, mrDB.MRIID); err != nil {
|
||
log.Error("SaveDatabaseMR HasMR(%+v)", err)
|
||
return
|
||
}
|
||
|
||
// found only one, so update
|
||
if total == 1 {
|
||
return s.dao.UpdateMR(c, mrDB.ProjectID, mrDB.MRIID, mrDB)
|
||
} else if total > 1 {
|
||
// found repeated row, this situation will not exist under normal
|
||
log.Warn("SaveDatabaseMR mr has more rows(%d)", total)
|
||
return
|
||
}
|
||
|
||
// insert row now
|
||
return s.dao.CreateMR(c, mrDB)
|
||
}
|
||
|
||
/*-------------------------------------- agg MR ----------------------------------------*/
|
||
|
||
// AggregateProjectMR ...
|
||
func (s *Service) AggregateProjectMR(c context.Context, projectID int) (err error) {
|
||
var (
|
||
//syncAllTime = conf.Conf.Property.SyncData.SyncAllTime
|
||
syncAllTime = false
|
||
mrs []*model.StatisticsMrs
|
||
projectInfo *model.ProjectInfo
|
||
since *time.Time
|
||
until *time.Time
|
||
)
|
||
if projectInfo, err = s.dao.ProjectInfoByID(projectID); err != nil {
|
||
return
|
||
}
|
||
|
||
if !syncAllTime {
|
||
since, until = utils.CalSyncTime()
|
||
}
|
||
|
||
if mrs, err = s.dao.MRByProjectID(c, projectID, since, until); err != nil {
|
||
return
|
||
}
|
||
|
||
for _, mr := range mrs {
|
||
if err = s.MRAddedInfoDB(c, projectID, mr); err != nil {
|
||
log.Error("MRAddedInfoDB Save Database err: projectID(%d), MRIID(%d)", projectID, mr.MRIID)
|
||
err = nil
|
||
}
|
||
if err = s.MRReviewerDB(c, projectID, mr.MRIID, projectInfo.Name, mr); err != nil {
|
||
log.Error("MRReviewerDB Save Database err: projectID(%d), MRIID(%d)", projectID, mr.MRIID)
|
||
err = nil
|
||
}
|
||
}
|
||
return
|
||
}
|
||
|
||
// MRAddedInfoDB ...
|
||
func (s *Service) MRAddedInfoDB(c context.Context, projectID int, mr *model.StatisticsMrs) (err error) {
|
||
if mr.ChangeAdd, mr.ChangeDel, err = s.QueryMergeRequestDiff(c, projectID, mr.MRIID); err != nil {
|
||
return
|
||
}
|
||
if mr.TotalDiscussion, mr.SolvedDiscussion, err = s.QueryMergeRequestDiscussion(c, projectID, mr.MRIID); err != nil {
|
||
return
|
||
}
|
||
return s.dao.UpdateMR(c, projectID, mr.MRIID, mr)
|
||
}
|
||
|
||
// MRReviewerDB 查询mr reviewer信息
|
||
func (s *Service) MRReviewerDB(c context.Context, projectID, mrIID int, projectName string, mr *model.StatisticsMrs) (err error) {
|
||
var (
|
||
notes []*model.StatisticsNotes
|
||
emojis []*model.StatisticsMRAwardEmojis
|
||
owners []string
|
||
r *regexp.Regexp
|
||
reviewers []*model.AggregateMrReviewer
|
||
)
|
||
|
||
//query note
|
||
if notes, err = s.dao.NoteByMRIID(c, projectID, mrIID); err != nil {
|
||
return
|
||
}
|
||
|
||
// query emoji
|
||
if emojis, err = s.dao.AwardEmojiByMRIID(c, projectID, mrIID); err != nil {
|
||
return
|
||
}
|
||
|
||
//评论中解析获取owner
|
||
if len(notes) == 0 {
|
||
return
|
||
}
|
||
if r, err = regexp.Compile("OWNER:.*?@(.*)(;|)"); err == nil {
|
||
matchResult := r.FindStringSubmatch(notes[len(notes)-1].Body)
|
||
if len(matchResult) > 0 {
|
||
owners = strings.Split(matchResult[1], " 或 @")
|
||
}
|
||
}
|
||
|
||
mrCreatedAt := notes[len(notes)-1].CreatedAt
|
||
|
||
// 从评论和表情中解析 reviewers
|
||
for _, note := range notes {
|
||
if note.Body == "+1" {
|
||
reviewer := &model.AggregateMrReviewer{
|
||
ReviewerID: note.AuthorID,
|
||
ReviewerName: note.AuthorName,
|
||
ReviewType: "note",
|
||
ReviewID: note.NoteID,
|
||
ReviewCommand: "+1",
|
||
CreatedAt: note.CreatedAt,
|
||
ApproveTime: int(note.CreatedAt.Sub(*mrCreatedAt).Seconds()),
|
||
MergeTime: int(mr.UpdatedAt.Sub(*note.CreatedAt).Seconds()),
|
||
}
|
||
reviewers = append(reviewers, reviewer)
|
||
}
|
||
}
|
||
for _, emoji := range emojis {
|
||
if emoji.Name == "thumbsup" {
|
||
reviewer := &model.AggregateMrReviewer{
|
||
ReviewerID: emoji.UserID,
|
||
ReviewerName: emoji.UserName,
|
||
ReviewType: "emoji",
|
||
ReviewID: emoji.AwardEmojiID,
|
||
ReviewCommand: "thumbsup",
|
||
CreatedAt: emoji.CreatedAt,
|
||
ApproveTime: int(emoji.CreatedAt.Sub(*mrCreatedAt).Seconds()),
|
||
MergeTime: int(mr.UpdatedAt.Sub(*emoji.CreatedAt).Seconds()),
|
||
}
|
||
reviewers = append(reviewers, reviewer)
|
||
}
|
||
}
|
||
|
||
// 判断reviewer类型
|
||
for _, r := range reviewers {
|
||
r.ProjectID = projectID
|
||
r.ProjectName = projectName
|
||
r.MrIID = mrIID
|
||
r.Title = mr.Title
|
||
r.WebUrl = mr.WebURL
|
||
r.AuthorName = mr.AuthorName
|
||
if utils.InSlice(r.ReviewerName, owners) {
|
||
r.UserType = "owner"
|
||
} else {
|
||
r.UserType = "other"
|
||
}
|
||
if err = s.SaveDatabaseAggMR(c, r); err != nil {
|
||
log.Error("mrReviewer 存数据库报错(%+V)", err)
|
||
continue
|
||
}
|
||
}
|
||
return
|
||
}
|
||
|
||
// SaveDatabaseAggMR ...
|
||
func (s *Service) SaveDatabaseAggMR(c context.Context, mrDB *model.AggregateMrReviewer) (err error) {
|
||
var total int
|
||
|
||
if total, err = s.dao.HasAggregateReviewer(c, mrDB.ProjectID, mrDB.MrIID, mrDB.ReviewerID, mrDB.ReviewID); err != nil {
|
||
log.Error("SaveDatabaseAggMR HasAggMR(%+v)", err)
|
||
return
|
||
}
|
||
|
||
// found only one, so update
|
||
if total == 1 {
|
||
return s.dao.UpdateAggregateReviewer(c, mrDB.ProjectID, mrDB.MrIID, mrDB.ReviewerID, mrDB.ReviewID, mrDB)
|
||
} else if total > 1 {
|
||
// found repeated row, this situation will not exist under normal
|
||
log.Warn("SaveDatabaseAggMR aggMR has more rows(%d)", total)
|
||
return
|
||
}
|
||
|
||
// insert row now
|
||
return s.dao.CreateAggregateReviewer(c, mrDB)
|
||
}
|