diff --git a/.gitignore b/.gitignore index fea4936..4d670ec 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,5 @@ bindata.go web/react/node_modules web/static/files/script.js #web/static/files/*.css + +.idea \ No newline at end of file diff --git a/api/tags.go b/api/tags.go new file mode 100644 index 0000000..b8fcf40 --- /dev/null +++ b/api/tags.go @@ -0,0 +1,37 @@ +package api + +import ( + log "github.com/Sirupsen/logrus" + "github.com/gin-gonic/gin" + "github.com/lgtmco/lgtm/cache" + "github.com/lgtmco/lgtm/model" + "github.com/lgtmco/lgtm/router/middleware/session" + "github.com/lgtmco/lgtm/store" +) + +func GetTags(c *gin.Context) { + var ( + owner = c.Param("owner") + name = c.Param("repo") + user = session.User(c) + ) + repo, err := store.GetRepoOwnerName(c, owner, name) + if err != nil { + log.Errorf("Error getting repository %s. %s", name, err) + c.AbortWithStatus(404) + return + } + tags, err := cache.GetTags(c, user, repo) + if err != nil { + log.Errorf("Error getting remote tag list. %s", err) + c.String(500, "Error getting remote tag list") + return + } + + // copy the slice since we don't + // want any nasty data races if the slice came from the cache. + tagsc := make(model.TagList, len(tags)) + copy(tagsc, tags) + + c.JSON(200, tagsc) +} diff --git a/cache/helper.go b/cache/helper.go index 7cee5d1..95d6d0d 100644 --- a/cache/helper.go +++ b/cache/helper.go @@ -93,3 +93,22 @@ func GetMembers(c context.Context, user *model.User, team string) ([]*model.Memb FromContext(c).Set(key, members) return members, nil } + +func GetTags(c context.Context, user *model.User, repo *model.Repo) (model.TagList, error) { + key := fmt.Sprintf("tags:%s", + user.Login, + ) + // if we fetch from the cache we can return immediately + val, err := FromContext(c).Get(key) + if err == nil { + return val.(model.TagList), nil + } + // else we try to grab from the remote system and + // populate our cache. + tags, err := remote.GetTagList(c, user, repo) + if err != nil { + return nil, err + } + FromContext(c).Set(key, tags) + return tags, nil +} diff --git a/model/branch_status.go b/model/branch_status.go new file mode 100644 index 0000000..452f937 --- /dev/null +++ b/model/branch_status.go @@ -0,0 +1,3 @@ +package model + +type BranchStatus string diff --git a/model/config.go b/model/config.go index a4c2599..756c106 100644 --- a/model/config.go +++ b/model/config.go @@ -11,6 +11,8 @@ type Config struct { Pattern string `json:"pattern" toml:"pattern"` Team string `json:"team" toml:"team"` SelfApprovalOff bool `json:"self_approval_off" toml:"self_approval_off"` + DoMerge bool `json:"do_merge" toml:"do_merge"` + DoVersion bool `json:"do_version" toml:"do_version"` re *regexp.Regexp } @@ -31,7 +33,7 @@ func ParseConfigStr(data string) (*Config, error) { c.Approvals = 2 } if len(c.Pattern) == 0 { - c.Pattern = "(?i)LGTM" + c.Pattern = `(?i)LGTM\s*(\S*)` } if len(c.Team) == 0 { c.Team = "MAINTAINERS" diff --git a/model/hook.go b/model/hook.go index 2a171f9..01a0ad1 100644 --- a/model/hook.go +++ b/model/hook.go @@ -1,6 +1,12 @@ package model type Hook struct { + Kind string + IssueComment *IssueCommentHook + Status *StatusHook +} + +type IssueCommentHook struct { Repo *Repo Issue *Issue Comment *Comment diff --git a/model/status_hook.go b/model/status_hook.go new file mode 100644 index 0000000..f7ea9c9 --- /dev/null +++ b/model/status_hook.go @@ -0,0 +1,17 @@ +package model + +type StatusHook struct { + SHA string + Repo *Repo +} + +type PullRequest struct { + Issue + Branch Branch +} + +type Branch struct { + Name string + Status string + Mergeable bool +} diff --git a/model/tag.go b/model/tag.go new file mode 100644 index 0000000..89b7ee2 --- /dev/null +++ b/model/tag.go @@ -0,0 +1,33 @@ +package model + +import ( + log "github.com/Sirupsen/logrus" + "github.com/hashicorp/go-version" +) + +type Tag string + +type TagList []Tag + +func (tl TagList) GetMaxTag() (Tag, *version.Version) { + //find the previous largest semver value + var maxVer *version.Version + var maxTag Tag + + for _, tag := range tl { + curVer, err := version.NewVersion(string(tag)) + if err != nil { + continue + } + if maxVer == nil || curVer.GreaterThan(maxVer) { + maxVer = curVer + maxTag = tag + } + } + + if maxVer == nil { + maxVer, _ = version.NewVersion("v0.0.0") + } + log.Debugf("maxVer found is %s", maxVer.String()) + return maxTag, maxVer +} diff --git a/remote/github/github.go b/remote/github/github.go index 65e3abe..ed67b0e 100644 --- a/remote/github/github.go +++ b/remote/github/github.go @@ -7,8 +7,10 @@ import ( "strings" "time" + "errors" log "github.com/Sirupsen/logrus" "github.com/google/go-github/github" + "github.com/hashicorp/go-version" "github.com/lgtmco/lgtm/model" "github.com/lgtmco/lgtm/shared/httputil" "golang.org/x/oauth2" @@ -180,11 +182,6 @@ func (g *Github) GetRepos(u *model.User) ([]*model.Repo, error) { func (g *Github) SetHook(user *model.User, repo *model.Repo, link string) error { client := setupClient(g.API, user.Token) - repo_, _, err := client.Repositories.Get(repo.Owner, repo.Name) - if err != nil { - return err - } - old, err := GetHook(client, repo.Owner, repo.Name, link) if err == nil && old != nil { client.Repositories.DeleteHook(repo.Owner, repo.Name, *old.ID) @@ -196,12 +193,24 @@ func (g *Github) SetHook(user *model.User, repo *model.Repo, link string) error return err } + repo_, _, err := client.Repositories.Get(repo.Owner, repo.Name) + if err != nil { + return err + } + in := new(Branch) in.Protection.Enabled = true in.Protection.Checks.Enforcement = "non_admins" - in.Protection.Checks.Contexts = []string{context} + /* + JCB 04/21/16 confirmed with Github support -- must specify all existing contexts when + adding a new one, otherwise the other contexts will be removed. + */ client_ := NewClientToken(g.API, user.Token) + branch, _ := client_.Branch(repo.Owner, repo.Name, *repo_.DefaultBranch) + + in.Protection.Checks.Contexts = append(buildOtherContextSlice(branch), context) + err = client_.BranchProtect(repo.Owner, repo.Name, *repo_.DefaultBranch, in) if err != nil { if g.URL == "https://github.com" { @@ -236,14 +245,21 @@ func (g *Github) DelHook(user *model.User, repo *model.Repo, link string) error if len(branch.Protection.Checks.Contexts) == 0 { return nil } + + branch.Protection.Checks.Contexts = buildOtherContextSlice(branch) + + return client_.BranchProtect(repo.Owner, repo.Name, *repo_.DefaultBranch, branch) +} + +// buildOtherContextSlice returns all contexts besides the one for LGTM +func buildOtherContextSlice(branch *Branch) []string { checks := []string{} for _, check := range branch.Protection.Checks.Contexts { if check != context { checks = append(checks, check) } } - branch.Protection.Checks.Contexts = checks - return client_.BranchProtect(repo.Owner, repo.Name, *repo_.DefaultBranch, branch) + return checks } func (g *Github) GetComments(u *model.User, r *model.Repo, num int) ([]*model.Comment, error) { @@ -300,12 +316,27 @@ func (g *Github) SetStatus(u *model.User, r *model.Repo, num int, ok bool) error } func (g *Github) GetHook(r *http.Request) (*model.Hook, error) { - - // only process comment hooks - if r.Header.Get("X-Github-Event") != "issue_comment" { - return nil, nil + hook := &model.Hook{} + kind := r.Header.Get("X-Github-Event") + hook.Kind = kind + switch kind { + case "issue_comment": + issueComment, err := processIssueCommentHook(r) + if err != nil { + return nil, err + } + hook.IssueComment = issueComment + case "status": + status, err := processStatusHook(r) + if err != nil { + return nil, err + } + hook.Status = status } + return hook, nil +} +func processIssueCommentHook(r *http.Request) (*model.IssueCommentHook, error) { data := commentHook{} err := json.NewDecoder(r.Body).Decode(&data) if err != nil { @@ -316,7 +347,7 @@ func (g *Github) GetHook(r *http.Request) (*model.Hook, error) { return nil, nil } - hook := new(model.Hook) + hook := new(model.IssueCommentHook) hook.Issue = new(model.Issue) hook.Issue.Number = data.Issue.Number hook.Issue.Author = data.Issue.User.Login @@ -330,3 +361,142 @@ func (g *Github) GetHook(r *http.Request) (*model.Hook, error) { return hook, nil } + +func processStatusHook(r *http.Request) (*model.StatusHook, error) { + data := statusHook{} + err := json.NewDecoder(r.Body).Decode(&data) + if err != nil { + return nil, err + } + + log.Debug(data) + + if data.State != "success" { + return nil, nil + } + + hook := new(model.StatusHook) + + hook.SHA = data.SHA + + hook.Repo = new(model.Repo) + hook.Repo.Owner = data.Repository.Owner.Login + hook.Repo.Name = data.Repository.Name + hook.Repo.Slug = data.Repository.FullName + + log.Debug(*hook) + + return hook, nil +} + +func (g *Github) GetPullRequestsForCommit(u *model.User, r *model.Repo, sha *string) ([]model.PullRequest, error) { + client := setupClient(g.API, u.Token) + fmt.Println("sha == ", sha, *sha) + issues, _, err := client.Search.Issues(fmt.Sprintf("%s&type=pr", *sha), &github.SearchOptions{ + TextMatch: false, + }) + if err != nil { + return nil, err + } + out := make([]model.PullRequest, len(issues.Issues)) + for k, v := range issues.Issues { + pr, _, err := client.PullRequests.Get(r.Owner, r.Name, *v.Number) + if err != nil { + return nil, err + } + + mergeable := true + if pr.Mergeable != nil { + mergeable = *pr.Mergeable + } + + status, _, err := client.Repositories.GetCombinedStatus(r.Owner, r.Name, *sha, nil) + if err != nil { + return nil, err + } + + out[k] = model.PullRequest{ + Issue: model.Issue{ + Number: *v.Number, + Title: *v.Title, + Author: *v.User.Login, + }, + Branch: model.Branch{ + Name: *pr.Head.Ref, + Status: *status.State, + Mergeable: mergeable, + }, + } + } + return out, nil +} + +func (g *Github) GetBranchStatus(u *model.User, r *model.Repo, branch string) (*model.BranchStatus, error) { + client := setupClient(g.API, u.Token) + statuses, _, err := client.Repositories.GetCombinedStatus(r.Owner, r.Name, branch, nil) + if err != nil { + return nil, err + } + + return (*model.BranchStatus)(statuses.State), nil +} + +func (g *Github) MergePR(u *model.User, r *model.Repo, pullRequest model.PullRequest) (*string, error) { + client := setupClient(g.API, u.Token) + + result, _, err := client.PullRequests.Merge(r.Owner, r.Name, pullRequest.Number, "Merged by LGTM") + if err != nil { + return nil, err + } + + if !(*result.Merged) { + return nil, errors.New(*result.Message) + } + return result.SHA, nil +} + +func (g *Github) GetTagList(u *model.User, r *model.Repo) (model.TagList, error) { + client := setupClient(g.API, u.Token) + + tags, _, err := client.Repositories.ListTags(r.Owner, r.Name, nil) + if err != nil { + return nil, err + } + out := make(model.TagList, len(tags)) + for k, v := range tags { + out[k] = model.Tag(*v.Name) + } + return out, nil +} + +func (g *Github) Tag(u *model.User, r *model.Repo, version *version.Version, sha *string) error { + client := setupClient(g.API, u.Token) + + t := time.Now() + tag, _, err := client.Git.CreateTag(r.Owner, r.Name, &github.Tag{ + Tag: github.String(version.String()), + SHA: sha, + Message: github.String("Tagged by LGTM"), + Tagger: &github.CommitAuthor{ + Date: &t, + Name: github.String("LGTM"), + Email: github.String("LGTM@lgtm.co"), + }, + Object: &github.GitObject{ + SHA: sha, + Type: github.String("commit"), + }, + }) + + if err != nil { + return err + } + _, _, err = client.Git.CreateRef(r.Owner, r.Name, &github.Reference{ + Ref: github.String("refs/tags/" + version.String()), + Object: &github.GitObject{ + SHA: tag.SHA, + }, + }) + + return err +} diff --git a/remote/github/types.go b/remote/github/types.go index 4f990e1..608fc5e 100644 --- a/remote/github/types.go +++ b/remote/github/types.go @@ -38,15 +38,32 @@ type commentHook struct { } `json:"user"` } `json:"comment"` - Repository struct { - Name string `json:"name"` - FullName string `json:"full_name"` - Desc string `json:"description"` - Private bool `json:"private"` - Owner struct { - Login string `json:"login"` - Type string `json:"type"` - Avatar string `json:"avatar_url"` - } `json:"owner"` - } `json:"repository"` + Repository Repository `json:"repository"` +} + +type statusHook struct { + SHA string `json:"sha"` + State string `json:"state"` + + Branches []struct { + Name string `json:"name"` + Commit struct { + SHA string `json:"sha"` + URL string `json:"url"` + } `json:"commit"` + } `json:"branches"` + + Repository Repository `json:"repository"` +} + +type Repository struct { + Name string `json:"name"` + FullName string `json:"full_name"` + Desc string `json:"description"` + Private bool `json:"private"` + Owner struct { + Login string `json:"login"` + Type string `json:"type"` + Avatar string `json:"avatar_url"` + } `json:"owner"` } diff --git a/remote/github/utils.go b/remote/github/utils.go index d81823c..279096e 100644 --- a/remote/github/utils.go +++ b/remote/github/utils.go @@ -59,12 +59,12 @@ func DeleteHook(client *github.Client, owner, name, url string) error { return err } -// CreateHook is a heper function that creates a post-commit hook +// CreateHook is a helper function that creates a post-commit hook // for the specified repository. func CreateHook(client *github.Client, owner, name, url string) (*github.Hook, error) { var hook = new(github.Hook) hook.Name = github.String("web") - hook.Events = []string{"issue_comment"} + hook.Events = []string{"issue_comment", "status"} hook.Config = map[string]interface{}{} hook.Config["url"] = url hook.Config["content_type"] = "json" diff --git a/remote/mock/remote.go b/remote/mock/remote.go index 98d361f..ef999c7 100644 --- a/remote/mock/remote.go +++ b/remote/mock/remote.go @@ -1,75 +1,125 @@ package mock -import "github.com/stretchr/testify/mock" +import ( + "net/http" -import "net/http" -import "github.com/lgtmco/lgtm/model" + "github.com/hashicorp/go-version" + "github.com/lgtmco/lgtm/model" + "github.com/stretchr/testify/mock" +) +// Remote is an autogenerated mock type for the Remote type type Remote struct { mock.Mock } -func (_m *Remote) GetUser(_a0 http.ResponseWriter, _a1 *http.Request) (*model.User, error) { - ret := _m.Called(_a0, _a1) +// DelHook provides a mock function with given fields: _a0, _a1, _a2 +func (_m *Remote) DelHook(_a0 *model.User, _a1 *model.Repo, _a2 string) error { + ret := _m.Called(_a0, _a1, _a2) - var r0 *model.User - if rf, ok := ret.Get(0).(func(http.ResponseWriter, *http.Request) *model.User); ok { - r0 = rf(_a0, _a1) + var r0 error + if rf, ok := ret.Get(0).(func(*model.User, *model.Repo, string) error); ok { + r0 = rf(_a0, _a1, _a2) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// GetBranchStatus provides a mock function with given fields: _a0, _a1, _a2 +func (_m *Remote) GetBranchStatus(_a0 *model.User, _a1 *model.Repo, _a2 string) (*model.BranchStatus, error) { + ret := _m.Called(_a0, _a1, _a2) + + var r0 *model.BranchStatus + if rf, ok := ret.Get(0).(func(*model.User, *model.Repo, string) *model.BranchStatus); ok { + r0 = rf(_a0, _a1, _a2) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*model.User) + r0 = ret.Get(0).(*model.BranchStatus) } } var r1 error - if rf, ok := ret.Get(1).(func(http.ResponseWriter, *http.Request) error); ok { - r1 = rf(_a0, _a1) + if rf, ok := ret.Get(1).(func(*model.User, *model.Repo, string) error); ok { + r1 = rf(_a0, _a1, _a2) } else { r1 = ret.Error(1) } return r0, r1 } -func (_m *Remote) GetUserToken(_a0 string) (string, error) { - ret := _m.Called(_a0) - var r0 string - if rf, ok := ret.Get(0).(func(string) string); ok { - r0 = rf(_a0) +// GetComments provides a mock function with given fields: _a0, _a1, _a2 +func (_m *Remote) GetComments(_a0 *model.User, _a1 *model.Repo, _a2 int) ([]*model.Comment, error) { + ret := _m.Called(_a0, _a1, _a2) + + var r0 []*model.Comment + if rf, ok := ret.Get(0).(func(*model.User, *model.Repo, int) []*model.Comment); ok { + r0 = rf(_a0, _a1, _a2) } else { - r0 = ret.Get(0).(string) + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*model.Comment) + } } var r1 error - if rf, ok := ret.Get(1).(func(string) error); ok { - r1 = rf(_a0) + if rf, ok := ret.Get(1).(func(*model.User, *model.Repo, int) error); ok { + r1 = rf(_a0, _a1, _a2) } else { r1 = ret.Error(1) } return r0, r1 } -func (_m *Remote) GetTeams(_a0 *model.User) ([]*model.Team, error) { - ret := _m.Called(_a0) - var r0 []*model.Team - if rf, ok := ret.Get(0).(func(*model.User) []*model.Team); ok { - r0 = rf(_a0) +// GetContents provides a mock function with given fields: _a0, _a1, _a2 +func (_m *Remote) GetContents(_a0 *model.User, _a1 *model.Repo, _a2 string) ([]byte, error) { + ret := _m.Called(_a0, _a1, _a2) + + var r0 []byte + if rf, ok := ret.Get(0).(func(*model.User, *model.Repo, string) []byte); ok { + r0 = rf(_a0, _a1, _a2) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).([]*model.Team) + r0 = ret.Get(0).([]byte) } } var r1 error - if rf, ok := ret.Get(1).(func(*model.User) error); ok { - r1 = rf(_a0) + if rf, ok := ret.Get(1).(func(*model.User, *model.Repo, string) error); ok { + r1 = rf(_a0, _a1, _a2) } else { r1 = ret.Error(1) } return r0, r1 } + +// GetHook provides a mock function with given fields: r +func (_m *Remote) GetHook(r *http.Request) (*model.Hook, error) { + ret := _m.Called(r) + + var r0 *model.Hook + if rf, ok := ret.Get(0).(func(*http.Request) *model.Hook); ok { + r0 = rf(r) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.Hook) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(*http.Request) error); ok { + r1 = rf(r) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetMembers provides a mock function with given fields: _a0, _a1 func (_m *Remote) GetMembers(_a0 *model.User, _a1 string) ([]*model.Member, error) { ret := _m.Called(_a0, _a1) @@ -91,15 +141,17 @@ func (_m *Remote) GetMembers(_a0 *model.User, _a1 string) ([]*model.Member, erro return r0, r1 } -func (_m *Remote) GetRepo(_a0 *model.User, _a1 string, _a2 string) (*model.Repo, error) { + +// GetPerm provides a mock function with given fields: _a0, _a1, _a2 +func (_m *Remote) GetPerm(_a0 *model.User, _a1 string, _a2 string) (*model.Perm, error) { ret := _m.Called(_a0, _a1, _a2) - var r0 *model.Repo - if rf, ok := ret.Get(0).(func(*model.User, string, string) *model.Repo); ok { + var r0 *model.Perm + if rf, ok := ret.Get(0).(func(*model.User, string, string) *model.Perm); ok { r0 = rf(_a0, _a1, _a2) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*model.Repo) + r0 = ret.Get(0).(*model.Perm) } } @@ -112,15 +164,40 @@ func (_m *Remote) GetRepo(_a0 *model.User, _a1 string, _a2 string) (*model.Repo, return r0, r1 } -func (_m *Remote) GetPerm(_a0 *model.User, _a1 string, _a2 string) (*model.Perm, error) { + +// GetPullRequestsForCommit provides a mock function with given fields: u, r, sha +func (_m *Remote) GetPullRequestsForCommit(u *model.User, r *model.Repo, sha *string) ([]model.PullRequest, error) { + ret := _m.Called(u, r, sha) + + var r0 []model.PullRequest + if rf, ok := ret.Get(0).(func(*model.User, *model.Repo, *string) []model.PullRequest); ok { + r0 = rf(u, r, sha) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]model.PullRequest) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(*model.User, *model.Repo, *string) error); ok { + r1 = rf(u, r, sha) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetRepo provides a mock function with given fields: _a0, _a1, _a2 +func (_m *Remote) GetRepo(_a0 *model.User, _a1 string, _a2 string) (*model.Repo, error) { ret := _m.Called(_a0, _a1, _a2) - var r0 *model.Perm - if rf, ok := ret.Get(0).(func(*model.User, string, string) *model.Perm); ok { + var r0 *model.Repo + if rf, ok := ret.Get(0).(func(*model.User, string, string) *model.Repo); ok { r0 = rf(_a0, _a1, _a2) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*model.Perm) + r0 = ret.Get(0).(*model.Repo) } } @@ -133,6 +210,8 @@ func (_m *Remote) GetPerm(_a0 *model.User, _a1 string, _a2 string) (*model.Perm, return r0, r1 } + +// GetRepos provides a mock function with given fields: _a0 func (_m *Remote) GetRepos(_a0 *model.User) ([]*model.Repo, error) { ret := _m.Called(_a0) @@ -154,102 +233,181 @@ func (_m *Remote) GetRepos(_a0 *model.User) ([]*model.Repo, error) { return r0, r1 } -func (_m *Remote) SetHook(_a0 *model.User, _a1 *model.Repo, _a2 string) error { - ret := _m.Called(_a0, _a1, _a2) - var r0 error - if rf, ok := ret.Get(0).(func(*model.User, *model.Repo, string) error); ok { - r0 = rf(_a0, _a1, _a2) +// GetStatusHook provides a mock function with given fields: r +func (_m *Remote) GetStatusHook(r *http.Request) (*model.StatusHook, error) { + ret := _m.Called(r) + + var r0 *model.StatusHook + if rf, ok := ret.Get(0).(func(*http.Request) *model.StatusHook); ok { + r0 = rf(r) } else { - r0 = ret.Error(0) + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.StatusHook) + } } - return r0 + var r1 error + if rf, ok := ret.Get(1).(func(*http.Request) error); ok { + r1 = rf(r) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } -func (_m *Remote) DelHook(_a0 *model.User, _a1 *model.Repo, _a2 string) error { - ret := _m.Called(_a0, _a1, _a2) - var r0 error - if rf, ok := ret.Get(0).(func(*model.User, *model.Repo, string) error); ok { - r0 = rf(_a0, _a1, _a2) +// GetTagList provides a mock function with given fields: u, r +func (_m *Remote) GetTagList(u *model.User, r *model.Repo) (model.TagList, error) { + ret := _m.Called(u, r) + + var r0 model.TagList + if rf, ok := ret.Get(0).(func(*model.User, *model.Repo) model.TagList); ok { + r0 = rf(u, r) } else { - r0 = ret.Error(0) + if ret.Get(0) != nil { + r0 = ret.Get(0).(model.TagList) + } } - return r0 + var r1 error + if rf, ok := ret.Get(1).(func(*model.User, *model.Repo) error); ok { + r1 = rf(u, r) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } -func (_m *Remote) GetComments(_a0 *model.User, _a1 *model.Repo, _a2 int) ([]*model.Comment, error) { - ret := _m.Called(_a0, _a1, _a2) - var r0 []*model.Comment - if rf, ok := ret.Get(0).(func(*model.User, *model.Repo, int) []*model.Comment); ok { - r0 = rf(_a0, _a1, _a2) +// GetTeams provides a mock function with given fields: _a0 +func (_m *Remote) GetTeams(_a0 *model.User) ([]*model.Team, error) { + ret := _m.Called(_a0) + + var r0 []*model.Team + if rf, ok := ret.Get(0).(func(*model.User) []*model.Team); ok { + r0 = rf(_a0) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).([]*model.Comment) + r0 = ret.Get(0).([]*model.Team) } } var r1 error - if rf, ok := ret.Get(1).(func(*model.User, *model.Repo, int) error); ok { - r1 = rf(_a0, _a1, _a2) + if rf, ok := ret.Get(1).(func(*model.User) error); ok { + r1 = rf(_a0) } else { r1 = ret.Error(1) } return r0, r1 } -func (_m *Remote) GetContents(_a0 *model.User, _a1 *model.Repo, _a2 string) ([]byte, error) { - ret := _m.Called(_a0, _a1, _a2) - var r0 []byte - if rf, ok := ret.Get(0).(func(*model.User, *model.Repo, string) []byte); ok { - r0 = rf(_a0, _a1, _a2) +// GetUser provides a mock function with given fields: _a0, _a1 +func (_m *Remote) GetUser(_a0 http.ResponseWriter, _a1 *http.Request) (*model.User, error) { + ret := _m.Called(_a0, _a1) + + var r0 *model.User + if rf, ok := ret.Get(0).(func(http.ResponseWriter, *http.Request) *model.User); ok { + r0 = rf(_a0, _a1) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).([]byte) + r0 = ret.Get(0).(*model.User) } } var r1 error - if rf, ok := ret.Get(1).(func(*model.User, *model.Repo, string) error); ok { - r1 = rf(_a0, _a1, _a2) + if rf, ok := ret.Get(1).(func(http.ResponseWriter, *http.Request) error); ok { + r1 = rf(_a0, _a1) } else { r1 = ret.Error(1) } return r0, r1 } -func (_m *Remote) SetStatus(_a0 *model.User, _a1 *model.Repo, _a2 int, _a3 bool) error { - ret := _m.Called(_a0, _a1, _a2, _a3) - var r0 error - if rf, ok := ret.Get(0).(func(*model.User, *model.Repo, int, bool) error); ok { - r0 = rf(_a0, _a1, _a2, _a3) +// GetUserToken provides a mock function with given fields: _a0 +func (_m *Remote) GetUserToken(_a0 string) (string, error) { + ret := _m.Called(_a0) + + var r0 string + if rf, ok := ret.Get(0).(func(string) string); ok { + r0 = rf(_a0) } else { - r0 = ret.Error(0) + r0 = ret.Get(0).(string) } - return r0 + var r1 error + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } -func (_m *Remote) GetHook(r *http.Request) (*model.Hook, error) { - ret := _m.Called(r) - var r0 *model.Hook - if rf, ok := ret.Get(0).(func(*http.Request) *model.Hook); ok { - r0 = rf(r) +// MergePR provides a mock function with given fields: u, r, pullRequest +func (_m *Remote) MergePR(u *model.User, r *model.Repo, pullRequest model.PullRequest) (*string, error) { + ret := _m.Called(u, r, pullRequest) + + var r0 *string + if rf, ok := ret.Get(0).(func(*model.User, *model.Repo, model.PullRequest) *string); ok { + r0 = rf(u, r, pullRequest) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*model.Hook) + r0 = ret.Get(0).(*string) } } var r1 error - if rf, ok := ret.Get(1).(func(*http.Request) error); ok { - r1 = rf(r) + if rf, ok := ret.Get(1).(func(*model.User, *model.Repo, model.PullRequest) error); ok { + r1 = rf(u, r, pullRequest) } else { r1 = ret.Error(1) } return r0, r1 } + +// SetHook provides a mock function with given fields: _a0, _a1, _a2 +func (_m *Remote) SetHook(_a0 *model.User, _a1 *model.Repo, _a2 string) error { + ret := _m.Called(_a0, _a1, _a2) + + var r0 error + if rf, ok := ret.Get(0).(func(*model.User, *model.Repo, string) error); ok { + r0 = rf(_a0, _a1, _a2) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SetStatus provides a mock function with given fields: _a0, _a1, _a2, _a3 +func (_m *Remote) SetStatus(_a0 *model.User, _a1 *model.Repo, _a2 int, _a3 bool) error { + ret := _m.Called(_a0, _a1, _a2, _a3) + + var r0 error + if rf, ok := ret.Get(0).(func(*model.User, *model.Repo, int, bool) error); ok { + r0 = rf(_a0, _a1, _a2, _a3) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Tag provides a mock function with given fields: u, r, _a2, sha +func (_m *Remote) Tag(u *model.User, r *model.Repo, _a2 *version.Version, sha *string) error { + ret := _m.Called(u, r, _a2, sha) + + var r0 error + if rf, ok := ret.Get(0).(func(*model.User, *model.Repo, *version.Version, *string) error); ok { + r0 = rf(u, r, _a2, sha) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/remote/remote.go b/remote/remote.go index 77a0d8b..1ccfa86 100644 --- a/remote/remote.go +++ b/remote/remote.go @@ -5,6 +5,7 @@ package remote import ( "net/http" + "github.com/hashicorp/go-version" "github.com/lgtmco/lgtm/model" "golang.org/x/net/context" ) @@ -49,6 +50,21 @@ type Remote interface { // GetHook gets the hook from the http Request. GetHook(r *http.Request) (*model.Hook, error) + + // GetBranchStatus returns overall status for the named branch from the remote system + GetBranchStatus(*model.User, *model.Repo, string) (*model.BranchStatus, error) + + // MergePR merges the named pull request from the remote system + MergePR(u *model.User, r *model.Repo, pullRequest model.PullRequest) (*string, error) + + // GetTagList gets all tags in the repo + GetTagList(u *model.User, r *model.Repo) (model.TagList, error) + + // Tag applies a tag with the specified version to the specified sha + Tag(u *model.User, r *model.Repo, version *version.Version, sha *string) error + + // GetPullRequestsForCommit returns all pull requests associated with a commit SHA + GetPullRequestsForCommit(u *model.User, r *model.Repo, sha *string) ([]model.PullRequest, error) } // GetUser authenticates a user with the remote system. @@ -116,3 +132,24 @@ func SetStatus(c context.Context, u *model.User, r *model.Repo, num int, ok bool func GetHook(c context.Context, r *http.Request) (*model.Hook, error) { return FromContext(c).GetHook(r) } + +// GetBranchStatus gets the overal status for a branch from the remote repository. +func GetBranchStatus(c context.Context, u *model.User, r *model.Repo, branch string) (*model.BranchStatus, error) { + return FromContext(c).GetBranchStatus(u, r, branch) +} + +func MergePR(c context.Context, u *model.User, r *model.Repo, pullRequest model.PullRequest) (*string, error) { + return FromContext(c).MergePR(u, r, pullRequest) +} + +func GetTagList(c context.Context, u *model.User, r *model.Repo) (model.TagList, error) { + return FromContext(c).GetTagList(u, r) +} + +func Tag(c context.Context, u *model.User, r *model.Repo, version *version.Version, sha *string) error { + return FromContext(c).Tag(u, r, version, sha) +} + +func GetPullRequestsForCommit(c context.Context, u *model.User, r *model.Repo, sha *string) ([]model.PullRequest, error) { + return FromContext(c).GetPullRequestsForCommit(u, r, sha) +} diff --git a/router/router.go b/router/router.go index 9813ee7..3d2c3b6 100644 --- a/router/router.go +++ b/router/router.go @@ -34,6 +34,7 @@ func Load(middleware ...gin.HandlerFunc) http.Handler { e.DELETE("/api/repos/:owner/:repo", session.UserMust, access.RepoAdmin, api.DeleteRepo) e.GET("/api/repos/:owner/:repo/maintainers", session.UserMust, access.RepoPull, api.GetMaintainer) e.GET("/api/repos/:owner/:repo/maintainers/:org", session.UserMust, access.RepoPull, api.GetMaintainerOrg) + e.GET("/api/repos/:owner/:repo/tags", session.UserMust, access.RepoPull, api.GetTags) e.POST("/hook", web.Hook) e.GET("/login", web.Login) diff --git a/store/mock/store.go b/store/mock/store.go index 506f50e..6a3d49e 100644 --- a/store/mock/store.go +++ b/store/mock/store.go @@ -1,55 +1,30 @@ -package mock +package mocks -import "github.com/stretchr/testify/mock" - -import "github.com/lgtmco/lgtm/model" +import ( + "github.com/lgtmco/lgtm/model" + "github.com/stretchr/testify/mock" +) +// Store is an autogenerated mock type for the Store type type Store struct { mock.Mock } -func (_m *Store) GetUser(_a0 int64) (*model.User, error) { +// CreateRepo provides a mock function with given fields: _a0 +func (_m *Store) CreateRepo(_a0 *model.Repo) error { ret := _m.Called(_a0) - var r0 *model.User - if rf, ok := ret.Get(0).(func(int64) *model.User); ok { + var r0 error + if rf, ok := ret.Get(0).(func(*model.Repo) error); ok { r0 = rf(_a0) } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*model.User) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(int64) error); ok { - r1 = rf(_a0) - } else { - r1 = ret.Error(1) + r0 = ret.Error(0) } - return r0, r1 + return r0 } -func (_m *Store) GetUserLogin(_a0 string) (*model.User, error) { - ret := _m.Called(_a0) - - var r0 *model.User - if rf, ok := ret.Get(0).(func(string) *model.User); ok { - r0 = rf(_a0) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*model.User) - } - } - var r1 error - if rf, ok := ret.Get(1).(func(string) error); ok { - r1 = rf(_a0) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} +// CreateUser provides a mock function with given fields: _a0 func (_m *Store) CreateUser(_a0 *model.User) error { ret := _m.Called(_a0) @@ -62,11 +37,13 @@ func (_m *Store) CreateUser(_a0 *model.User) error { return r0 } -func (_m *Store) UpdateUser(_a0 *model.User) error { + +// DeleteRepo provides a mock function with given fields: _a0 +func (_m *Store) DeleteRepo(_a0 *model.Repo) error { ret := _m.Called(_a0) var r0 error - if rf, ok := ret.Get(0).(func(*model.User) error); ok { + if rf, ok := ret.Get(0).(func(*model.Repo) error); ok { r0 = rf(_a0) } else { r0 = ret.Error(0) @@ -74,6 +51,8 @@ func (_m *Store) UpdateUser(_a0 *model.User) error { return r0 } + +// DeleteUser provides a mock function with given fields: _a0 func (_m *Store) DeleteUser(_a0 *model.User) error { ret := _m.Called(_a0) @@ -86,6 +65,8 @@ func (_m *Store) DeleteUser(_a0 *model.User) error { return r0 } + +// GetRepo provides a mock function with given fields: _a0 func (_m *Store) GetRepo(_a0 int64) (*model.Repo, error) { ret := _m.Called(_a0) @@ -107,33 +88,37 @@ func (_m *Store) GetRepo(_a0 int64) (*model.Repo, error) { return r0, r1 } -func (_m *Store) GetRepoSlug(_a0 string) (*model.Repo, error) { + +// GetRepoMulti provides a mock function with given fields: _a0 +func (_m *Store) GetRepoMulti(_a0 ...string) ([]*model.Repo, error) { ret := _m.Called(_a0) - var r0 *model.Repo - if rf, ok := ret.Get(0).(func(string) *model.Repo); ok { - r0 = rf(_a0) + var r0 []*model.Repo + if rf, ok := ret.Get(0).(func(...string) []*model.Repo); ok { + r0 = rf(_a0...) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*model.Repo) + r0 = ret.Get(0).([]*model.Repo) } } var r1 error - if rf, ok := ret.Get(1).(func(string) error); ok { - r1 = rf(_a0) + if rf, ok := ret.Get(1).(func(...string) error); ok { + r1 = rf(_a0...) } else { r1 = ret.Error(1) } return r0, r1 } -func (_m *Store) GetRepoMulti(_a0 ...string) ([]*model.Repo, error) { + +// GetRepoOwner provides a mock function with given fields: _a0 +func (_m *Store) GetRepoOwner(_a0 string) ([]*model.Repo, error) { ret := _m.Called(_a0) var r0 []*model.Repo - if rf, ok := ret.Get(0).(func(...string) []*model.Repo); ok { - r0 = rf(_a0...) + if rf, ok := ret.Get(0).(func(string) []*model.Repo); ok { + r0 = rf(_a0) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]*model.Repo) @@ -141,23 +126,25 @@ func (_m *Store) GetRepoMulti(_a0 ...string) ([]*model.Repo, error) { } var r1 error - if rf, ok := ret.Get(1).(func(...string) error); ok { - r1 = rf(_a0...) + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(_a0) } else { r1 = ret.Error(1) } return r0, r1 } -func (_m *Store) GetRepoOwner(_a0 string) ([]*model.Repo, error) { + +// GetRepoSlug provides a mock function with given fields: _a0 +func (_m *Store) GetRepoSlug(_a0 string) (*model.Repo, error) { ret := _m.Called(_a0) - var r0 []*model.Repo - if rf, ok := ret.Get(0).(func(string) []*model.Repo); ok { + var r0 *model.Repo + if rf, ok := ret.Get(0).(func(string) *model.Repo); ok { r0 = rf(_a0) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).([]*model.Repo) + r0 = ret.Get(0).(*model.Repo) } } @@ -170,18 +157,54 @@ func (_m *Store) GetRepoOwner(_a0 string) ([]*model.Repo, error) { return r0, r1 } -func (_m *Store) CreateRepo(_a0 *model.Repo) error { + +// GetUser provides a mock function with given fields: _a0 +func (_m *Store) GetUser(_a0 int64) (*model.User, error) { ret := _m.Called(_a0) - var r0 error - if rf, ok := ret.Get(0).(func(*model.Repo) error); ok { + var r0 *model.User + if rf, ok := ret.Get(0).(func(int64) *model.User); ok { r0 = rf(_a0) } else { - r0 = ret.Error(0) + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.User) + } } - return r0 + var r1 error + if rf, ok := ret.Get(1).(func(int64) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetUserLogin provides a mock function with given fields: _a0 +func (_m *Store) GetUserLogin(_a0 string) (*model.User, error) { + ret := _m.Called(_a0) + + var r0 *model.User + if rf, ok := ret.Get(0).(func(string) *model.User); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.User) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } + +// UpdateRepo provides a mock function with given fields: _a0 func (_m *Store) UpdateRepo(_a0 *model.Repo) error { ret := _m.Called(_a0) @@ -194,11 +217,13 @@ func (_m *Store) UpdateRepo(_a0 *model.Repo) error { return r0 } -func (_m *Store) DeleteRepo(_a0 *model.Repo) error { + +// UpdateUser provides a mock function with given fields: _a0 +func (_m *Store) UpdateUser(_a0 *model.User) error { ret := _m.Called(_a0) var r0 error - if rf, ok := ret.Get(0).(func(*model.Repo) error); ok { + if rf, ok := ret.Get(0).(func(*model.User) error); ok { r0 = rf(_a0) } else { r0 = ret.Error(0) diff --git a/web/hook.go b/web/hook.go index 1378415..15b85df 100644 --- a/web/hook.go +++ b/web/hook.go @@ -8,8 +8,10 @@ import ( "github.com/lgtmco/lgtm/remote" "github.com/lgtmco/lgtm/store" + "fmt" log "github.com/Sirupsen/logrus" "github.com/gin-gonic/gin" + "github.com/hashicorp/go-version" ) func Hook(c *gin.Context) { @@ -19,10 +21,111 @@ func Hook(c *gin.Context) { c.String(500, "Error parsing hook. %s", err) return } - if hook == nil { + switch hook.Kind { + case "issue_comment": + processCommentHook(c, hook.IssueComment) + case "status": + processStatusHook(c, hook.Status) + default: c.String(200, "pong") + } +} + +func processStatusHook(c *gin.Context, hook *model.StatusHook) { + repo, err := store.GetRepoSlug(c, hook.Repo.Slug) + if err != nil { + log.Errorf("Error getting repository %s. %s", hook.Repo.Slug, err) + c.String(404, "Repository not found.") + return + } + user, err := store.GetUser(c, repo.UserID) + if err != nil { + log.Errorf("Error getting repository owner %s. %s", repo.Slug, err) + c.String(404, "Repository owner not found.") + return + } + + config, maintainer, err := getConfigAndMaintainers(c, user, repo) + if err != nil { + return + } + + if !config.DoMerge { + c.IndentedJSON(200, gin.H{}) + return + } + + merged := map[string]string{} + vers := map[string]string{} + + pullRequests, err := remote.GetPullRequestsForCommit(c, user, hook.Repo, &hook.SHA) + log.Debugf("sha for commit is %s, pull requests are: %s", hook.SHA, pullRequests) + + if err != nil { + log.Errorf("Error while getting pull requests for commit %s %s", hook.SHA, err) + c.String(500, "Error while getting pull requests for commit %s %s", hook.SHA, err) return } + //check the statuses of all of the checks on the branches for this commit + for _, v := range pullRequests { + //if all of the statuses are success, then merge and create a tag for the version + if v.Branch.Status == "success" && v.Branch.Mergeable { + sha, err := remote.MergePR(c, user, hook.Repo, v) + if err != nil { + log.Warnf("Unable to merge pull request %s: %s", v.Title, err) + continue + } else { + log.Debugf("Merged pull request %s", v.Title) + } + + merged[v.Title] = *sha + + if !config.DoVersion { + continue + } + + // to create the version, need to scan the comments on the pull request to see if anyone specified a version # + // if so, use the largest specified version #. if not, increment the last version version # for the release + tags, err := remote.GetTagList(c, user, hook.Repo) + if err != nil { + log.Warnf("Unable to get the tags for %s/%s: %s", hook.Repo.Owner, hook.Repo.Name, err) + continue + } + + _, maxVer := tags.GetMaxTag() + + comments, err := getComments(c, user, repo, v.Number) + if err != nil { + log.Warnf("Unable to find the comments for pull request %s: %s", v.Title, err) + continue + } + + foundVersion := getMaxVersionComment(config, maintainer, v.Issue, comments) + + if foundVersion != nil && foundVersion.GreaterThan(maxVer) { + maxVer = foundVersion + } else { + maxParts := maxVer.Segments() + maxVer, _ = version.NewVersion(fmt.Sprintf("%d.%d.%d", maxParts[0], maxParts[1], maxParts[2]+1)) + } + + err = remote.Tag(c, user, repo, maxVer, sha) + if err != nil { + log.Warnf("Unable to tag branch %s: %s", v.Title, err) + continue + } + vers[v.Title] = maxVer.String() + } + } + log.Debugf("processed status for %s. received %v ", repo.Slug, hook) + + c.IndentedJSON(200, gin.H{ + "merged": merged, + "versions": vers, + }) +} + +func processCommentHook(c *gin.Context, hook *model.IssueCommentHook) { repo, err := store.GetRepoSlug(c, hook.Repo.Slug) if err != nil { @@ -37,12 +140,42 @@ func Hook(c *gin.Context) { return } + config, maintainer, err := getConfigAndMaintainers(c, user, repo) + if err != nil { + return + } + + comments, err := getComments(c, user, repo, hook.Issue.Number) + if err != nil { + return + } + + approvers := getApprovers(config, maintainer, hook.Issue, comments) + approved := len(approvers) >= config.Approvals + err = remote.SetStatus(c, user, repo, hook.Issue.Number, approved) + if err != nil { + log.Errorf("Error setting status for %s pr %d. %s", repo.Slug, hook.Issue.Number, err) + c.String(500, "Error setting status. %s.", err) + return + } + + log.Debugf("processed comment for %s. received %d of %d approvals", repo.Slug, len(approvers), config.Approvals) + + c.IndentedJSON(200, gin.H{ + "approvers": maintainer.People, + "settings": config, + "approved": approved, + "approved_by": approvers, + }) +} + +func getConfigAndMaintainers(c *gin.Context, user *model.User, repo *model.Repo) (*model.Config, *model.Maintainer, error) { rcfile, _ := remote.GetContents(c, user, repo, ".lgtm") config, err := model.ParseConfig(rcfile) if err != nil { log.Errorf("Error parsing .lgtm file for %s. %s", repo.Slug, err) c.String(500, "Error parsing .lgtm file. %s.", err) - return + return nil, nil, err } // THIS IS COMPLETELY DUPLICATED IN THE API SECTION. NOT IDEAL @@ -54,7 +187,7 @@ func Hook(c *gin.Context) { log.Errorf("Error getting repository %s. %s", repo.Slug, err) log.Errorf("Error getting org members %s. %s", repo.Owner, merr) c.String(404, "MAINTAINERS file not found. %s", err) - return + return nil, nil, err } else { for _, member := range members { file = append(file, member.Login...) @@ -67,32 +200,19 @@ func Hook(c *gin.Context) { if err != nil { log.Errorf("Error parsing MAINTAINERS file for %s. %s", repo.Slug, err) c.String(500, "Error parsing MAINTAINERS file. %s.", err) - return + return nil, nil, err } + return config, maintainer, nil +} - comments, err := remote.GetComments(c, user, repo, hook.Issue.Number) +func getComments(c *gin.Context, user *model.User, repo *model.Repo, num int) ([]*model.Comment, error) { + comments, err := remote.GetComments(c, user, repo, num) if err != nil { - log.Errorf("Error retrieving comments for %s pr %d. %s", repo.Slug, hook.Issue.Number, err) + log.Errorf("Error retrieving comments for %s pr %d. %s", repo.Slug, num, err) c.String(500, "Error retrieving comments. %s.", err) - return - } - approvers := getApprovers(config, maintainer, hook.Issue, comments) - approved := len(approvers) >= config.Approvals - err = remote.SetStatus(c, user, repo, hook.Issue.Number, approved) - if err != nil { - log.Errorf("Error setting status for %s pr %d. %s", repo.Slug, hook.Issue.Number, err) - c.String(500, "Error setting status. %s.", err) - return + return nil, err } - - log.Debugf("processed comment for %s. received %d of %d approvals", repo.Slug, len(approvers), config.Approvals) - - c.IndentedJSON(200, gin.H{ - "approvers": maintainer.People, - "settings": config, - "approved": approved, - "approved_by": approvers, - }) + return comments, nil } // getApprovers is a helper function that analyzes the list of comments @@ -130,3 +250,53 @@ func getApprovers(config *model.Config, maintainer *model.Maintainer, issue *mod return approvers } + +// getMaxVersionComment is a helper function that analyzes the list of comments +// and returns the maximum version found in a comment. +func getMaxVersionComment(config *model.Config, maintainer *model.Maintainer, issue model.Issue, comments []*model.Comment) *version.Version { + approverm := map[string]bool{} + approvers := []*model.Person{} + + matcher, err := regexp.Compile(config.Pattern) + if err != nil { + // this should never happen + return nil + } + + var maxVersion *version.Version + + for _, comment := range comments { + // cannot lgtm your own pull request + if config.SelfApprovalOff && comment.Author == issue.Author { + continue + } + // the user must be a valid maintainer of the project + person, ok := maintainer.People[comment.Author] + if !ok { + continue + } + // the same author can't approve something twice + if _, ok := approverm[comment.Author]; ok { + continue + } + // verify the comment matches the approval pattern + m := matcher.FindStringSubmatch(comment.Body) + if len(m) > 0 { + approverm[comment.Author] = true + approvers = append(approvers, person) + + if len(m) > 1 { + //has a version + curVersion, err := version.NewVersion(m[1]) + if err != nil { + continue + } + if maxVersion == nil || curVersion.GreaterThan(maxVersion) { + maxVersion = curVersion + } + } + } + } + + return maxVersion +}