package common import ( "regexp" "slices" "strings" "src.opensuse.org/autogits/common/gitea-generated/models" ) type ReviewInterface interface { IsManualMergeOK() bool IsApproved() bool MisingReviews() []string FindReviewRequester(reviewer string) *models.TimelineComment HasPendingReviewBy(reviewer string) bool IsReviewedBy(reviewer string) bool IsReviewedByOneOf(reviewers ...string) bool SetRequiredReviewers(reviewers []string) } type PRReviews struct { Reviews []*models.PullReview RequestedReviewers []*models.TimelineComment Comments []*models.TimelineComment RequiredReviewers []string } func (r *PRReviews) SetRequiredReviewers(reviewers []string) { r.RequiredReviewers = reviewers } func FetchGiteaReviews(rf GiteaReviewTimelineFetcher, org, repo string, no int64) (*PRReviews, error) { timeline, err := rf.GetTimeline(org, repo, no) if err != nil { return nil, err } rawReviews, err := rf.GetPullRequestReviews(org, repo, no) if err != nil { return nil, err } reviews := make([]*models.PullReview, 0, 10) var comments []*models.TimelineComment var foundUsers []string alreadyHaveUserReview := func(user string) bool { if slices.Contains(foundUsers, user) { return true } for _, r := range reviews { if r.User != nil && r.User.UserName == user { return true } } return false } LogDebug("FetchingGiteaReviews for", org, repo, no) LogDebug("Number of reviews:", len(rawReviews)) LogDebug("Number of items in timeline:", len(timeline)) cutOffIdx := len(timeline) var PendingRequestedReviews []*models.TimelineComment for idx, item := range timeline { if item.Type == TimelineCommentType_Review { for _, r := range rawReviews { if r.ID == item.ReviewID && r.User != nil { if !alreadyHaveUserReview(r.User.UserName) { if idx < cutOffIdx { reviews = append(reviews, r) } foundUsers = append(foundUsers, r.User.UserName) } break } } } else if item.Type == TimelineCommentType_ReviewRequested && item.Assignee != nil && !alreadyHaveUserReview(item.Assignee.UserName) { PendingRequestedReviews = append(PendingRequestedReviews, item) } else if item.Type == TimelineCommentType_DismissReview && item.Assignee != nil && !alreadyHaveUserReview(item.Assignee.UserName) { foundUsers = append(foundUsers, item.Assignee.UserName) } else if item.Type == TimelineCommentType_Comment && cutOffIdx > idx { comments = append(comments, item) } else if item.Type == TimelineCommentType_PushPull && cutOffIdx == len(timeline) { LogDebug("cut-off", item.Created, "@", idx) cutOffIdx = idx } else { LogDebug("Unhandled timeline type:", item.Type) } } LogDebug("num comments:", len(comments), "timeline:", len(reviews)) return &PRReviews{ Reviews: reviews, Comments: comments, RequestedReviewers: PendingRequestedReviews, }, nil } const ManualMergeOK = "^merge\\s+ok(\\W|$)" var merge_ok_regex *regexp.Regexp = regexp.MustCompile(ManualMergeOK) func bodyCommandManualMergeOK(body string) bool { lines := SplitLines(body) for _, line := range lines { if merge_ok_regex.MatchString(strings.ToLower(line)) { return true } } return false } func (r *PRReviews) IsManualMergeOK() bool { if r == nil { return false } for _, c := range r.Comments { if c.Updated != c.Created { continue } LogDebug("comment:", c.User.UserName, c.Body) if slices.Contains(r.RequiredReviewers, c.User.UserName) { if bodyCommandManualMergeOK(c.Body) { return true } } } for _, c := range r.Reviews { if c.Updated != c.Submitted { continue } if slices.Contains(r.RequiredReviewers, c.User.UserName) { if bodyCommandManualMergeOK(c.Body) { return true } } } return false } func (r *PRReviews) IsApproved() bool { if r == nil { return false } goodReview := true for _, reviewer := range r.RequiredReviewers { goodReview = false for _, review := range r.Reviews { if review.User.UserName == reviewer && review.State == ReviewStateApproved && !review.Stale && !review.Dismissed { LogDebug(" -- found review: ", review.User.UserName) goodReview = true break } } if !goodReview { break } } return goodReview } func (r *PRReviews) MissingReviews() []string { missing := []string{} if r == nil { return missing } for _, reviewer := range r.RequiredReviewers { if !r.IsReviewedBy(reviewer) { missing = append(missing, reviewer) } } return missing } func (r *PRReviews) FindReviewRequester(reviewer string) *models.TimelineComment { if r == nil { return nil } for _, t := range r.RequestedReviewers { if t.Assignee.UserName == reviewer { return t } } return nil } func (r *PRReviews) HasPendingReviewBy(reviewer string) bool { if r == nil { return false } for _, r := range r.Reviews { if r.User.UserName == reviewer { switch r.State { case ReviewStateRequestReview, ReviewStatePending: return true default: return false } } } // at this point, we do not have actual review by user. Check if we have a pending review for _, t := range r.RequestedReviewers { if t.Assignee != nil && t.Assignee.UserName == reviewer { return true } } return false } func (r *PRReviews) IsReviewedBy(reviewer string) bool { if r == nil { return false } for _, r := range r.Reviews { if r.User.UserName == reviewer && !r.Stale { switch r.State { case ReviewStateApproved, ReviewStateRequestChanges: return true default: return false } } } return false } func (r *PRReviews) IsReviewedByOneOf(reviewers ...string) bool { for _, reviewer := range reviewers { if r.IsReviewedBy(reviewer) { return true } } return false }