gitea

Development moved to Codeberg

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20
  21. 21
  22. 22
  23. 23
  24. 24
  25. 25
  26. 26
  27. 27
  28. 28
  29. 29
  30. 30
  31. 31
  32. 32
  33. 33
  34. 34
  35. 35
  36. 36
  37. 37
  38. 38
  39. 39
  40. 40
  41. 41
  42. 42
  43. 43
  44. 44
  45. 45
  46. 46
  47. 47
  48. 48
  49. 49
  50. 50
  51. 51
  52. 52
  53. 53
  54. 54
  55. 55
  56. 56
  57. 57
  58. 58
  59. 59
  60. 60
  61. 61
  62. 62
  63. 63
  64. 64
  65. 65
  66. 66
  67. 67
  68. 68
  69. 69
  70. 70
  71. 71
  72. 72
  73. 73
  74. 74
  75. 75
  76. 76
  77. 77
  78. 78
  79. 79
  80. 80
  81. 81
  82. 82
  83. 83
  84. 84
  85. 85
  86. 86
  87. 87
  88. 88
  89. 89
  90. 90
  91. 91
  92. 92
  93. 93
  94. 94
  95. 95
  96. 96
  97. 97
  98. 98
  99. 99
  100. 100
  101. 101
  102. 102
  103. 103
  104. 104
  105. 105
  106. 106
  107. 107
  108. 108
  109. 109
  110. 110
  111. 111
  112. 112
  113. 113
  114. 114
  115. 115
  116. 116
  117. 117
  118. 118
  119. 119
  120. 120
  121. 121
  122. 122
  123. 123
  124. 124
  125. 125
  126. 126
  127. 127
  128. 128
  129. 129
  130. 130
  131. 131
  132. 132
  133. 133
  134. 134
  135. 135
  136. 136
  137. 137
  138. 138
  139. 139
// 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(40) 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
}