github.com/shashidharatd/test-infra@v0.0.0-20171006011030-71304e1ca560/prow/plugins/assign/assign.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 assign
    18  
    19  import (
    20  	"fmt"
    21  	"regexp"
    22  	"strings"
    23  
    24  	"github.com/sirupsen/logrus"
    25  
    26  	"k8s.io/test-infra/prow/github"
    27  	"k8s.io/test-infra/prow/plugins"
    28  )
    29  
    30  const pluginName = "assign"
    31  
    32  var (
    33  	assignRe = regexp.MustCompile(`(?mi)^/(un)?assign(( @?[-\w]+?)*)\s*$`)
    34  	ccRe     = regexp.MustCompile(`(?mi)^/(un)?cc(( +@?[-\w]+?)*)\s*$`)
    35  )
    36  
    37  func init() {
    38  	plugins.RegisterGenericCommentHandler(pluginName, handleGenericComment)
    39  }
    40  
    41  type githubClient interface {
    42  	AssignIssue(owner, repo string, number int, logins []string) error
    43  	UnassignIssue(owner, repo string, number int, logins []string) error
    44  
    45  	RequestReview(org, repo string, number int, logins []string) error
    46  	UnrequestReview(org, repo string, number int, logins []string) error
    47  
    48  	CreateComment(owner, repo string, number int, comment string) error
    49  }
    50  
    51  func handleGenericComment(pc plugins.PluginClient, e github.GenericCommentEvent) error {
    52  	if e.Action != github.GenericCommentActionCreated {
    53  		return nil
    54  	}
    55  	err := handle(newAssignHandler(e, pc.GitHubClient, pc.Logger))
    56  	if e.IsPR {
    57  		err = combineErrors(err, handle(newReviewHandler(e, pc.GitHubClient, pc.Logger)))
    58  	}
    59  	return err
    60  }
    61  
    62  func parseLogins(text string) []string {
    63  	var parts []string
    64  	for _, p := range strings.Split(text, " ") {
    65  		t := strings.Trim(p, "@ ")
    66  		if t == "" {
    67  			continue
    68  		}
    69  		parts = append(parts, t)
    70  	}
    71  	return parts
    72  }
    73  
    74  func combineErrors(err1, err2 error) error {
    75  	if err1 != nil && err2 != nil {
    76  		return fmt.Errorf("two errors: 1) %v 2) %v", err1, err2)
    77  	} else if err1 != nil {
    78  		return err1
    79  	} else {
    80  		return err2
    81  	}
    82  }
    83  
    84  // handle is the generic handler for the assign plugin. It uses the handler's regexp and affectedLogins
    85  // functions to identify the users to add and/or remove and then passes the appropriate users to the
    86  // handler's add and remove functions. If add fails to add some of the users, a response comment is
    87  // created where the body of the response is generated by the handler's addFailureResponse function.
    88  func handle(h *handler) error {
    89  	e := h.event
    90  	org := e.Repo.Owner.Login
    91  	repo := e.Repo.Name
    92  	matches := h.regexp.FindAllStringSubmatch(e.Body, -1)
    93  	if matches == nil {
    94  		return nil
    95  	}
    96  	users := make(map[string]bool)
    97  	for _, re := range matches {
    98  		add := re[1] != "un" // un<cmd> == !add
    99  		if re[2] == "" {
   100  			users[e.User.Login] = add
   101  		} else {
   102  			for _, login := range parseLogins(re[2]) {
   103  				users[login] = add
   104  			}
   105  		}
   106  	}
   107  	var toAdd, toRemove []string
   108  	for login, add := range users {
   109  		if add {
   110  			toAdd = append(toAdd, login)
   111  		} else {
   112  			toRemove = append(toRemove, login)
   113  		}
   114  	}
   115  
   116  	if len(toRemove) > 0 {
   117  		h.log.Printf("Removing %s from %s/%s#%d: %v", h.userType, org, repo, e.Number, toRemove)
   118  		if err := h.remove(org, repo, e.Number, toRemove); err != nil {
   119  			return err
   120  		}
   121  	}
   122  	if len(toAdd) > 0 {
   123  		h.log.Printf("Adding %s to %s/%s#%d: %v", h.userType, org, repo, e.Number, toAdd)
   124  		if err := h.add(org, repo, e.Number, toAdd); err != nil {
   125  			if mu, ok := err.(github.MissingUsers); ok {
   126  				msg := h.addFailureResponse(mu)
   127  				if len(msg) == 0 {
   128  					return nil
   129  				}
   130  				if err := h.gc.CreateComment(org, repo, e.Number, plugins.FormatResponseRaw(e.Body, e.HTMLURL, e.User.Login, msg)); err != nil {
   131  					return fmt.Errorf("comment err: %v", err)
   132  				}
   133  				return nil
   134  			}
   135  			return err
   136  		}
   137  	}
   138  	return nil
   139  }
   140  
   141  // handler is a struct that contains data about a github event and provides functions to help handle it.
   142  type handler struct {
   143  	// addFailureResponse generates the body of a response comment in the event that the add function fails.
   144  	addFailureResponse func(mu github.MissingUsers) string
   145  	// remove is the function that is called on the affected logins for a command prefixed with 'un'.
   146  	remove func(org, repo string, number int, users []string) error
   147  	// add is the function that is called on the affected logins for a command with no 'un' prefix.
   148  	add func(org, repo string, number int, users []string) error
   149  
   150  	// event is a pointer to the github.GenericCommentEvent struct that triggered the handler.
   151  	event *github.GenericCommentEvent
   152  	// regexp is the regular expression describing the command. It must have an optional 'un' prefix
   153  	// as the first subgroup and the arguments to the command as the second subgroup.
   154  	regexp *regexp.Regexp
   155  	// gc is the githubClient to use for creating response comments in the event of a failure.
   156  	gc githubClient
   157  
   158  	// log is a logrus.Entry used to record actions the handler takes.
   159  	log *logrus.Entry
   160  	// userType is a string that represents the type of users affected by this handler. (e.g. 'assignees')
   161  	userType string
   162  }
   163  
   164  func newAssignHandler(e github.GenericCommentEvent, gc githubClient, log *logrus.Entry) *handler {
   165  	org := e.Repo.Owner.Login
   166  	addFailureResponse := func(mu github.MissingUsers) string {
   167  		return fmt.Sprintf("GitHub didn't allow me to assign the following users: %s.\n\nNote that only [%s members](https://github.com/orgs/%s/people) can be assigned.", strings.Join(mu.Users, ", "), org, org)
   168  	}
   169  
   170  	return &handler{
   171  		addFailureResponse: addFailureResponse,
   172  		remove:             gc.UnassignIssue,
   173  		add:                gc.AssignIssue,
   174  		event:              &e,
   175  		regexp:             assignRe,
   176  		gc:                 gc,
   177  		log:                log,
   178  		userType:           "assignee(s)",
   179  	}
   180  }
   181  
   182  func newReviewHandler(e github.GenericCommentEvent, gc githubClient, log *logrus.Entry) *handler {
   183  	org := e.Repo.Owner.Login
   184  	addFailureResponse := func(mu github.MissingUsers) string {
   185  		return fmt.Sprintf("GitHub didn't allow me to request PR reviews from the following users: %s.\n\nNote that only [%s members](https://github.com/orgs/%s/people) can review this PR, and authors cannot review their own PRs.", strings.Join(mu.Users, ", "), org, org)
   186  	}
   187  
   188  	return &handler{
   189  		addFailureResponse: addFailureResponse,
   190  		remove:             gc.UnrequestReview,
   191  		add:                gc.RequestReview,
   192  		event:              &e,
   193  		regexp:             ccRe,
   194  		gc:                 gc,
   195  		log:                log,
   196  		userType:           "reviewer(s)",
   197  	}
   198  }