summaryrefslogtreecommitdiffstats
path: root/models/pull
diff options
context:
space:
mode:
authorDaniel Baumann <daniel@debian.org>2024-10-18 20:33:49 +0200
committerDaniel Baumann <daniel@debian.org>2024-10-18 20:33:49 +0200
commitdd136858f1ea40ad3c94191d647487fa4f31926c (patch)
tree58fec94a7b2a12510c9664b21793f1ed560c6518 /models/pull
parentInitial commit. (diff)
downloadforgejo-debian.tar.xz
forgejo-debian.zip
Adding upstream version 9.0.0.upstream/9.0.0upstreamdebian
Signed-off-by: Daniel Baumann <daniel@debian.org>
Diffstat (limited to 'models/pull')
-rw-r--r--models/pull/automerge.go97
-rw-r--r--models/pull/review_state.go139
2 files changed, 236 insertions, 0 deletions
diff --git a/models/pull/automerge.go b/models/pull/automerge.go
new file mode 100644
index 0000000..f31159a
--- /dev/null
+++ b/models/pull/automerge.go
@@ -0,0 +1,97 @@
+// Copyright 2022 Gitea. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package pull
+
+import (
+ "context"
+ "fmt"
+
+ "code.gitea.io/gitea/models/db"
+ repo_model "code.gitea.io/gitea/models/repo"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/timeutil"
+)
+
+// AutoMerge represents a pull request scheduled for merging when checks succeed
+type AutoMerge struct {
+ ID int64 `xorm:"pk autoincr"`
+ PullID int64 `xorm:"UNIQUE"`
+ DoerID int64 `xorm:"INDEX NOT NULL"`
+ Doer *user_model.User `xorm:"-"`
+ MergeStyle repo_model.MergeStyle `xorm:"varchar(30)"`
+ Message string `xorm:"LONGTEXT"`
+ CreatedUnix timeutil.TimeStamp `xorm:"created"`
+}
+
+// TableName return database table name for xorm
+func (AutoMerge) TableName() string {
+ return "pull_auto_merge"
+}
+
+func init() {
+ db.RegisterModel(new(AutoMerge))
+}
+
+// ErrAlreadyScheduledToAutoMerge represents a "PullRequestHasMerged"-error
+type ErrAlreadyScheduledToAutoMerge struct {
+ PullID int64
+}
+
+func (err ErrAlreadyScheduledToAutoMerge) Error() string {
+ return fmt.Sprintf("pull request is already scheduled to auto merge when checks succeed [pull_id: %d]", err.PullID)
+}
+
+// IsErrAlreadyScheduledToAutoMerge checks if an error is a ErrAlreadyScheduledToAutoMerge.
+func IsErrAlreadyScheduledToAutoMerge(err error) bool {
+ _, ok := err.(ErrAlreadyScheduledToAutoMerge)
+ return ok
+}
+
+// ScheduleAutoMerge schedules a pull request to be merged when all checks succeed
+func ScheduleAutoMerge(ctx context.Context, doer *user_model.User, pullID int64, style repo_model.MergeStyle, message string) error {
+ // Check if we already have a merge scheduled for that pull request
+ if exists, _, err := GetScheduledMergeByPullID(ctx, pullID); err != nil {
+ return err
+ } else if exists {
+ return ErrAlreadyScheduledToAutoMerge{PullID: pullID}
+ }
+
+ _, err := db.GetEngine(ctx).Insert(&AutoMerge{
+ DoerID: doer.ID,
+ PullID: pullID,
+ MergeStyle: style,
+ Message: message,
+ })
+ return err
+}
+
+// GetScheduledMergeByPullID gets a scheduled pull request merge by pull request id
+func GetScheduledMergeByPullID(ctx context.Context, pullID int64) (bool, *AutoMerge, error) {
+ scheduledPRM := &AutoMerge{}
+ exists, err := db.GetEngine(ctx).Where("pull_id = ?", pullID).Get(scheduledPRM)
+ if err != nil || !exists {
+ return false, nil, err
+ }
+
+ doer, err := user_model.GetPossibleUserByID(ctx, scheduledPRM.DoerID)
+ if err != nil {
+ return false, nil, err
+ }
+
+ scheduledPRM.Doer = doer
+ return true, scheduledPRM, nil
+}
+
+// DeleteScheduledAutoMerge delete a scheduled pull request
+func DeleteScheduledAutoMerge(ctx context.Context, pullID int64) error {
+ exist, scheduledPRM, err := GetScheduledMergeByPullID(ctx, pullID)
+ if err != nil {
+ return err
+ } else if !exist {
+ return db.ErrNotExist{Resource: "auto_merge", ID: pullID}
+ }
+
+ _, err = db.GetEngine(ctx).ID(scheduledPRM.ID).Delete(&AutoMerge{})
+ return err
+}
diff --git a/models/pull/review_state.go b/models/pull/review_state.go
new file mode 100644
index 0000000..e46a22a
--- /dev/null
+++ b/models/pull/review_state.go
@@ -0,0 +1,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(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
+}