package common import ( "regexp" "slices" "strings" "src.opensuse.org/autogits/common/gitea-generated/models" ) type PRReviews struct { Reviews []*models.PullReview RequestedReviewers []string Comments []*models.TimelineComment FullTimeline []*models.TimelineComment } 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) needNewReviews := []string{} var comments []*models.TimelineComment alreadyHaveUserReview := func(user string) bool { if slices.Contains(needNewReviews, 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) for idx, item := range timeline { if item.Type == TimelineCommentType_Review || item.Type == TimelineCommentType_ReviewRequested { for _, r := range rawReviews { if r.ID == item.ReviewID { if !alreadyHaveUserReview(r.User.UserName) { if item.Type == TimelineCommentType_Review && idx > cutOffIdx { needNewReviews = append(needNewReviews, r.User.UserName) } else { reviews = append(reviews, r) } } break } } } 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, FullTimeline: timeline, }, 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.RequestedReviewers, c.User.UserName) { if bodyCommandManualMergeOK(c.Body) { return true } } } for _, c := range r.Reviews { if c.Updated != c.Submitted { continue } if slices.Contains(r.RequestedReviewers, 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.RequestedReviewers { 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.RequestedReviewers { if !r.IsReviewedBy(reviewer) { missing = append(missing, reviewer) } } return missing } func (r *PRReviews) FindReviewRequester(reviewer string) *models.TimelineComment { if r == nil { return nil } for _, r := range r.FullTimeline { if r.Type == TimelineCommentType_ReviewRequested && r.Assignee.UserName == reviewer { return r } } 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 } } } 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 }