first-commit
This commit is contained in:
104
models/pull/automerge.go
Normal file
104
models/pull/automerge.go
Normal file
@@ -0,0 +1,104 @@
|
||||
// Copyright 2022 Gitea. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package pull
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"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"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
// 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"`
|
||||
DeleteBranchAfterMerge bool
|
||||
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, deleteBranchAfterMerge bool) 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,
|
||||
DeleteBranchAfterMerge: deleteBranchAfterMerge,
|
||||
})
|
||||
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 errors.Is(err, util.ErrNotExist) {
|
||||
doer, err = user_model.NewGhostUser(), nil
|
||||
}
|
||||
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
|
||||
}
|
138
models/pull/review_state.go
Normal file
138
models/pull/review_state.go
Normal file
@@ -0,0 +1,138 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package pull
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"maps"
|
||||
|
||||
"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
|
||||
}
|
||||
|
||||
maps.Copy(oldFiles, newFiles)
|
||||
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
|
||||
}
|
Reference in New Issue
Block a user