Files
autogits/common/reviews.go
Adam Majer af2ff0bdd2 common: check for old pending request reviews
Timeline events will contain Reviews and ReviewRequests and
ReviewDismissed events. We need to handle this at event parsing
time and not to punt this to the query functions later on.

If the last event is an actual review, we use this.
If no review, check if last event associated with the reviewer
is Dismissed or Requested Review but not if a dismissed Review
preceeds it.
2026-02-27 01:03:26 +01:00

253 lines
5.7 KiB
Go

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
}