github.com/munnerz/test-infra@v0.0.0-20190108210205-ce3d181dc989/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.Agent, 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 and that issues/PRs can only have 10 assignees at the same time.\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 }