mirror of
https://github.com/go-gitea/gitea.git
synced 2025-04-27 11:31:48 +00:00
Option to delay conflict checking of old pull requests until page view (#27779)
`[repository.pull-request] DELAY_CHECK_FOR_INACTIVE_DAYS` is a new setting to delay the mergeable check for pull requests that have been inactive for the specified number of days. This avoids potentially long delays for big repositories with many pull requests. and reduces system load overall when there are many repositories or pull requests. When viewing the PR, checking will start immediately and the PR merge box will automatically reload when complete. Accessing the PR through the API will also start checking immediately. The default value of `7` provides a balance between system load, and keeping behavior similar to what it was before both for users and API access. With `0` all conflict checking will be delayed, while `-1` always checks immediately to restore the previous behavior. --------- Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
parent
d1ad8e1e80
commit
a9343896f4
@ -1155,6 +1155,10 @@ LEVEL = Info
|
|||||||
;;
|
;;
|
||||||
;; Retarget child pull requests to the parent pull request branch target on merge of parent pull request. It only works on merged PRs where the head and base branch target the same repo.
|
;; Retarget child pull requests to the parent pull request branch target on merge of parent pull request. It only works on merged PRs where the head and base branch target the same repo.
|
||||||
;RETARGET_CHILDREN_ON_MERGE = true
|
;RETARGET_CHILDREN_ON_MERGE = true
|
||||||
|
;;
|
||||||
|
;; Delay mergeable check until page view or API access, for pull requests that have not been updated in the specified days when their base branches get updated.
|
||||||
|
;; Use "-1" to always check all pull requests (old behavior). Use "0" to always delay the checks.
|
||||||
|
;DELAY_CHECK_FOR_INACTIVE_DAYS = 7
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
@ -10,7 +10,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
@ -104,27 +103,6 @@ const (
|
|||||||
PullRequestStatusAncestor
|
PullRequestStatusAncestor
|
||||||
)
|
)
|
||||||
|
|
||||||
func (status PullRequestStatus) String() string {
|
|
||||||
switch status {
|
|
||||||
case PullRequestStatusConflict:
|
|
||||||
return "CONFLICT"
|
|
||||||
case PullRequestStatusChecking:
|
|
||||||
return "CHECKING"
|
|
||||||
case PullRequestStatusMergeable:
|
|
||||||
return "MERGEABLE"
|
|
||||||
case PullRequestStatusManuallyMerged:
|
|
||||||
return "MANUALLY_MERGED"
|
|
||||||
case PullRequestStatusError:
|
|
||||||
return "ERROR"
|
|
||||||
case PullRequestStatusEmpty:
|
|
||||||
return "EMPTY"
|
|
||||||
case PullRequestStatusAncestor:
|
|
||||||
return "ANCESTOR"
|
|
||||||
default:
|
|
||||||
return strconv.Itoa(int(status))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PullRequestFlow the flow of pull request
|
// PullRequestFlow the flow of pull request
|
||||||
type PullRequestFlow int
|
type PullRequestFlow int
|
||||||
|
|
||||||
|
@ -82,6 +82,7 @@ var (
|
|||||||
AddCoCommitterTrailers bool
|
AddCoCommitterTrailers bool
|
||||||
TestConflictingPatchesWithGitApply bool
|
TestConflictingPatchesWithGitApply bool
|
||||||
RetargetChildrenOnMerge bool
|
RetargetChildrenOnMerge bool
|
||||||
|
DelayCheckForInactiveDays int
|
||||||
} `ini:"repository.pull-request"`
|
} `ini:"repository.pull-request"`
|
||||||
|
|
||||||
// Issue Setting
|
// Issue Setting
|
||||||
@ -200,6 +201,7 @@ var (
|
|||||||
AddCoCommitterTrailers bool
|
AddCoCommitterTrailers bool
|
||||||
TestConflictingPatchesWithGitApply bool
|
TestConflictingPatchesWithGitApply bool
|
||||||
RetargetChildrenOnMerge bool
|
RetargetChildrenOnMerge bool
|
||||||
|
DelayCheckForInactiveDays int
|
||||||
}{
|
}{
|
||||||
WorkInProgressPrefixes: []string{"WIP:", "[WIP]"},
|
WorkInProgressPrefixes: []string{"WIP:", "[WIP]"},
|
||||||
// Same as GitHub. See
|
// Same as GitHub. See
|
||||||
@ -215,6 +217,7 @@ var (
|
|||||||
PopulateSquashCommentWithCommitMessages: false,
|
PopulateSquashCommentWithCommitMessages: false,
|
||||||
AddCoCommitterTrailers: true,
|
AddCoCommitterTrailers: true,
|
||||||
RetargetChildrenOnMerge: true,
|
RetargetChildrenOnMerge: true,
|
||||||
|
DelayCheckForInactiveDays: 7,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Issue settings
|
// Issue settings
|
||||||
|
@ -1880,7 +1880,7 @@ pulls.add_prefix = Add <strong>%s</strong> prefix
|
|||||||
pulls.remove_prefix = Remove <strong>%s</strong> prefix
|
pulls.remove_prefix = Remove <strong>%s</strong> prefix
|
||||||
pulls.data_broken = This pull request is broken due to missing fork information.
|
pulls.data_broken = This pull request is broken due to missing fork information.
|
||||||
pulls.files_conflicted = This pull request has changes conflicting with the target branch.
|
pulls.files_conflicted = This pull request has changes conflicting with the target branch.
|
||||||
pulls.is_checking = "Merge conflict checking is in progress. Try again in few moments."
|
pulls.is_checking = Checking for merge conflicts ...
|
||||||
pulls.is_ancestor = "This branch is already included in the target branch. There is nothing to merge."
|
pulls.is_ancestor = "This branch is already included in the target branch. There is nothing to merge."
|
||||||
pulls.is_empty = "The changes on this branch are already on the target branch. This will be an empty commit."
|
pulls.is_empty = "The changes on this branch are already on the target branch. This will be an empty commit."
|
||||||
pulls.required_status_check_failed = Some required checks were not successful.
|
pulls.required_status_check_failed = Some required checks were not successful.
|
||||||
|
@ -202,6 +202,10 @@ func GetPullRequest(ctx *context.APIContext) {
|
|||||||
ctx.APIErrorInternal(err)
|
ctx.APIErrorInternal(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Consider API access a view for delayed checking.
|
||||||
|
pull_service.StartPullRequestCheckOnView(ctx, pr)
|
||||||
|
|
||||||
ctx.JSON(http.StatusOK, convert.ToAPIPullRequest(ctx, pr, ctx.Doer))
|
ctx.JSON(http.StatusOK, convert.ToAPIPullRequest(ctx, pr, ctx.Doer))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -287,6 +291,10 @@ func GetPullRequestByBaseHead(ctx *context.APIContext) {
|
|||||||
ctx.APIErrorInternal(err)
|
ctx.APIErrorInternal(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Consider API access a view for delayed checking.
|
||||||
|
pull_service.StartPullRequestCheckOnView(ctx, pr)
|
||||||
|
|
||||||
ctx.JSON(http.StatusOK, convert.ToAPIPullRequest(ctx, pr, ctx.Doer))
|
ctx.JSON(http.StatusOK, convert.ToAPIPullRequest(ctx, pr, ctx.Doer))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -921,7 +929,7 @@ func MergePullRequest(ctx *context.APIContext) {
|
|||||||
if err := pull_service.CheckPullMergeable(ctx, ctx.Doer, &ctx.Repo.Permission, pr, mergeCheckType, form.ForceMerge); err != nil {
|
if err := pull_service.CheckPullMergeable(ctx, ctx.Doer, &ctx.Repo.Permission, pr, mergeCheckType, form.ForceMerge); err != nil {
|
||||||
if errors.Is(err, pull_service.ErrIsClosed) {
|
if errors.Is(err, pull_service.ErrIsClosed) {
|
||||||
ctx.APIErrorNotFound()
|
ctx.APIErrorNotFound()
|
||||||
} else if errors.Is(err, pull_service.ErrUserNotAllowedToMerge) {
|
} else if errors.Is(err, pull_service.ErrNoPermissionToMerge) {
|
||||||
ctx.APIError(http.StatusMethodNotAllowed, "User not allowed to merge PR")
|
ctx.APIError(http.StatusMethodNotAllowed, "User not allowed to merge PR")
|
||||||
} else if errors.Is(err, pull_service.ErrHasMerged) {
|
} else if errors.Is(err, pull_service.ErrHasMerged) {
|
||||||
ctx.APIError(http.StatusMethodNotAllowed, "")
|
ctx.APIError(http.StatusMethodNotAllowed, "")
|
||||||
@ -929,7 +937,7 @@ func MergePullRequest(ctx *context.APIContext) {
|
|||||||
ctx.APIError(http.StatusMethodNotAllowed, "Work in progress PRs cannot be merged")
|
ctx.APIError(http.StatusMethodNotAllowed, "Work in progress PRs cannot be merged")
|
||||||
} else if errors.Is(err, pull_service.ErrNotMergeableState) {
|
} else if errors.Is(err, pull_service.ErrNotMergeableState) {
|
||||||
ctx.APIError(http.StatusMethodNotAllowed, "Please try again later")
|
ctx.APIError(http.StatusMethodNotAllowed, "Please try again later")
|
||||||
} else if pull_service.IsErrDisallowedToMerge(err) {
|
} else if errors.Is(err, pull_service.ErrNotReadyToMerge) {
|
||||||
ctx.APIError(http.StatusMethodNotAllowed, err)
|
ctx.APIError(http.StatusMethodNotAllowed, err)
|
||||||
} else if asymkey_service.IsErrWontSign(err) {
|
} else if asymkey_service.IsErrWontSign(err) {
|
||||||
ctx.APIError(http.StatusMethodNotAllowed, err)
|
ctx.APIError(http.StatusMethodNotAllowed, err)
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
package private
|
package private
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
@ -374,7 +375,7 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, r
|
|||||||
|
|
||||||
// Check all status checks and reviews are ok
|
// Check all status checks and reviews are ok
|
||||||
if err := pull_service.CheckPullBranchProtections(ctx, pr, true); err != nil {
|
if err := pull_service.CheckPullBranchProtections(ctx, pr, true); err != nil {
|
||||||
if pull_service.IsErrDisallowedToMerge(err) {
|
if errors.Is(err, pull_service.ErrNotReadyToMerge) {
|
||||||
log.Warn("Forbidden: User %d is not allowed push to protected branch %s in %-v and pr #%d is not ready to be merged: %s", ctx.opts.UserID, branchName, repo, pr.Index, err.Error())
|
log.Warn("Forbidden: User %d is not allowed push to protected branch %s in %-v and pr #%d is not ready to be merged: %s", ctx.opts.UserID, branchName, repo, pr.Index, err.Error())
|
||||||
ctx.JSON(http.StatusForbidden, private.Response{
|
ctx.JSON(http.StatusForbidden, private.Response{
|
||||||
UserMsg: fmt.Sprintf("Not allowed to push to protected branch %s and pr #%d is not ready to be merged: %s", branchName, ctx.opts.PullRequestID, err.Error()),
|
UserMsg: fmt.Sprintf("Not allowed to push to protected branch %s and pr #%d is not ready to be merged: %s", branchName, ctx.opts.PullRequestID, err.Error()),
|
||||||
|
@ -43,7 +43,8 @@ const (
|
|||||||
tplIssueChoose templates.TplName = "repo/issue/choose"
|
tplIssueChoose templates.TplName = "repo/issue/choose"
|
||||||
tplIssueView templates.TplName = "repo/issue/view"
|
tplIssueView templates.TplName = "repo/issue/view"
|
||||||
|
|
||||||
tplReactions templates.TplName = "repo/issue/view_content/reactions"
|
tplPullMergeBox templates.TplName = "repo/issue/view_content/pull_merge_box"
|
||||||
|
tplReactions templates.TplName = "repo/issue/view_content/reactions"
|
||||||
|
|
||||||
issueTemplateKey = "IssueTemplate"
|
issueTemplateKey = "IssueTemplate"
|
||||||
issueTemplateTitleKey = "IssueTemplateTitle"
|
issueTemplateTitleKey = "IssueTemplateTitle"
|
||||||
|
@ -96,7 +96,7 @@ func NewComment(ctx *context.Context) {
|
|||||||
// Regenerate patch and test conflict.
|
// Regenerate patch and test conflict.
|
||||||
if pr == nil {
|
if pr == nil {
|
||||||
issue.PullRequest.HeadCommitID = ""
|
issue.PullRequest.HeadCommitID = ""
|
||||||
pull_service.AddToTaskQueue(ctx, issue.PullRequest)
|
pull_service.StartPullRequestCheckImmediately(ctx, issue.PullRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
// check whether the ref of PR <refs/pulls/pr_index/head> in base repo is consistent with the head commit of head branch in the head repo
|
// check whether the ref of PR <refs/pulls/pr_index/head> in base repo is consistent with the head commit of head branch in the head repo
|
||||||
|
@ -31,6 +31,7 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/templates"
|
"code.gitea.io/gitea/modules/templates"
|
||||||
"code.gitea.io/gitea/modules/templates/vars"
|
"code.gitea.io/gitea/modules/templates/vars"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
asymkey_service "code.gitea.io/gitea/services/asymkey"
|
asymkey_service "code.gitea.io/gitea/services/asymkey"
|
||||||
"code.gitea.io/gitea/services/context"
|
"code.gitea.io/gitea/services/context"
|
||||||
"code.gitea.io/gitea/services/context/upload"
|
"code.gitea.io/gitea/services/context/upload"
|
||||||
@ -271,8 +272,23 @@ func combineLabelComments(issue *issues_model.Issue) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ViewIssue render issue view page
|
func prepareIssueViewLoad(ctx *context.Context) *issues_model.Issue {
|
||||||
func ViewIssue(ctx *context.Context) {
|
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
||||||
|
if err != nil {
|
||||||
|
ctx.NotFoundOrServerError("GetIssueByIndex", issues_model.IsErrIssueNotExist, err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
issue.Repo = ctx.Repo.Repository
|
||||||
|
ctx.Data["Issue"] = issue
|
||||||
|
|
||||||
|
if err = issue.LoadPullRequest(ctx); err != nil {
|
||||||
|
ctx.ServerError("LoadPullRequest", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return issue
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleViewIssueRedirectExternal(ctx *context.Context) {
|
||||||
if ctx.PathParam("type") == "issues" {
|
if ctx.PathParam("type") == "issues" {
|
||||||
// If issue was requested we check if repo has external tracker and redirect
|
// If issue was requested we check if repo has external tracker and redirect
|
||||||
extIssueUnit, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypeExternalTracker)
|
extIssueUnit, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypeExternalTracker)
|
||||||
@ -294,18 +310,18 @@ func ViewIssue(ctx *context.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
// ViewIssue render issue view page
|
||||||
if err != nil {
|
func ViewIssue(ctx *context.Context) {
|
||||||
if issues_model.IsErrIssueNotExist(err) {
|
handleViewIssueRedirectExternal(ctx)
|
||||||
ctx.NotFound(err)
|
if ctx.Written() {
|
||||||
} else {
|
|
||||||
ctx.ServerError("GetIssueByIndex", err)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if issue.Repo == nil {
|
|
||||||
issue.Repo = ctx.Repo.Repository
|
issue := prepareIssueViewLoad(ctx)
|
||||||
|
if ctx.Written() {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure type and URL matches.
|
// Make sure type and URL matches.
|
||||||
@ -337,12 +353,12 @@ func ViewIssue(ctx *context.Context) {
|
|||||||
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
|
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
|
||||||
upload.AddUploadContext(ctx, "comment")
|
upload.AddUploadContext(ctx, "comment")
|
||||||
|
|
||||||
if err = issue.LoadAttributes(ctx); err != nil {
|
if err := issue.LoadAttributes(ctx); err != nil {
|
||||||
ctx.ServerError("LoadAttributes", err)
|
ctx.ServerError("LoadAttributes", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = filterXRefComments(ctx, issue); err != nil {
|
if err := filterXRefComments(ctx, issue); err != nil {
|
||||||
ctx.ServerError("filterXRefComments", err)
|
ctx.ServerError("filterXRefComments", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -351,7 +367,7 @@ func ViewIssue(ctx *context.Context) {
|
|||||||
|
|
||||||
if ctx.IsSigned {
|
if ctx.IsSigned {
|
||||||
// Update issue-user.
|
// Update issue-user.
|
||||||
if err = activities_model.SetIssueReadBy(ctx, issue.ID, ctx.Doer.ID); err != nil {
|
if err := activities_model.SetIssueReadBy(ctx, issue.ID, ctx.Doer.ID); err != nil {
|
||||||
ctx.ServerError("ReadBy", err)
|
ctx.ServerError("ReadBy", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -365,15 +381,13 @@ func ViewIssue(ctx *context.Context) {
|
|||||||
|
|
||||||
prepareFuncs := []func(*context.Context, *issues_model.Issue){
|
prepareFuncs := []func(*context.Context, *issues_model.Issue){
|
||||||
prepareIssueViewContent,
|
prepareIssueViewContent,
|
||||||
func(ctx *context.Context, issue *issues_model.Issue) {
|
|
||||||
preparePullViewPullInfo(ctx, issue)
|
|
||||||
},
|
|
||||||
prepareIssueViewCommentsAndSidebarParticipants,
|
prepareIssueViewCommentsAndSidebarParticipants,
|
||||||
preparePullViewReviewAndMerge,
|
|
||||||
prepareIssueViewSidebarWatch,
|
prepareIssueViewSidebarWatch,
|
||||||
prepareIssueViewSidebarTimeTracker,
|
prepareIssueViewSidebarTimeTracker,
|
||||||
prepareIssueViewSidebarDependency,
|
prepareIssueViewSidebarDependency,
|
||||||
prepareIssueViewSidebarPin,
|
prepareIssueViewSidebarPin,
|
||||||
|
func(ctx *context.Context, issue *issues_model.Issue) { preparePullViewPullInfo(ctx, issue) },
|
||||||
|
preparePullViewReviewAndMerge,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, prepareFunc := range prepareFuncs {
|
for _, prepareFunc := range prepareFuncs {
|
||||||
@ -412,9 +426,25 @@ func ViewIssue(ctx *context.Context) {
|
|||||||
return user_service.CanBlockUser(ctx, ctx.Doer, blocker, blockee)
|
return user_service.CanBlockUser(ctx, ctx.Doer, blocker, blockee)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if issue.PullRequest != nil && !issue.PullRequest.IsChecking() && !setting.IsProd {
|
||||||
|
ctx.Data["PullMergeBoxReloadingInterval"] = 1 // in dev env, force using the reloading logic to make sure it won't break
|
||||||
|
}
|
||||||
|
|
||||||
ctx.HTML(http.StatusOK, tplIssueView)
|
ctx.HTML(http.StatusOK, tplIssueView)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ViewPullMergeBox(ctx *context.Context) {
|
||||||
|
issue := prepareIssueViewLoad(ctx)
|
||||||
|
if !issue.IsPull {
|
||||||
|
ctx.NotFound(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
preparePullViewPullInfo(ctx, issue)
|
||||||
|
preparePullViewReviewAndMerge(ctx, issue)
|
||||||
|
ctx.Data["PullMergeBoxReloading"] = issue.PullRequest.IsChecking()
|
||||||
|
ctx.HTML(http.StatusOK, tplPullMergeBox)
|
||||||
|
}
|
||||||
|
|
||||||
func prepareIssueViewSidebarDependency(ctx *context.Context, issue *issues_model.Issue) {
|
func prepareIssueViewSidebarDependency(ctx *context.Context, issue *issues_model.Issue) {
|
||||||
if issue.IsPull && !ctx.Repo.CanRead(unit.TypeIssues) {
|
if issue.IsPull && !ctx.Repo.CanRead(unit.TypeIssues) {
|
||||||
ctx.Data["IssueDependencySearchType"] = "pulls"
|
ctx.Data["IssueDependencySearchType"] = "pulls"
|
||||||
@ -792,6 +822,8 @@ func preparePullViewReviewAndMerge(ctx *context.Context, issue *issues_model.Iss
|
|||||||
allowMerge := false
|
allowMerge := false
|
||||||
canWriteToHeadRepo := false
|
canWriteToHeadRepo := false
|
||||||
|
|
||||||
|
pull_service.StartPullRequestCheckOnView(ctx, pull)
|
||||||
|
|
||||||
if ctx.IsSigned {
|
if ctx.IsSigned {
|
||||||
if err := pull.LoadHeadRepo(ctx); err != nil {
|
if err := pull.LoadHeadRepo(ctx); err != nil {
|
||||||
log.Error("LoadHeadRepo: %v", err)
|
log.Error("LoadHeadRepo: %v", err)
|
||||||
@ -838,6 +870,7 @@ func preparePullViewReviewAndMerge(ctx *context.Context, issue *issues_model.Iss
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx.Data["PullMergeBoxReloadingInterval"] = util.Iif(pull != nil && pull.IsChecking(), 2000, 0)
|
||||||
ctx.Data["CanWriteToHeadRepo"] = canWriteToHeadRepo
|
ctx.Data["CanWriteToHeadRepo"] = canWriteToHeadRepo
|
||||||
ctx.Data["ShowMergeInstructions"] = canWriteToHeadRepo
|
ctx.Data["ShowMergeInstructions"] = canWriteToHeadRepo
|
||||||
ctx.Data["AllowMerge"] = allowMerge
|
ctx.Data["AllowMerge"] = allowMerge
|
||||||
@ -958,5 +991,4 @@ func prepareIssueViewContent(ctx *context.Context, issue *issues_model.Issue) {
|
|||||||
ctx.ServerError("roleDescriptor", err)
|
ctx.ServerError("roleDescriptor", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.Data["Issue"] = issue
|
|
||||||
}
|
}
|
||||||
|
@ -1052,7 +1052,7 @@ func MergePullRequest(ctx *context.Context) {
|
|||||||
} else {
|
} else {
|
||||||
ctx.JSONError(ctx.Tr("repo.issues.closed_title"))
|
ctx.JSONError(ctx.Tr("repo.issues.closed_title"))
|
||||||
}
|
}
|
||||||
case errors.Is(err, pull_service.ErrUserNotAllowedToMerge):
|
case errors.Is(err, pull_service.ErrNoPermissionToMerge):
|
||||||
ctx.JSONError(ctx.Tr("repo.pulls.update_not_allowed"))
|
ctx.JSONError(ctx.Tr("repo.pulls.update_not_allowed"))
|
||||||
case errors.Is(err, pull_service.ErrHasMerged):
|
case errors.Is(err, pull_service.ErrHasMerged):
|
||||||
ctx.JSONError(ctx.Tr("repo.pulls.has_merged"))
|
ctx.JSONError(ctx.Tr("repo.pulls.has_merged"))
|
||||||
@ -1060,7 +1060,7 @@ func MergePullRequest(ctx *context.Context) {
|
|||||||
ctx.JSONError(ctx.Tr("repo.pulls.no_merge_wip"))
|
ctx.JSONError(ctx.Tr("repo.pulls.no_merge_wip"))
|
||||||
case errors.Is(err, pull_service.ErrNotMergeableState):
|
case errors.Is(err, pull_service.ErrNotMergeableState):
|
||||||
ctx.JSONError(ctx.Tr("repo.pulls.no_merge_not_ready"))
|
ctx.JSONError(ctx.Tr("repo.pulls.no_merge_not_ready"))
|
||||||
case pull_service.IsErrDisallowedToMerge(err):
|
case errors.Is(err, pull_service.ErrNotReadyToMerge):
|
||||||
ctx.JSONError(ctx.Tr("repo.pulls.no_merge_not_ready"))
|
ctx.JSONError(ctx.Tr("repo.pulls.no_merge_not_ready"))
|
||||||
case asymkey_service.IsErrWontSign(err):
|
case asymkey_service.IsErrWontSign(err):
|
||||||
ctx.JSONError(err.Error()) // has no translation ...
|
ctx.JSONError(err.Error()) // has no translation ...
|
||||||
|
@ -1505,6 +1505,7 @@ func registerWebRoutes(m *web.Router) {
|
|||||||
m.Get("", repo.SetWhitespaceBehavior, repo.GetPullDiffStats, repo.ViewIssue)
|
m.Get("", repo.SetWhitespaceBehavior, repo.GetPullDiffStats, repo.ViewIssue)
|
||||||
m.Get(".diff", repo.DownloadPullDiff)
|
m.Get(".diff", repo.DownloadPullDiff)
|
||||||
m.Get(".patch", repo.DownloadPullPatch)
|
m.Get(".patch", repo.DownloadPullPatch)
|
||||||
|
m.Get("/merge_box", repo.ViewPullMergeBox)
|
||||||
m.Group("/commits", func() {
|
m.Group("/commits", func() {
|
||||||
m.Get("", repo.SetWhitespaceBehavior, repo.GetPullDiffStats, repo.ViewPullCommits)
|
m.Get("", repo.SetWhitespaceBehavior, repo.GetPullDiffStats, repo.ViewPullCommits)
|
||||||
m.Get("/list", repo.GetPullCommits)
|
m.Get("/list", repo.GetPullCommits)
|
||||||
|
@ -204,7 +204,7 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.
|
|||||||
return nil, fmt.Errorf("failed to update pull ref. Error: %w", err)
|
return nil, fmt.Errorf("failed to update pull ref. Error: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
pull_service.AddToTaskQueue(ctx, pr)
|
pull_service.StartPullRequestCheckImmediately(ctx, pr)
|
||||||
err = pr.LoadIssue(ctx)
|
err = pr.LoadIssue(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to load pull issue. Error: %w", err)
|
return nil, fmt.Errorf("failed to load pull issue. Error: %w", err)
|
||||||
|
@ -289,7 +289,7 @@ func handlePullRequestAutoMerge(pullID int64, sha string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := pull_service.CheckPullMergeable(ctx, doer, &perm, pr, pull_service.MergeCheckTypeGeneral, false); err != nil {
|
if err := pull_service.CheckPullMergeable(ctx, doer, &perm, pr, pull_service.MergeCheckTypeGeneral, false); err != nil {
|
||||||
if errors.Is(err, pull_service.ErrUserNotAllowedToMerge) {
|
if errors.Is(err, pull_service.ErrNotReadyToMerge) {
|
||||||
log.Info("%-v was scheduled to automerge by an unauthorized user", pr)
|
log.Info("%-v was scheduled to automerge by an unauthorized user", pr)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -556,7 +556,7 @@ func (g *GiteaLocalUploader) CreatePullRequests(ctx context.Context, prs ...*bas
|
|||||||
}
|
}
|
||||||
for _, pr := range gprs {
|
for _, pr := range gprs {
|
||||||
g.issues[pr.Issue.Index] = pr.Issue
|
g.issues[pr.Issue.Index] = pr.Issue
|
||||||
pull.AddToTaskQueue(ctx, pr)
|
pull.StartPullRequestCheckImmediately(ctx, pr)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
git_model "code.gitea.io/gitea/models/git"
|
git_model "code.gitea.io/gitea/models/git"
|
||||||
@ -25,6 +26,7 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/process"
|
"code.gitea.io/gitea/modules/process"
|
||||||
"code.gitea.io/gitea/modules/queue"
|
"code.gitea.io/gitea/modules/queue"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/timeutil"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
asymkey_service "code.gitea.io/gitea/services/asymkey"
|
asymkey_service "code.gitea.io/gitea/services/asymkey"
|
||||||
notify_service "code.gitea.io/gitea/services/notify"
|
notify_service "code.gitea.io/gitea/services/notify"
|
||||||
@ -34,27 +36,88 @@ import (
|
|||||||
var prPatchCheckerQueue *queue.WorkerPoolQueue[string]
|
var prPatchCheckerQueue *queue.WorkerPoolQueue[string]
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrIsClosed = errors.New("pull is closed")
|
ErrIsClosed = errors.New("pull is closed")
|
||||||
ErrUserNotAllowedToMerge = ErrDisallowedToMerge{}
|
ErrNoPermissionToMerge = errors.New("no permission to merge")
|
||||||
ErrHasMerged = errors.New("has already been merged")
|
ErrNotReadyToMerge = errors.New("not ready to merge")
|
||||||
ErrIsWorkInProgress = errors.New("work in progress PRs cannot be merged")
|
ErrHasMerged = errors.New("has already been merged")
|
||||||
ErrIsChecking = errors.New("cannot merge while conflict checking is in progress")
|
ErrIsWorkInProgress = errors.New("work in progress PRs cannot be merged")
|
||||||
ErrNotMergeableState = errors.New("not in mergeable state")
|
ErrIsChecking = errors.New("cannot merge while conflict checking is in progress")
|
||||||
ErrDependenciesLeft = errors.New("is blocked by an open dependency")
|
ErrNotMergeableState = errors.New("not in mergeable state")
|
||||||
|
ErrDependenciesLeft = errors.New("is blocked by an open dependency")
|
||||||
)
|
)
|
||||||
|
|
||||||
// AddToTaskQueue adds itself to pull request test task queue.
|
func markPullRequestStatusAsChecking(ctx context.Context, pr *issues_model.PullRequest) bool {
|
||||||
func AddToTaskQueue(ctx context.Context, pr *issues_model.PullRequest) {
|
|
||||||
pr.Status = issues_model.PullRequestStatusChecking
|
pr.Status = issues_model.PullRequestStatusChecking
|
||||||
err := pr.UpdateColsIfNotMerged(ctx, "status")
|
err := pr.UpdateColsIfNotMerged(ctx, "status")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("AddToTaskQueue(%-v).UpdateCols.(add to queue): %v", pr, err)
|
log.Error("UpdateColsIfNotMerged failed, pr: %-v, err: %v", pr, err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
pr, err = issues_model.GetPullRequestByID(ctx, pr.ID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("GetPullRequestByID failed, pr: %-v, err: %v", pr, err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return pr.Status == issues_model.PullRequestStatusChecking
|
||||||
|
}
|
||||||
|
|
||||||
|
var AddPullRequestToCheckQueue = realAddPullRequestToCheckQueue
|
||||||
|
|
||||||
|
func realAddPullRequestToCheckQueue(prID int64) {
|
||||||
|
err := prPatchCheckerQueue.Push(strconv.FormatInt(prID, 10))
|
||||||
|
if err != nil && !errors.Is(err, queue.ErrAlreadyInQueue) {
|
||||||
|
log.Error("Error adding %v to the pull requests check queue: %v", prID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func StartPullRequestCheckImmediately(ctx context.Context, pr *issues_model.PullRequest) {
|
||||||
|
if !markPullRequestStatusAsChecking(ctx, pr) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Trace("Adding %-v to the test pull requests queue", pr)
|
AddPullRequestToCheckQueue(pr.ID)
|
||||||
err = prPatchCheckerQueue.Push(strconv.FormatInt(pr.ID, 10))
|
}
|
||||||
if err != nil && err != queue.ErrAlreadyInQueue {
|
|
||||||
log.Error("Error adding %-v to the test pull requests queue: %v", pr, err)
|
// StartPullRequestCheckDelayable will delay the check if the pull request was not updated recently.
|
||||||
|
// When the "base" branch gets updated, all PRs targeting that "base" branch need to re-check whether
|
||||||
|
// they are mergeable.
|
||||||
|
// When there are too many stale PRs, each "base" branch update will consume a lot of system resources.
|
||||||
|
// So we can delay the checks for PRs that were not updated recently, only mark their status as
|
||||||
|
// "checking", and then next time when these PRs are updated or viewed, the real checks will run.
|
||||||
|
func StartPullRequestCheckDelayable(ctx context.Context, pr *issues_model.PullRequest) {
|
||||||
|
if !markPullRequestStatusAsChecking(ctx, pr) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if setting.Repository.PullRequest.DelayCheckForInactiveDays >= 0 {
|
||||||
|
if err := pr.LoadIssue(ctx); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
duration := 24 * time.Hour * time.Duration(setting.Repository.PullRequest.DelayCheckForInactiveDays)
|
||||||
|
if pr.Issue.UpdatedUnix.AddDuration(duration) <= timeutil.TimeStampNow() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AddPullRequestToCheckQueue(pr.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func StartPullRequestCheckOnView(ctx context.Context, pr *issues_model.PullRequest) {
|
||||||
|
// TODO: its correctness totally depends on the "unique queue" feature and the global lock.
|
||||||
|
// So duplicate "start" requests will be ignored if there is already a task in the queue or one is running.
|
||||||
|
// Ideally in the future we should decouple the "unique queue" feature from the "start" request.
|
||||||
|
if pr.Status == issues_model.PullRequestStatusChecking {
|
||||||
|
if setting.IsInTesting {
|
||||||
|
// In testing mode, there might be an "immediate" queue, which is not a real queue, everything is executed in the same goroutine
|
||||||
|
// So we can't use the global lock here, otherwise it will cause a deadlock.
|
||||||
|
AddPullRequestToCheckQueue(pr.ID)
|
||||||
|
} else {
|
||||||
|
// When a PR check starts, the task is popped from the queue and the task handler acquires the global lock
|
||||||
|
// So we need to acquire the global lock here to prevent from duplicate tasks
|
||||||
|
_, _ = globallock.TryLockAndDo(ctx, getPullWorkingLockKey(pr.ID), func(ctx context.Context) error {
|
||||||
|
AddPullRequestToCheckQueue(pr.ID) // the queue is a unique queue and won't add the same task again
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,7 +147,7 @@ func CheckPullMergeable(stdCtx context.Context, doer *user_model.User, perm *acc
|
|||||||
log.Error("Error whilst checking if %-v is allowed to merge %-v: %v", doer, pr, err)
|
log.Error("Error whilst checking if %-v is allowed to merge %-v: %v", doer, pr, err)
|
||||||
return err
|
return err
|
||||||
} else if !allowedMerge {
|
} else if !allowedMerge {
|
||||||
return ErrUserNotAllowedToMerge
|
return ErrNoPermissionToMerge
|
||||||
}
|
}
|
||||||
|
|
||||||
if mergeCheckType == MergeCheckTypeManually {
|
if mergeCheckType == MergeCheckTypeManually {
|
||||||
@ -105,7 +168,7 @@ func CheckPullMergeable(stdCtx context.Context, doer *user_model.User, perm *acc
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := CheckPullBranchProtections(ctx, pr, false); err != nil {
|
if err := CheckPullBranchProtections(ctx, pr, false); err != nil {
|
||||||
if !IsErrDisallowedToMerge(err) {
|
if !errors.Is(err, ErrNotReadyToMerge) {
|
||||||
log.Error("Error whilst checking pull branch protection for %-v: %v", pr, err)
|
log.Error("Error whilst checking pull branch protection for %-v: %v", pr, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -172,10 +235,10 @@ func isSignedIfRequired(ctx context.Context, pr *issues_model.PullRequest, doer
|
|||||||
return sign, err
|
return sign, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkAndUpdateStatus checks if pull request is possible to leaving checking status,
|
// markPullRequestAsMergeable checks if pull request is possible to leaving checking status,
|
||||||
// and set to be either conflict or mergeable.
|
// and set to be either conflict or mergeable.
|
||||||
func checkAndUpdateStatus(ctx context.Context, pr *issues_model.PullRequest) {
|
func markPullRequestAsMergeable(ctx context.Context, pr *issues_model.PullRequest) {
|
||||||
// If status has not been changed to conflict by testPatch then we are mergeable
|
// If status has not been changed to conflict by testPullRequestTmpRepoBranchMergeable then we are mergeable
|
||||||
if pr.Status == issues_model.PullRequestStatusChecking {
|
if pr.Status == issues_model.PullRequestStatusChecking {
|
||||||
pr.Status = issues_model.PullRequestStatusMergeable
|
pr.Status = issues_model.PullRequestStatusMergeable
|
||||||
}
|
}
|
||||||
@ -310,6 +373,10 @@ func manuallyMerged(ctx context.Context, pr *issues_model.PullRequest) bool {
|
|||||||
|
|
||||||
// InitializePullRequests checks and tests untested patches of pull requests.
|
// InitializePullRequests checks and tests untested patches of pull requests.
|
||||||
func InitializePullRequests(ctx context.Context) {
|
func InitializePullRequests(ctx context.Context) {
|
||||||
|
// If we prefer to delay the checks, then no need to do any check during startup, there should be not much difference
|
||||||
|
if setting.Repository.PullRequest.DelayCheckForInactiveDays >= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
prs, err := issues_model.GetPullRequestIDsByCheckStatus(ctx, issues_model.PullRequestStatusChecking)
|
prs, err := issues_model.GetPullRequestIDsByCheckStatus(ctx, issues_model.PullRequestStatusChecking)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Find Checking PRs: %v", err)
|
log.Error("Find Checking PRs: %v", err)
|
||||||
@ -320,24 +387,12 @@ func InitializePullRequests(ctx context.Context) {
|
|||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
log.Trace("Adding PR[%d] to the pull requests patch checking queue", prID)
|
AddPullRequestToCheckQueue(prID)
|
||||||
if err := prPatchCheckerQueue.Push(strconv.FormatInt(prID, 10)); err != nil {
|
|
||||||
log.Error("Error adding PR[%d] to the pull requests patch checking queue %v", prID, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle passed PR IDs and test the PRs
|
func checkPullRequestMergeable(id int64) {
|
||||||
func handler(items ...string) []string {
|
|
||||||
for _, s := range items {
|
|
||||||
id, _ := strconv.ParseInt(s, 10, 64)
|
|
||||||
testPR(id)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func testPR(id int64) {
|
|
||||||
ctx := graceful.GetManager().HammerContext()
|
ctx := graceful.GetManager().HammerContext()
|
||||||
releaser, err := globallock.Lock(ctx, getPullWorkingLockKey(id))
|
releaser, err := globallock.Lock(ctx, getPullWorkingLockKey(id))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -351,7 +406,7 @@ func testPR(id int64) {
|
|||||||
|
|
||||||
pr, err := issues_model.GetPullRequestByID(ctx, id)
|
pr, err := issues_model.GetPullRequestByID(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Unable to GetPullRequestByID[%d] for testPR: %v", id, err)
|
log.Error("Unable to GetPullRequestByID[%d] for checkPullRequestMergeable: %v", id, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -370,15 +425,15 @@ func testPR(id int64) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := TestPatch(pr); err != nil {
|
if err := testPullRequestBranchMergeable(pr); err != nil {
|
||||||
log.Error("testPatch[%-v]: %v", pr, err)
|
log.Error("testPullRequestTmpRepoBranchMergeable[%-v]: %v", pr, err)
|
||||||
pr.Status = issues_model.PullRequestStatusError
|
pr.Status = issues_model.PullRequestStatusError
|
||||||
if err := pr.UpdateCols(ctx, "status"); err != nil {
|
if err := pr.UpdateCols(ctx, "status"); err != nil {
|
||||||
log.Error("update pr [%-v] status to PullRequestStatusError failed: %v", pr, err)
|
log.Error("update pr [%-v] status to PullRequestStatusError failed: %v", pr, err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
checkAndUpdateStatus(ctx, pr)
|
markPullRequestAsMergeable(ctx, pr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckPRsForBaseBranch check all pulls with baseBrannch
|
// CheckPRsForBaseBranch check all pulls with baseBrannch
|
||||||
@ -387,17 +442,21 @@ func CheckPRsForBaseBranch(ctx context.Context, baseRepo *repo_model.Repository,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, pr := range prs {
|
for _, pr := range prs {
|
||||||
AddToTaskQueue(ctx, pr)
|
StartPullRequestCheckImmediately(ctx, pr)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init runs the task queue to test all the checking status pull requests
|
// Init runs the task queue to test all the checking status pull requests
|
||||||
func Init() error {
|
func Init() error {
|
||||||
prPatchCheckerQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "pr_patch_checker", handler)
|
prPatchCheckerQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "pr_patch_checker", func(items ...string) []string {
|
||||||
|
for _, s := range items {
|
||||||
|
id, _ := strconv.ParseInt(s, 10, 64)
|
||||||
|
checkPullRequestMergeable(id)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
if prPatchCheckerQueue == nil {
|
if prPatchCheckerQueue == nil {
|
||||||
return errors.New("unable to create pr_patch_checker queue")
|
return errors.New("unable to create pr_patch_checker queue")
|
||||||
|
@ -36,7 +36,7 @@ func TestPullRequest_AddToTaskQueue(t *testing.T) {
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2})
|
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2})
|
||||||
AddToTaskQueue(db.DefaultContext, pr)
|
StartPullRequestCheckImmediately(db.DefaultContext, pr)
|
||||||
|
|
||||||
assert.Eventually(t, func() bool {
|
assert.Eventually(t, func() bool {
|
||||||
pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2})
|
pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2})
|
||||||
|
@ -518,25 +518,6 @@ func IsUserAllowedToMerge(ctx context.Context, pr *issues_model.PullRequest, p a
|
|||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrDisallowedToMerge represents an error that a branch is protected and the current user is not allowed to modify it.
|
|
||||||
type ErrDisallowedToMerge struct {
|
|
||||||
Reason string
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsErrDisallowedToMerge checks if an error is an ErrDisallowedToMerge.
|
|
||||||
func IsErrDisallowedToMerge(err error) bool {
|
|
||||||
_, ok := err.(ErrDisallowedToMerge)
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func (err ErrDisallowedToMerge) Error() string {
|
|
||||||
return fmt.Sprintf("not allowed to merge [reason: %s]", err.Reason)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (err ErrDisallowedToMerge) Unwrap() error {
|
|
||||||
return util.ErrPermissionDenied
|
|
||||||
}
|
|
||||||
|
|
||||||
// CheckPullBranchProtections checks whether the PR is ready to be merged (reviews and status checks)
|
// CheckPullBranchProtections checks whether the PR is ready to be merged (reviews and status checks)
|
||||||
func CheckPullBranchProtections(ctx context.Context, pr *issues_model.PullRequest, skipProtectedFilesCheck bool) (err error) {
|
func CheckPullBranchProtections(ctx context.Context, pr *issues_model.PullRequest, skipProtectedFilesCheck bool) (err error) {
|
||||||
if err = pr.LoadBaseRepo(ctx); err != nil {
|
if err = pr.LoadBaseRepo(ctx); err != nil {
|
||||||
@ -556,31 +537,21 @@ func CheckPullBranchProtections(ctx context.Context, pr *issues_model.PullReques
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if !isPass {
|
if !isPass {
|
||||||
return ErrDisallowedToMerge{
|
return util.ErrorWrap(ErrNotReadyToMerge, "Not all required status checks successful")
|
||||||
Reason: "Not all required status checks successful",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !issues_model.HasEnoughApprovals(ctx, pb, pr) {
|
if !issues_model.HasEnoughApprovals(ctx, pb, pr) {
|
||||||
return ErrDisallowedToMerge{
|
return util.ErrorWrap(ErrNotReadyToMerge, "Does not have enough approvals")
|
||||||
Reason: "Does not have enough approvals",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if issues_model.MergeBlockedByRejectedReview(ctx, pb, pr) {
|
if issues_model.MergeBlockedByRejectedReview(ctx, pb, pr) {
|
||||||
return ErrDisallowedToMerge{
|
return util.ErrorWrap(ErrNotReadyToMerge, "There are requested changes")
|
||||||
Reason: "There are requested changes",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if issues_model.MergeBlockedByOfficialReviewRequests(ctx, pb, pr) {
|
if issues_model.MergeBlockedByOfficialReviewRequests(ctx, pb, pr) {
|
||||||
return ErrDisallowedToMerge{
|
return util.ErrorWrap(ErrNotReadyToMerge, "There are official review requests")
|
||||||
Reason: "There are official review requests",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if issues_model.MergeBlockedByOutdatedBranch(pb, pr) {
|
if issues_model.MergeBlockedByOutdatedBranch(pb, pr) {
|
||||||
return ErrDisallowedToMerge{
|
return util.ErrorWrap(ErrNotReadyToMerge, "The head branch is behind the base branch")
|
||||||
Reason: "The head branch is behind the base branch",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if skipProtectedFilesCheck {
|
if skipProtectedFilesCheck {
|
||||||
@ -588,9 +559,7 @@ func CheckPullBranchProtections(ctx context.Context, pr *issues_model.PullReques
|
|||||||
}
|
}
|
||||||
|
|
||||||
if pb.MergeBlockedByProtectedFiles(pr.ChangedProtectedFiles) {
|
if pb.MergeBlockedByProtectedFiles(pr.ChangedProtectedFiles) {
|
||||||
return ErrDisallowedToMerge{
|
return util.ErrorWrap(ErrNotReadyToMerge, "Changed protected files")
|
||||||
Reason: "Changed protected files",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -709,7 +678,7 @@ func SetMerged(ctx context.Context, pr *issues_model.PullRequest, mergedCommitID
|
|||||||
return false, fmt.Errorf("ChangeIssueStatus: %w", err)
|
return false, fmt.Errorf("ChangeIssueStatus: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// We need to save all of the data used to compute this merge as it may have already been changed by TestPatch. FIXME: need to set some state to prevent TestPatch from running whilst we are merging.
|
// We need to save all of the data used to compute this merge as it may have already been changed by testPullRequestBranchMergeable. FIXME: need to set some state to prevent testPullRequestBranchMergeable from running whilst we are merging.
|
||||||
if cnt, err := db.GetEngine(ctx).Where("id = ?", pr.ID).
|
if cnt, err := db.GetEngine(ctx).Where("id = ?", pr.ID).
|
||||||
And("has_merged = ?", false).
|
And("has_merged = ?", false).
|
||||||
Cols("has_merged, status, merge_base, merged_commit_id, merger_id, merged_unix, conflicted_files").
|
Cols("has_merged, status, merge_base, merged_commit_id, merger_id, merged_unix, conflicted_files").
|
||||||
|
@ -23,7 +23,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type mergeContext struct {
|
type mergeContext struct {
|
||||||
*prContext
|
*prTmpRepoContext
|
||||||
doer *user_model.User
|
doer *user_model.User
|
||||||
sig *git.Signature
|
sig *git.Signature
|
||||||
committer *git.Signature
|
committer *git.Signature
|
||||||
@ -68,8 +68,8 @@ func createTemporaryRepoForMerge(ctx context.Context, pr *issues_model.PullReque
|
|||||||
}
|
}
|
||||||
|
|
||||||
mergeCtx = &mergeContext{
|
mergeCtx = &mergeContext{
|
||||||
prContext: prCtx,
|
prTmpRepoContext: prCtx,
|
||||||
doer: doer,
|
doer: doer,
|
||||||
}
|
}
|
||||||
|
|
||||||
if expectedHeadCommitID != "" {
|
if expectedHeadCommitID != "" {
|
||||||
|
@ -67,9 +67,8 @@ var patchErrorSuffices = []string{
|
|||||||
": does not exist in index",
|
": does not exist in index",
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestPatch will test whether a simple patch will apply
|
func testPullRequestBranchMergeable(pr *issues_model.PullRequest) error {
|
||||||
func TestPatch(pr *issues_model.PullRequest) error {
|
ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("testPullRequestBranchMergeable: %s", pr))
|
||||||
ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("TestPatch: %s", pr))
|
|
||||||
defer finished()
|
defer finished()
|
||||||
|
|
||||||
prCtx, cancel, err := createTemporaryRepoForPR(ctx, pr)
|
prCtx, cancel, err := createTemporaryRepoForPR(ctx, pr)
|
||||||
@ -81,10 +80,10 @@ func TestPatch(pr *issues_model.PullRequest) error {
|
|||||||
}
|
}
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
return testPatch(ctx, prCtx, pr)
|
return testPullRequestTmpRepoBranchMergeable(ctx, prCtx, pr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testPatch(ctx context.Context, prCtx *prContext, pr *issues_model.PullRequest) error {
|
func testPullRequestTmpRepoBranchMergeable(ctx context.Context, prCtx *prTmpRepoContext, pr *issues_model.PullRequest) error {
|
||||||
gitRepo, err := git.OpenRepository(ctx, prCtx.tmpBasePath)
|
gitRepo, err := git.OpenRepository(ctx, prCtx.tmpBasePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("OpenRepository: %w", err)
|
return fmt.Errorf("OpenRepository: %w", err)
|
||||||
@ -380,7 +379,7 @@ func checkConflicts(ctx context.Context, pr *issues_model.PullRequest, gitRepo *
|
|||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Trace("PullRequest[%d].testPatch (patchPath): %s", pr.ID, patchPath)
|
log.Trace("PullRequest[%d].testPullRequestTmpRepoBranchMergeable (patchPath): %s", pr.ID, patchPath)
|
||||||
|
|
||||||
// 4. Read the base branch in to the index of the temporary repository
|
// 4. Read the base branch in to the index of the temporary repository
|
||||||
_, _, err = git.NewCommand("read-tree", "base").RunStdString(gitRepo.Ctx, &git.RunOpts{Dir: tmpBasePath})
|
_, _, err = git.NewCommand("read-tree", "base").RunStdString(gitRepo.Ctx, &git.RunOpts{Dir: tmpBasePath})
|
||||||
@ -450,7 +449,7 @@ func checkConflicts(ctx context.Context, pr *issues_model.PullRequest, gitRepo *
|
|||||||
scanner := bufio.NewScanner(stderrReader)
|
scanner := bufio.NewScanner(stderrReader)
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
line := scanner.Text()
|
line := scanner.Text()
|
||||||
log.Trace("PullRequest[%d].testPatch: stderr: %s", pr.ID, line)
|
log.Trace("PullRequest[%d].testPullRequestTmpRepoBranchMergeable: stderr: %s", pr.ID, line)
|
||||||
if strings.HasPrefix(line, prefix) {
|
if strings.HasPrefix(line, prefix) {
|
||||||
conflict = true
|
conflict = true
|
||||||
filepath := strings.TrimSpace(strings.Split(line[len(prefix):], ":")[0])
|
filepath := strings.TrimSpace(strings.Split(line[len(prefix):], ":")[0])
|
||||||
|
@ -96,7 +96,7 @@ func NewPullRequest(ctx context.Context, opts *NewPullRequestOptions) error {
|
|||||||
}
|
}
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
if err := testPatch(ctx, prCtx, pr); err != nil {
|
if err := testPullRequestTmpRepoBranchMergeable(ctx, prCtx, pr); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -314,12 +314,12 @@ func ChangeTargetBranch(ctx context.Context, pr *issues_model.PullRequest, doer
|
|||||||
pr.BaseBranch = targetBranch
|
pr.BaseBranch = targetBranch
|
||||||
|
|
||||||
// Refresh patch
|
// Refresh patch
|
||||||
if err := TestPatch(pr); err != nil {
|
if err := testPullRequestBranchMergeable(pr); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update target branch, PR diff and status
|
// Update target branch, PR diff and status
|
||||||
// This is the same as checkAndUpdateStatus in check service, but also updates base_branch
|
// This is the same as markPullRequestAsMergeable in check service, but also updates base_branch
|
||||||
if pr.Status == issues_model.PullRequestStatusChecking {
|
if pr.Status == issues_model.PullRequestStatusChecking {
|
||||||
pr.Status = issues_model.PullRequestStatusMergeable
|
pr.Status = issues_model.PullRequestStatusMergeable
|
||||||
}
|
}
|
||||||
@ -409,7 +409,7 @@ func AddTestPullRequestTask(opts TestPullRequestOptions) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
AddToTaskQueue(ctx, pr)
|
StartPullRequestCheckImmediately(ctx, pr)
|
||||||
comment, err := CreatePushPullComment(ctx, opts.Doer, pr, opts.OldCommitID, opts.NewCommitID)
|
comment, err := CreatePushPullComment(ctx, opts.Doer, pr, opts.OldCommitID, opts.NewCommitID)
|
||||||
if err == nil && comment != nil {
|
if err == nil && comment != nil {
|
||||||
notify_service.PullRequestPushCommits(ctx, opts.Doer, pr, comment)
|
notify_service.PullRequestPushCommits(ctx, opts.Doer, pr, comment)
|
||||||
@ -502,7 +502,7 @@ func AddTestPullRequestTask(opts TestPullRequestOptions) {
|
|||||||
log.Error("UpdateCommitDivergence: %v", err)
|
log.Error("UpdateCommitDivergence: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AddToTaskQueue(ctx, pr)
|
StartPullRequestCheckDelayable(ctx, pr)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,7 @@ const (
|
|||||||
stagingBranch = "staging" // this is used for a working branch
|
stagingBranch = "staging" // this is used for a working branch
|
||||||
)
|
)
|
||||||
|
|
||||||
type prContext struct {
|
type prTmpRepoContext struct {
|
||||||
context.Context
|
context.Context
|
||||||
tmpBasePath string
|
tmpBasePath string
|
||||||
pr *issues_model.PullRequest
|
pr *issues_model.PullRequest
|
||||||
@ -36,7 +36,7 @@ type prContext struct {
|
|||||||
errbuf *strings.Builder // any use should be preceded by a Reset and preferably after use
|
errbuf *strings.Builder // any use should be preceded by a Reset and preferably after use
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *prContext) RunOpts() *git.RunOpts {
|
func (ctx *prTmpRepoContext) RunOpts() *git.RunOpts {
|
||||||
ctx.outbuf.Reset()
|
ctx.outbuf.Reset()
|
||||||
ctx.errbuf.Reset()
|
ctx.errbuf.Reset()
|
||||||
return &git.RunOpts{
|
return &git.RunOpts{
|
||||||
@ -48,7 +48,7 @@ func (ctx *prContext) RunOpts() *git.RunOpts {
|
|||||||
|
|
||||||
// createTemporaryRepoForPR creates a temporary repo with "base" for pr.BaseBranch and "tracking" for pr.HeadBranch
|
// createTemporaryRepoForPR creates a temporary repo with "base" for pr.BaseBranch and "tracking" for pr.HeadBranch
|
||||||
// it also create a second base branch called "original_base"
|
// it also create a second base branch called "original_base"
|
||||||
func createTemporaryRepoForPR(ctx context.Context, pr *issues_model.PullRequest) (prCtx *prContext, cancel context.CancelFunc, err error) {
|
func createTemporaryRepoForPR(ctx context.Context, pr *issues_model.PullRequest) (prCtx *prTmpRepoContext, cancel context.CancelFunc, err error) {
|
||||||
if err := pr.LoadHeadRepo(ctx); err != nil {
|
if err := pr.LoadHeadRepo(ctx); err != nil {
|
||||||
log.Error("%-v LoadHeadRepo: %v", pr, err)
|
log.Error("%-v LoadHeadRepo: %v", pr, err)
|
||||||
return nil, nil, fmt.Errorf("%v LoadHeadRepo: %w", pr, err)
|
return nil, nil, fmt.Errorf("%v LoadHeadRepo: %w", pr, err)
|
||||||
@ -81,7 +81,7 @@ func createTemporaryRepoForPR(ctx context.Context, pr *issues_model.PullRequest)
|
|||||||
}
|
}
|
||||||
cancel = cleanup
|
cancel = cleanup
|
||||||
|
|
||||||
prCtx = &prContext{
|
prCtx = &prTmpRepoContext{
|
||||||
Context: ctx,
|
Context: ctx,
|
||||||
tmpBasePath: tmpBasePath,
|
tmpBasePath: tmpBasePath,
|
||||||
pr: pr,
|
pr: pr,
|
||||||
|
@ -68,7 +68,7 @@
|
|||||||
{{template "repo/issue/view_content/comments" .}}
|
{{template "repo/issue/view_content/comments" .}}
|
||||||
|
|
||||||
{{if and .Issue.IsPull (not $.Repository.IsArchived)}}
|
{{if and .Issue.IsPull (not $.Repository.IsArchived)}}
|
||||||
{{template "repo/issue/view_content/pull".}}
|
{{template "repo/issue/view_content/pull_merge_box".}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{if .IsSigned}}
|
{{if .IsSigned}}
|
||||||
|
@ -1,7 +1,13 @@
|
|||||||
{{if and .Issue.PullRequest.HasMerged (not .IsPullBranchDeletable)}}
|
{{if and .Issue.PullRequest.HasMerged (not .IsPullBranchDeletable)}}
|
||||||
{{/* Then the merge box will not be displayed because this page already contains enough information */}}
|
{{/* Then the merge box will not be displayed because this page already contains enough information */}}
|
||||||
{{else}}
|
{{else}}
|
||||||
<div class="timeline-item comment merge box">
|
<div class="timeline-item comment pull-merge-box"
|
||||||
|
data-global-init="initRepoPullMergeBox"
|
||||||
|
{{if .PullMergeBoxReloadingInterval}}
|
||||||
|
data-pull-merge-box-reloading-interval="{{.PullMergeBoxReloadingInterval}}"
|
||||||
|
data-pull-link="{{.Issue.Link}}"
|
||||||
|
{{end}}
|
||||||
|
>
|
||||||
<div class="timeline-avatar text {{if .Issue.PullRequest.HasMerged}}purple
|
<div class="timeline-avatar text {{if .Issue.PullRequest.HasMerged}}purple
|
||||||
{{- else if .Issue.IsClosed}}grey
|
{{- else if .Issue.IsClosed}}grey
|
||||||
{{- else if .IsPullWorkInProgress}}grey
|
{{- else if .IsPullWorkInProgress}}grey
|
||||||
@ -97,7 +103,7 @@
|
|||||||
{{template "repo/issue/view_content/update_branch_by_merge" $}}
|
{{template "repo/issue/view_content/update_branch_by_merge" $}}
|
||||||
{{else if .Issue.PullRequest.IsChecking}}
|
{{else if .Issue.PullRequest.IsChecking}}
|
||||||
<div class="item">
|
<div class="item">
|
||||||
{{svg "octicon-sync"}}
|
{{svg "octicon-sync" 16 "circular-spin"}}
|
||||||
{{ctx.Locale.Tr "repo.pulls.is_checking"}}
|
{{ctx.Locale.Tr "repo.pulls.is_checking"}}
|
||||||
</div>
|
</div>
|
||||||
{{else if .Issue.PullRequest.IsAncestor}}
|
{{else if .Issue.PullRequest.IsAncestor}}
|
||||||
@ -191,10 +197,11 @@
|
|||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{template "repo/issue/view_content/update_branch_by_merge" $}}
|
{{template "repo/issue/view_content/update_branch_by_merge" $}}
|
||||||
|
|
||||||
{{if .Issue.PullRequest.IsEmpty}}
|
{{if .Issue.PullRequest.IsEmpty}}
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
|
|
||||||
<div class="item">
|
<div class="item">
|
||||||
{{svg "octicon-alert"}}
|
{{svg "octicon-alert"}}
|
||||||
{{ctx.Locale.Tr "repo.pulls.is_empty"}}
|
{{ctx.Locale.Tr "repo.pulls.is_empty"}}
|
||||||
@ -216,7 +223,7 @@
|
|||||||
const defaultMergeMessage = {{.DefaultMergeBody}};
|
const defaultMergeMessage = {{.DefaultMergeBody}};
|
||||||
const defaultSquashMergeMessage = {{.DefaultSquashMergeBody}};
|
const defaultSquashMergeMessage = {{.DefaultSquashMergeBody}};
|
||||||
const mergeForm = {
|
const mergeForm = {
|
||||||
'baseLink': {{.Link}},
|
'baseLink': {{.Issue.Link}},
|
||||||
'textCancel': {{ctx.Locale.Tr "cancel"}},
|
'textCancel': {{ctx.Locale.Tr "cancel"}},
|
||||||
'textDeleteBranch': {{ctx.Locale.Tr "repo.branch.delete" .HeadTarget}},
|
'textDeleteBranch': {{ctx.Locale.Tr "repo.branch.delete" .HeadTarget}},
|
||||||
'textAutoMergeButtonWhenSucceed': {{ctx.Locale.Tr "repo.pulls.auto_merge_button_when_succeed"}},
|
'textAutoMergeButtonWhenSucceed': {{ctx.Locale.Tr "repo.pulls.auto_merge_button_when_succeed"}},
|
||||||
@ -318,7 +325,7 @@
|
|||||||
{{if .IsBlockedByApprovals}}
|
{{if .IsBlockedByApprovals}}
|
||||||
<div class="item text red">
|
<div class="item text red">
|
||||||
{{svg "octicon-x"}}
|
{{svg "octicon-x"}}
|
||||||
{{ctx.Locale.Tr "repo.pulls.blocked_by_approvals" .GrantedApprovals .ProtectedBranch.RequiredApprovals}}
|
{{ctx.Locale.Tr "repo.pulls.blocked_by_approvals" .GrantedApprovals .ProtectedBranch.RequiredApprovals}}
|
||||||
</div>
|
</div>
|
||||||
{{else if .IsBlockedByRejection}}
|
{{else if .IsBlockedByRejection}}
|
||||||
<div class="item text red">
|
<div class="item text red">
|
||||||
@ -377,7 +384,7 @@
|
|||||||
*/}}
|
*/}}
|
||||||
{{if and $.StillCanManualMerge (not $showGeneralMergeForm)}}
|
{{if and $.StillCanManualMerge (not $showGeneralMergeForm)}}
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
<form class="ui form form-fetch-action" action="{{.Link}}/merge" method="post">{{/* another similar form is in PullRequestMergeForm.vue*/}}
|
<form class="ui form form-fetch-action" action="{{.Issue.Link}}/merge" method="post">{{/* another similar form is in PullRequestMergeForm.vue*/}}
|
||||||
{{.CsrfTokenHtml}}
|
{{.CsrfTokenHtml}}
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<input type="text" name="merge_commit_id" placeholder="{{ctx.Locale.Tr "repo.pulls.merge_commit_id"}}">
|
<input type="text" name="merge_commit_id" placeholder="{{ctx.Locale.Tr "repo.pulls.merge_commit_id"}}">
|
@ -9,7 +9,7 @@
|
|||||||
{{if and $.UpdateAllowed $.UpdateByRebaseAllowed}}
|
{{if and $.UpdateAllowed $.UpdateByRebaseAllowed}}
|
||||||
<div class="tw-inline-block">
|
<div class="tw-inline-block">
|
||||||
<div id="update-pr-branch-with-base" class="ui buttons">
|
<div id="update-pr-branch-with-base" class="ui buttons">
|
||||||
<button class="ui button" data-do="{{$.Link}}/update" data-redirect="{{$.Link}}">
|
<button class="ui button" data-do="{{$.Issue.Link}}/update" data-redirect="{{$.Issue.Link}}">
|
||||||
<span class="button-text">
|
<span class="button-text">
|
||||||
{{ctx.Locale.Tr "repo.pulls.update_branch"}}
|
{{ctx.Locale.Tr "repo.pulls.update_branch"}}
|
||||||
</span>
|
</span>
|
||||||
@ -17,15 +17,15 @@
|
|||||||
<div class="ui dropdown icon button">
|
<div class="ui dropdown icon button">
|
||||||
{{svg "octicon-triangle-down"}}
|
{{svg "octicon-triangle-down"}}
|
||||||
<div class="menu">
|
<div class="menu">
|
||||||
<a class="item active selected" data-do="{{$.Link}}/update">{{ctx.Locale.Tr "repo.pulls.update_branch"}}</a>
|
<a class="item active selected" data-do="{{$.Issue.Link}}/update">{{ctx.Locale.Tr "repo.pulls.update_branch"}}</a>
|
||||||
<a class="item" data-do="{{$.Link}}/update?style=rebase">{{ctx.Locale.Tr "repo.pulls.update_branch_rebase"}}</a>
|
<a class="item" data-do="{{$.Issue.Link}}/update?style=rebase">{{ctx.Locale.Tr "repo.pulls.update_branch_rebase"}}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if and $.UpdateAllowed (not $.UpdateByRebaseAllowed)}}
|
{{if and $.UpdateAllowed (not $.UpdateByRebaseAllowed)}}
|
||||||
<form action="{{$.Link}}/update" method="post" class="ui update-branch-form">
|
<form action="{{$.Issue.Link}}/update" method="post" class="ui update-branch-form">
|
||||||
{{$.CsrfTokenHtml}}
|
{{$.CsrfTokenHtml}}
|
||||||
<button class="ui compact button">
|
<button class="ui compact button">
|
||||||
<span class="ui text">{{ctx.Locale.Tr "repo.pulls.update_branch"}}</span>
|
<span class="ui text">{{ctx.Locale.Tr "repo.pulls.update_branch"}}</span>
|
||||||
|
@ -13,9 +13,13 @@ import (
|
|||||||
|
|
||||||
auth_model "code.gitea.io/gitea/models/auth"
|
auth_model "code.gitea.io/gitea/models/auth"
|
||||||
git_model "code.gitea.io/gitea/models/git"
|
git_model "code.gitea.io/gitea/models/git"
|
||||||
|
"code.gitea.io/gitea/models/issues"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
|
"code.gitea.io/gitea/modules/test"
|
||||||
|
"code.gitea.io/gitea/services/pull"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
@ -165,3 +169,75 @@ func TestPullCreate_EmptyChangesWithSameCommits(t *testing.T) {
|
|||||||
assert.Contains(t, text, "This branch is already included in the target branch. There is nothing to merge.")
|
assert.Contains(t, text, "This branch is already included in the target branch. There is nothing to merge.")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPullStatusDelayCheck(t *testing.T) {
|
||||||
|
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||||
|
defer test.MockVariableValue(&setting.IsProd)()
|
||||||
|
defer test.MockVariableValue(&setting.Repository.PullRequest.DelayCheckForInactiveDays, 1)()
|
||||||
|
defer test.MockVariableValue(&pull.AddPullRequestToCheckQueue)()
|
||||||
|
|
||||||
|
session := loginUser(t, "user2")
|
||||||
|
|
||||||
|
run := func(t *testing.T, fn func(*testing.T)) (issue3 *issues.Issue, checkedPrID int64) {
|
||||||
|
pull.AddPullRequestToCheckQueue = func(prID int64) {
|
||||||
|
checkedPrID = prID
|
||||||
|
}
|
||||||
|
fn(t)
|
||||||
|
issue3 = unittest.AssertExistsAndLoadBean(t, &issues.Issue{RepoID: 1, Index: 3})
|
||||||
|
_ = issue3.LoadPullRequest(t.Context())
|
||||||
|
return issue3, checkedPrID
|
||||||
|
}
|
||||||
|
|
||||||
|
assertReloadingInterval := func(t *testing.T, interval string) {
|
||||||
|
req := NewRequest(t, "GET", "/user2/repo1/pulls/3")
|
||||||
|
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
attr := "data-pull-merge-box-reloading-interval"
|
||||||
|
if interval == "" {
|
||||||
|
assert.NotContains(t, resp.Body.String(), attr)
|
||||||
|
} else {
|
||||||
|
assert.Contains(t, resp.Body.String(), fmt.Sprintf(`%s="%v"`, attr, interval))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PR issue3 is merageable at the beginning
|
||||||
|
issue3, checkedPrID := run(t, func(t *testing.T) {})
|
||||||
|
assert.Equal(t, issues.PullRequestStatusMergeable, issue3.PullRequest.Status)
|
||||||
|
assert.Zero(t, checkedPrID)
|
||||||
|
setting.IsProd = true
|
||||||
|
assertReloadingInterval(t, "") // the PR is mergeable, so no need to reload the merge box
|
||||||
|
setting.IsProd = false
|
||||||
|
assertReloadingInterval(t, "1") // make sure dev mode always do merge box reloading, to make sure the UI logic won't break
|
||||||
|
setting.IsProd = true
|
||||||
|
|
||||||
|
// when base branch changes, PR status should be updated, but it is inactive for long time, so no real check
|
||||||
|
issue3, checkedPrID = run(t, func(t *testing.T) {
|
||||||
|
testEditFile(t, session, "user2", "repo1", "master", "README.md", "new content 1")
|
||||||
|
})
|
||||||
|
assert.Equal(t, issues.PullRequestStatusChecking, issue3.PullRequest.Status)
|
||||||
|
assert.Zero(t, checkedPrID)
|
||||||
|
assertReloadingInterval(t, "2000") // the PR status is "checking", so try to reload the merge box
|
||||||
|
|
||||||
|
// view a PR with status=checking, it starts the real check
|
||||||
|
issue3, checkedPrID = run(t, func(t *testing.T) {
|
||||||
|
req := NewRequest(t, "GET", "/user2/repo1/pulls/3")
|
||||||
|
session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
})
|
||||||
|
assert.Equal(t, issues.PullRequestStatusChecking, issue3.PullRequest.Status)
|
||||||
|
assert.Equal(t, issue3.PullRequest.ID, checkedPrID)
|
||||||
|
|
||||||
|
// when base branch changes, still so no real check
|
||||||
|
issue3, checkedPrID = run(t, func(t *testing.T) {
|
||||||
|
testEditFile(t, session, "user2", "repo1", "master", "README.md", "new content 2")
|
||||||
|
})
|
||||||
|
assert.Equal(t, issues.PullRequestStatusChecking, issue3.PullRequest.Status)
|
||||||
|
assert.Zero(t, checkedPrID)
|
||||||
|
|
||||||
|
// then allow to check PRs without delay, when base branch changes, the PRs will be checked
|
||||||
|
setting.Repository.PullRequest.DelayCheckForInactiveDays = -1
|
||||||
|
issue3, checkedPrID = run(t, func(t *testing.T) {
|
||||||
|
testEditFile(t, session, "user2", "repo1", "master", "README.md", "new content 3")
|
||||||
|
})
|
||||||
|
assert.Equal(t, issues.PullRequestStatusChecking, issue3.PullRequest.Status)
|
||||||
|
assert.Equal(t, issue3.PullRequest.ID, checkedPrID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -117,6 +117,8 @@ code.language-math.is-loading::after {
|
|||||||
animation-timing-function: ease-in-out;
|
animation-timing-function: ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* FIXME: `octicon-sync` is counterclockwise, so this animation is also counterclockwise, it looks somewhat strange.
|
||||||
|
Ideally in the future we should use a better image for clockwise animation. */
|
||||||
.circular-spin {
|
.circular-spin {
|
||||||
animation: circular-spin-keyframes 1s linear infinite;
|
animation: circular-spin-keyframes 1s linear infinite;
|
||||||
}
|
}
|
||||||
|
@ -476,14 +476,6 @@ td .commit-summary {
|
|||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repository.view.issue .merge.box .branch-update.grid .row {
|
|
||||||
padding-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.repository.view.issue .merge.box .branch-update.grid .row .icon {
|
|
||||||
margin-top: 1.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.repository.view.issue .comment-list:not(.prevent-before-timeline)::before {
|
.repository.view.issue .comment-list:not(.prevent-before-timeline)::before {
|
||||||
display: block;
|
display: block;
|
||||||
content: "";
|
content: "";
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
import {createApp} from 'vue';
|
|
||||||
import PullRequestMergeForm from '../components/PullRequestMergeForm.vue';
|
|
||||||
|
|
||||||
export function initRepoPullRequestMergeForm() {
|
|
||||||
const el = document.querySelector('#pull-request-merge-form');
|
|
||||||
if (!el) return;
|
|
||||||
|
|
||||||
const view = createApp(PullRequestMergeForm);
|
|
||||||
view.mount(el);
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
export function initRepoPullRequestCommitStatus() {
|
|
||||||
for (const btn of document.querySelectorAll('.commit-status-hide-checks')) {
|
|
||||||
const panel = btn.closest('.commit-status-panel');
|
|
||||||
const list = panel.querySelector<HTMLElement>('.commit-status-list');
|
|
||||||
btn.addEventListener('click', () => {
|
|
||||||
list.style.maxHeight = list.style.maxHeight ? '' : '0px'; // toggle
|
|
||||||
btn.textContent = btn.getAttribute(list.style.maxHeight ? 'data-show-all' : 'data-hide-all');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
133
web_src/js/features/repo-issue-pull.ts
Normal file
133
web_src/js/features/repo-issue-pull.ts
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
import {createApp} from 'vue';
|
||||||
|
import PullRequestMergeForm from '../components/PullRequestMergeForm.vue';
|
||||||
|
import {GET, POST} from '../modules/fetch.ts';
|
||||||
|
import {fomanticQuery} from '../modules/fomantic/base.ts';
|
||||||
|
import {createElementFromHTML} from '../utils/dom.ts';
|
||||||
|
|
||||||
|
function initRepoPullRequestUpdate(el: HTMLElement) {
|
||||||
|
const prUpdateButtonContainer = el.querySelector('#update-pr-branch-with-base');
|
||||||
|
if (!prUpdateButtonContainer) return;
|
||||||
|
|
||||||
|
const prUpdateButton = prUpdateButtonContainer.querySelector<HTMLButtonElement>(':scope > button');
|
||||||
|
const prUpdateDropdown = prUpdateButtonContainer.querySelector(':scope > .ui.dropdown');
|
||||||
|
prUpdateButton.addEventListener('click', async function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const redirect = this.getAttribute('data-redirect');
|
||||||
|
this.classList.add('is-loading');
|
||||||
|
let response: Response;
|
||||||
|
try {
|
||||||
|
response = await POST(this.getAttribute('data-do'));
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
this.classList.remove('is-loading');
|
||||||
|
}
|
||||||
|
let data: Record<string, any>;
|
||||||
|
try {
|
||||||
|
data = await response?.json(); // the response is probably not a JSON
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
if (data?.redirect) {
|
||||||
|
window.location.href = data.redirect;
|
||||||
|
} else if (redirect) {
|
||||||
|
window.location.href = redirect;
|
||||||
|
} else {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
fomanticQuery(prUpdateDropdown).dropdown({
|
||||||
|
onChange(_text: string, _value: string, $choice: any) {
|
||||||
|
const choiceEl = $choice[0];
|
||||||
|
const url = choiceEl.getAttribute('data-do');
|
||||||
|
if (url) {
|
||||||
|
const buttonText = prUpdateButton.querySelector('.button-text');
|
||||||
|
if (buttonText) {
|
||||||
|
buttonText.textContent = choiceEl.textContent;
|
||||||
|
}
|
||||||
|
prUpdateButton.setAttribute('data-do', url);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function initRepoPullRequestCommitStatus(el: HTMLElement) {
|
||||||
|
for (const btn of el.querySelectorAll('.commit-status-hide-checks')) {
|
||||||
|
const panel = btn.closest('.commit-status-panel');
|
||||||
|
const list = panel.querySelector<HTMLElement>('.commit-status-list');
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
list.style.maxHeight = list.style.maxHeight ? '' : '0px'; // toggle
|
||||||
|
btn.textContent = btn.getAttribute(list.style.maxHeight ? 'data-show-all' : 'data-hide-all');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initRepoPullRequestMergeForm(box: HTMLElement) {
|
||||||
|
const el = box.querySelector('#pull-request-merge-form');
|
||||||
|
if (!el) return;
|
||||||
|
|
||||||
|
const view = createApp(PullRequestMergeForm);
|
||||||
|
view.mount(el);
|
||||||
|
}
|
||||||
|
|
||||||
|
function executeScripts(elem: HTMLElement) {
|
||||||
|
for (const oldScript of elem.querySelectorAll('script')) {
|
||||||
|
// TODO: that's the only way to load the data for the merge form. In the future
|
||||||
|
// we need to completely decouple the page data and embedded script
|
||||||
|
// eslint-disable-next-line github/no-dynamic-script-tag
|
||||||
|
const newScript = document.createElement('script');
|
||||||
|
for (const attr of oldScript.attributes) {
|
||||||
|
if (attr.name === 'type' && attr.value === 'module') continue;
|
||||||
|
newScript.setAttribute(attr.name, attr.value);
|
||||||
|
}
|
||||||
|
newScript.text = oldScript.text;
|
||||||
|
document.body.append(newScript);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initRepoPullMergeBox(el: HTMLElement) {
|
||||||
|
initRepoPullRequestCommitStatus(el);
|
||||||
|
initRepoPullRequestUpdate(el);
|
||||||
|
initRepoPullRequestMergeForm(el);
|
||||||
|
|
||||||
|
const reloadingIntervalValue = el.getAttribute('data-pull-merge-box-reloading-interval');
|
||||||
|
if (!reloadingIntervalValue) return;
|
||||||
|
|
||||||
|
const reloadingInterval = parseInt(reloadingIntervalValue);
|
||||||
|
const pullLink = el.getAttribute('data-pull-link');
|
||||||
|
let timerId: number;
|
||||||
|
|
||||||
|
let reloadMergeBox: () => Promise<void>;
|
||||||
|
const stopReloading = () => {
|
||||||
|
if (!timerId) return;
|
||||||
|
clearTimeout(timerId);
|
||||||
|
timerId = null;
|
||||||
|
};
|
||||||
|
const startReloading = () => {
|
||||||
|
if (timerId) return;
|
||||||
|
setTimeout(reloadMergeBox, reloadingInterval);
|
||||||
|
};
|
||||||
|
const onVisibilityChange = () => {
|
||||||
|
if (document.hidden) {
|
||||||
|
stopReloading();
|
||||||
|
} else {
|
||||||
|
startReloading();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reloadMergeBox = async () => {
|
||||||
|
const resp = await GET(`${pullLink}/merge_box`);
|
||||||
|
stopReloading();
|
||||||
|
if (!resp.ok) {
|
||||||
|
startReloading();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
document.removeEventListener('visibilitychange', onVisibilityChange);
|
||||||
|
const newElem = createElementFromHTML(await resp.text());
|
||||||
|
executeScripts(newElem);
|
||||||
|
el.replaceWith(newElem);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('visibilitychange', onVisibilityChange);
|
||||||
|
startReloading();
|
||||||
|
}
|
@ -197,54 +197,6 @@ export function initRepoIssueCodeCommentCancel() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initRepoPullRequestUpdate() {
|
|
||||||
const prUpdateButtonContainer = document.querySelector('#update-pr-branch-with-base');
|
|
||||||
if (!prUpdateButtonContainer) return;
|
|
||||||
|
|
||||||
const prUpdateButton = prUpdateButtonContainer.querySelector<HTMLButtonElement>(':scope > button');
|
|
||||||
const prUpdateDropdown = prUpdateButtonContainer.querySelector(':scope > .ui.dropdown');
|
|
||||||
prUpdateButton.addEventListener('click', async function (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
const redirect = this.getAttribute('data-redirect');
|
|
||||||
this.classList.add('is-loading');
|
|
||||||
let response: Response;
|
|
||||||
try {
|
|
||||||
response = await POST(this.getAttribute('data-do'));
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
} finally {
|
|
||||||
this.classList.remove('is-loading');
|
|
||||||
}
|
|
||||||
let data: Record<string, any>;
|
|
||||||
try {
|
|
||||||
data = await response?.json(); // the response is probably not a JSON
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
if (data?.redirect) {
|
|
||||||
window.location.href = data.redirect;
|
|
||||||
} else if (redirect) {
|
|
||||||
window.location.href = redirect;
|
|
||||||
} else {
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
fomanticQuery(prUpdateDropdown).dropdown({
|
|
||||||
onChange(_text: string, _value: string, $choice: any) {
|
|
||||||
const choiceEl = $choice[0];
|
|
||||||
const url = choiceEl.getAttribute('data-do');
|
|
||||||
if (url) {
|
|
||||||
const buttonText = prUpdateButton.querySelector('.button-text');
|
|
||||||
if (buttonText) {
|
|
||||||
buttonText.textContent = choiceEl.textContent;
|
|
||||||
}
|
|
||||||
prUpdateButton.setAttribute('data-do', url);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function initRepoPullRequestAllowMaintainerEdit() {
|
export function initRepoPullRequestAllowMaintainerEdit() {
|
||||||
const wrapper = document.querySelector('#allow-edits-from-maintainers');
|
const wrapper = document.querySelector('#allow-edits-from-maintainers');
|
||||||
if (!wrapper) return;
|
if (!wrapper) return;
|
||||||
|
@ -4,7 +4,6 @@ import {
|
|||||||
initRepoIssueBranchSelect, initRepoIssueCodeCommentCancel, initRepoIssueCommentDelete,
|
initRepoIssueBranchSelect, initRepoIssueCodeCommentCancel, initRepoIssueCommentDelete,
|
||||||
initRepoIssueComments, initRepoIssueReferenceIssue,
|
initRepoIssueComments, initRepoIssueReferenceIssue,
|
||||||
initRepoIssueTitleEdit, initRepoIssueWipNewTitle, initRepoIssueWipToggle,
|
initRepoIssueTitleEdit, initRepoIssueWipNewTitle, initRepoIssueWipToggle,
|
||||||
initRepoPullRequestUpdate,
|
|
||||||
} from './repo-issue.ts';
|
} from './repo-issue.ts';
|
||||||
import {initUnicodeEscapeButton} from './repo-unicode-escape.ts';
|
import {initUnicodeEscapeButton} from './repo-unicode-escape.ts';
|
||||||
import {initRepoCloneButtons} from './repo-common.ts';
|
import {initRepoCloneButtons} from './repo-common.ts';
|
||||||
@ -12,14 +11,13 @@ import {initCitationFileCopyContent} from './citation.ts';
|
|||||||
import {initCompLabelEdit} from './comp/LabelEdit.ts';
|
import {initCompLabelEdit} from './comp/LabelEdit.ts';
|
||||||
import {initCompReactionSelector} from './comp/ReactionSelector.ts';
|
import {initCompReactionSelector} from './comp/ReactionSelector.ts';
|
||||||
import {initRepoSettings} from './repo-settings.ts';
|
import {initRepoSettings} from './repo-settings.ts';
|
||||||
import {initRepoPullRequestMergeForm} from './repo-issue-pr-form.ts';
|
|
||||||
import {initRepoPullRequestCommitStatus} from './repo-issue-pr-status.ts';
|
|
||||||
import {hideElem, queryElemChildren, queryElems, showElem} from '../utils/dom.ts';
|
import {hideElem, queryElemChildren, queryElems, showElem} from '../utils/dom.ts';
|
||||||
import {initRepoIssueCommentEdit} from './repo-issue-edit.ts';
|
import {initRepoIssueCommentEdit} from './repo-issue-edit.ts';
|
||||||
import {initRepoMilestone} from './repo-milestone.ts';
|
import {initRepoMilestone} from './repo-milestone.ts';
|
||||||
import {initRepoNew} from './repo-new.ts';
|
import {initRepoNew} from './repo-new.ts';
|
||||||
import {createApp} from 'vue';
|
import {createApp} from 'vue';
|
||||||
import RepoBranchTagSelector from '../components/RepoBranchTagSelector.vue';
|
import RepoBranchTagSelector from '../components/RepoBranchTagSelector.vue';
|
||||||
|
import {initRepoPullMergeBox} from './repo-issue-pull.ts';
|
||||||
|
|
||||||
function initRepoBranchTagSelector() {
|
function initRepoBranchTagSelector() {
|
||||||
registerGlobalInitFunc('initRepoBranchTagSelector', async (elRoot: HTMLInputElement) => {
|
registerGlobalInitFunc('initRepoBranchTagSelector', async (elRoot: HTMLInputElement) => {
|
||||||
@ -69,11 +67,9 @@ export function initRepository() {
|
|||||||
|
|
||||||
initRepoIssueCommentDelete();
|
initRepoIssueCommentDelete();
|
||||||
initRepoIssueCodeCommentCancel();
|
initRepoIssueCodeCommentCancel();
|
||||||
initRepoPullRequestUpdate();
|
|
||||||
initCompReactionSelector();
|
initCompReactionSelector();
|
||||||
|
|
||||||
initRepoPullRequestMergeForm();
|
registerGlobalInitFunc('initRepoPullMergeBox', initRepoPullMergeBox);
|
||||||
initRepoPullRequestCommitStatus();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
initUnicodeEscapeButton();
|
initUnicodeEscapeButton();
|
||||||
|
Loading…
Reference in New Issue
Block a user