From f01352c02ee999b783d5f4b1108449594bc8bc1d Mon Sep 17 00:00:00 2001 From: Simon Latapie <garf@videolan.org> Date: Wed, 20 Oct 2021 18:10:23 +0200 Subject: [PATCH] add MR actions (stats mainly) --- api.go | 7 +- issue.go | 10 +- main.go | 20 +--- mr.go | 333 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 348 insertions(+), 22 deletions(-) create mode 100644 mr.go diff --git a/api.go b/api.go index 489be3b..95d1568 100644 --- a/api.go +++ b/api.go @@ -429,7 +429,7 @@ func (p *Project) GetAPIURL() string { } // GetMRs retrieves all the MRs inside the project, with given optional filters -func (p *Project) GetMRs(filters map[string]string) ([]MergeRequest, error) { +func (p *Project) GetMRs(filters map[string]string, showProgress bool) ([]MergeRequest, error) { var mrs, partial []MergeRequest url := fmt.Sprintf(`%s/merge_requests`, p.GetAPIURL()) params := defaultPagination() @@ -447,6 +447,11 @@ func (p *Project) GetMRs(filters map[string]string) ([]MergeRequest, error) { mrs = append(mrs, partial...) url = next params = map[string]string{} + if showProgress { + fmt.Print("\033[G\033[K") // move the cursor left and clear the line + log.Print("Counting MergeRequests: ", len(mrs)) + fmt.Print("\033[A") // move the cursor up + } } for index := range mrs { mrs[index].Project = p diff --git a/issue.go b/issue.go index 1adf453..0f51d26 100644 --- a/issue.go +++ b/issue.go @@ -66,11 +66,7 @@ func (i IssueDelete) Action(wg *sync.WaitGroup, issue Issue, filters map[string] } func (i IssueDelete) Result([]interface{}, map[string]string, map[string]string) {} -type UserCount struct { - User User - Count int -} -type StatsReport struct { +type IssueStatsReport struct { NoteUsers []UserCount User User } @@ -78,7 +74,7 @@ type StatsReport struct { type IssueStats struct{} func (i IssueStats) Action(wg *sync.WaitGroup, issue Issue, filters map[string]string, options map[string]string, results chan interface{}) { - report := StatsReport{} + report := IssueStatsReport{} defer func() { results <- report wg.Done() @@ -119,7 +115,7 @@ func (i IssueStats) Result(results []interface{}, filters map[string]string, opt wordsUserMap := map[int64]int64{} issueUserMap := map[int64]int64{} for _, r := range results { - report := r.(StatsReport) + report := r.(IssueStatsReport) for _, uw := range report.NoteUsers { userMap[uw.User.ID] = uw.User if _, ok := noteUserMap[uw.User.ID]; ok { diff --git a/main.go b/main.go index bb0dd07..aeb10fa 100644 --- a/main.go +++ b/main.go @@ -32,19 +32,6 @@ var ( func bold(s string) string { return fmt.Sprintf("\033[1m%s\033[0m", s) } -func logMR(mrID int64, v ...interface{}) { - header := fmt.Sprintf("[MR %d]", mrID) - if colorful { - color := mrID % 256 - header = fmt.Sprintf("\033[38;5;%dm[MR %d]\033[39;49m", color, mrID) - } - logStderrln(header, fmt.Sprint(v...)) -} -func logMRVerbose(mrID int64, v ...interface{}) { - if verbose || debug { - logMR(mrID, v...) - } -} func logStderrln(v ...interface{}) { fmt.Fprintln(os.Stderr, v...) } @@ -75,6 +62,11 @@ func checkDate(m map[string]string, key string) { } } +type UserCount struct { + User User + Count int +} + type Item struct { Key, Value int64 } @@ -163,7 +155,7 @@ func main() { case "issues": ActOnIssues(&project, arrayOptionToMap(filters), maxParallel, action, arrayOptionToMap(actionOptions)) case "mrs": - // pass + ActOnMergeRequests(&project, arrayOptionToMap(filters), maxParallel, action, arrayOptionToMap(actionOptions)) default: log.Println("Unknown category: ", on) os.Exit(-1) diff --git a/mr.go b/mr.go new file mode 100644 index 0000000..e575201 --- /dev/null +++ b/mr.go @@ -0,0 +1,333 @@ +package main + +import ( + "fmt" + "log" + "os" + "regexp" + "sync" + "time" +) + +func logMR(mrID int64, v ...interface{}) { + header := fmt.Sprintf("[MR %d]", mrID) + if colorful { + color := mrID % 256 + header = fmt.Sprintf("\033[38;5;%dm[MR %d]\033[39;49m", color, mrID) + } + logStderrln(header, fmt.Sprint(v...)) +} +func logMRVerbose(mrID int64, v ...interface{}) { + if verbose || debug { + logMR(mrID, v...) + } +} + +type MergeRequestAction interface { + Action(*sync.WaitGroup, MergeRequest, map[string]string, map[string]string, chan interface{}) + Result([]interface{}, map[string]string, map[string]string) +} + +type MergeRequestShow struct{} + +func (m MergeRequestShow) Action(wg *sync.WaitGroup, mr MergeRequest, filters map[string]string, options map[string]string, results chan interface{}) { + defer func() { + results <- nil + wg.Done() + }() + logMR(mr.IID, "From: ", mr.Author.Username, " (", mr.Author.Name, ") ", "Subject: ", mr.Title) +} +func (i MergeRequestShow) Result([]interface{}, map[string]string, map[string]string) {} + +type MergeRequestDummy struct{} + +func (i MergeRequestDummy) Action(wg *sync.WaitGroup, mr MergeRequest, filters map[string]string, options map[string]string, results chan interface{}) { + defer func() { + results <- nil + wg.Done() + }() +} +func (i MergeRequestDummy) Result([]interface{}, map[string]string, map[string]string) {} + +type MRNbVersions struct { + MRID int64 + Nb int +} + +type MRReport struct { + NoteUsers []UserCount + UpUsers []User + DownUsers []User + User User + Versions MRNbVersions +} + +type MergeRequestStats struct{} + +func (i MergeRequestStats) Action(wg *sync.WaitGroup, mr MergeRequest, filters map[string]string, options map[string]string, results chan interface{}) { + report := MRReport{} + defer func() { + results <- report + wg.Done() + }() + logMR(mr.IID, "MR:", mr.IID, " from ", mr.Author.Name) + var mrNoteUsers []UserCount + var upVotes []User + var downVotes []User + report.User = mr.Author + versions, err := mr.GetVersions() + if err != nil { + logMR(mr.IID, "WARNING: error during versions processing: "+err.Error()) + panic(err.Error()) + } + report.Versions = MRNbVersions{mr.IID, len(*versions)} + // NOTES + notes, err := mr.GetAllNotes(false) + if err != nil { + logMR(mr.IID, "WARNING: error during notes processing: "+err.Error()) + panic(err.Error()) + } + re := regexp.MustCompile(`\S+`) + timeBefore := time.Time{} + if before, ok := filters[UpdatedBefore]; ok { + timeBefore, _ = time.Parse(time.RFC3339, before) + } + timeAfter := time.Time{} + if after, ok := filters[UpdatedAfter]; ok { + timeAfter, _ = time.Parse(time.RFC3339, after) + } + + for _, note := range *notes { + if (!timeBefore.IsZero() && note.UpdatedAt.After(timeBefore)) || (!timeAfter.IsZero() && note.UpdatedAt.Before(timeAfter)) { + logMR(mr.IID, "Skipping Note ", note.ID, " updated at ", note.UpdatedAt) + continue + } + nbWords := len(re.FindAllString(note.Body, -1)) + logMR(mr.IID, "One note from ", note.Author.Name, ", nb words: ", nbWords) + mrNoteUsers = append(mrNoteUsers, UserCount{note.Author, nbWords}) + } + report.NoteUsers = mrNoteUsers + // VOTES + awards, err := mr.GetAwardEmojis() + if err != nil { + logMR(mr.IID, "WARNING: error during emojis processing: "+err.Error()) + return + } + for _, award := range *awards { + switch emoji := award.Name; emoji { + case VoteUp: + logMR(mr.IID, "FOUND +1 from ", award.User.Name) + upVotes = append(upVotes, award.User) + case VoteDown: + logMR(mr.IID, "FOUND -1 from ", award.User.Name) + downVotes = append(downVotes, award.User) + } + } + report.UpUsers = upVotes + report.DownUsers = downVotes +} + +func (i MergeRequestStats) Result(results []interface{}, filters map[string]string, options map[string]string) { + + userMap := map[int64]User{} + noteUserMap := map[int64]int64{} + wordsUserMap := map[int64]int64{} + upMap := map[int64]int64{} + downMap := map[int64]int64{} + mrUserMap := map[int64]int64{} + maxNbVersion := MRNbVersions{0, 0} + + numberOfMRs := len(results) + + for _, r := range results { + report := r.(MRReport) + for _, uw := range report.NoteUsers { + userMap[uw.User.ID] = uw.User + if _, ok := noteUserMap[uw.User.ID]; ok { + noteUserMap[uw.User.ID] += 1 + wordsUserMap[uw.User.ID] += int64(uw.Count) + } else { + noteUserMap[uw.User.ID] = 1 + wordsUserMap[uw.User.ID] = int64(uw.Count) + } + } + for _, u := range report.UpUsers { + userMap[u.ID] = u + if i, ok := upMap[u.ID]; ok { + upMap[u.ID] = i + 1 + } else { + upMap[u.ID] = 1 + } + } + for _, u := range report.DownUsers { + userMap[u.ID] = u + if i, ok := downMap[u.ID]; ok { + downMap[u.ID] = i + 1 + } else { + downMap[u.ID] = 1 + } + } + if _, ok := mrUserMap[report.User.ID]; ok { + mrUserMap[report.User.ID] += 1 + } else { + mrUserMap[report.User.ID] = 1 + } + if report.Versions.Nb > maxNbVersion.Nb { + maxNbVersion = report.Versions + } + } + output := "md" + if o, ok := options["output"]; ok { + output = o + } + switch output { + case "csv": + printMergeRequestCSV(filters, options, numberOfMRs, userMap, noteUserMap, wordsUserMap, upMap, downMap, mrUserMap, maxNbVersion) + default: + printMRMarkdown(filters, options, numberOfMRs, userMap, noteUserMap, wordsUserMap, upMap, downMap, mrUserMap, maxNbVersion) + } +} + +func printMRMarkdown(filters map[string]string, options map[string]string, numberOfMRs int, userMap map[int64]User, noteUserMap map[int64]int64, wordsUserMap map[int64]int64, upMap map[int64]int64, downMap map[int64]int64, mrUserMap map[int64]int64, maxNbVersion MRNbVersions) { + logStdoutln("## Merge Requests") + logStdoutln("") + before := "" + if b, ok := filters[UpdatedBefore]; ok { + before = b + } + after := "" + if a, ok := filters[UpdatedAfter]; ok { + after = a + } + if after != "" { + logStdoutln("After ", after) + } + if before != "" { + logStdoutln("Before ", before) + } + logStdoutln("") + logStdoutln("Number of MRs found: ", numberOfMRs) + logStdoutln("") + for _, item := range sortMap(mrUserMap) { + uid, nbMrs := item.Key, item.Value + logStdoutln(userMap[uid].Name, " (", userMap[uid].Username, ") : ", nbMrs) + } + logStdoutln("") + logStdoutln("### Comments") + logStdoutln("") + for _, item := range sortMap(noteUserMap) { + uid, nbNote := item.Key, item.Value + logStdoutln(userMap[uid].Name, " (", userMap[uid].Username, ") : ", nbNote) + } + logStdoutln("") + logStdoutln("### Words") + logStdoutln("") + for _, item := range sortMap(wordsUserMap) { + uid, nbWords := item.Key, item.Value + logStdoutln(userMap[uid].Name, " (", userMap[uid].Username, ") : ", nbWords) + } + logStdoutln("") + logStdoutln("### Votes") + logStdoutln("") + logStdoutln("#### Upvotes") + logStdoutln("") + for _, item := range sortMap(upMap) { + uid, nb := item.Key, item.Value + logStdoutln(userMap[uid].Name, " (", userMap[uid].Username, ") : ", nb) + } + logStdoutln("") + logStdoutln("#### Downvotes") + logStdoutln("") + for _, item := range sortMap(downMap) { + uid, nb := item.Key, item.Value + logStdoutln(userMap[uid].Name, " (", userMap[uid].Username, ") : ", nb) + } + logStdoutln("") + logStdoutln("MR With most nb of versions: MR ID:", maxNbVersion.MRID, " with ", maxNbVersion.Nb, " versions") + logStdoutln("") + +} + +func printMergeRequestCSV(filters map[string]string, options map[string]string, numberOfMRs int, userMap map[int64]User, noteUserMap map[int64]int64, wordsUserMap map[int64]int64, upMap map[int64]int64, downMap map[int64]int64, mrUserMap map[int64]int64, maxNbVersion MRNbVersions) { + logStdoutln(" After , Before , Username , Type , Value , NumberOf ") + before := "" + if b, ok := filters[UpdatedBefore]; ok { + before = b + } + after := "" + if a, ok := filters[UpdatedAfter]; ok { + after = a + } + for _, item := range sortMap(mrUserMap) { + uid, nbMrs := item.Key, item.Value + logStdoutln("\""+after+"\"", " , ", "\""+before+"\"", " , ", "\""+userMap[uid].Username+"\"", " , ", "MRNumber", " , ", nbMrs, " , ", numberOfMRs) + } + for _, item := range sortMap(noteUserMap) { + uid, nbNote := item.Key, item.Value + logStdoutln("\""+after+"\"", " , ", "\""+before+"\"", " , ", "\""+userMap[uid].Username+"\"", " , ", "MRComments", " , ", nbNote, " , ", "") + } + for _, item := range sortMap(wordsUserMap) { + uid, nbWords := item.Key, item.Value + logStdoutln("\""+after+"\"", " , ", "\""+before+"\"", " , ", "\""+userMap[uid].Username+"\"", " , ", "MRWords", " , ", nbWords, " , ", "") + } + for _, item := range sortMap(upMap) { + uid, nb := item.Key, item.Value + logStdoutln("\""+after+"\"", " , ", "\""+before+"\"", " , ", "\""+userMap[uid].Username+"\"", " , ", "MRUpVotes", " , ", nb, " , ", "") + } + for _, item := range sortMap(downMap) { + uid, nb := item.Key, item.Value + logStdoutln("\""+after+"\"", " , ", "\""+before+"\"", " , ", "\""+userMap[uid].Username+"\"", " , ", "MRDownVotes", " , ", nb, " , ", "") + } +} + +func ActOnMergeRequests(project *Project, filters map[string]string, maxParallel int, actionStr string, options map[string]string) { + checkDate(filters, UpdatedBefore) + checkDate(filters, UpdatedAfter) + mrs, err := project.GetMRs(filters, true) + if err != nil { + log.Println("Error while retrieving Project's MergeRequests: ", err.Error()) + os.Exit(-1) + } + log.Println("Found", len(mrs), "MergeRequests in Project Number ", project.ID, "with the following criteria: ") + log.Println(filters) + + var action MergeRequestAction + switch actionStr { + case ActionDummy: + action = MergeRequestDummy{} + case ActionShow: + action = MergeRequestShow{} + case ActionStats: + action = MergeRequestStats{} + default: + log.Println("Action", actionStr, "Unknown") + os.Exit(-1) + } + log.Println("") + log.Println("You asked for Action", bold(actionStr)) + log.Println("") + if !askConfirm("Do you want to procede?") { + log.Println("Aborting...") + os.Exit(-1) + } + results := make(chan (interface{}), len(mrs)) + for i := 0; i < len(mrs); i += maxParallel { + var sub []MergeRequest + if i+maxParallel > len(mrs) { + sub = mrs[i:] + } else { + sub = mrs[i : i+maxParallel] + } + var wg sync.WaitGroup + for _, mr := range sub { + wg.Add(1) + go action.Action(&wg, mr, filters, options, results) + } + wg.Wait() + } + var resultArray []interface{} + for i := 0; i < len(mrs); i++ { + resultArray = append(resultArray, <-results) + } + action.Result(resultArray, filters, options) +} -- GitLab