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