github.com/abayer/test-infra@v0.0.5/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/pluginhelp"
    28  	"k8s.io/test-infra/prow/plugins"
    29  )
    30  
    31  const pluginName = "assign"
    32  
    33  var (
    34  	assignRe = regexp.MustCompile(`(?mi)^/(un)?assign(( @?[-\w]+?)*)\s*$`)
    35  	// CCRegexp parses and validates /cc commands, also used by blunderbuss
    36  	CCRegexp = regexp.MustCompile(`(?mi)^/(un)?cc(( +@?[-/\w]+?)*)\s*$`)
    37  )
    38  
    39  func init() {
    40  	plugins.RegisterGenericCommentHandler(pluginName, handleGenericComment, helpProvider)
    41  }
    42  
    43  func helpProvider(config *plugins.Configuration, enabledRepos []string) (*pluginhelp.PluginHelp, error) {
    44  	// The Config field is omitted because this plugin is not configurable.
    45  	pluginHelp := &pluginhelp.PluginHelp{
    46  		Description: "The assign plugin assigns or requests reviews from users. Specific users can be assigned with the command '/assign @user1' or have reviews requested of them with the command '/cc @user1'. If no user is specified the commands default to targeting the user who created the command. Assignments and requested reviews can be removed in the same way that they are added by prefixing the commands with 'un'.",
    47  	}
    48  	pluginHelp.AddCommand(pluginhelp.Command{
    49  		Usage:       "/[un]assign [[@]<username>...]",
    50  		Description: "Assigns an assignee to the PR",
    51  		Featured:    true,
    52  		WhoCanUse:   "Anyone can use the command, but the target user must be a member of the org that owns the repository.",
    53  		Examples:    []string{"/assign", "/unassign", "/assign @k8s-ci-robot"},
    54  	})
    55  	pluginHelp.AddCommand(pluginhelp.Command{
    56  		Usage:       "/[un]cc [[@]<username>...]",
    57  		Description: "Requests a review from the user(s).",
    58  		Featured:    true,
    59  		WhoCanUse:   "Anyone can use the command, but the target user must be a member of the org that owns the repository.",
    60  		Examples:    []string{"/cc", "/uncc", "/cc @k8s-ci-robot"},
    61  	})
    62  	return pluginHelp, nil
    63  }
    64  
    65  type githubClient interface {
    66  	AssignIssue(owner, repo string, number int, logins []string) error
    67  	UnassignIssue(owner, repo string, number int, logins []string) error
    68  
    69  	RequestReview(org, repo string, number int, logins []string) error
    70  	UnrequestReview(org, repo string, number int, logins []string) error
    71  
    72  	CreateComment(owner, repo string, number int, comment string) error
    73  }
    74  
    75  func handleGenericComment(pc plugins.PluginClient, e github.GenericCommentEvent) error {
    76  	if e.Action != github.GenericCommentActionCreated {
    77  		return nil
    78  	}
    79  	err := handle(newAssignHandler(e, pc.GitHubClient, pc.Logger))
    80  	if e.IsPR {
    81  		err = combineErrors(err, handle(newReviewHandler(e, pc.GitHubClient, pc.Logger)))
    82  	}
    83  	return err
    84  }
    85  
    86  func parseLogins(text string) []string {
    87  	var parts []string
    88  	for _, p := range strings.Split(text, " ") {
    89  		t := strings.Trim(p, "@ ")
    90  		if t == "" {
    91  			continue
    92  		}
    93  		parts = append(parts, t)
    94  	}
    95  	return parts
    96  }
    97  
    98  func combineErrors(err1, err2 error) error {
    99  	if err1 != nil && err2 != nil {
   100  		return fmt.Errorf("two errors: 1) %v 2) %v", err1, err2)
   101  	} else if err1 != nil {
   102  		return err1
   103  	} else {
   104  		return err2
   105  	}
   106  }
   107  
   108  // handle is the generic handler for the assign plugin. It uses the handler's regexp and affectedLogins
   109  // functions to identify the users to add and/or remove and then passes the appropriate users to the
   110  // handler's add and remove functions. If add fails to add some of the users, a response comment is
   111  // created where the body of the response is generated by the handler's addFailureResponse function.
   112  func handle(h *handler) error {
   113  	e := h.event
   114  	org := e.Repo.Owner.Login
   115  	repo := e.Repo.Name
   116  	matches := h.regexp.FindAllStringSubmatch(e.Body, -1)
   117  	if matches == nil {
   118  		return nil
   119  	}
   120  	users := make(map[string]bool)
   121  	for _, re := range matches {
   122  		add := re[1] != "un" // un<cmd> == !add
   123  		if re[2] == "" {
   124  			users[e.User.Login] = add
   125  		} else {
   126  			for _, login := range parseLogins(re[2]) {
   127  				users[login] = add
   128  			}
   129  		}
   130  	}
   131  	var toAdd, toRemove []string
   132  	for login, add := range users {
   133  		if add {
   134  			toAdd = append(toAdd, login)
   135  		} else {
   136  			toRemove = append(toRemove, login)
   137  		}
   138  	}
   139  
   140  	if len(toRemove) > 0 {
   141  		h.log.Printf("Removing %s from %s/%s#%d: %v", h.userType, org, repo, e.Number, toRemove)
   142  		if err := h.remove(org, repo, e.Number, toRemove); err != nil {
   143  			return err
   144  		}
   145  	}
   146  	if len(toAdd) > 0 {
   147  		h.log.Printf("Adding %s to %s/%s#%d: %v", h.userType, org, repo, e.Number, toAdd)
   148  		if err := h.add(org, repo, e.Number, toAdd); err != nil {
   149  			if mu, ok := err.(github.MissingUsers); ok {
   150  				msg := h.addFailureResponse(mu)
   151  				if len(msg) == 0 {
   152  					return nil
   153  				}
   154  				if err := h.gc.CreateComment(org, repo, e.Number, plugins.FormatResponseRaw(e.Body, e.HTMLURL, e.User.Login, msg)); err != nil {
   155  					return fmt.Errorf("comment err: %v", err)
   156  				}
   157  				return nil
   158  			}
   159  			return err
   160  		}
   161  	}
   162  	return nil
   163  }
   164  
   165  // handler is a struct that contains data about a github event and provides functions to help handle it.
   166  type handler struct {
   167  	// addFailureResponse generates the body of a response comment in the event that the add function fails.
   168  	addFailureResponse func(mu github.MissingUsers) string
   169  	// remove is the function that is called on the affected logins for a command prefixed with 'un'.
   170  	remove func(org, repo string, number int, users []string) error
   171  	// add is the function that is called on the affected logins for a command with no 'un' prefix.
   172  	add func(org, repo string, number int, users []string) error
   173  
   174  	// event is a pointer to the github.GenericCommentEvent struct that triggered the handler.
   175  	event *github.GenericCommentEvent
   176  	// regexp is the regular expression describing the command. It must have an optional 'un' prefix
   177  	// as the first subgroup and the arguments to the command as the second subgroup.
   178  	regexp *regexp.Regexp
   179  	// gc is the githubClient to use for creating response comments in the event of a failure.
   180  	gc githubClient
   181  
   182  	// log is a logrus.Entry used to record actions the handler takes.
   183  	log *logrus.Entry
   184  	// userType is a string that represents the type of users affected by this handler. (e.g. 'assignees')
   185  	userType string
   186  }
   187  
   188  func newAssignHandler(e github.GenericCommentEvent, gc githubClient, log *logrus.Entry) *handler {
   189  	org := e.Repo.Owner.Login
   190  	addFailureResponse := func(mu github.MissingUsers) string {
   191  		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) and repo collaborators can be assigned. \nFor more information please see [the contributor guide](https://git.k8s.io/community/contributors/guide/#issue-assignment-in-github)", strings.Join(mu.Users, ", "), org, org)
   192  	}
   193  
   194  	return &handler{
   195  		addFailureResponse: addFailureResponse,
   196  		remove:             gc.UnassignIssue,
   197  		add:                gc.AssignIssue,
   198  		event:              &e,
   199  		regexp:             assignRe,
   200  		gc:                 gc,
   201  		log:                log,
   202  		userType:           "assignee(s)",
   203  	}
   204  }
   205  
   206  func newReviewHandler(e github.GenericCommentEvent, gc githubClient, log *logrus.Entry) *handler {
   207  	org := e.Repo.Owner.Login
   208  	addFailureResponse := func(mu github.MissingUsers) string {
   209  		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) and repo collaborators can review this PR, and authors cannot review their own PRs.", strings.Join(mu.Users, ", "), org, org)
   210  	}
   211  
   212  	return &handler{
   213  		addFailureResponse: addFailureResponse,
   214  		remove:             gc.UnrequestReview,
   215  		add:                gc.RequestReview,
   216  		event:              &e,
   217  		regexp:             CCRegexp,
   218  		gc:                 gc,
   219  		log:                log,
   220  		userType:           "reviewer(s)",
   221  	}
   222  }