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