Skip to content

Commit a934389

Browse files
brechtvlwxiaoguang
andauthored
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>
1 parent d1ad8e1 commit a934389

32 files changed

+437
-244
lines changed

custom/conf/app.example.ini

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1155,6 +1155,10 @@ LEVEL = Info
11551155
;;
11561156
;; 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.
11571157
;RETARGET_CHILDREN_ON_MERGE = true
1158+
;;
1159+
;; 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.
1160+
;; Use "-1" to always check all pull requests (old behavior). Use "0" to always delay the checks.
1161+
;DELAY_CHECK_FOR_INACTIVE_DAYS = 7
11581162

11591163
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
11601164
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

models/issues/pull.go

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import (
1010
"fmt"
1111
"io"
1212
"regexp"
13-
"strconv"
1413
"strings"
1514

1615
"code.gitea.io/gitea/models/db"
@@ -104,27 +103,6 @@ const (
104103
PullRequestStatusAncestor
105104
)
106105

107-
func (status PullRequestStatus) String() string {
108-
switch status {
109-
case PullRequestStatusConflict:
110-
return "CONFLICT"
111-
case PullRequestStatusChecking:
112-
return "CHECKING"
113-
case PullRequestStatusMergeable:
114-
return "MERGEABLE"
115-
case PullRequestStatusManuallyMerged:
116-
return "MANUALLY_MERGED"
117-
case PullRequestStatusError:
118-
return "ERROR"
119-
case PullRequestStatusEmpty:
120-
return "EMPTY"
121-
case PullRequestStatusAncestor:
122-
return "ANCESTOR"
123-
default:
124-
return strconv.Itoa(int(status))
125-
}
126-
}
127-
128106
// PullRequestFlow the flow of pull request
129107
type PullRequestFlow int
130108

modules/setting/repository.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ var (
8282
AddCoCommitterTrailers bool
8383
TestConflictingPatchesWithGitApply bool
8484
RetargetChildrenOnMerge bool
85+
DelayCheckForInactiveDays int
8586
} `ini:"repository.pull-request"`
8687

8788
// Issue Setting
@@ -200,6 +201,7 @@ var (
200201
AddCoCommitterTrailers bool
201202
TestConflictingPatchesWithGitApply bool
202203
RetargetChildrenOnMerge bool
204+
DelayCheckForInactiveDays int
203205
}{
204206
WorkInProgressPrefixes: []string{"WIP:", "[WIP]"},
205207
// Same as GitHub. See
@@ -215,6 +217,7 @@ var (
215217
PopulateSquashCommentWithCommitMessages: false,
216218
AddCoCommitterTrailers: true,
217219
RetargetChildrenOnMerge: true,
220+
DelayCheckForInactiveDays: 7,
218221
},
219222

220223
// Issue settings

options/locale/locale_en-US.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1880,7 +1880,7 @@ pulls.add_prefix = Add <strong>%s</strong> prefix
18801880
pulls.remove_prefix = Remove <strong>%s</strong> prefix
18811881
pulls.data_broken = This pull request is broken due to missing fork information.
18821882
pulls.files_conflicted = This pull request has changes conflicting with the target branch.
1883-
pulls.is_checking = "Merge conflict checking is in progress. Try again in few moments."
1883+
pulls.is_checking = Checking for merge conflicts ...
18841884
pulls.is_ancestor = "This branch is already included in the target branch. There is nothing to merge."
18851885
pulls.is_empty = "The changes on this branch are already on the target branch. This will be an empty commit."
18861886
pulls.required_status_check_failed = Some required checks were not successful.

routers/api/v1/repo/pull.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,10 @@ func GetPullRequest(ctx *context.APIContext) {
202202
ctx.APIErrorInternal(err)
203203
return
204204
}
205+
206+
// Consider API access a view for delayed checking.
207+
pull_service.StartPullRequestCheckOnView(ctx, pr)
208+
205209
ctx.JSON(http.StatusOK, convert.ToAPIPullRequest(ctx, pr, ctx.Doer))
206210
}
207211

@@ -287,6 +291,10 @@ func GetPullRequestByBaseHead(ctx *context.APIContext) {
287291
ctx.APIErrorInternal(err)
288292
return
289293
}
294+
295+
// Consider API access a view for delayed checking.
296+
pull_service.StartPullRequestCheckOnView(ctx, pr)
297+
290298
ctx.JSON(http.StatusOK, convert.ToAPIPullRequest(ctx, pr, ctx.Doer))
291299
}
292300

@@ -921,15 +929,15 @@ func MergePullRequest(ctx *context.APIContext) {
921929
if err := pull_service.CheckPullMergeable(ctx, ctx.Doer, &ctx.Repo.Permission, pr, mergeCheckType, form.ForceMerge); err != nil {
922930
if errors.Is(err, pull_service.ErrIsClosed) {
923931
ctx.APIErrorNotFound()
924-
} else if errors.Is(err, pull_service.ErrUserNotAllowedToMerge) {
932+
} else if errors.Is(err, pull_service.ErrNoPermissionToMerge) {
925933
ctx.APIError(http.StatusMethodNotAllowed, "User not allowed to merge PR")
926934
} else if errors.Is(err, pull_service.ErrHasMerged) {
927935
ctx.APIError(http.StatusMethodNotAllowed, "")
928936
} else if errors.Is(err, pull_service.ErrIsWorkInProgress) {
929937
ctx.APIError(http.StatusMethodNotAllowed, "Work in progress PRs cannot be merged")
930938
} else if errors.Is(err, pull_service.ErrNotMergeableState) {
931939
ctx.APIError(http.StatusMethodNotAllowed, "Please try again later")
932-
} else if pull_service.IsErrDisallowedToMerge(err) {
940+
} else if errors.Is(err, pull_service.ErrNotReadyToMerge) {
933941
ctx.APIError(http.StatusMethodNotAllowed, err)
934942
} else if asymkey_service.IsErrWontSign(err) {
935943
ctx.APIError(http.StatusMethodNotAllowed, err)

routers/private/hook_pre_receive.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package private
55

66
import (
7+
"errors"
78
"fmt"
89
"net/http"
910
"os"
@@ -374,7 +375,7 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, r
374375

375376
// Check all status checks and reviews are ok
376377
if err := pull_service.CheckPullBranchProtections(ctx, pr, true); err != nil {
377-
if pull_service.IsErrDisallowedToMerge(err) {
378+
if errors.Is(err, pull_service.ErrNotReadyToMerge) {
378379
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())
379380
ctx.JSON(http.StatusForbidden, private.Response{
380381
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()),

routers/web/repo/issue.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ const (
4343
tplIssueChoose templates.TplName = "repo/issue/choose"
4444
tplIssueView templates.TplName = "repo/issue/view"
4545

46-
tplReactions templates.TplName = "repo/issue/view_content/reactions"
46+
tplPullMergeBox templates.TplName = "repo/issue/view_content/pull_merge_box"
47+
tplReactions templates.TplName = "repo/issue/view_content/reactions"
4748

4849
issueTemplateKey = "IssueTemplate"
4950
issueTemplateTitleKey = "IssueTemplateTitle"

routers/web/repo/issue_comment.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ func NewComment(ctx *context.Context) {
9696
// Regenerate patch and test conflict.
9797
if pr == nil {
9898
issue.PullRequest.HeadCommitID = ""
99-
pull_service.AddToTaskQueue(ctx, issue.PullRequest)
99+
pull_service.StartPullRequestCheckImmediately(ctx, issue.PullRequest)
100100
}
101101

102102
// 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

routers/web/repo/issue_view.go

Lines changed: 51 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
"code.gitea.io/gitea/modules/setting"
3232
"code.gitea.io/gitea/modules/templates"
3333
"code.gitea.io/gitea/modules/templates/vars"
34+
"code.gitea.io/gitea/modules/util"
3435
asymkey_service "code.gitea.io/gitea/services/asymkey"
3536
"code.gitea.io/gitea/services/context"
3637
"code.gitea.io/gitea/services/context/upload"
@@ -271,8 +272,23 @@ func combineLabelComments(issue *issues_model.Issue) {
271272
}
272273
}
273274

274-
// ViewIssue render issue view page
275-
func ViewIssue(ctx *context.Context) {
275+
func prepareIssueViewLoad(ctx *context.Context) *issues_model.Issue {
276+
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
277+
if err != nil {
278+
ctx.NotFoundOrServerError("GetIssueByIndex", issues_model.IsErrIssueNotExist, err)
279+
return nil
280+
}
281+
issue.Repo = ctx.Repo.Repository
282+
ctx.Data["Issue"] = issue
283+
284+
if err = issue.LoadPullRequest(ctx); err != nil {
285+
ctx.ServerError("LoadPullRequest", err)
286+
return nil
287+
}
288+
return issue
289+
}
290+
291+
func handleViewIssueRedirectExternal(ctx *context.Context) {
276292
if ctx.PathParam("type") == "issues" {
277293
// If issue was requested we check if repo has external tracker and redirect
278294
extIssueUnit, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypeExternalTracker)
@@ -294,18 +310,18 @@ func ViewIssue(ctx *context.Context) {
294310
return
295311
}
296312
}
313+
}
297314

298-
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
299-
if err != nil {
300-
if issues_model.IsErrIssueNotExist(err) {
301-
ctx.NotFound(err)
302-
} else {
303-
ctx.ServerError("GetIssueByIndex", err)
304-
}
315+
// ViewIssue render issue view page
316+
func ViewIssue(ctx *context.Context) {
317+
handleViewIssueRedirectExternal(ctx)
318+
if ctx.Written() {
305319
return
306320
}
307-
if issue.Repo == nil {
308-
issue.Repo = ctx.Repo.Repository
321+
322+
issue := prepareIssueViewLoad(ctx)
323+
if ctx.Written() {
324+
return
309325
}
310326

311327
// Make sure type and URL matches.
@@ -337,12 +353,12 @@ func ViewIssue(ctx *context.Context) {
337353
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
338354
upload.AddUploadContext(ctx, "comment")
339355

340-
if err = issue.LoadAttributes(ctx); err != nil {
356+
if err := issue.LoadAttributes(ctx); err != nil {
341357
ctx.ServerError("LoadAttributes", err)
342358
return
343359
}
344360

345-
if err = filterXRefComments(ctx, issue); err != nil {
361+
if err := filterXRefComments(ctx, issue); err != nil {
346362
ctx.ServerError("filterXRefComments", err)
347363
return
348364
}
@@ -351,7 +367,7 @@ func ViewIssue(ctx *context.Context) {
351367

352368
if ctx.IsSigned {
353369
// Update issue-user.
354-
if err = activities_model.SetIssueReadBy(ctx, issue.ID, ctx.Doer.ID); err != nil {
370+
if err := activities_model.SetIssueReadBy(ctx, issue.ID, ctx.Doer.ID); err != nil {
355371
ctx.ServerError("ReadBy", err)
356372
return
357373
}
@@ -365,15 +381,13 @@ func ViewIssue(ctx *context.Context) {
365381

366382
prepareFuncs := []func(*context.Context, *issues_model.Issue){
367383
prepareIssueViewContent,
368-
func(ctx *context.Context, issue *issues_model.Issue) {
369-
preparePullViewPullInfo(ctx, issue)
370-
},
371384
prepareIssueViewCommentsAndSidebarParticipants,
372-
preparePullViewReviewAndMerge,
373385
prepareIssueViewSidebarWatch,
374386
prepareIssueViewSidebarTimeTracker,
375387
prepareIssueViewSidebarDependency,
376388
prepareIssueViewSidebarPin,
389+
func(ctx *context.Context, issue *issues_model.Issue) { preparePullViewPullInfo(ctx, issue) },
390+
preparePullViewReviewAndMerge,
377391
}
378392

379393
for _, prepareFunc := range prepareFuncs {
@@ -412,9 +426,25 @@ func ViewIssue(ctx *context.Context) {
412426
return user_service.CanBlockUser(ctx, ctx.Doer, blocker, blockee)
413427
}
414428

429+
if issue.PullRequest != nil && !issue.PullRequest.IsChecking() && !setting.IsProd {
430+
ctx.Data["PullMergeBoxReloadingInterval"] = 1 // in dev env, force using the reloading logic to make sure it won't break
431+
}
432+
415433
ctx.HTML(http.StatusOK, tplIssueView)
416434
}
417435

436+
func ViewPullMergeBox(ctx *context.Context) {
437+
issue := prepareIssueViewLoad(ctx)
438+
if !issue.IsPull {
439+
ctx.NotFound(nil)
440+
return
441+
}
442+
preparePullViewPullInfo(ctx, issue)
443+
preparePullViewReviewAndMerge(ctx, issue)
444+
ctx.Data["PullMergeBoxReloading"] = issue.PullRequest.IsChecking()
445+
ctx.HTML(http.StatusOK, tplPullMergeBox)
446+
}
447+
418448
func prepareIssueViewSidebarDependency(ctx *context.Context, issue *issues_model.Issue) {
419449
if issue.IsPull && !ctx.Repo.CanRead(unit.TypeIssues) {
420450
ctx.Data["IssueDependencySearchType"] = "pulls"
@@ -792,6 +822,8 @@ func preparePullViewReviewAndMerge(ctx *context.Context, issue *issues_model.Iss
792822
allowMerge := false
793823
canWriteToHeadRepo := false
794824

825+
pull_service.StartPullRequestCheckOnView(ctx, pull)
826+
795827
if ctx.IsSigned {
796828
if err := pull.LoadHeadRepo(ctx); err != nil {
797829
log.Error("LoadHeadRepo: %v", err)
@@ -838,6 +870,7 @@ func preparePullViewReviewAndMerge(ctx *context.Context, issue *issues_model.Iss
838870
}
839871
}
840872

873+
ctx.Data["PullMergeBoxReloadingInterval"] = util.Iif(pull != nil && pull.IsChecking(), 2000, 0)
841874
ctx.Data["CanWriteToHeadRepo"] = canWriteToHeadRepo
842875
ctx.Data["ShowMergeInstructions"] = canWriteToHeadRepo
843876
ctx.Data["AllowMerge"] = allowMerge
@@ -958,5 +991,4 @@ func prepareIssueViewContent(ctx *context.Context, issue *issues_model.Issue) {
958991
ctx.ServerError("roleDescriptor", err)
959992
return
960993
}
961-
ctx.Data["Issue"] = issue
962994
}

routers/web/repo/pull.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1052,15 +1052,15 @@ func MergePullRequest(ctx *context.Context) {
10521052
} else {
10531053
ctx.JSONError(ctx.Tr("repo.issues.closed_title"))
10541054
}
1055-
case errors.Is(err, pull_service.ErrUserNotAllowedToMerge):
1055+
case errors.Is(err, pull_service.ErrNoPermissionToMerge):
10561056
ctx.JSONError(ctx.Tr("repo.pulls.update_not_allowed"))
10571057
case errors.Is(err, pull_service.ErrHasMerged):
10581058
ctx.JSONError(ctx.Tr("repo.pulls.has_merged"))
10591059
case errors.Is(err, pull_service.ErrIsWorkInProgress):
10601060
ctx.JSONError(ctx.Tr("repo.pulls.no_merge_wip"))
10611061
case errors.Is(err, pull_service.ErrNotMergeableState):
10621062
ctx.JSONError(ctx.Tr("repo.pulls.no_merge_not_ready"))
1063-
case pull_service.IsErrDisallowedToMerge(err):
1063+
case errors.Is(err, pull_service.ErrNotReadyToMerge):
10641064
ctx.JSONError(ctx.Tr("repo.pulls.no_merge_not_ready"))
10651065
case asymkey_service.IsErrWontSign(err):
10661066
ctx.JSONError(err.Error()) // has no translation ...

routers/web/web.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1505,6 +1505,7 @@ func registerWebRoutes(m *web.Router) {
15051505
m.Get("", repo.SetWhitespaceBehavior, repo.GetPullDiffStats, repo.ViewIssue)
15061506
m.Get(".diff", repo.DownloadPullDiff)
15071507
m.Get(".patch", repo.DownloadPullPatch)
1508+
m.Get("/merge_box", repo.ViewPullMergeBox)
15081509
m.Group("/commits", func() {
15091510
m.Get("", repo.SetWhitespaceBehavior, repo.GetPullDiffStats, repo.ViewPullCommits)
15101511
m.Get("/list", repo.GetPullCommits)

services/agit/agit.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.
204204
return nil, fmt.Errorf("failed to update pull ref. Error: %w", err)
205205
}
206206

207-
pull_service.AddToTaskQueue(ctx, pr)
207+
pull_service.StartPullRequestCheckImmediately(ctx, pr)
208208
err = pr.LoadIssue(ctx)
209209
if err != nil {
210210
return nil, fmt.Errorf("failed to load pull issue. Error: %w", err)

services/automerge/automerge.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,7 @@ func handlePullRequestAutoMerge(pullID int64, sha string) {
289289
}
290290

291291
if err := pull_service.CheckPullMergeable(ctx, doer, &perm, pr, pull_service.MergeCheckTypeGeneral, false); err != nil {
292-
if errors.Is(err, pull_service.ErrUserNotAllowedToMerge) {
292+
if errors.Is(err, pull_service.ErrNotReadyToMerge) {
293293
log.Info("%-v was scheduled to automerge by an unauthorized user", pr)
294294
return
295295
}

services/migrations/gitea_uploader.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -556,7 +556,7 @@ func (g *GiteaLocalUploader) CreatePullRequests(ctx context.Context, prs ...*bas
556556
}
557557
for _, pr := range gprs {
558558
g.issues[pr.Issue.Index] = pr.Issue
559-
pull.AddToTaskQueue(ctx, pr)
559+
pull.StartPullRequestCheckImmediately(ctx, pr)
560560
}
561561
return nil
562562
}

0 commit comments

Comments
 (0)