github.com/shashidharatd/test-infra@v0.0.0-20171006011030-71304e1ca560/mungegithub/mungers/approval-handler.go (about)

     1  /*
     2  Copyright 2016 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 mungers
    18  
    19  import (
    20  	"regexp"
    21  	"sort"
    22  	"strconv"
    23  
    24  	githubapi "github.com/google/go-github/github"
    25  
    26  	"k8s.io/kubernetes/pkg/util/sets"
    27  	"k8s.io/test-infra/mungegithub/features"
    28  	"k8s.io/test-infra/mungegithub/github"
    29  	"k8s.io/test-infra/mungegithub/mungers/approvers"
    30  	c "k8s.io/test-infra/mungegithub/mungers/matchers/comment"
    31  	"k8s.io/test-infra/mungegithub/mungers/matchers/event"
    32  	"k8s.io/test-infra/mungegithub/options"
    33  )
    34  
    35  const (
    36  	approveCommand  = "APPROVE"
    37  	lgtmCommand     = "LGTM"
    38  	cancelArgument  = "cancel"
    39  	noIssueArgument = "no-issue"
    40  )
    41  
    42  var AssociatedIssueRegex = regexp.MustCompile(`(?:kubernetes/[^/]+/issues/|#)(\d+)`)
    43  
    44  // ApprovalHandler will try to add "approved" label once
    45  // all files of change has been approved by approvers.
    46  type ApprovalHandler struct {
    47  	botName              string
    48  	features             *features.Features
    49  	issueRequired        bool
    50  	implicitSelfApproval bool
    51  }
    52  
    53  func init() {
    54  	h := &ApprovalHandler{}
    55  	RegisterMungerOrDie(h)
    56  }
    57  
    58  // Name is the name usable in --pr-mungers
    59  func (*ApprovalHandler) Name() string { return "approval-handler" }
    60  
    61  // RequiredFeatures is a slice of 'features' that must be provided
    62  func (*ApprovalHandler) RequiredFeatures() []string {
    63  	return []string{features.RepoFeatureName}
    64  }
    65  
    66  // Initialize will initialize the munger
    67  func (h *ApprovalHandler) Initialize(config *github.Config, features *features.Features) error {
    68  	h.botName = config.BotName
    69  	h.features = features
    70  	return nil
    71  }
    72  
    73  // EachLoop is called at the start of every munge loop
    74  func (*ApprovalHandler) EachLoop() error { return nil }
    75  
    76  // RegisterOptions registers options for this munger; returns any that require a restart when changed.
    77  func (h *ApprovalHandler) RegisterOptions(opts *options.Options) sets.String {
    78  	opts.RegisterBool(&h.issueRequired, "approval-requires-issue", false, "[approval-handler] flag indicating if all "+
    79  		"PRs must be associated with an issue in order to get approved label")
    80  	opts.RegisterBool(&h.implicitSelfApproval, "implicit-self-approval", false, "[approval-handler] flag indicating if "+
    81  		"a PR author implicitly approves their own PR")
    82  	return nil
    83  }
    84  
    85  // Returns associated issue, or 0 if it can't find any.
    86  // This is really simple, and could be improved later.
    87  func findAssociatedIssue(body *string) int {
    88  	if body == nil {
    89  		return 0
    90  	}
    91  	match := AssociatedIssueRegex.FindStringSubmatch(*body)
    92  	if len(match) == 0 {
    93  		return 0
    94  	}
    95  	v, err := strconv.Atoi(match[1])
    96  	if err != nil {
    97  		return 0
    98  	}
    99  	return v
   100  }
   101  
   102  // Munge is the workhorse the will actually make updates to the PR
   103  // The algorithm goes as:
   104  // - Initially, we build an approverSet
   105  //   - Go through all comments in order of creation.
   106  //		 - (Issue/PR comments, PR review comments, and PR review bodies are considered as comments)
   107  //	 - If anyone said "/approve" or "/lgtm", add them to approverSet.
   108  // - Then, for each file, we see if any approver of this file is in approverSet and keep track of files without approval
   109  //   - An approver of a file is defined as:
   110  //     - Someone listed as an "approver" in an OWNERS file in the files directory OR
   111  //     - in one of the file's parent directorie
   112  // - Iff all files have been approved, the bot will add the "approved" label.
   113  // - Iff a cancel command is found, that reviewer will be removed from the approverSet
   114  // 	and the munger will remove the approved label if it has been applied
   115  func (h *ApprovalHandler) Munge(obj *github.MungeObject) {
   116  	if !obj.IsPR() {
   117  		return
   118  	}
   119  	filenames := []string{}
   120  	files, ok := obj.ListFiles()
   121  	if !ok {
   122  		return
   123  	}
   124  	for _, fn := range files {
   125  		filenames = append(filenames, *fn.Filename)
   126  	}
   127  	issueComments, ok := obj.ListComments()
   128  	if !ok {
   129  		return
   130  	}
   131  	reviewComments, ok := obj.ListReviewComments()
   132  	if !ok {
   133  		return
   134  	}
   135  	reviews, ok := obj.ListReviews()
   136  	if !ok {
   137  		return
   138  	}
   139  	commentsFromIssueComments := c.FromIssueComments(issueComments)
   140  	comments := append(c.FromReviewComments(reviewComments), commentsFromIssueComments...)
   141  	comments = append(comments, c.FromReviews(reviews)...)
   142  	sort.SliceStable(comments, func(i, j int) bool {
   143  		return comments[i].CreatedAt.Before(*comments[j].CreatedAt)
   144  	})
   145  	approveComments := getApproveComments(comments, h.botName)
   146  
   147  	approversHandler := approvers.NewApprovers(
   148  		approvers.NewOwners(
   149  			filenames,
   150  			h.features.Repos,
   151  			int64(*obj.Issue.Number)))
   152  	approversHandler.AssociatedIssue = findAssociatedIssue(obj.Issue.Body)
   153  	approversHandler.RequireIssue = h.issueRequired
   154  	addApprovers(&approversHandler, approveComments)
   155  	// Author implicitly approves their own PR if config allows it
   156  	if h.implicitSelfApproval {
   157  		if obj.Issue.User != nil && obj.Issue.User.Login != nil {
   158  			url := ""
   159  			if obj.Issue.HTMLURL != nil {
   160  				// Append extra # so that it doesn't reload the page.
   161  				url = *obj.Issue.HTMLURL + "#"
   162  			}
   163  			approversHandler.AddAuthorSelfApprover(*obj.Issue.User.Login, url)
   164  		}
   165  	}
   166  
   167  	for _, user := range obj.Issue.Assignees {
   168  		if user != nil && user.Login != nil {
   169  			approversHandler.AddAssignees(*user.Login)
   170  		}
   171  	}
   172  
   173  	notificationMatcher := c.MungerNotificationName(approvers.ApprovalNotificationName, h.botName)
   174  
   175  	notifications := c.FilterComments(commentsFromIssueComments, notificationMatcher)
   176  	latestNotification := notifications.GetLast()
   177  	latestApprove := approveComments.GetLast()
   178  	newMessage := h.updateNotification(obj.Org(), obj.Project(), latestNotification, latestApprove, approversHandler)
   179  	if newMessage != nil {
   180  		for _, notif := range notifications {
   181  			obj.DeleteComment(notif.Source.(*githubapi.IssueComment))
   182  		}
   183  		obj.WriteComment(*newMessage)
   184  	}
   185  
   186  	if !approversHandler.IsApproved() {
   187  		if obj.HasLabel(approvedLabel) && !humanAddedApproved(obj) {
   188  			obj.RemoveLabel(approvedLabel)
   189  		}
   190  	} else {
   191  		//pr is fully approved
   192  		if !obj.HasLabel(approvedLabel) {
   193  			obj.AddLabel(approvedLabel)
   194  		}
   195  	}
   196  
   197  }
   198  
   199  func humanAddedApproved(obj *github.MungeObject) bool {
   200  	events, ok := obj.GetEvents()
   201  	if !ok {
   202  		return false
   203  	}
   204  	approveAddedMatcher := event.And([]event.Matcher{event.AddLabel{}, event.LabelName(approvedLabel)})
   205  	labelEvents := event.FilterEvents(events, approveAddedMatcher)
   206  	lastAdded := labelEvents.GetLast()
   207  	if lastAdded == nil || lastAdded.Actor == nil || lastAdded.Actor.Login == nil {
   208  		return false
   209  	}
   210  	return !obj.IsRobot(lastAdded.Actor)
   211  }
   212  
   213  func getApproveComments(comments []*c.Comment, botName string) c.FilteredComments {
   214  	approverMatcher := c.CommandName(approveCommand)
   215  	lgtmMatcher := c.CommandName(lgtmLabel)
   216  	return c.FilterComments(comments, c.And{c.HumanActor(botName), c.Or{approverMatcher, lgtmMatcher}})
   217  }
   218  
   219  func (h *ApprovalHandler) updateNotification(org, project string, latestNotification, latestApprove *c.Comment, approversHandler approvers.Approvers) *string {
   220  	if latestNotification != nil && (latestApprove == nil || latestApprove.CreatedAt.Before(*latestNotification.CreatedAt)) {
   221  		// if we have an existing notification AND
   222  		// the latestApprove happened before we updated
   223  		// the notification, we do NOT need to update
   224  		return nil
   225  	}
   226  	return approvers.GetMessage(approversHandler, org, project)
   227  }
   228  
   229  // addApprovers iterates through the list of comments on a PR
   230  // and identifies all of the people that have said /approve and adds
   231  // them to the Approvers.  The function uses the latest approve or cancel comment
   232  // to determine the Users intention
   233  func addApprovers(approversHandler *approvers.Approvers, approveComments c.FilteredComments) {
   234  	for _, comment := range approveComments {
   235  		commands := c.ParseCommands(comment)
   236  		for _, cmd := range commands {
   237  			if cmd.Name != approveCommand && cmd.Name != lgtmCommand {
   238  				continue
   239  			}
   240  			if comment.Author == nil {
   241  				continue
   242  			}
   243  
   244  			if cmd.Arguments == cancelArgument {
   245  				approversHandler.RemoveApprover(*comment.Author)
   246  			} else {
   247  				url := ""
   248  				if comment.HTMLURL != nil {
   249  					url = *comment.HTMLURL
   250  				}
   251  
   252  				if cmd.Name == approveCommand {
   253  					approversHandler.AddApprover(
   254  						*comment.Author,
   255  						url,
   256  						cmd.Arguments == noIssueArgument,
   257  					)
   258  				} else {
   259  					approversHandler.AddLGTMer(
   260  						*comment.Author,
   261  						url,
   262  						cmd.Arguments == noIssueArgument,
   263  					)
   264  				}
   265  
   266  			}
   267  		}
   268  	}
   269  }