code.gitea.io/gitea@v1.21.7/models/pull/review_state.go (about) 1 // Copyright 2022 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package pull 5 6 import ( 7 "context" 8 "fmt" 9 10 "code.gitea.io/gitea/models/db" 11 "code.gitea.io/gitea/modules/log" 12 "code.gitea.io/gitea/modules/timeutil" 13 ) 14 15 // ViewedState stores for a file in which state it is currently viewed 16 type ViewedState uint8 17 18 const ( 19 Unviewed ViewedState = iota 20 HasChanged // cannot be set from the UI/ API, only internally 21 Viewed 22 ) 23 24 func (viewedState ViewedState) String() string { 25 switch viewedState { 26 case Unviewed: 27 return "unviewed" 28 case HasChanged: 29 return "has-changed" 30 case Viewed: 31 return "viewed" 32 default: 33 return fmt.Sprintf("unknown(value=%d)", viewedState) 34 } 35 } 36 37 // ReviewState stores for a user-PR-commit combination which files the user has already viewed 38 type ReviewState struct { 39 ID int64 `xorm:"pk autoincr"` 40 UserID int64 `xorm:"NOT NULL UNIQUE(pull_commit_user)"` 41 PullID int64 `xorm:"NOT NULL INDEX UNIQUE(pull_commit_user) DEFAULT 0"` // Which PR was the review on? 42 CommitSHA string `xorm:"NOT NULL VARCHAR(40) UNIQUE(pull_commit_user)"` // Which commit was the head commit for the review? 43 UpdatedFiles map[string]ViewedState `xorm:"NOT NULL LONGTEXT JSON"` // Stores for each of the changed files of a PR whether they have been viewed, changed since last viewed, or not viewed 44 UpdatedUnix timeutil.TimeStamp `xorm:"updated"` // Is an accurate indicator of the order of commits as we do not expect it to be possible to make reviews on previous commits 45 } 46 47 func init() { 48 db.RegisterModel(new(ReviewState)) 49 } 50 51 // GetReviewState returns the ReviewState with all given values prefilled, whether or not it exists in the database. 52 // If the review didn't exist before in the database, it won't afterwards either. 53 // The returned boolean shows whether the review exists in the database 54 func GetReviewState(ctx context.Context, userID, pullID int64, commitSHA string) (*ReviewState, bool, error) { 55 review := &ReviewState{UserID: userID, PullID: pullID, CommitSHA: commitSHA} 56 has, err := db.GetEngine(ctx).Get(review) 57 return review, has, err 58 } 59 60 // UpdateReviewState updates the given review inside the database, regardless of whether it existed before or not 61 // The given map of files with their viewed state will be merged with the previous review, if present 62 func UpdateReviewState(ctx context.Context, userID, pullID int64, commitSHA string, updatedFiles map[string]ViewedState) error { 63 log.Trace("Updating review for user %d, repo %d, commit %s with the updated files %v.", userID, pullID, commitSHA, updatedFiles) 64 65 review, exists, err := GetReviewState(ctx, userID, pullID, commitSHA) 66 if err != nil { 67 return err 68 } 69 70 if exists { 71 review.UpdatedFiles = mergeFiles(review.UpdatedFiles, updatedFiles) 72 } else if previousReview, err := getNewestReviewStateApartFrom(ctx, userID, pullID, commitSHA); err != nil { 73 return err 74 75 // Overwrite the viewed files of the previous review if present 76 } else if previousReview != nil { 77 review.UpdatedFiles = mergeFiles(previousReview.UpdatedFiles, updatedFiles) 78 } else { 79 review.UpdatedFiles = updatedFiles 80 } 81 82 // Insert or Update review 83 engine := db.GetEngine(ctx) 84 if !exists { 85 log.Trace("Inserting new review for user %d, repo %d, commit %s with the updated files %v.", userID, pullID, commitSHA, review.UpdatedFiles) 86 _, err := engine.Insert(review) 87 return err 88 } 89 log.Trace("Updating already existing review with ID %d (user %d, repo %d, commit %s) with the updated files %v.", review.ID, userID, pullID, commitSHA, review.UpdatedFiles) 90 _, err = engine.ID(review.ID).Update(&ReviewState{UpdatedFiles: review.UpdatedFiles}) 91 return err 92 } 93 94 // mergeFiles merges the given maps of files with their viewing state into one map. 95 // Values from oldFiles will be overridden with values from newFiles 96 func mergeFiles(oldFiles, newFiles map[string]ViewedState) map[string]ViewedState { 97 if oldFiles == nil { 98 return newFiles 99 } else if newFiles == nil { 100 return oldFiles 101 } 102 103 for file, viewed := range newFiles { 104 oldFiles[file] = viewed 105 } 106 return oldFiles 107 } 108 109 // GetNewestReviewState gets the newest review of the current user in the current PR. 110 // The returned PR Review will be nil if the user has not yet reviewed this PR. 111 func GetNewestReviewState(ctx context.Context, userID, pullID int64) (*ReviewState, error) { 112 var review ReviewState 113 has, err := db.GetEngine(ctx).Where("user_id = ?", userID).And("pull_id = ?", pullID).OrderBy("updated_unix DESC").Get(&review) 114 if err != nil || !has { 115 return nil, err 116 } 117 return &review, err 118 } 119 120 // getNewestReviewStateApartFrom is like GetNewestReview, except that the second newest review will be returned if the newest review points at the given commit. 121 // The returned PR Review will be nil if the user has not yet reviewed this PR. 122 func getNewestReviewStateApartFrom(ctx context.Context, userID, pullID int64, commitSHA string) (*ReviewState, error) { 123 var reviews []ReviewState 124 err := db.GetEngine(ctx).Where("user_id = ?", userID).And("pull_id = ?", pullID).OrderBy("updated_unix DESC").Limit(2).Find(&reviews) 125 // It would also be possible to use ".And("commit_sha != ?", commitSHA)" instead of the error handling below 126 // However, benchmarks show drastically improved performance by not doing that 127 128 // Error cases in which no review should be returned 129 if err != nil || len(reviews) == 0 || (len(reviews) == 1 && reviews[0].CommitSHA == commitSHA) { 130 return nil, err 131 132 // The first review points at the commit to exclude, hence skip to the second review 133 } else if len(reviews) >= 2 && reviews[0].CommitSHA == commitSHA { 134 return &reviews[1], nil 135 } 136 137 // As we have no error cases left, the result must be the first element in the list 138 return &reviews[0], nil 139 }