// Munge is the workhorse the will actually make updates to the PR func (b *BlockPath) Munge(obj *github.MungeObject) { if !obj.IsPR() { return } if obj.HasLabel(doNotMergeLabel) { return } files, err := obj.ListFiles() if err != nil { return } for _, f := range files { if matchesAny(*f.Filename, b.blockRegexp) { if matchesAny(*f.Filename, b.doNotBlockRegexp) { continue } obj.WriteComment(blockPathBody) obj.AddLabels([]string{doNotMergeLabel}) return } } }
func mergeTime(obj *github.MungeObject) time.Time { t := obj.MergedAt() if t == nil { t = &maxTime } return *t }
// Munge is the workhorse the will actually make updates to the PR func (h AssignUnassignHandler) Munge(obj *github.MungeObject) { if !obj.IsPR() { return } comments, err := obj.ListComments() if err != nil { glog.Errorf("unexpected error getting comments: %v", err) return } fileList, err := obj.ListFiles() if err != nil { glog.Errorf("Could not list the files for PR %v: %v", obj.Issue.Number, err) return } //get ALL (not just leaf) the people that could potentially own the file based on the blunderbuss.go implementation potentialOwners, _ := getPotentialOwners(*obj.Issue.User.Login, h.features, fileList, false) toAssign, toUnassign := h.getAssigneesAndUnassignees(obj, comments, fileList, potentialOwners) for _, username := range toAssign.List() { obj.AssignPR(username) } obj.UnassignPR(toUnassign.List()...) }
func (h *LGTMHandler) addLGTMIfCommented(obj *github.MungeObject, comments []*githubapi.IssueComment, reviewers mungerutil.UserSet) { // Assumption: The comments should be sorted (by default from github api) from oldest to latest for i := len(comments) - 1; i >= 0; i-- { comment := comments[i] if !mungerutil.IsValidUser(comment.User) { continue } // TODO: An approver should be acceptable. // See https://github.com/kubernetes/contrib/pull/1428#discussion_r72563935 if !mungerutil.IsMungeBot(comment.User) && !isReviewer(comment.User, reviewers) { continue } fields := getFields(*comment.Body) if isCancelComment(fields) { // "/lgtm cancel" if commented more recently than "/lgtm" return } if !isLGTMComment(fields) { continue } // TODO: support more complex policies for multiple reviewers. // See https://github.com/kubernetes/contrib/issues/1389#issuecomment-235161164 glog.Infof("Adding lgtm label. Reviewer (%s) LGTM", *comment.User.Login) obj.AddLabel(lgtmLabel) return } }
// Munge is the workhorse the will actually make updates to the PR func (c *CherrypickQueue) Munge(obj *github.MungeObject) { if !obj.HasLabel(cpCandidateLabel) { return } if !obj.IsPR() { return } // This will cache the PR and events so when we try to view the queue we don't // hit github while trying to load the page obj.GetPR() num := *obj.Issue.Number c.Lock() merged, _ := obj.IsMerged() if merged { if obj.HasLabel(cpApprovedLabel) { c.mergedAndApproved[num] = obj } else { c.merged[num] = obj } } else { c.unmerged[num] = obj } c.Unlock() return }
// Munge is the workhorse the will actually make updates to the PR func (b *BlockPath) Munge(obj *github.MungeObject) { if !obj.IsPR() { return } if obj.HasLabel(doNotMergeLabel) { return } commits, err := obj.GetCommits() if err != nil { return } for _, c := range commits { for _, f := range c.Files { if matchesAny(*f.Filename, b.blockRegexp) { if matchesAny(*f.Filename, b.doNotBlockRegexp) { continue } body := fmt.Sprintf(`Adding label:%s because PR changes docs prohibited to auto merge See http://kubernetes.io/editdocs/ for information about editing docs`, doNotMergeLabel) obj.WriteComment(body) obj.AddLabels([]string{doNotMergeLabel}) return } } } }
func (h *LGTMHandler) removeLGTMIfCancelled(obj *github.MungeObject, comments []*githubapi.IssueComment, reviewers mungerutil.UserSet) { for i := len(comments) - 1; i >= 0; i-- { comment := comments[i] if !mungerutil.IsValidUser(comment.User) { continue } if !mungerutil.IsMungeBot(comment.User) && !isReviewer(comment.User, reviewers) { continue } fields := getFields(*comment.Body) if isLGTMComment(fields) { // "/lgtm" if commented more recently than "/lgtm cancel" return } if !isCancelComment(fields) { continue } glog.Infof("Removing lgtm label. Reviewer (%s) cancelled", *comment.User.Login) obj.RemoveLabel(lgtmLabel) return } }
// Find the last warning comment that the bot has posted. // It can return an empty comment if it fails to find one, even if there are no errors. func findLatestWarningComment(obj *github.MungeObject) (*githubapi.IssueComment, error) { var lastFoundComment *githubapi.IssueComment comments, err := obj.ListComments() if err != nil { return nil, err } for i := range comments { comment := comments[i] if !validComment(comment) { continue } if !mergeBotComment(comment) { continue } if !warningCommentRE.MatchString(*comment.Body) { continue } if lastFoundComment == nil || lastFoundComment.CreatedAt.Before(*comment.UpdatedAt) { if lastFoundComment != nil { obj.DeleteComment(lastFoundComment) } lastFoundComment = comment } } return lastFoundComment, nil }
func (h *LGTMHandler) removeLGTMIfCancelled(obj *github.MungeObject, comments []*githubapi.IssueComment, events []*githubapi.IssueEvent, reviewers mungerutil.UserSet) { // Get time when the last (unlabeled, lgtm) event occurred. addLGTMTime := e.LastEvent(events, e.And{e.AddLabel{}, e.LabelName(lgtmLabel), e.HumanActor()}, nil) for i := len(comments) - 1; i >= 0; i-- { comment := comments[i] if !mungerutil.IsValidUser(comment.User) { continue } if !mungerutil.IsMungeBot(comment.User) && !isReviewer(comment.User, reviewers) { continue } fields := getFields(*comment.Body) if isLGTMComment(fields) { // "/lgtm" if commented more recently than "/lgtm cancel" return } if !isCancelComment(fields) { continue } // check if someone manually added the lgtm label after the `/lgtm cancel` comment // and honor it. if addLGTMTime != nil && addLGTMTime.After(*comment.CreatedAt) { return } glog.Infof("Removing lgtm label. Reviewer (%s) cancelled", *comment.User.Login) obj.RemoveLabel(lgtmLabel) return } }
// Munge is the workhorse that will actually close the PRs func (CloseStalePR) Munge(obj *github.MungeObject) { if !obj.IsPR() { return } if obj.HasLabel(keepOpenLabel) { return } lastModif, err := findLastModificationTime(obj) if err != nil { glog.Errorf("Failed to find last modification: %v", err) return } closeIn := -time.Since(lastModif.Add(stalePullRequest)) inactiveFor := time.Since(*lastModif) if closeIn <= 0 { closePullRequest(obj, inactiveFor) } else if closeIn <= startWarning { checkAndWarn(obj, inactiveFor, closeIn) } else { // Pull-request is active. Do nothing } }
// Process does the necessary processing to compute whether to stay in // this state, or proceed to the next. func (c *ChangesNeeded) Process(obj *github.MungeObject) (State, error) { if !obj.HasLabel(labelChangesNeeded) { obj.AddLabel(labelChangesNeeded) glog.Infof("PR #%v needs changes from author", *obj.Issue.Number) } return &End{}, nil }
func findLastHumanPullRequestUpdate(obj *github.MungeObject) (*time.Time, error) { pr, err := obj.GetPR() if err != nil { return nil, err } comments, err := obj.ListReviewComments() if err != nil { return nil, err } lastHuman := pr.CreatedAt for i := range comments { comment := comments[i] if comment.User == nil || comment.User.Login == nil || comment.CreatedAt == nil || comment.Body == nil { continue } if *comment.User.Login == botName || *comment.User.Login == jenkinsBotName { continue } if lastHuman.Before(*comment.UpdatedAt) { lastHuman = comment.UpdatedAt } } return lastHuman, nil }
// MungeIssue is the real worker. It is called for every open github Issue // But note that all PRs are Issues. (Not all Issues are PRs.) This particular // function ignores all issues that are not PRs and prints the number for the // PRs. func MungeIssue(obj *github.MungeObject) error { if !obj.IsPR() { return nil } glog.Infof("PR: %d", *obj.Issue.Number) return nil }
// SetMergeStatus will set the status given a particular PR. This function should // be used instead of manipulating the prStatus directly as sq.Lock() must be // called when manipulating that structure // `obj` is the active github object // `reason` is the new 'status' for this object func (sq *SubmitQueue) SetMergeStatus(obj *github.MungeObject, reason string) { glog.V(4).Infof("SubmitQueue not merging %d because %q", *obj.Issue.Number, reason) submitStatus := submitStatus{ Time: sq.clock.Now(), statusPullRequest: *objToStatusPullRequest(obj), Reason: reason, } status := obj.GetStatus(sqContext) if status == nil || *status.Description != reason { state := reasonToState(reason) url := fmt.Sprintf("http://submit-queue.k8s.io/#/prs/?prDisplay=%d&historyDisplay=%d", *obj.Issue.Number, *obj.Issue.Number) _ = obj.SetStatus(state, url, reason, sqContext) } sq.Lock() defer sq.Unlock() // If we are currently retesting E2E the normal munge loop might find // that the ci tests are not green. That's normal and expected and we // should just ignore that status update entirely. if sq.githubE2ERunning != nil && *sq.githubE2ERunning.Issue.Number == *obj.Issue.Number && reason == ciFailure { return } if sq.onQueue(obj) { sq.statusHistory = append(sq.statusHistory, submitStatus) if len(sq.statusHistory) > 128 { sq.statusHistory = sq.statusHistory[1:] } } sq.prStatus[strconv.Itoa(*obj.Issue.Number)] = submitStatus sq.cleanupOldE2E(obj, reason) }
// getGeneratedFiles returns a list of all automatically generated files in the repo. These include // docs, deep_copy, and conversions // // It would be 'better' to call this for every commit but that takes // a whole lot of time for almost always the same information, and if // our results are slightly wrong, who cares? Instead look for the // generated files once and if someone changed what files are generated // we'll size slightly wrong. No biggie. func (s *SizeMunger) getGeneratedFiles(obj *github.MungeObject) { if s.genFiles != nil { return } files := sets.NewString() prefixes := []string{} s.genFiles = &files s.genPrefixes = &prefixes file := s.generatedFilesFile if len(file) == 0 { glog.Infof("No --generated-files-config= supplied, applying no labels") return } fp, err := os.Open(file) if err != nil { glog.Errorf("Unable to open %q: %v", file, err) return } defer fp.Close() scanner := bufio.NewScanner(fp) for scanner.Scan() { line := scanner.Text() if strings.HasPrefix(line, "#") || line == "" { continue } fields := strings.Fields(line) if len(fields) != 2 { glog.Errorf("Invalid line in generated docs config %s: %q", file, line) continue } eType := fields[0] file := fields[1] if eType == "prefix" { prefixes = append(prefixes, file) } else if eType == "path" { files.Insert(file) } else if eType == "paths-from-repo" { docs, err := obj.GetFileContents(file, "") if err != nil { continue } docSlice := strings.Split(docs, "\n") files.Insert(docSlice...) } else { glog.Errorf("Invalid line in generated docs config, unknown type: %s, %q", eType, line) continue } } if scanner.Err() != nil { glog.Errorf("Error scanning %s: %v", file, err) return } s.genFiles = &files s.genPrefixes = &prefixes return }
// Munge is the workhorse the will actually make updates to the PR func (s *SizeMunger) Munge(obj *github.MungeObject) { if !obj.IsPR() { return } issue := obj.Issue s.getGeneratedFiles(obj) genFiles := *s.genFiles genPrefixes := *s.genPrefixes files, err := obj.ListFiles() if err != nil { return } adds := 0 dels := 0 for _, f := range files { skip := false for _, p := range genPrefixes { if strings.HasPrefix(*f.Filename, p) { skip = true break } } if skip { continue } if genFiles.Has(*f.Filename) { continue } if f.Additions != nil { adds += *f.Additions } if f.Deletions != nil { dels += *f.Deletions } } newSize := calculateSize(adds, dels) newLabel := labelSizePrefix + newSize existing := github.GetLabelsWithPrefix(issue.Labels, labelSizePrefix) needsUpdate := true for _, l := range existing { if l == newLabel { needsUpdate = false continue } obj.RemoveLabel(l) } if needsUpdate { obj.AddLabels([]string{newLabel}) body := fmt.Sprintf("Labelling this PR as %s", newLabel) obj.WriteComment(body) } }
// Priority implements IssueSource func (p *brokenJobSource) Priority(obj *github.MungeObject) (sync.Priority, error) { comments, err := obj.ListComments() if err != nil { return sync.PriorityP2, fmt.Errorf("Failed to list comment of issue: %v", err) } // Different IssueSource's Priority calculation may differ return autoPrioritize(comments, obj.Issue.CreatedAt), nil }
func handleFound(obj *github.MungeObject, gitMsg []byte, branch string) error { msg := string(gitMsg) o := strings.SplitN(msg, "\n", 2) sha := o[0] msg = fmt.Sprintf("Commit %s found in the %q branch appears to be this PR. Removing the %q label. If this s an error find help to get your PR picked.", sha, branch, cpCandidateLabel) obj.WriteComment(msg) obj.RemoveLabel(cpCandidateLabel) return nil }
func newInterruptedObject(obj *github.MungeObject) *submitQueueInterruptedObject { if headSHA, baseRef, gotHeadSHA := obj.GetHeadAndBase(); !gotHeadSHA { return nil } else if baseSHA, gotBaseSHA := obj.GetSHAFromRef(baseRef); !gotBaseSHA { return nil } else { return &submitQueueInterruptedObject{obj, headSHA, baseSHA} } }
// Munge is the workhorse the will actually make updates to the PR func (c *ClearPickAfterMerge) Munge(obj *github.MungeObject) { if !obj.IsPR() { return } if !obj.HasLabel(cpCandidateLabel) { return } if merged, err := obj.IsMerged(); !merged || err != nil { return } releaseMilestone := obj.ReleaseMilestone() if releaseMilestone == "" || len(releaseMilestone) != 4 { glog.Errorf("Found invalid milestone: %q", releaseMilestone) return } rel := releaseMilestone[1:] branch := "release-" + rel sha := obj.MergeCommit() if sha == nil { glog.Errorf("Unable to get SHA of merged %d", sha) return } logMsg := fmt.Sprintf("Merge pull request #%d from ", *obj.Issue.Number) bLogMsg := []byte(logMsg) cherrypickMsg := fmt.Sprintf("(cherry picked from commit %s)", *sha) args := []string{"log", "--pretty=tformat:%H%n%s%n%b", "--grep", cherrypickMsg, "origin/" + branch} out, err := c.features.Repos.GitCommand(args) if err != nil { glog.Errorf("Error grepping for cherrypick -x message out=%q: %v", string(out), err) return } if bytes.Contains(out, bLogMsg) { glog.Infof("Found cherry-pick using -x information") handleFound(obj, out, branch) return } args = []string{"log", "--pretty=tformat:%H%n%s%n%b", "--grep", logMsg, "origin/" + branch} out, err = c.features.Repos.GitCommand(args) if err != nil { glog.Errorf("Error grepping for log message out=%q: %v", string(out), err) return } if bytes.Contains(out, bLogMsg) { glog.Infof("Found cherry-pick using log matching") handleFound(obj, out, branch) return } return }
func (sq *SubmitQueue) isStaleWhitelistComment(obj *github.MungeObject, comment *githubapi.IssueComment) bool { if *comment.Body != notInWhitelistBody { return false } stale := obj.HasLabel(okToMergeLabel) if stale { glog.V(6).Infof("Found stale SubmitQueue Whitelist comment") } return stale }
func (sq *SubmitQueue) requiredStatusContexts(obj *github.MungeObject) []string { contexts := sq.RequiredStatusContexts if len(sq.E2EStatusContext) > 0 && !obj.HasLabel(e2eNotRequiredLabel) { contexts = append(contexts, sq.E2EStatusContext) } if len(sq.UnitStatusContext) > 0 { contexts = append(contexts, sq.UnitStatusContext) } return contexts }
func (LabelUnapprovedPicks) isStaleComment(obj *github.MungeObject, comment *githubapi.IssueComment) bool { if *comment.Body != labelUnapprovedBody { return false } stale := obj.HasLabel(cpApprovedLabel) if stale { glog.V(6).Infof("Found stale LabelUnapprovedPicks comment") } return stale }
func (b *BlockPath) isStaleComment(obj *github.MungeObject, comment *githubapi.IssueComment) bool { if *comment.Body != blockPathBody { return false } stale := !obj.HasLabel(doNotMergeLabel) if stale { glog.V(6).Infof("Found stale BlockPath comment") } return stale }
func (PickMustHaveMilestone) isStaleComment(obj *github.MungeObject, comment *githubapi.IssueComment) bool { if *comment.Body != pickMustHaveMilestoneBody { return false } stale := obj.ReleaseMilestone() != "" if stale { glog.V(6).Infof("Found stale PickMustHaveMilestone comment") } return stale }
func (NeedsRebaseMunger) isStaleComment(obj *github.MungeObject, comment *githubapi.IssueComment) bool { if !rebaseRE.MatchString(*comment.Body) { return false } stale := !obj.HasLabel(needsRebaseLabel) if stale { glog.V(6).Infof("Found stale NeedsRebaseMunger comment") } return stale }
func (sq *SubmitQueue) requiredStatusContexts(obj *github.MungeObject) []string { contexts := sq.RequiredStatusContexts // If the pr has a jenkins ci status, require it, otherwise require shippable if status, err := obj.GetStatus([]string{jenkinsCIContext}); err == nil && status != "incomplete" { contexts = append(contexts, jenkinsCIContext) } else { contexts = append(contexts, shippableContext) } return contexts }
func getCommentsAfterLastModified(obj *github.MungeObject) ([]*githubapi.IssueComment, error) { afterLastModified := func(opt *githubapi.IssueListCommentsOptions) *githubapi.IssueListCommentsOptions { // Only comments updated at or after this time are returned. // One possible case is that reviewer might "/lgtm" first, contributor updated PR, and reviewer updated "/lgtm". // This is still valid. We don't recommend user to update it. lastModified := *obj.LastModifiedTime() opt.Since = lastModified return opt } return obj.ListComments(afterLastModified) }
func getEarliestApprovedTime(obj *github.MungeObject) *time.Time { lgtmTime := obj.LabelTime(lgtmLabel) approvedTime := obj.LabelTime(approvedLabel) // if both lgtmTime and approvedTime are nil, this func will return nil pointer if lgtmTime == nil { return approvedTime } else if approvedTime == nil { return lgtmTime } else if lgtmTime.Before(*approvedTime) { return lgtmTime } return approvedTime }
func priority(obj *github.MungeObject) int { // jump to the front of the queue if you don't need retested if obj.HasLabel(retestNotRequiredLabel) { return retestNotRequiredMergePriority } prio := obj.Priority() // eparis randomly decided that unlabel issues count at p3 if prio == math.MaxInt32 { return defaultMergePriority } return prio }