Skip to content
This repository has been archived by the owner on Jan 26, 2018. It is now read-only.

Add multiple approval algorithms. #35

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,109 @@ make build # Build the binary
```

If you are having trouble building this project please reference its .drone.yml file. Everything you need to know about building LGTM is defined in that file.

### Usage

#### .lgtm file

Each repository managed by LGTM can have a .lgtm file in the root of the repository. This file provides the configuration properties that LGTM uses for the repository. If the .lgtm file is missing then default values are used for all properties.

#### MAINTAINERS file

Each repository managed by LGTM should have a MAINTAINERS file that specifies who is allowed to approve pull requests.

For simple approval, you can use a flat-file format for the list of approvers:
```
Brad Rydzewski <[email protected]> (@bradrydzewski)
Matt Norris <[email protected]> (@mattnorris)
```

The name and email are optional, but recommended. The login is required.

If only the login is specified, do not put the login in parenthesis or prefix it with an @.

To use organizational approval, you must use the TOML file format to specify the members of each team. This format looks like:
```
[people]
[people.bob]
name = "Bob Bobson"
email = "[email protected]"
login = "bob"
[people.fred]
name = "Fred Fredson"
email = "[email protected]"
login = "fred"
[people.jon]
name = "Jon Jonson"
email = "[email protected]"
login = "jon"
[people.ralph]
name = "Ralph Ralphington"
email = "[email protected]"
login = "ralph"
[people.george]
name = "George McGeorge"
email = "[email protected]"
login = "george"

[org]
[org.cap]
people = [
"bob",
"fred",
"jon"
]

[org.iron]
people = [
"ralph",
"george"
]
```

If no MAINTAINERS file is present, LGTM will fall back to using a github team with the name MAINTAINERS. You cannot enable organizational approval without a MAINTAINERS file.

#### Approval Management

In order for LGTM to pass its status check there must be a number of valid approvals. This is controlled by the `approvals` property in the .lgtm file:
```
approvals = 2
```

If this property is not specified, it defaults to the value 2.

The message that approvers use to signal that they have granted approval is controlled by the `pattern` property in the .lgtm file:
```
pattern = '(?i)LGTM'
```

If no pattern is specified, `(?i)LGTM` is used as the pattern.

##### Simple approval

By default, any approver on the list
```
approval_algorithm = "simple"
```

If you want to prevent the creator of a pull request from approving their own pull request, set the `self_approval_off` property in the .lgtm file:

```
self_approval_off = true
```

This property defaults to false; i.e., if you are on the approver list, you can approve a pull request that you create.

##### Organizational approval

Organizational approval means that at most one person from a specified organizational team can approve a pull request.

If self-approval is disabled (`self_approval_off = true`), then no one on the same team as the author can approve the pull request.

It's possible to configure organizations so that a person is on multiple teams, but it is not recommended, as the team assignment recognized by LGTM is undefined.

To enable organizational approval, add the following line to the .lgtm file:

```
approval_algorithm = "org"
```
73 changes: 73 additions & 0 deletions approval/approval.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package approval

import (
"fmt"
log "github.com/Sirupsen/logrus"
"github.com/lgtmco/lgtm/model"
"regexp"
"strings"
)

// Func takes in the information needed to figure out which approvers were in the PR comments
// and returns a slice of the approvers that were found
type Func func(*model.Config, *model.Maintainer, *model.Issue, []*model.Comment, Processor)

var approvalMap = map[string]Func{}

func Register(name string, f Func) error {
if _, ok := approvalMap[strings.ToLower(name)]; ok {
return fmt.Errorf("Approval Algorithm %s is already registered.", name)
}
approvalMap[strings.ToLower(name)] = f
log.Debug("added to approvalMap:", name, f)
return nil
}

func Lookup(name string) (Func, error) {
log.Debug("approvalMap has", approvalMap)
log.Debugf("looking for '%s'\n", name)
f, ok := approvalMap[strings.ToLower(name)]
if !ok {
return nil, fmt.Errorf("Unknown Approval Algorithm %s", name)
}
return f, nil
}

func init() {
Register("simple", Simple)
}

type Processor func(*model.Maintainer, *model.Comment)

// Simple is a helper function that analyzes the list of comments
// and finds the ones that have approvers on the maintainers list.
func Simple(config *model.Config, maintainer *model.Maintainer, issue *model.Issue, comments []*model.Comment, p Processor) {
approverm := map[string]bool{}

matcher, err := regexp.Compile(config.Pattern)
if err != nil {
// this should never happen
return
}

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
_, 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
if matcher.MatchString(comment.Body) {
approverm[comment.Author] = true
p(maintainer, comment)
}
}
}
78 changes: 78 additions & 0 deletions approval/org/org.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package org

import (
"github.com/lgtmco/lgtm/approval"
"github.com/lgtmco/lgtm/model"
"regexp"
)

func init() {
approval.Register("org", Org)
}

// Org is a helper function that analyzes the list of comments
// and returns the list of approvers.
// The rules for Org are:
// - At most one approver from each team
// - If SelfApprovalOff is true, no member of the team of the creator of the Pull Request is allowed to approve the PR
// - If a person appears on more than one team, they only count once, for the first team in which they appear
// (not solving the optimal grouping problem yet)
func Org(config *model.Config, maintainer *model.Maintainer, issue *model.Issue, comments []*model.Comment, p approval.Processor) {
//groups that have already approved
approvergm := map[string]bool{}

//org that the author belongs to
authorOrg := ""

//key is person, value is map of the orgs for that person
orgMap := map[string]map[string]bool{}

//key is org name, value is org
for k, v := range maintainer.Org {
//value is name of person in the org
for _, name := range v.People {
if name == issue.Author {
authorOrg = k
}
m, ok := orgMap[name]
if !ok {
m = map[string]bool{}
orgMap[name] = m
}
m[k] = true
}
}

matcher, err := regexp.Compile(config.Pattern)
if err != nil {
// this should never happen
return
}

for _, comment := range comments {
// verify the comment matches the approval pattern
if !matcher.MatchString(comment.Body) {
continue
}
//get the orgs for the current comment's author
curOrgs, ok := orgMap[comment.Author]
if !ok {
// the user must be a valid maintainer of the project
continue
}
for curOrg := range curOrgs {
// your group cannot lgtm your own pull request
if config.SelfApprovalOff && curOrg == authorOrg {
continue
}
// your group cannot approve twice
if approvergm[curOrg] {
continue
}
//found one!
approvergm[curOrg] = true
p(maintainer, comment)
}
}
return
}
80 changes: 80 additions & 0 deletions approval/org/org_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package org

import (
"github.com/lgtmco/lgtm/model"
"testing"
)

var maintainerToml = `
[people]
[people.bob]
login = "bob"
[people.fred]
login = "fred"
[people.jon]
login = "jon"
[people.ralph]
login = "ralph"
[people.george]
login = "george"

[org]
[org.cap]
people = [
"bob",
"fred",
"jon"
]

[org.iron]
people = [
"ralph",
"george"
]
`

func TestOrg(t *testing.T) {
config := &model.Config{
Pattern: `(?i)LGTM\s*(\S*)`,
SelfApprovalOff: true,
}
m, err := model.ParseMaintainerStr(maintainerToml)
if err != nil {
t.Fatal(err)
}
issue := &model.Issue{
Author: "jon",
}
comments := []*model.Comment{
{
Body: "lgtm",
Author: "bob",
},
{
Body: "lgtm",
Author: "qwerty",
},
{
Body: "not an approval",
Author: "ralph",
},
{
Body: "lgtm",
Author: "george",
},
{
Body: "lgtm",
Author: "ralph",
},
}
people := []string{}
Org(config, m, issue, comments, func(m *model.Maintainer, c *model.Comment) {
people = append(people, c.Author)
})
if len(people) != 1 {
t.Errorf("Expected one person, had %d", len(people))
}
if people[0] != "george" {
t.Errorf("Expected george, had %s", people[0])
}
}
1 change: 1 addition & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"net/http"
"time"

_ "github.com/lgtmco/lgtm/approval/org"
"github.com/lgtmco/lgtm/router"
"github.com/lgtmco/lgtm/router/middleware"

Expand Down
5 changes: 5 additions & 0 deletions model/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ type Config struct {
Approvals int `json:"approvals" toml:"approvals"`
Pattern string `json:"pattern" toml:"pattern"`
Team string `json:"team" toml:"team"`
ApprovalAlg string `json:"approval_algorithm" toml:"approval_algorithm"`
SelfApprovalOff bool `json:"self_approval_off" toml:"self_approval_off"`

re *regexp.Regexp
Expand All @@ -20,6 +21,7 @@ var (
approvals = envflag.Int("LGTM_APPROVALS", 2, "")
pattern = envflag.String("LGTM_PATTERN", "(?i)LGTM", "")
team = envflag.String("LGTM_TEAM", "MAINTAINERS", "")
approvalAlg = envflag.String("LGTM_APPROVAL_ALGORITHM", "simple", "")
selfApprovalOff = envflag.Bool("LGTM_SELF_APPROVAL_OFF", false, "")
)

Expand All @@ -44,6 +46,9 @@ func ParseConfigStr(data string) (*Config, error) {
if len(c.Team) == 0 {
c.Team = *team
}
if len(c.ApprovalAlg) == 0 {
c.ApprovalAlg = *approvalAlg
}
if c.SelfApprovalOff == false {
c.SelfApprovalOff = *selfApprovalOff
}
Expand Down
Loading