-
- {{range .ChangedProtectedFiles}}
-
- {{.}} - {{end}} -
{{ctx.Locale.Tr "repo.pulls.status_checks_need_approvals_helper"}}
+diff --git a/routers/web/repo/issue_view.go b/routers/web/repo/issue_view.go index 852b880ab05..28fdfb34faa 100644 --- a/routers/web/repo/issue_view.go +++ b/routers/web/repo/issue_view.go @@ -835,14 +835,14 @@ func (prInfo *pullRequestViewInfo) prepareMergeBox(ctx *context.Context, issue * } canDelete := false - allowMerge := false canWriteToHeadRepo := false pull_service.StartPullRequestCheckOnView(ctx, pull) if !prInfo.IsPullRequestBroken { + data.ShowUpdatePullInfo = pull.CommitsBehind > 0 && !issue.IsClosed && !pull.IsChecking() && !pull.IsFilesConflicted() && !prInfo.IsPullRequestBroken var err error - ctx.Data["UpdateAllowed"], ctx.Data["UpdateByRebaseAllowed"], err = pull_service.IsUserAllowedToUpdate(ctx, pull, ctx.Doer) + data.UpdateAllowed, data.UpdateByRebaseAllowed, err = pull_service.IsUserAllowedToUpdate(ctx, pull, ctx.Doer) if err != nil { ctx.ServerError("IsUserAllowedToUpdate", err) return @@ -888,7 +888,7 @@ func (prInfo *pullRequestViewInfo) prepareMergeBox(ctx *context.Context, issue * if !canWriteToHeadRepo { // maintainers maybe allowed to push to head repo even if they can't write to it canWriteToHeadRepo = pull.AllowMaintainerEdit && perm.CanWrite(unit.TypeCode) } - allowMerge, err = pull_service.IsUserAllowedToMerge(ctx, pull, perm, ctx.Doer) + data.allowMerge, err = pull_service.IsUserAllowedToMerge(ctx, pull, perm, ctx.Doer) if err != nil { ctx.ServerError("IsUserAllowedToMerge", err) return @@ -903,7 +903,7 @@ func (prInfo *pullRequestViewInfo) prepareMergeBox(ctx *context.Context, issue * data.ReloadingInterval = util.Iif(pull.IsChecking(), 2000, 0) data.ShowMergeInstructions = canWriteToHeadRepo data.ShowPullCommands = pull.HeadRepo != nil && !pull.HasMerged && !issue.IsClosed - ctx.Data["AllowMerge"] = allowMerge + ctx.Data["AllowMerge"] = data.allowMerge pb := prInfo.ProtectedBranchRule if pb != nil { @@ -947,18 +947,6 @@ func (prInfo *pullRequestViewInfo) prepareMergeBox(ctx *context.Context, issue * prConfig := issue.Repo.MustGetUnit(ctx, unit.TypePullRequests).PullRequestsConfig() data.AutodetectManualMerge = prConfig.AutodetectManualMerge - stillCanManualMerge := func() bool { - if pull.HasMerged || issue.IsClosed || !ctx.IsSigned { - return false - } - if pull.IsStatusMergeable() || pull.IsWorkInProgress(ctx) || pull.IsChecking() { - return false - } - return allowMerge && prConfig.AllowManualMerge - } - - ctx.Data["StillCanManualMerge"] = stillCanManualMerge() - enableStatusCheck := pb != nil && pb.EnableStatusCheck ctx.Data["EnableStatusCheck"] = enableStatusCheck @@ -989,6 +977,7 @@ func (prInfo *pullRequestViewInfo) prepareMergeBox(ctx *context.Context, issue * ctx.Data["PullMergeBoxData"] = prInfo.MergeBoxData prInfo.prepareMergeBoxFormProps(ctx) + prInfo.prepareMergeBoxIconColor() } func prepareIssueViewContent(ctx *context.Context, issue *issues_model.Issue) { diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index 7406c1d1228..404f77beb7a 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -266,8 +266,15 @@ type pullMergeBoxData struct { ShowMergeBox bool ReloadingInterval int + TimelineIconClass string + HasOverridableBlockers bool - CanMergeNow bool + CanMergeNow bool // PR is mergeable, either no blocker, or doer is admin and can bypass the blockers + allowMerge bool // doer has permission to merge + + ShowUpdatePullInfo bool + UpdateAllowed bool + UpdateByRebaseAllowed bool MergeFormProps map[string]any ShowPullCommands bool @@ -302,6 +309,9 @@ type pullRequestViewInfo struct { StatusCheckData *pullCommitStatusCheckData CommitStatuses []*git_model.CommitStatus MergeBoxData *pullMergeBoxData + + enableStatusCheck bool + workInProgressPrefix string } func newPullRequestViewInfo() *pullRequestViewInfo { @@ -430,8 +440,8 @@ func (prInfo *pullRequestViewInfo) prepareViewFillCommitStatusInfoForOpen(ctx *c } pb := prInfo.ProtectedBranchRule - enableStatusCheck := pb != nil && pb.EnableStatusCheck - if !enableStatusCheck { + prInfo.enableStatusCheck = pb != nil && pb.EnableStatusCheck + if !prInfo.enableStatusCheck { return } @@ -549,9 +559,10 @@ func (prInfo *pullRequestViewInfo) prepareViewOpenPullInfo(ctx *context.Context) } // this one is used by both sidebar and merge-box + prInfo.workInProgressPrefix = pull.GetWorkInProgressPrefix(ctx) if pull.IsWorkInProgress(ctx) { - ctx.Data["IsPullWorkInProgress"] = true - ctx.Data["WorkInProgressPrefix"] = pull.GetWorkInProgressPrefix(ctx) + ctx.Data["IsPullWorkInProgress"] = prInfo.workInProgressPrefix != "" + ctx.Data["WorkInProgressPrefix"] = prInfo.workInProgressPrefix } } diff --git a/routers/web/repo/pull_merge_box.go b/routers/web/repo/pull_merge_box.go new file mode 100644 index 00000000000..4163d98cbfd --- /dev/null +++ b/routers/web/repo/pull_merge_box.go @@ -0,0 +1,33 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +func (prInfo *pullRequestViewInfo) prepareMergeBoxIconColor() { + pull := prInfo.issue.PullRequest + mergeBoxData := prInfo.MergeBoxData + statusCheckData := prInfo.StatusCheckData + switch { + case pull.HasMerged: + prInfo.MergeBoxData.TimelineIconClass = "tw-text-purple" + case prInfo.issue.IsClosed, prInfo.workInProgressPrefix != "", pull.IsFilesConflicted(): + prInfo.MergeBoxData.TimelineIconClass = "tw-text-text-light" + case prInfo.IsPullRequestBroken, mergeBoxData.isBlockedByApprovals, mergeBoxData.isBlockedByRejection, + mergeBoxData.isBlockedByOfficialReviewRequests, mergeBoxData.isBlockedByOutdatedBranch, mergeBoxData.isBlockedByChangedProtectedFiles: + prInfo.MergeBoxData.TimelineIconClass = "tw-text-red" + case prInfo.enableStatusCheck && (statusCheckData.RequiredChecksState.IsFailure() || statusCheckData.RequiredChecksState.IsError()): + prInfo.MergeBoxData.TimelineIconClass = "tw-text-red" + case prInfo.enableStatusCheck && (statusCheckData.LatestCommitStatus == nil || statusCheckData.RequiredChecksState.IsPending() || statusCheckData.RequiredChecksState.IsWarning()): + prInfo.MergeBoxData.TimelineIconClass = "tw-text-yellow" + case mergeBoxData.allowMerge && mergeBoxData.requireSigned && !mergeBoxData.willSign: + prInfo.MergeBoxData.TimelineIconClass = "tw-text-red" + case pull.IsChecking(): + prInfo.MergeBoxData.TimelineIconClass = "tw-text-yellow" + case pull.IsEmpty(): + prInfo.MergeBoxData.TimelineIconClass = "tw-text-text-light" + case pull.IsStatusMergeable(): + prInfo.MergeBoxData.TimelineIconClass = "tw-text-green" + default: + prInfo.MergeBoxData.TimelineIconClass = "tw-text-red" + } +} diff --git a/routers/web/repo/pull_merge_form.go b/routers/web/repo/pull_merge_form.go index b390fd69349..0cc2bfc27b0 100644 --- a/routers/web/repo/pull_merge_form.go +++ b/routers/web/repo/pull_merge_form.go @@ -16,6 +16,13 @@ import ( func (prInfo *pullRequestViewInfo) prepareMergeBoxFormProps(ctx *context.Context) { pull := prInfo.issue.PullRequest + if pull.HasMerged || prInfo.issue.IsClosed { + return + } + if !prInfo.MergeBoxData.allowMerge { + return + } + prConfig := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypePullRequests).PullRequestsConfig() // Check correct values and select default @@ -69,7 +76,7 @@ func (prInfo *pullRequestViewInfo) prepareMergeBoxFormProps(ctx *context.Context } allOverridableChecksOk := !prInfo.MergeBoxData.HasOverridableBlockers - prInfo.MergeBoxData.MergeFormProps = map[string]any{ + mergeFormProps := map[string]any{ "baseLink": prInfo.issue.Link(), "textCancel": ctx.Locale.Tr("cancel"), "textDeleteBranch": ctx.Locale.Tr("repo.branch.delete", prInfo.headTarget), @@ -97,51 +104,75 @@ func (prInfo *pullRequestViewInfo) prepareMergeBoxFormProps(ctx *context.Context // if this pr can be merged now, then hide the auto merge generalHideAutoMerge := prInfo.MergeBoxData.CanMergeNow && allOverridableChecksOk - prInfo.MergeBoxData.MergeFormProps["mergeStyles"] = []any{ - map[string]any{ - "name": "merge", - "allowed": prConfig.AllowMerge, - "textDoMerge": ctx.Locale.Tr("repo.pulls.merge_pull_request"), - "mergeTitleFieldText": defaultMergeTitle, - "mergeMessageFieldText": defaultMergeBody, - "hideAutoMerge": generalHideAutoMerge, - }, - map[string]any{ - "name": "rebase", - "allowed": prConfig.AllowRebase, - "textDoMerge": ctx.Locale.Tr("repo.pulls.rebase_merge_pull_request"), - "hideMergeMessageTexts": true, - "hideAutoMerge": generalHideAutoMerge, - }, - map[string]any{ - "name": "rebase-merge", - "allowed": prConfig.AllowRebaseMerge, - "textDoMerge": ctx.Locale.Tr("repo.pulls.rebase_merge_commit_pull_request"), - "mergeTitleFieldText": defaultMergeTitle, - "mergeMessageFieldText": defaultMergeBody, - "hideAutoMerge": generalHideAutoMerge, - }, - map[string]any{ - "name": "squash", - "allowed": prConfig.AllowSquash, - "textDoMerge": ctx.Locale.Tr("repo.pulls.squash_merge_pull_request"), - "mergeTitleFieldText": defaultSquashMergeTitle, - "mergeMessageFieldText": defaultSquashMergeCommitMessages + defaultSquashMergeBody, - "hideAutoMerge": generalHideAutoMerge, - }, - map[string]any{ - "name": "fast-forward-only", - "allowed": prConfig.AllowFastForwardOnly && pull.CommitsBehind == 0, - "textDoMerge": ctx.Locale.Tr("repo.pulls.fast_forward_only_merge_pull_request"), - "hideMergeMessageTexts": true, - "hideAutoMerge": generalHideAutoMerge, - }, - map[string]any{ + var mergeStyles []any + if pull.IsStatusMergeable() { + mergeStyles = []any{ + map[string]any{ + "name": "merge", + "allowed": prConfig.AllowMerge, + "textDoMerge": ctx.Locale.Tr("repo.pulls.merge_pull_request"), + "mergeTitleFieldText": defaultMergeTitle, + "mergeMessageFieldText": defaultMergeBody, + "hideAutoMerge": generalHideAutoMerge, + }, + map[string]any{ + "name": "rebase", + "allowed": prConfig.AllowRebase, + "textDoMerge": ctx.Locale.Tr("repo.pulls.rebase_merge_pull_request"), + "hideMergeMessageTexts": true, + "hideAutoMerge": generalHideAutoMerge, + }, + map[string]any{ + "name": "rebase-merge", + "allowed": prConfig.AllowRebaseMerge, + "textDoMerge": ctx.Locale.Tr("repo.pulls.rebase_merge_commit_pull_request"), + "mergeTitleFieldText": defaultMergeTitle, + "mergeMessageFieldText": defaultMergeBody, + "hideAutoMerge": generalHideAutoMerge, + }, + map[string]any{ + "name": "squash", + "allowed": prConfig.AllowSquash, + "textDoMerge": ctx.Locale.Tr("repo.pulls.squash_merge_pull_request"), + "mergeTitleFieldText": defaultSquashMergeTitle, + "mergeMessageFieldText": defaultSquashMergeCommitMessages + defaultSquashMergeBody, + "hideAutoMerge": generalHideAutoMerge, + }, + map[string]any{ + "name": "fast-forward-only", + "allowed": prConfig.AllowFastForwardOnly && pull.CommitsBehind == 0, + "textDoMerge": ctx.Locale.Tr("repo.pulls.fast_forward_only_merge_pull_request"), + "hideMergeMessageTexts": true, + "hideAutoMerge": generalHideAutoMerge, + }, + } + } + + canUseManualMerge := func() bool { + if pull.IsWorkInProgress(ctx) || pull.IsChecking() { + return false + } + return prConfig.AllowManualMerge + } + // Manually Merged is not a well-known feature, it is used to mark a non-mergeable PR (already merged, conflicted) as merged + // To test it: + // Enable "Manually Merged" feature in the Repository Settings + // Create a pull request, either: + // - Merge the pull request branch locally and push the merged commit to Gitea + // - Make some conflicts between the base branch and the pull request branch + // Then the Manually Merged form will be shown in the merge form + if canUseManualMerge() { + mergeStyles = append(mergeStyles, map[string]any{ "name": "manually-merged", "allowed": prConfig.AllowManualMerge, "textDoMerge": ctx.Locale.Tr("repo.pulls.merge_manually"), "hideMergeMessageTexts": true, "hideAutoMerge": true, - }, + }) + } + + if len(mergeStyles) > 0 { + mergeFormProps["mergeStyles"] = mergeStyles + prInfo.MergeBoxData.MergeFormProps = mergeFormProps } } diff --git a/templates/devtest/flex-list.tmpl b/templates/devtest/flex-list.tmpl index 02f0df9ff92..22030202440 100644 --- a/templates/devtest/flex-list.tmpl +++ b/templates/devtest/flex-list.tmpl @@ -102,34 +102,26 @@
{{ctx.Locale.Tr "repo.pulls.status_checks_need_approvals_helper"}}
+