github.com/munnerz/test-infra@v0.0.0-20190108210205-ce3d181dc989/prow/plugins/approve/approve.go (about)

     1  /*
     2  Copyright 2017 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package approve
    18  
    19  import (
    20  	"fmt"
    21  	"regexp"
    22  	"sort"
    23  	"strconv"
    24  	"strings"
    25  	"time"
    26  
    27  	"github.com/sirupsen/logrus"
    28  
    29  	"k8s.io/test-infra/prow/github"
    30  	"k8s.io/test-infra/prow/labels"
    31  	"k8s.io/test-infra/prow/pluginhelp"
    32  	"k8s.io/test-infra/prow/plugins"
    33  	"k8s.io/test-infra/prow/plugins/approve/approvers"
    34  	"k8s.io/test-infra/prow/repoowners"
    35  )
    36  
    37  const (
    38  	// PluginName defines this plugin's registered name.
    39  	PluginName = "approve"
    40  
    41  	approveCommand  = "APPROVE"
    42  	cancelArgument  = "cancel"
    43  	lgtmCommand     = "LGTM"
    44  	noIssueArgument = "no-issue"
    45  )
    46  
    47  var (
    48  	associatedIssueRegex = regexp.MustCompile(`(?:kubernetes/[^/]+/issues/|#)(\d+)`)
    49  	commandRegex         = regexp.MustCompile(`(?m)^/([^\s]+)[\t ]*([^\n\r]*)`)
    50  	notificationRegex    = regexp.MustCompile(`(?is)^\[` + approvers.ApprovalNotificationName + `\] *?([^\n]*)(?:\n\n(.*))?`)
    51  
    52  	// deprecatedBotNames are the names of the bots that previously handled approvals.
    53  	// Each can be removed once every PR approved by the old bot has been merged or unapproved.
    54  	deprecatedBotNames = []string{"k8s-merge-robot", "openshift-merge-robot"}
    55  
    56  	// handleFunc is used to allow mocking out the behavior of 'handle' while testing.
    57  	handleFunc = handle
    58  )
    59  
    60  type githubClient interface {
    61  	GetPullRequest(org, repo string, number int) (*github.PullRequest, error)
    62  	GetPullRequestChanges(org, repo string, number int) ([]github.PullRequestChange, error)
    63  	GetIssueLabels(org, repo string, number int) ([]github.Label, error)
    64  	ListIssueComments(org, repo string, number int) ([]github.IssueComment, error)
    65  	ListReviews(org, repo string, number int) ([]github.Review, error)
    66  	ListPullRequestComments(org, repo string, number int) ([]github.ReviewComment, error)
    67  	DeleteComment(org, repo string, ID int) error
    68  	CreateComment(org, repo string, number int, comment string) error
    69  	BotName() (string, error)
    70  	AddLabel(org, repo string, number int, label string) error
    71  	RemoveLabel(org, repo string, number int, label string) error
    72  	ListIssueEvents(org, repo string, num int) ([]github.ListedIssueEvent, error)
    73  }
    74  
    75  type ownersClient interface {
    76  	LoadRepoOwners(org, repo, base string) (repoowners.RepoOwner, error)
    77  }
    78  
    79  type state struct {
    80  	org    string
    81  	repo   string
    82  	branch string
    83  	number int
    84  
    85  	body      string
    86  	author    string
    87  	assignees []github.User
    88  	htmlURL   string
    89  }
    90  
    91  func init() {
    92  	plugins.RegisterGenericCommentHandler(PluginName, handleGenericCommentEvent, helpProvider)
    93  	plugins.RegisterReviewEventHandler(PluginName, handleReviewEvent, helpProvider)
    94  	plugins.RegisterPullRequestHandler(PluginName, handlePullRequestEvent, helpProvider)
    95  }
    96  
    97  func helpProvider(config *plugins.Configuration, enabledRepos []string) (*pluginhelp.PluginHelp, error) {
    98  	doNot := func(b bool) string {
    99  		if b {
   100  			return ""
   101  		}
   102  		return "do not "
   103  	}
   104  	willNot := func(b bool) string {
   105  		if b {
   106  			return "will "
   107  		}
   108  		return "will not "
   109  	}
   110  
   111  	approveConfig := map[string]string{}
   112  	for _, repo := range enabledRepos {
   113  		parts := strings.Split(repo, "/")
   114  		if len(parts) != 2 {
   115  			return nil, fmt.Errorf("invalid repo in enabledRepos: %q", repo)
   116  		}
   117  		opts := optionsForRepo(config, parts[0], parts[1])
   118  		approveConfig[repo] = fmt.Sprintf("Pull requests %s require an associated issue.<br>Pull request authors %s implicitly approve their own PRs.<br>The /lgtm [cancel] command(s) %s act as approval.<br>A GitHub approved or changes requested review %s act as approval or cancel respectively.", doNot(opts.IssueRequired), doNot(opts.ImplicitSelfApprove), willNot(opts.LgtmActsAsApprove), willNot(opts.ReviewActsAsApprove))
   119  	}
   120  	pluginHelp := &pluginhelp.PluginHelp{
   121  		Description: `The approve plugin implements a pull request approval process that manages the '` + labels.Approved + `' label and an approval notification comment. Approval is achieved when the set of users that have approved the PR is capable of approving every file changed by the PR. A user is able to approve a file if their username or an alias they belong to is listed in the 'approvers' section of an OWNERS file in the directory of the file or higher in the directory tree.
   122  <br>
   123  <br>Per-repo configuration may be used to require that PRs link to an associated issue before approval is granted. It may also be used to specify that the PR authors implicitly approve their own PRs.
   124  <br>For more information see <a href="https://git.k8s.io/test-infra/prow/plugins/approve/approvers/README.md">here</a>.`,
   125  		Config: approveConfig,
   126  	}
   127  	pluginHelp.AddCommand(pluginhelp.Command{
   128  		Usage:       "/approve [no-issue|cancel]",
   129  		Description: "Approves a pull request",
   130  		Featured:    true,
   131  		WhoCanUse:   "Users listed as 'approvers' in appropriate OWNERS files.",
   132  		Examples:    []string{"/approve", "/approve no-issue"},
   133  	})
   134  	return pluginHelp, nil
   135  }
   136  
   137  func handleGenericCommentEvent(pc plugins.Agent, ce github.GenericCommentEvent) error {
   138  	return handleGenericComment(
   139  		pc.Logger,
   140  		pc.GitHubClient,
   141  		pc.OwnersClient,
   142  		pc.PluginConfig,
   143  		&ce,
   144  	)
   145  }
   146  
   147  func handleGenericComment(log *logrus.Entry, ghc githubClient, oc ownersClient, config *plugins.Configuration, ce *github.GenericCommentEvent) error {
   148  	if ce.Action != github.GenericCommentActionCreated || !ce.IsPR || ce.IssueState == "closed" {
   149  		return nil
   150  	}
   151  
   152  	botName, err := ghc.BotName()
   153  	if err != nil {
   154  		return err
   155  	}
   156  
   157  	opts := optionsForRepo(config, ce.Repo.Owner.Login, ce.Repo.Name)
   158  	if !isApprovalCommand(botName, opts.LgtmActsAsApprove, &comment{Body: ce.Body, Author: ce.User.Login}) {
   159  		return nil
   160  	}
   161  
   162  	pr, err := ghc.GetPullRequest(ce.Repo.Owner.Login, ce.Repo.Name, ce.Number)
   163  	if err != nil {
   164  		return err
   165  	}
   166  
   167  	repo, err := oc.LoadRepoOwners(ce.Repo.Owner.Login, ce.Repo.Name, pr.Base.Ref)
   168  	if err != nil {
   169  		return err
   170  	}
   171  
   172  	return handleFunc(
   173  		log,
   174  		ghc,
   175  		repo,
   176  		opts,
   177  		&state{
   178  			org:       ce.Repo.Owner.Login,
   179  			repo:      ce.Repo.Name,
   180  			branch:    pr.Base.Ref,
   181  			number:    ce.Number,
   182  			body:      ce.IssueBody,
   183  			author:    ce.IssueAuthor.Login,
   184  			assignees: ce.Assignees,
   185  			htmlURL:   ce.IssueHTMLURL,
   186  		},
   187  	)
   188  }
   189  
   190  // handleReviewEvent should only handle reviews that have no approval command.
   191  // Reviews with approval commands will be handled by handleGenericCommentEvent.
   192  func handleReviewEvent(pc plugins.Agent, re github.ReviewEvent) error {
   193  	return handleReview(
   194  		pc.Logger,
   195  		pc.GitHubClient,
   196  		pc.OwnersClient,
   197  		pc.PluginConfig,
   198  		&re,
   199  	)
   200  }
   201  
   202  func handleReview(log *logrus.Entry, ghc githubClient, oc ownersClient, config *plugins.Configuration, re *github.ReviewEvent) error {
   203  	if re.Action != github.ReviewActionSubmitted && re.Action != github.ReviewActionDismissed {
   204  		return nil
   205  	}
   206  
   207  	botName, err := ghc.BotName()
   208  	if err != nil {
   209  		return err
   210  	}
   211  
   212  	opts := optionsForRepo(config, re.Repo.Owner.Login, re.Repo.Name)
   213  
   214  	// Check for an approval command is in the body. If one exists, let the
   215  	// genericCommentEventHandler handle this event. Approval commands override
   216  	// review state.
   217  	if isApprovalCommand(botName, opts.LgtmActsAsApprove, &comment{Body: re.Review.Body, Author: re.Review.User.Login}) {
   218  		return nil
   219  	}
   220  
   221  	// Check for an approval command via review state. If none exists, don't
   222  	// handle this event.
   223  	if !isApprovalState(botName, opts.ReviewActsAsApprove, &comment{Author: re.Review.User.Login, ReviewState: re.Review.State}) {
   224  		return nil
   225  	}
   226  
   227  	repo, err := oc.LoadRepoOwners(re.Repo.Owner.Login, re.Repo.Name, re.PullRequest.Base.Ref)
   228  	if err != nil {
   229  		return err
   230  	}
   231  
   232  	return handleFunc(
   233  		log,
   234  		ghc,
   235  		repo,
   236  		optionsForRepo(config, re.Repo.Owner.Login, re.Repo.Name),
   237  		&state{
   238  			org:       re.Repo.Owner.Login,
   239  			repo:      re.Repo.Name,
   240  			branch:    re.PullRequest.Base.Ref,
   241  			number:    re.PullRequest.Number,
   242  			body:      re.PullRequest.Body,
   243  			author:    re.PullRequest.User.Login,
   244  			assignees: re.PullRequest.Assignees,
   245  			htmlURL:   re.PullRequest.HTMLURL,
   246  		},
   247  	)
   248  
   249  }
   250  
   251  func handlePullRequestEvent(pc plugins.Agent, pre github.PullRequestEvent) error {
   252  	return handlePullRequest(
   253  		pc.Logger,
   254  		pc.GitHubClient,
   255  		pc.OwnersClient,
   256  		pc.PluginConfig,
   257  		&pre,
   258  	)
   259  }
   260  
   261  func handlePullRequest(log *logrus.Entry, ghc githubClient, oc ownersClient, config *plugins.Configuration, pre *github.PullRequestEvent) error {
   262  	if pre.Action != github.PullRequestActionOpened &&
   263  		pre.Action != github.PullRequestActionReopened &&
   264  		pre.Action != github.PullRequestActionSynchronize &&
   265  		pre.Action != github.PullRequestActionLabeled {
   266  		return nil
   267  	}
   268  	botName, err := ghc.BotName()
   269  	if err != nil {
   270  		return err
   271  	}
   272  	if pre.Action == github.PullRequestActionLabeled &&
   273  		(pre.Label.Name != labels.Approved || pre.Sender.Login == botName || pre.PullRequest.State == "closed") {
   274  		return nil
   275  	}
   276  
   277  	repo, err := oc.LoadRepoOwners(pre.Repo.Owner.Login, pre.Repo.Name, pre.PullRequest.Base.Ref)
   278  	if err != nil {
   279  		return err
   280  	}
   281  
   282  	return handleFunc(
   283  		log,
   284  		ghc,
   285  		repo,
   286  		optionsForRepo(config, pre.Repo.Owner.Login, pre.Repo.Name),
   287  		&state{
   288  			org:       pre.Repo.Owner.Login,
   289  			repo:      pre.Repo.Name,
   290  			branch:    pre.PullRequest.Base.Ref,
   291  			number:    pre.Number,
   292  			body:      pre.PullRequest.Body,
   293  			author:    pre.PullRequest.User.Login,
   294  			assignees: pre.PullRequest.Assignees,
   295  			htmlURL:   pre.PullRequest.HTMLURL,
   296  		},
   297  	)
   298  }
   299  
   300  // Returns associated issue, or 0 if it can't find any.
   301  // This is really simple, and could be improved later.
   302  func findAssociatedIssue(body string) int {
   303  	match := associatedIssueRegex.FindStringSubmatch(body)
   304  	if len(match) == 0 {
   305  		return 0
   306  	}
   307  	v, err := strconv.Atoi(match[1])
   308  	if err != nil {
   309  		return 0
   310  	}
   311  	return v
   312  }
   313  
   314  // handle is the workhorse the will actually make updates to the PR.
   315  // The algorithm goes as:
   316  // - Initially, we build an approverSet
   317  //   - Go through all comments in order of creation.
   318  //     - (Issue/PR comments, PR review comments, and PR review bodies are considered as comments)
   319  //   - If anyone said "/approve", add them to approverSet.
   320  //   - If anyone said "/lgtm" AND LgtmActsAsApprove is enabled, add them to approverSet.
   321  //   - If anyone created an approved review AND ReviewActsAsApprove is enabled, add them to approverSet.
   322  // - Then, for each file, we see if any approver of this file is in approverSet and keep track of files without approval
   323  //   - An approver of a file is defined as:
   324  //     - Someone listed as an "approver" in an OWNERS file in the files directory OR
   325  //     - in one of the file's parent directories
   326  // - Iff all files have been approved, the bot will add the "approved" label.
   327  // - Iff a cancel command is found, that reviewer will be removed from the approverSet
   328  // 	and the munger will remove the approved label if it has been applied
   329  func handle(log *logrus.Entry, ghc githubClient, repo approvers.Repo, opts *plugins.Approve, pr *state) error {
   330  	fetchErr := func(context string, err error) error {
   331  		return fmt.Errorf("failed to get %s for %s/%s#%d: %v", context, pr.org, pr.repo, pr.number, err)
   332  	}
   333  
   334  	changes, err := ghc.GetPullRequestChanges(pr.org, pr.repo, pr.number)
   335  	if err != nil {
   336  		return fetchErr("PR file changes", err)
   337  	}
   338  	var filenames []string
   339  	for _, change := range changes {
   340  		filenames = append(filenames, change.Filename)
   341  	}
   342  	issueLabels, err := ghc.GetIssueLabels(pr.org, pr.repo, pr.number)
   343  	if err != nil {
   344  		return fetchErr("issue labels", err)
   345  	}
   346  	hasApprovedLabel := false
   347  	for _, label := range issueLabels {
   348  		if label.Name == labels.Approved {
   349  			hasApprovedLabel = true
   350  			break
   351  		}
   352  	}
   353  	botName, err := ghc.BotName()
   354  	if err != nil {
   355  		return fetchErr("bot name", err)
   356  	}
   357  	issueComments, err := ghc.ListIssueComments(pr.org, pr.repo, pr.number)
   358  	if err != nil {
   359  		return fetchErr("issue comments", err)
   360  	}
   361  	reviewComments, err := ghc.ListPullRequestComments(pr.org, pr.repo, pr.number)
   362  	if err != nil {
   363  		return fetchErr("review comments", err)
   364  	}
   365  	reviews, err := ghc.ListReviews(pr.org, pr.repo, pr.number)
   366  	if err != nil {
   367  		return fetchErr("reviews", err)
   368  	}
   369  
   370  	approversHandler := approvers.NewApprovers(
   371  		approvers.NewOwners(
   372  			log,
   373  			filenames,
   374  			repo,
   375  			int64(pr.number),
   376  		),
   377  	)
   378  	approversHandler.AssociatedIssue = findAssociatedIssue(pr.body)
   379  	approversHandler.RequireIssue = opts.IssueRequired
   380  	approversHandler.ManuallyApproved = humanAddedApproved(ghc, log, pr.org, pr.repo, pr.number, botName, hasApprovedLabel)
   381  
   382  	// Author implicitly approves their own PR if config allows it
   383  	if opts.ImplicitSelfApprove {
   384  		approversHandler.AddAuthorSelfApprover(pr.author, pr.htmlURL+"#", false)
   385  	} else {
   386  		// Treat the author as an assignee, and suggest them if possible
   387  		approversHandler.AddAssignees(pr.author)
   388  	}
   389  
   390  	commentsFromIssueComments := commentsFromIssueComments(issueComments)
   391  	comments := append(commentsFromReviewComments(reviewComments), commentsFromIssueComments...)
   392  	comments = append(comments, commentsFromReviews(reviews)...)
   393  	sort.SliceStable(comments, func(i, j int) bool {
   394  		return comments[i].CreatedAt.Before(comments[j].CreatedAt)
   395  	})
   396  	approveComments := filterComments(comments, approvalMatcher(botName, opts.LgtmActsAsApprove, opts.ReviewActsAsApprove))
   397  	addApprovers(&approversHandler, approveComments, pr.author, opts.ReviewActsAsApprove)
   398  
   399  	for _, user := range pr.assignees {
   400  		approversHandler.AddAssignees(user.Login)
   401  	}
   402  
   403  	notifications := filterComments(commentsFromIssueComments, notificationMatcher(botName))
   404  	latestNotification := getLast(notifications)
   405  	newMessage := updateNotification(pr.org, pr.repo, pr.branch, latestNotification, approversHandler)
   406  	if newMessage != nil {
   407  		for _, notif := range notifications {
   408  			if err := ghc.DeleteComment(pr.org, pr.repo, notif.ID); err != nil {
   409  				log.WithError(err).Errorf("Failed to delete comment from %s/%s#%d, ID: %d.", pr.org, pr.repo, pr.number, notif.ID)
   410  			}
   411  		}
   412  		if err := ghc.CreateComment(pr.org, pr.repo, pr.number, *newMessage); err != nil {
   413  			log.WithError(err).Errorf("Failed to create comment on %s/%s#%d: %q.", pr.org, pr.repo, pr.number, *newMessage)
   414  		}
   415  	}
   416  
   417  	if !approversHandler.IsApproved() {
   418  		if hasApprovedLabel {
   419  			if err := ghc.RemoveLabel(pr.org, pr.repo, pr.number, labels.Approved); err != nil {
   420  				log.WithError(err).Errorf("Failed to remove %q label from %s/%s#%d.", labels.Approved, pr.org, pr.repo, pr.number)
   421  			}
   422  		}
   423  	} else if !hasApprovedLabel {
   424  		if err := ghc.AddLabel(pr.org, pr.repo, pr.number, labels.Approved); err != nil {
   425  			log.WithError(err).Errorf("Failed to add %q label to %s/%s#%d.", labels.Approved, pr.org, pr.repo, pr.number)
   426  		}
   427  	}
   428  	return nil
   429  }
   430  
   431  func humanAddedApproved(ghc githubClient, log *logrus.Entry, org, repo string, number int, botName string, hasLabel bool) func() bool {
   432  	findOut := func() bool {
   433  		if !hasLabel {
   434  			return false
   435  		}
   436  		events, err := ghc.ListIssueEvents(org, repo, number)
   437  		if err != nil {
   438  			log.WithError(err).Errorf("Failed to list issue events for %s/%s#%d.", org, repo, number)
   439  			return false
   440  		}
   441  		var lastAdded github.ListedIssueEvent
   442  		for _, event := range events {
   443  			// Only consider "approved" label added events.
   444  			if event.Event != github.IssueActionLabeled || event.Label.Name != labels.Approved {
   445  				continue
   446  			}
   447  			lastAdded = event
   448  		}
   449  
   450  		if lastAdded.Actor.Login == "" || lastAdded.Actor.Login == botName || isDeprecatedBot(lastAdded.Actor.Login) {
   451  			return false
   452  		}
   453  		return true
   454  	}
   455  
   456  	var cache *bool
   457  	return func() bool {
   458  		if cache == nil {
   459  			val := findOut()
   460  			cache = &val
   461  		}
   462  		return *cache
   463  	}
   464  }
   465  
   466  func approvalMatcher(botName string, lgtmActsAsApprove, reviewActsAsApprove bool) func(*comment) bool {
   467  	return func(c *comment) bool {
   468  		return isApprovalCommand(botName, lgtmActsAsApprove, c) || isApprovalState(botName, reviewActsAsApprove, c)
   469  	}
   470  }
   471  
   472  func isApprovalCommand(botName string, lgtmActsAsApprove bool, c *comment) bool {
   473  	if c.Author == botName || isDeprecatedBot(c.Author) {
   474  		return false
   475  	}
   476  
   477  	for _, match := range commandRegex.FindAllStringSubmatch(c.Body, -1) {
   478  		cmd := strings.ToUpper(match[1])
   479  		if (cmd == lgtmCommand && lgtmActsAsApprove) || cmd == approveCommand {
   480  			return true
   481  		}
   482  	}
   483  	return false
   484  }
   485  
   486  func isApprovalState(botName string, reviewActsAsApprove bool, c *comment) bool {
   487  	if c.Author == botName || isDeprecatedBot(c.Author) {
   488  		return false
   489  	}
   490  
   491  	// The review webhook returns state as lowercase, while the review API
   492  	// returns state as uppercase. Uppercase the value here so it always
   493  	// matches the constant.
   494  	reviewState := github.ReviewState(strings.ToUpper(string(c.ReviewState)))
   495  
   496  	// ReviewStateApproved = /approve
   497  	// ReviewStateChangesRequested = /approve cancel
   498  	// ReviewStateDismissed = remove previous approval or disapproval
   499  	// (Reviews can go from Approved or ChangesRequested to Dismissed
   500  	// state if the Dismiss action is used)
   501  	if reviewActsAsApprove && (reviewState == github.ReviewStateApproved ||
   502  		reviewState == github.ReviewStateChangesRequested ||
   503  		reviewState == github.ReviewStateDismissed) {
   504  		return true
   505  	}
   506  	return false
   507  }
   508  
   509  func notificationMatcher(botName string) func(*comment) bool {
   510  	return func(c *comment) bool {
   511  		if c.Author != botName && !isDeprecatedBot(c.Author) {
   512  			return false
   513  		}
   514  		match := notificationRegex.FindStringSubmatch(c.Body)
   515  		return len(match) > 0
   516  	}
   517  }
   518  
   519  func updateNotification(org, project, branch string, latestNotification *comment, approversHandler approvers.Approvers) *string {
   520  	message := approvers.GetMessage(approversHandler, org, project, branch)
   521  	if message == nil || (latestNotification != nil && strings.Contains(latestNotification.Body, *message)) {
   522  		return nil
   523  	}
   524  	return message
   525  }
   526  
   527  // addApprovers iterates through the list of comments on a PR
   528  // and identifies all of the people that have said /approve and adds
   529  // them to the Approvers.  The function uses the latest approve or cancel comment
   530  // to determine the Users intention. A review in requested changes state is
   531  // considered a cancel.
   532  func addApprovers(approversHandler *approvers.Approvers, approveComments []*comment, author string, reviewActsAsApprove bool) {
   533  	for _, c := range approveComments {
   534  		if c.Author == "" {
   535  			continue
   536  		}
   537  
   538  		if reviewActsAsApprove && c.ReviewState == github.ReviewStateApproved {
   539  			approversHandler.AddApprover(
   540  				c.Author,
   541  				c.HTMLURL,
   542  				false,
   543  			)
   544  		}
   545  		if reviewActsAsApprove && c.ReviewState == github.ReviewStateChangesRequested {
   546  			approversHandler.RemoveApprover(c.Author)
   547  		}
   548  
   549  		for _, match := range commandRegex.FindAllStringSubmatch(c.Body, -1) {
   550  			name := strings.ToUpper(match[1])
   551  			if name != approveCommand && name != lgtmCommand {
   552  				continue
   553  			}
   554  			args := strings.ToLower(strings.TrimSpace(match[2]))
   555  			if strings.Contains(args, cancelArgument) {
   556  				approversHandler.RemoveApprover(c.Author)
   557  				continue
   558  			}
   559  
   560  			if c.Author == author {
   561  				approversHandler.AddAuthorSelfApprover(
   562  					c.Author,
   563  					c.HTMLURL,
   564  					args == noIssueArgument,
   565  				)
   566  			}
   567  
   568  			if name == approveCommand {
   569  				approversHandler.AddApprover(
   570  					c.Author,
   571  					c.HTMLURL,
   572  					args == noIssueArgument,
   573  				)
   574  			} else {
   575  				approversHandler.AddLGTMer(
   576  					c.Author,
   577  					c.HTMLURL,
   578  					args == noIssueArgument,
   579  				)
   580  			}
   581  
   582  		}
   583  	}
   584  }
   585  
   586  // optionsForRepo gets the plugins.Approve struct that is applicable to the indicated repo.
   587  func optionsForRepo(config *plugins.Configuration, org, repo string) *plugins.Approve {
   588  	fullName := fmt.Sprintf("%s/%s", org, repo)
   589  
   590  	// First search for repo config
   591  	for _, c := range config.Approve {
   592  		if !strInSlice(fullName, c.Repos) {
   593  			continue
   594  		}
   595  		return &c
   596  	}
   597  
   598  	// If you don't find anything, loop again looking for an org config
   599  	for _, c := range config.Approve {
   600  		if !strInSlice(org, c.Repos) {
   601  			continue
   602  		}
   603  		return &c
   604  	}
   605  
   606  	// Return an empty config, and use plugin defaults
   607  	return &plugins.Approve{}
   608  }
   609  
   610  func strInSlice(str string, slice []string) bool {
   611  	for _, elem := range slice {
   612  		if elem == str {
   613  			return true
   614  		}
   615  	}
   616  	return false
   617  }
   618  
   619  type comment struct {
   620  	Body        string
   621  	Author      string
   622  	CreatedAt   time.Time
   623  	HTMLURL     string
   624  	ID          int
   625  	ReviewState github.ReviewState
   626  }
   627  
   628  func commentFromIssueComment(ic *github.IssueComment) *comment {
   629  	if ic == nil {
   630  		return nil
   631  	}
   632  	return &comment{
   633  		Body:      ic.Body,
   634  		Author:    ic.User.Login,
   635  		CreatedAt: ic.CreatedAt,
   636  		HTMLURL:   ic.HTMLURL,
   637  		ID:        ic.ID,
   638  	}
   639  }
   640  
   641  func commentsFromIssueComments(ics []github.IssueComment) []*comment {
   642  	comments := []*comment{}
   643  	for i := range ics {
   644  		comments = append(comments, commentFromIssueComment(&ics[i]))
   645  	}
   646  	return comments
   647  }
   648  
   649  func commentFromReviewComment(rc *github.ReviewComment) *comment {
   650  	if rc == nil {
   651  		return nil
   652  	}
   653  	return &comment{
   654  		Body:      rc.Body,
   655  		Author:    rc.User.Login,
   656  		CreatedAt: rc.CreatedAt,
   657  		HTMLURL:   rc.HTMLURL,
   658  		ID:        rc.ID,
   659  	}
   660  }
   661  
   662  func commentsFromReviewComments(rcs []github.ReviewComment) []*comment {
   663  	comments := []*comment{}
   664  	for i := range rcs {
   665  		comments = append(comments, commentFromReviewComment(&rcs[i]))
   666  	}
   667  	return comments
   668  }
   669  
   670  func commentFromReview(review *github.Review) *comment {
   671  	if review == nil {
   672  		return nil
   673  	}
   674  	return &comment{
   675  		Body:        review.Body,
   676  		Author:      review.User.Login,
   677  		CreatedAt:   review.SubmittedAt,
   678  		HTMLURL:     review.HTMLURL,
   679  		ID:          review.ID,
   680  		ReviewState: review.State,
   681  	}
   682  }
   683  
   684  func commentsFromReviews(reviews []github.Review) []*comment {
   685  	comments := []*comment{}
   686  	for i := range reviews {
   687  		comments = append(comments, commentFromReview(&reviews[i]))
   688  	}
   689  	return comments
   690  }
   691  
   692  func filterComments(comments []*comment, filter func(*comment) bool) []*comment {
   693  	var filtered []*comment
   694  	for _, c := range comments {
   695  		if filter(c) {
   696  			filtered = append(filtered, c)
   697  		}
   698  	}
   699  	return filtered
   700  }
   701  
   702  func getLast(cs []*comment) *comment {
   703  	if len(cs) == 0 {
   704  		return nil
   705  	}
   706  	return cs[len(cs)-1]
   707  }
   708  
   709  func isDeprecatedBot(login string) bool {
   710  	for _, deprecated := range deprecatedBotNames {
   711  		if deprecated == login {
   712  			return true
   713  		}
   714  	}
   715  	return false
   716  }