You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

review_state.go 5.8KB

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