gitea源码

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