Skip to content

Commit

Permalink
Public Release
Browse files Browse the repository at this point in the history
  • Loading branch information
Simon Jürgensmeyer committed Apr 4, 2019
0 parents commit cb09cc1
Show file tree
Hide file tree
Showing 10 changed files with 565 additions and 0 deletions.
58 changes: 58 additions & 0 deletions .gitignore
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
21 changes: 21 additions & 0 deletions LICENSE
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.
58 changes: 58 additions & 0 deletions README.md
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.
```
212 changes: 212 additions & 0 deletions gitlab/gitlab.go
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
}
3 changes: 3 additions & 0 deletions go.mod
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
16 changes: 16 additions & 0 deletions go.sum
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=
Loading

0 comments on commit cb09cc1

Please sign in to comment.