-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Simon Jürgensmeyer
committed
Apr 4, 2019
0 parents
commit cb09cc1
Showing
10 changed files
with
565 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
|
||
# Created by https://www.gitignore.io/api/go,macos,visualstudiocode | ||
# Edit at https://www.gitignore.io/?templates=go,macos,visualstudiocode | ||
|
||
### Go ### | ||
# Binaries for programs and plugins | ||
*.exe | ||
*.exe~ | ||
*.dll | ||
*.so | ||
*.dylib | ||
|
||
# Test binary, build with `go test -c` | ||
*.test | ||
|
||
# Output of the go coverage tool, specifically when used with LiteIDE | ||
*.out | ||
|
||
### Go Patch ### | ||
/vendor/ | ||
/Godeps/ | ||
|
||
### macOS ### | ||
# General | ||
.DS_Store | ||
.AppleDouble | ||
.LSOverride | ||
|
||
# Icon must end with two \r | ||
Icon | ||
|
||
# Thumbnails | ||
._* | ||
|
||
# Files that might appear in the root of a volume | ||
.DocumentRevisions-V100 | ||
.fseventsd | ||
.Spotlight-V100 | ||
.TemporaryItems | ||
.Trashes | ||
.VolumeIcon.icns | ||
.com.apple.timemachine.donotpresent | ||
|
||
# Directories potentially created on remote AFP share | ||
.AppleDB | ||
.AppleDesktop | ||
Network Trash Folder | ||
Temporary Items | ||
.apdisk | ||
|
||
### VisualStudioCode ### | ||
.vscode/* | ||
|
||
### VisualStudioCode Patch ### | ||
# Ignore all local history of files | ||
.history | ||
|
||
# End of https://www.gitignore.io/api/go,macos,visualstudiocode |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
MIT License | ||
|
||
Copyright (c) 2018 Brabbler AG | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the "Software"), to deal | ||
in the Software without restriction, including without limitation the rights | ||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
copies of the Software, and to permit persons to whom the Software is | ||
furnished to do so, subject to the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be included in all | ||
copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
SOFTWARE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
# Review Reminder Bot | ||
|
||
## Usage | ||
|
||
### Command Line Flags | ||
|
||
``` text | ||
-channel string | ||
Mattermost channel (e.g. MyChannel) or user (e.g. @AnyUser) | ||
-host string | ||
GitLab host address (default "gitlab.com") | ||
-project int | ||
GitLab project id (default 1) | ||
-reviewers string | ||
file path to the reviewers config file (default "reviewers.json") | ||
-token string | ||
GitLab API token | ||
-webhook string | ||
Mattermost webhook URL | ||
``` | ||
|
||
### Examples | ||
|
||
The reviewers.json file contains the `gitlab_user_id: "@mattermost_name"`. | ||
|
||
```json | ||
{ | ||
"5": "@hulk", | ||
"17": "@iron_man", | ||
"92": "@groot", | ||
"95": "@batman", | ||
"123": "@daredevil" | ||
} | ||
``` | ||
|
||
Get all open merge requests from project 1 and post it to the specified Mattermost channel: | ||
|
||
``` text | ||
go run main.go -host=$GITLAB_HOST -token=$GITLAB_API_TOKEN -project=1 -webhook=$WEBHOOK_ADDRESS -channel=$MATTERMOST_CHANNEL | ||
``` | ||
|
||
This will output the merge requests with the number of open discussions (💬) and the number of 👍 and 👎. The missing reviewers will be mentioned. | ||
Adding the "sleeping" 😴 emoji to a merge request means the user won't review the code and/or doesn't want to be mentioned. | ||
When all reviewers gave their thumps, the owner of the MR will be informed. | ||
|
||
``` markdown | ||
**[Support SHIELD](https://gitlab.com/my_user/my_project/merge_requests/1940)** | ||
1 💬 3 :thumbsup: @hulk | ||
|
||
**[Ask Deadpool to join us](https://gitlab.com/my_user/my_project/merge_requests/1923)** | ||
3 💬 3 :thumbsup: @batman | ||
|
||
**[Repair the Helicarrier](https://gitlab.com/my_user/my_project/merge_requests/1777)** | ||
3 💬 @hulk @batman @groot @iron_man | ||
|
||
**[Find Kingpin](https://gitlab.com/my_user/my_project/merge_requests/1099)** | ||
2 💬 7 :thumbsup: You got all reviews, @daredevil. | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,212 @@ | ||
package gitlab | ||
|
||
import ( | ||
"fmt" | ||
"log" | ||
"net/http" | ||
|
||
"github.com/xanzy/go-gitlab" | ||
) | ||
|
||
// NewClient returns a new gitlab client. | ||
func NewClient(host, token string) *gitlab.Client { | ||
client := gitlab.NewClient(nil, token) | ||
if err := client.SetBaseURL(fmt.Sprintf("https://%s/api/v4", host)); err != nil { | ||
log.Fatalf("failed to set gitlab host: %v", err) | ||
} | ||
return client | ||
} | ||
|
||
// ResponsiblePerson returns the mattermost name of the assignee or author of the MR | ||
// (fallback: gitlab author name) | ||
func ResponsiblePerson(mr *gitlab.MergeRequest, reviewers map[int]string) string { | ||
if mr.Assignee.ID != 0 { | ||
assignee, ok := reviewers[mr.Assignee.ID] | ||
if ok { | ||
return assignee | ||
} | ||
} | ||
|
||
author, ok := reviewers[mr.Author.ID] | ||
if ok { | ||
return author | ||
} | ||
|
||
return mr.Author.Name | ||
} | ||
|
||
// OpenMergeRequests returns all open merge requests of the given project. | ||
func OpenMergeRequests(git *gitlab.Client, projectID int) []*gitlab.MergeRequest { | ||
// options | ||
state := "opened" | ||
opts := &gitlab.ListProjectMergeRequestsOptions{State: &state, ListOptions: gitlab.ListOptions{PerPage: 100}} | ||
|
||
// first page | ||
mergeRequests, resp, err := git.MergeRequests.ListProjectMergeRequests(projectID, opts) | ||
if err != nil { | ||
log.Fatalf("failed to list project merge requests: %v", err) | ||
} | ||
if resp.StatusCode != http.StatusOK { | ||
log.Fatalf("failed to list project merge requests, status code: %v", resp.StatusCode) | ||
} | ||
|
||
// following pages | ||
for page := 2; page <= resp.TotalPages; page++ { | ||
opts.Page = page | ||
|
||
pageMRs, resp, err := git.MergeRequests.ListProjectMergeRequests(projectID, opts) | ||
if err != nil { | ||
log.Fatalf("failed to list project merge requests: %v", err) | ||
} | ||
if resp.StatusCode != http.StatusOK { | ||
log.Fatalf("failed to list project merge requests, status code: %v", resp.StatusCode) | ||
} | ||
mergeRequests = append(mergeRequests, pageMRs...) | ||
} | ||
|
||
return mergeRequests | ||
} | ||
|
||
// LoadDiscussions of the given MR. | ||
func LoadDiscussions(git *gitlab.Client, projectID int, mr *gitlab.MergeRequest) []*gitlab.Discussion { | ||
opts := &gitlab.ListMergeRequestDiscussionsOptions{PerPage: 100} | ||
|
||
// first page | ||
discussions, resp, err := git.Discussions.ListMergeRequestDiscussions(projectID, mr.IID, opts) | ||
if err != nil { | ||
log.Fatalf("failed to get discussions for mr %v: %v", mr.IID, err) | ||
} | ||
if resp.StatusCode != http.StatusOK { | ||
log.Fatalf("failed to list emojis, status code: %v", resp.StatusCode) | ||
} | ||
|
||
// following pages | ||
for page := 2; page <= resp.TotalPages; page++ { | ||
opts.Page = page | ||
|
||
pageDiscussions, resp, err := git.Discussions.ListMergeRequestDiscussions(projectID, mr.IID, opts) | ||
if err != nil { | ||
log.Fatalf("failed to list emojis for MR %v: %v", mr.IID, err) | ||
} | ||
if resp.StatusCode != http.StatusOK { | ||
log.Fatalf("failed to list emojis, status code: %v", resp.StatusCode) | ||
} | ||
discussions = append(discussions, pageDiscussions...) | ||
} | ||
|
||
return discussions | ||
} | ||
|
||
// OpenDiscussionsCount returns the number of open discussions. | ||
func OpenDiscussionsCount(discussions []*gitlab.Discussion) int { | ||
// check if any of the discussions are unresolved | ||
count := 0 | ||
for _, d := range discussions { | ||
for _, n := range d.Notes { | ||
if !n.Resolved && n.Resolvable { | ||
count++ | ||
} | ||
} | ||
} | ||
return count | ||
} | ||
|
||
// FilterOpenDiscussions returns only merge requests which have no open discussions. | ||
func FilterOpenDiscussions(mergeRequests []*gitlab.MergeRequest, discussions []*gitlab.Discussion) []*gitlab.MergeRequest { | ||
result := []*gitlab.MergeRequest{} | ||
|
||
for _, mr := range mergeRequests { | ||
// check if any of the discussions are unresolved | ||
anyUnresolved := false | ||
LoopDiscussions: | ||
for _, d := range discussions { | ||
for _, n := range d.Notes { | ||
if !n.Resolved && n.Resolvable { | ||
anyUnresolved = true | ||
break LoopDiscussions | ||
} | ||
} | ||
} | ||
|
||
// don't add merge request with unresolved discussion | ||
if !anyUnresolved { | ||
result = append(result, mr) | ||
} | ||
} | ||
return result | ||
} | ||
|
||
// LoadEmojis returns all emoji reactions of the particular MR. | ||
func LoadEmojis(git *gitlab.Client, projectID int, mr *gitlab.MergeRequest) []*gitlab.AwardEmoji { | ||
opts := &gitlab.ListAwardEmojiOptions{PerPage: 100} | ||
|
||
// first page | ||
emojis, resp, err := git.AwardEmoji.ListMergeRequestAwardEmoji(projectID, mr.IID, opts) | ||
if err != nil { | ||
log.Fatalf("failed to list emojis for MR %v: %v", mr.IID, err) | ||
} | ||
|
||
// following pages | ||
for page := 2; page <= resp.TotalPages; page++ { | ||
opts.Page = page | ||
|
||
pageEmojis, resp, err := git.AwardEmoji.ListMergeRequestAwardEmoji(projectID, mr.IID, opts) | ||
if err != nil { | ||
log.Fatalf("failed to list emojis for MR %v: %v", mr.IID, err) | ||
} | ||
if resp.StatusCode != http.StatusOK { | ||
log.Fatalf("failed to list emojis, status code: %v", resp.StatusCode) | ||
} | ||
emojis = append(emojis, pageEmojis...) | ||
} | ||
|
||
return emojis | ||
} | ||
|
||
// GetReviewed returns the gitlab user id of the people who have already reviewed the MR. | ||
// The emojis "thumbsup" 👍 and "thumbsdown" 👎 signal the user reviewed the merge request and won't receive a reminder. | ||
// The emoji "sleeping" 😴 means the user won't review the code and/or doesn't want to be reminded. | ||
func GetReviewed(mr *gitlab.MergeRequest, emojis []*gitlab.AwardEmoji) []int { | ||
var reviewedBy []int | ||
reviewedBy = append(reviewedBy, mr.Author.ID) | ||
for _, emoji := range emojis { | ||
if emoji.Name == "thumbsup" || emoji.Name == "thumbsdown" || emoji.Name == "sleeping" { | ||
reviewedBy = append(reviewedBy, emoji.User.ID) | ||
} | ||
} | ||
|
||
return reviewedBy | ||
} | ||
|
||
// AggregateEmojis lists all emojis with their usage count. | ||
func AggregateEmojis(emojis []*gitlab.AwardEmoji) map[string]int { | ||
var aggregate map[string]int | ||
aggregate = make(map[string]int) | ||
|
||
for _, emoji := range emojis { | ||
count := aggregate[emoji.Name] | ||
count++ | ||
aggregate[emoji.Name] = count | ||
} | ||
|
||
return aggregate | ||
} | ||
|
||
// MissingReviewers returns all reviewers which have not reacted with 👍, 👎 or 😴. | ||
func MissingReviewers(reviewedBy []int, approvers map[int]string) []string { | ||
var result []string | ||
for userID, userName := range approvers { | ||
approved := false | ||
for _, approverID := range reviewedBy { | ||
if userID == approverID { | ||
approved = true | ||
break | ||
} | ||
} | ||
if !approved { | ||
result = append(result, userName) | ||
} | ||
} | ||
|
||
return result | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
module github.com/sj14/review-reminder | ||
|
||
require github.com/xanzy/go-gitlab v0.11.6 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= | ||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= | ||
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= | ||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= | ||
github.com/xanzy/go-gitlab v0.11.6 h1:UZDsJ6Q05sw1CtTq2ZX4R4Syg5NacdB5g2tdWBIIkTA= | ||
github.com/xanzy/go-gitlab v0.11.6/go.mod h1:8zdQa/ri1dfn8eS3Ir1SyfvOKlw7WBJ8DVThkpGiXrs= | ||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||
golang.org/x/net v0.0.0-20181108082009-03003ca0c849 h1:FSqE2GGG7wzsYUsWiQ8MZrvEd1EOyU3NCF0AW3Wtltg= | ||
golang.org/x/net v0.0.0-20181108082009-03003ca0c849/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288 h1:JIqe8uIcRBHXDQVvZtHwp80ai3Lw3IJAeJEs55Dc1W0= | ||
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= | ||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ= | ||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||
google.golang.org/appengine v1.3.0 h1:FBSsiFRMz3LBeXIomRnVzrQwSDj4ibvcRexLG0LZGQk= | ||
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= |
Oops, something went wrong.