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.
253 lines
5.7 KiB
Go
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
|
|
}
|