// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package pull
import (
"context"
"fmt"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/timeutil"
)
// ViewedState stores for a file in which state it is currently viewed
type ViewedState uint8
const (
Unviewed ViewedState = iota
HasChanged // cannot be set from the UI/ API, only internally
Viewed
)
func ( viewedState ViewedState ) String ( ) string {
switch viewedState {
case Unviewed :
return "unviewed"
case HasChanged :
return "has-changed"
case Viewed :
return "viewed"
default :
return fmt . Sprintf ( "unknown(value=%d)" , viewedState )
}
}
// ReviewState stores for a user-PR-commit combination which files the user has already viewed
type ReviewState struct {
ID int64 ` xorm:"pk autoincr" `
UserID int64 ` xorm:"NOT NULL UNIQUE(pull_commit_user)" `
PullID int64 ` xorm:"NOT NULL INDEX UNIQUE(pull_commit_user) DEFAULT 0" ` // Which PR was the review on?
CommitSHA string ` xorm:"NOT NULL VARCHAR(64) UNIQUE(pull_commit_user)" ` // Which commit was the head commit for the review?
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
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
}
func init ( ) {
db . RegisterModel ( new ( ReviewState ) )
}
// GetReviewState returns the ReviewState with all given values prefilled, whether or not it exists in the database.
// If the review didn't exist before in the database, it won't afterwards either.
// The returned boolean shows whether the review exists in the database
func GetReviewState ( ctx context . Context , userID , pullID int64 , commitSHA string ) ( * ReviewState , bool , error ) {
review := & ReviewState { UserID : userID , PullID : pullID , CommitSHA : commitSHA }
has , err := db . GetEngine ( ctx ) . Get ( review )
return review , has , err
}
// UpdateReviewState updates the given review inside the database, regardless of whether it existed before or not
// The given map of files with their viewed state will be merged with the previous review, if present
func UpdateReviewState ( ctx context . Context , userID , pullID int64 , commitSHA string , updatedFiles map [ string ] ViewedState ) error {
log . Trace ( "Updating review for user %d, repo %d, commit %s with the updated files %v." , userID , pullID , commitSHA , updatedFiles )
review , exists , err := GetReviewState ( ctx , userID , pullID , commitSHA )
if err != nil {
return err
}
if exists {
review . UpdatedFiles = mergeFiles ( review . UpdatedFiles , updatedFiles )
} else if previousReview , err := getNewestReviewStateApartFrom ( ctx , userID , pullID , commitSHA ) ; err != nil {
return err
// Overwrite the viewed files of the previous review if present
} else if previousReview != nil {
review . UpdatedFiles = mergeFiles ( previousReview . UpdatedFiles , updatedFiles )
} else {
review . UpdatedFiles = updatedFiles
}
// Insert or Update review
engine := db . GetEngine ( ctx )
if ! exists {
log . Trace ( "Inserting new review for user %d, repo %d, commit %s with the updated files %v." , userID , pullID , commitSHA , review . UpdatedFiles )
_ , err := engine . Insert ( review )
return err
}
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 )
_ , err = engine . ID ( review . ID ) . Update ( & ReviewState { UpdatedFiles : review . UpdatedFiles } )
return err
}
// mergeFiles merges the given maps of files with their viewing state into one map.
// Values from oldFiles will be overridden with values from newFiles
func mergeFiles ( oldFiles , newFiles map [ string ] ViewedState ) map [ string ] ViewedState {
if oldFiles == nil {
return newFiles
} else if newFiles == nil {
return oldFiles
}
for file , viewed := range newFiles {
oldFiles [ file ] = viewed
}
return oldFiles
}
// GetNewestReviewState gets the newest review of the current user in the current PR.
// The returned PR Review will be nil if the user has not yet reviewed this PR.
func GetNewestReviewState ( ctx context . Context , userID , pullID int64 ) ( * ReviewState , error ) {
var review ReviewState
has , err := db . GetEngine ( ctx ) . Where ( "user_id = ?" , userID ) . And ( "pull_id = ?" , pullID ) . OrderBy ( "updated_unix DESC" ) . Get ( & review )
if err != nil || ! has {
return nil , err
}
return & review , err
}
// getNewestReviewStateApartFrom is like GetNewestReview, except that the second newest review will be returned if the newest review points at the given commit.
// The returned PR Review will be nil if the user has not yet reviewed this PR.
func getNewestReviewStateApartFrom ( ctx context . Context , userID , pullID int64 , commitSHA string ) ( * ReviewState , error ) {
var reviews [ ] ReviewState
err := db . GetEngine ( ctx ) . Where ( "user_id = ?" , userID ) . And ( "pull_id = ?" , pullID ) . OrderBy ( "updated_unix DESC" ) . Limit ( 2 ) . Find ( & reviews )
// It would also be possible to use ".And("commit_sha != ?", commitSHA)" instead of the error handling below
// However, benchmarks show drastically improved performance by not doing that
// Error cases in which no review should be returned
if err != nil || len ( reviews ) == 0 || ( len ( reviews ) == 1 && reviews [ 0 ] . CommitSHA == commitSHA ) {
return nil , err
// The first review points at the commit to exclude, hence skip to the second review
} else if len ( reviews ) >= 2 && reviews [ 0 ] . CommitSHA == commitSHA {
return & reviews [ 1 ] , nil
}
// As we have no error cases left, the result must be the first element in the list
return & reviews [ 0 ] , nil
}