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  }