Files
forge/models/issues/review_list.go
zeekayandHanzo Dev aa87442fdd Record upstream authorship centrally in NOTICE instead of in every file header
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the
Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in
NOTICE, alongside LICENSE. The header lint accepts any copyright line above the
SPDX line, so it no longer demands an upstream one.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-10 17:50:35 -07:00

215 lines
6.7 KiB
Go

// Copyright 2026 Hanzo AI, Inc. All rights reserved.
// SPDX-License-Identifier: MIT
package issues
import (
"context"
"slices"
"sort"
"github.com/hanzoai/git/models/db"
organization_model "github.com/hanzoai/git/models/organization"
user_model "github.com/hanzoai/git/models/user"
"github.com/hanzoai/git/modules/container"
"github.com/hanzoai/git/modules/optional"
"github.com/hanzoai/builder"
)
type ReviewList []*Review
// LoadReviewers loads reviewers
func (reviews ReviewList) LoadReviewers(ctx context.Context) error {
reviewerIDs := make([]int64, len(reviews))
for i := range reviews {
reviewerIDs[i] = reviews[i].ReviewerID
}
reviewers, err := user_model.GetPossibleUserByIDs(ctx, reviewerIDs)
if err != nil {
return err
}
userMap := make(map[int64]*user_model.User, len(reviewers))
for _, reviewer := range reviewers {
userMap[reviewer.ID] = reviewer
}
for _, review := range reviews {
review.Reviewer = userMap[review.ReviewerID]
}
return nil
}
// LoadReviewersTeams loads reviewers teams
func (reviews ReviewList) LoadReviewersTeams(ctx context.Context) error {
reviewersTeamsIDs := make([]int64, 0)
for _, review := range reviews {
if review.ReviewerTeamID != 0 {
reviewersTeamsIDs = append(reviewersTeamsIDs, review.ReviewerTeamID)
}
}
teamsMap, err := organization_model.GetTeamsByIDs(ctx, reviewersTeamsIDs)
if err != nil {
return err
}
for _, review := range reviews {
if review.ReviewerTeamID != 0 {
review.ReviewerTeam = teamsMap[review.ReviewerTeamID]
}
}
return nil
}
func (reviews ReviewList) LoadIssues(ctx context.Context) error {
issueIDs := container.FilterSlice(reviews, func(review *Review) (int64, bool) {
return review.IssueID, true
})
issues, err := GetIssuesByIDs(ctx, issueIDs)
if err != nil {
return err
}
if _, err := issues.LoadRepositories(ctx); err != nil {
return err
}
issueMap := make(map[int64]*Issue, len(issues))
for _, issue := range issues {
issueMap[issue.ID] = issue
}
for _, review := range reviews {
review.Issue = issueMap[review.IssueID]
}
return nil
}
// FindReviewOptions represent possible filters to find reviews
type FindReviewOptions struct {
db.ListOptions
Types []ReviewType
IssueID int64
ReviewerID int64
OfficialOnly bool
Dismissed optional.Option[bool]
}
func (opts *FindReviewOptions) toCond() builder.Cond {
cond := builder.NewCond()
if opts.IssueID > 0 {
cond = cond.And(builder.Eq{"issue_id": opts.IssueID})
}
if opts.ReviewerID > 0 {
cond = cond.And(builder.Eq{"reviewer_id": opts.ReviewerID})
}
if len(opts.Types) > 0 {
cond = cond.And(builder.In("type", opts.Types))
}
if opts.OfficialOnly {
cond = cond.And(builder.Eq{"official": true})
}
if opts.Dismissed.Has() {
cond = cond.And(builder.Eq{"dismissed": opts.Dismissed.Value()})
}
return cond
}
// FindReviews returns reviews passing FindReviewOptions
func FindReviews(ctx context.Context, opts FindReviewOptions) (ReviewList, error) {
reviews := make([]*Review, 0, 10)
sess := db.GetEngine(ctx).Where(opts.toCond())
if opts.Page > 0 && !opts.IsListAll() {
db.SetSessionPagination(sess, &opts)
}
return reviews, sess.
Asc("created_unix").
Asc("id").
Find(&reviews)
}
// FindLatestReviews returns only latest reviews per user, passing FindReviewOptions
func FindLatestReviews(ctx context.Context, opts FindReviewOptions) (ReviewList, error) {
reviews := make([]*Review, 0, 10)
cond := opts.toCond()
sess := db.GetEngine(ctx).Where(cond)
if opts.Page > 0 {
db.SetSessionPagination(sess, &opts)
}
sess.In("id", builder.
Select("max(id)").
From("review").
Where(cond).
GroupBy("reviewer_id"))
return reviews, sess.
Asc("created_unix").
Asc("id").
Find(&reviews)
}
// CountReviews returns count of reviews passing FindReviewOptions
func CountReviews(ctx context.Context, opts FindReviewOptions) (int64, error) {
return db.GetEngine(ctx).Where(opts.toCond()).Count(&Review{})
}
// GetReviewsByIssueID gets the latest review of each reviewer for a pull request
// The first returned parameter is the latest review of each individual reviewer or team
// The second returned parameter is the latest review of each original author which is migrated from other systems
// The reviews are sorted by updated time
func GetReviewsByIssueID(ctx context.Context, issueID int64) (latestReviews, migratedOriginalReviews ReviewList, err error) {
reviews := make([]*Review, 0, 10)
// Get all reviews for the issue id
if err := db.GetEngine(ctx).Where("issue_id=?", issueID).OrderBy("updated_unix ASC").Find(&reviews); err != nil {
return nil, nil, err
}
// filter them in memory to get the latest review of each reviewer
// Since the reviews should not be too many for one issue, less than 100 commonly, it's acceptable to do this in memory
// And since there are too less indexes in review table, it will be very slow to filter in the database
reviewersMap := make(map[int64][]*Review) // key is reviewer id
originalReviewersMap := make(map[int64][]*Review) // key is original author id
reviewTeamsMap := make(map[int64][]*Review) // key is reviewer team id
countedReviewTypes := []ReviewType{ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest, ReviewTypeComment}
for _, review := range reviews {
if review.ReviewerTeamID == 0 && slices.Contains(countedReviewTypes, review.Type) && !review.Dismissed {
if review.OriginalAuthorID != 0 {
originalReviewersMap[review.OriginalAuthorID] = append(originalReviewersMap[review.OriginalAuthorID], review)
} else {
reviewersMap[review.ReviewerID] = append(reviewersMap[review.ReviewerID], review)
}
} else if review.ReviewerTeamID != 0 && review.OriginalAuthorID == 0 {
reviewTeamsMap[review.ReviewerTeamID] = append(reviewTeamsMap[review.ReviewerTeamID], review)
}
}
individualReviews := make([]*Review, 0, 10)
for _, reviews := range reviewersMap {
individualReviews = append(individualReviews, reviews[len(reviews)-1])
}
sort.Slice(individualReviews, func(i, j int) bool {
return individualReviews[i].UpdatedUnix < individualReviews[j].UpdatedUnix
})
originalReviews := make([]*Review, 0, 10)
for _, reviews := range originalReviewersMap {
originalReviews = append(originalReviews, reviews[len(reviews)-1])
}
sort.Slice(originalReviews, func(i, j int) bool {
return originalReviews[i].UpdatedUnix < originalReviews[j].UpdatedUnix
})
teamReviewRequests := make([]*Review, 0, 5)
for _, reviews := range reviewTeamsMap {
teamReviewRequests = append(teamReviewRequests, reviews[len(reviews)-1])
}
sort.Slice(teamReviewRequests, func(i, j int) bool {
return teamReviewRequests[i].UpdatedUnix < teamReviewRequests[j].UpdatedUnix
})
return append(individualReviews, teamReviewRequests...), originalReviews, nil
}