Refactor pull request view (7) (#37524)

Almost done

`pull_merge_box.tmpl` only has about 80 lines now, and (almost) all
variable accesses are strictly typed.

---------

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com>
Co-authored-by: Nicolas <bircni@icloud.com>
This commit is contained in:
wxiaoguang
2026-05-05 04:13:38 +08:00
committed by GitHub
parent 89a49de0fd
commit a90d5dd131
11 changed files with 439 additions and 380 deletions
-1
View File
@@ -1958,7 +1958,6 @@
"repo.signing.wont_sign.headsigned": "The merge will not be signed as the head commit is not signed.", "repo.signing.wont_sign.headsigned": "The merge will not be signed as the head commit is not signed.",
"repo.signing.wont_sign.commitssigned": "The merge will not be signed as all the associated commits are not signed.", "repo.signing.wont_sign.commitssigned": "The merge will not be signed as all the associated commits are not signed.",
"repo.signing.wont_sign.approved": "The merge will not be signed as the PR is not approved.", "repo.signing.wont_sign.approved": "The merge will not be signed as the PR is not approved.",
"repo.signing.wont_sign.not_signed_in": "You are not signed in.",
"repo.ext_wiki": "Access to External Wiki", "repo.ext_wiki": "Access to External Wiki",
"repo.ext_wiki.desc": "Link to an external wiki.", "repo.ext_wiki.desc": "Link to an external wiki.",
"repo.wiki": "Wiki", "repo.wiki": "Wiki",
-1
View File
@@ -624,7 +624,6 @@ func (cpi *comparePageInfoType) prepareCreatePullRequestPage(ctx *context.Contex
return return
} }
ctx.Data["PullRequest"] = pr ctx.Data["PullRequest"] = pr
ctx.HTML(http.StatusOK, tplCompareDiff)
return return
} }
+120 -58
View File
@@ -28,6 +28,7 @@ import (
"code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/markup/markdown"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/svg"
"code.gitea.io/gitea/modules/templates/vars" "code.gitea.io/gitea/modules/templates/vars"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/modules/web/middleware"
@@ -484,26 +485,57 @@ func prepareIssueViewSidebarDependency(ctx *context.Context, issue *issues_model
ctx.Data["BlockingDependencies"], ctx.Data["BlockingDependenciesNotPermitted"] = checkBlockedByIssues(ctx, blocking) ctx.Data["BlockingDependencies"], ctx.Data["BlockingDependenciesNotPermitted"] = checkBlockedByIssues(ctx, blocking)
} }
func (prInfo *pullRequestViewInfo) prepareMergeBoxRequireSigning(ctx *context.Context) { func (prInfo *pullRequestViewInfo) prepareMergeBoxCommitSigning(ctx *context.Context) {
pull := prInfo.issue.PullRequest pull := prInfo.issue.PullRequest
willSign := false data := prInfo.MergeBoxData
pb := prInfo.ProtectedBranchRule
data.requireSigned = pb != nil && pb.RequireSignedCommits
wontSignReason := ""
if ctx.Doer != nil { if ctx.Doer != nil {
sign, key, _, err := asymkey_service.SignMerge(ctx, pull, ctx.Doer, ctx.Repo.GitRepo) sign, key, _, err := asymkey_service.SignMerge(ctx, pull, ctx.Doer, ctx.Repo.GitRepo)
willSign = sign data.willSign = sign
ctx.Data["SigningKeyMergeDisplay"] = asymkey_model.GetDisplaySigningKey(key) data.signingKeyMergeDisplay = asymkey_model.GetDisplaySigningKey(key)
if err != nil { if err != nil {
if asymkey_service.IsErrWontSign(err) { if asymkey_service.IsErrWontSign(err) {
ctx.Data["WontSignReason"] = err.(*asymkey_service.ErrWontSign).Reason wontSignReason = string(err.(*asymkey_service.ErrWontSign).Reason)
} else { } else {
ctx.Data["WontSignReason"] = "error" wontSignReason = "error"
log.Error("Error whilst checking if could sign pr %d in repo %s. Error: %v", pull.ID, pull.BaseRepo.FullName(), err) log.Error("Error whilst checking if could sign pr %d in repo %s. Error: %v", pull.ID, pull.BaseRepo.FullName(), err)
} }
} }
} else {
ctx.Data["WontSignReason"] = "not_signed_in"
} }
ctx.Data["WillSign"] = willSign
prInfo.MergeBoxData.willSign = willSign if data.willSign {
prInfo.MergeBoxData.infoMergePrompts.AddInfoItem(
svg.RenderHTML("octicon-lock", 16, "tw-text-green"),
ctx.Locale.Tr("repo.signing.will_sign", data.signingKeyMergeDisplay),
)
}
if !data.requireSigned {
if wontSignReason != "" {
data.infoMergePrompts.AddInfoItem(
svg.RenderHTML("octicon-unlock"),
ctx.Locale.Tr("repo.signing.wont_sign."+wontSignReason),
)
}
return
}
if data.requireSigned && !data.willSign {
data.infoProtectionBlockers.AddErrorItem(
svg.RenderHTML("octicon-x"),
ctx.Locale.Tr("repo.pulls.require_signed_wont_sign"),
)
if wontSignReason != "" {
data.infoProtectionBlockers.AddInfoItem(
svg.RenderHTML("octicon-unlock"),
ctx.Locale.Tr("repo.signing.wont_sign."+wontSignReason),
)
}
}
} }
func prepareIssueViewSidebarWatch(ctx *context.Context, issue *issues_model.Issue) { func prepareIssueViewSidebarWatch(ctx *context.Context, issue *issues_model.Issue) {
@@ -571,8 +603,7 @@ func (prInfo *pullRequestViewInfo) prepareMergeBoxDeleteBranch(ctx *context.Cont
isPullBranchDeletable = !exist isPullBranchDeletable = !exist
} }
ctx.Data["IsPullBranchDeletable"] = isPullBranchDeletable prInfo.MergeBoxData.IsPullBranchDeletable = isPullBranchDeletable
prInfo.MergeBoxData.isPullBranchDeletable = isPullBranchDeletable
} }
func prepareIssueViewSidebarPin(ctx *context.Context, issue *issues_model.Issue) { func prepareIssueViewSidebarPin(ctx *context.Context, issue *issues_model.Issue) {
@@ -829,11 +860,6 @@ func (prInfo *pullRequestViewInfo) prepareMergeBox(ctx *context.Context, issue *
data := &pullMergeBoxData{} data := &pullMergeBoxData{}
prInfo.MergeBoxData = data prInfo.MergeBoxData = data
statusCheckData := prInfo.StatusCheckData
if statusCheckData == nil {
statusCheckData = &pullCommitStatusCheckData{} // make the following logic easier, no need to keep checking "nil"
}
canDelete := false canDelete := false
canWriteToHeadRepo := false canWriteToHeadRepo := false
@@ -849,11 +875,6 @@ func (prInfo *pullRequestViewInfo) prepareMergeBox(ctx *context.Context, issue *
} }
} }
if pull.IsFilesConflicted() {
ctx.Data["IsPullFilesConflicted"] = true
ctx.Data["ConflictedFiles"] = pull.ConflictedFiles
}
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)
@@ -903,38 +924,13 @@ func (prInfo *pullRequestViewInfo) prepareMergeBox(ctx *context.Context, issue *
data.ReloadingInterval = util.Iif(pull.IsChecking(), 2000, 0) data.ReloadingInterval = util.Iif(pull.IsChecking(), 2000, 0)
data.ShowMergeInstructions = canWriteToHeadRepo data.ShowMergeInstructions = canWriteToHeadRepo
data.ShowPullCommands = pull.HeadRepo != nil && !pull.HasMerged && !issue.IsClosed data.ShowPullCommands = pull.HeadRepo != nil && !pull.HasMerged && !issue.IsClosed
ctx.Data["AllowMerge"] = data.allowMerge
pb := prInfo.ProtectedBranchRule prInfo.prepareMergeBoxProtectionChecks(ctx)
if pb != nil { if ctx.Written() {
pb.Repo = pull.BaseRepo return
ctx.Data["ProtectedBranch"] = pb
data.isBlockedByApprovals = !issues_model.HasEnoughApprovals(ctx, pb, pull)
ctx.Data["IsBlockedByApprovals"] = data.isBlockedByApprovals
data.isBlockedByRejection = issues_model.MergeBlockedByRejectedReview(ctx, pb, pull)
ctx.Data["IsBlockedByRejection"] = data.isBlockedByRejection
data.isBlockedByOfficialReviewRequests = issues_model.MergeBlockedByOfficialReviewRequests(ctx, pb, pull)
ctx.Data["IsBlockedByOfficialReviewRequests"] = data.isBlockedByOfficialReviewRequests
data.isBlockedByOutdatedBranch = issues_model.MergeBlockedByOutdatedBranch(pb, pull)
ctx.Data["IsBlockedByOutdatedBranch"] = data.isBlockedByOutdatedBranch
data.isBlockedByChangedProtectedFiles = len(pull.ChangedProtectedFiles) != 0
ctx.Data["IsBlockedByChangedProtectedFiles"] = data.isBlockedByChangedProtectedFiles
data.requireSigned = pb.RequireSignedCommits
ctx.Data["RequireSigned"] = data.requireSigned
ctx.Data["GrantedApprovals"] = issues_model.GetGrantedApprovalsCount(ctx, pb, pull)
ctx.Data["ChangedProtectedFiles"] = pull.ChangedProtectedFiles
ctx.Data["ChangedProtectedFilesNum"] = len(pull.ChangedProtectedFiles)
ctx.Data["RequireApprovalsWhitelist"] = pb.EnableApprovalsWhitelist
} }
prInfo.prepareMergeBoxRequireSigning(ctx) prInfo.prepareMergeBoxCommitSigning(ctx)
if ctx.Written() { if ctx.Written() {
return return
} }
@@ -947,18 +943,15 @@ func (prInfo *pullRequestViewInfo) prepareMergeBox(ctx *context.Context, issue *
prConfig := issue.Repo.MustGetUnit(ctx, unit.TypePullRequests).PullRequestsConfig() prConfig := issue.Repo.MustGetUnit(ctx, unit.TypePullRequests).PullRequestsConfig()
data.AutodetectManualMerge = prConfig.AutodetectManualMerge data.AutodetectManualMerge = prConfig.AutodetectManualMerge
enableStatusCheck := pb != nil && pb.EnableStatusCheck
ctx.Data["EnableStatusCheck"] = enableStatusCheck
// Only show the merge box if the PR is not merged, or the branch is deletable. // Only show the merge box if the PR is not merged, or the branch is deletable.
// Otherwise, there is nothing to do, because the PR view page already contains enough information. // Otherwise, there is nothing to do, because the PR view page already contains enough information.
data.ShowMergeBox = !pull.HasMerged || data.isPullBranchDeletable data.ShowMergeBox = !pull.HasMerged || data.IsPullBranchDeletable
isRepoAdmin := ctx.IsSigned && (ctx.Repo.Permission.IsAdmin() || ctx.Doer.IsAdmin) isRepoAdmin := ctx.IsSigned && (ctx.Repo.Permission.IsAdmin() || ctx.Doer.IsAdmin)
// admin can merge without checks, writer can merge when checks succeed // admin can merge without checks, writer can merge when checks succeed
// admin and writer both can make an auto merge schedule (not affected by overridable blockers) // admin and writer both can make an auto merge schedule (not affected by overridable blockers)
data.hasStatusCheckBlocker = enableStatusCheck && !statusCheckData.RequiredChecksState.IsSuccess() data.hasStatusCheckBlocker = data.enableStatusCheck && !data.StatusCheckData.RequiredChecksState.IsSuccess()
// this logic is from: // this logic is from:
// {{$notAllOverridableChecksOk := or .IsBlockedByApprovals .IsBlockedByRejection .IsBlockedByOfficialReviewRequests .IsBlockedByOutdatedBranch .IsBlockedByChangedProtectedFiles (and .EnableStatusCheck (not $requiredStatusCheckState.IsSuccess))}} // {{$notAllOverridableChecksOk := or .IsBlockedByApprovals .IsBlockedByRejection .IsBlockedByOfficialReviewRequests .IsBlockedByOutdatedBranch .IsBlockedByChangedProtectedFiles (and .EnableStatusCheck (not $requiredStatusCheckState.IsSuccess))}}
@@ -971,13 +964,82 @@ func (prInfo *pullRequestViewInfo) prepareMergeBox(ctx *context.Context, issue *
// {{$canMergeNow := and (or (and (not $.ProtectedBranch.BlockAdminMergeOverride) $.IsRepoAdmin) (not $notAllOverridableChecksOk)) (or (not .AllowMerge) (not .RequireSigned) .WillSign)}} // {{$canMergeNow := and (or (and (not $.ProtectedBranch.BlockAdminMergeOverride) $.IsRepoAdmin) (not $notAllOverridableChecksOk)) (or (not .AllowMerge) (not .RequireSigned) .WillSign)}}
// HINT: legacy "(not .AllowMerge)" is not right (always false, does nothing), fixed here // HINT: legacy "(not .AllowMerge)" is not right (always false, does nothing), fixed here
// CanMergeNow means: if the doer has write permission, whether the PR can be merged now // CanMergeNow means: if the doer has write permission, whether the PR can be merged now
adminCanOverrideBlockers := (pb == nil || !pb.BlockAdminMergeOverride) && isRepoAdmin adminCanOverrideBlockers := (prInfo.ProtectedBranchRule == nil || !prInfo.ProtectedBranchRule.BlockAdminMergeOverride) && isRepoAdmin
data.CanMergeNow = (!data.HasOverridableBlockers || adminCanOverrideBlockers) && // status checks are satisfied data.CanMergeNow = (!data.HasOverridableBlockers || adminCanOverrideBlockers) && // status checks are satisfied
(!data.requireSigned || data.willSign) // signing requirement is satisfied (!data.requireSigned || data.willSign) // signing requirement is satisfied
ctx.Data["PullMergeBoxData"] = prInfo.MergeBoxData
prInfo.prepareMergeBoxFormProps(ctx) prInfo.prepareMergeBoxFormProps(ctx)
prInfo.prepareMergeBoxInfoItems(ctx)
prInfo.prepareMergeBoxIconColor() prInfo.prepareMergeBoxIconColor()
ctx.Data["PullMergeBoxData"] = prInfo.MergeBoxData
}
func (prInfo *pullRequestViewInfo) prepareMergeBoxProtectionChecks(ctx *context.Context) {
pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, ctx.Repo.Repository.ID, prInfo.issue.PullRequest.BaseBranch)
if err != nil {
ctx.ServerError("GetFirstMatchProtectedBranchRule", err)
return
}
if pb != nil {
pb.Repo = prInfo.issue.PullRequest.BaseRepo
prInfo.ProtectedBranchRule = pb
}
prInfo.prepareMergeBoxStatusCheckData(ctx)
if ctx.Written() {
return
}
prInfo.prepareMergeBoxProtectedRules(ctx)
if ctx.Written() {
return
}
}
func (prInfo *pullRequestViewInfo) prepareMergeBoxProtectedRules(ctx *context.Context) {
pb := prInfo.ProtectedBranchRule
if pb == nil {
return
}
pull := prInfo.issue.PullRequest
data := prInfo.MergeBoxData
data.isBlockedByApprovals = !issues_model.HasEnoughApprovals(ctx, pb, pull)
if data.isBlockedByApprovals {
grantedApprovals := issues_model.GetGrantedApprovalsCount(ctx, pb, pull)
blockerInfo := ctx.Locale.Tr("repo.pulls.blocked_by_approvals", grantedApprovals, pb.RequiredApprovals)
if pb.EnableApprovalsWhitelist {
blockerInfo = ctx.Locale.Tr("repo.pulls.blocked_by_approvals_whitelisted", grantedApprovals, pb.RequiredApprovals)
}
data.infoProtectionBlockers.AddErrorItem(svg.RenderHTML("octicon-x"), blockerInfo)
}
data.isBlockedByRejection = issues_model.MergeBlockedByRejectedReview(ctx, pb, pull)
if data.isBlockedByRejection {
data.infoProtectionBlockers.AddErrorItem(svg.RenderHTML("octicon-x"), ctx.Locale.Tr("repo.pulls.blocked_by_rejection"))
}
data.isBlockedByOfficialReviewRequests = issues_model.MergeBlockedByOfficialReviewRequests(ctx, pb, pull)
if data.isBlockedByOfficialReviewRequests {
data.infoProtectionBlockers.AddErrorItem(svg.RenderHTML("octicon-x"), ctx.Locale.Tr("repo.pulls.blocked_by_official_review_requests"))
}
data.isBlockedByOutdatedBranch = issues_model.MergeBlockedByOutdatedBranch(pb, pull)
if data.isBlockedByOutdatedBranch {
data.infoProtectionBlockers.AddErrorItem(svg.RenderHTML("octicon-x"), ctx.Locale.Tr("repo.pulls.blocked_by_outdated_branch"))
}
data.isBlockedByChangedProtectedFiles = len(pull.ChangedProtectedFiles) != 0
if data.isBlockedByChangedProtectedFiles {
detailItems := escapeStringSliceToHTML(pull.ChangedProtectedFiles)
data.infoProtectionBlockers.AddErrorItem(
svg.RenderHTML("octicon-x"),
ctx.Locale.TrN(len(pull.ChangedProtectedFiles), "repo.pulls.blocked_by_changed_protected_files_1", "repo.pulls.blocked_by_changed_protected_files_n"),
detailItems,
)
}
} }
func prepareIssueViewContent(ctx *context.Context, issue *issues_model.Issue) { func prepareIssueViewContent(ctx *context.Context, issue *issues_model.Issue) {
+58 -46
View File
@@ -9,6 +9,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"html" "html"
"html/template"
"net/http" "net/http"
"strconv" "strconv"
"strings" "strings"
@@ -35,6 +36,7 @@ import (
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/svg"
"code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/translation"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
@@ -56,7 +58,6 @@ import (
) )
const ( const (
tplCompareDiff templates.TplName = "repo/diff/compare"
tplPullCommits templates.TplName = "repo/pulls/commits" tplPullCommits templates.TplName = "repo/pulls/commits"
tplPullFiles templates.TplName = "repo/pulls/files" tplPullFiles templates.TplName = "repo/pulls/files"
@@ -268,6 +269,13 @@ type pullMergeBoxData struct {
TimelineIconClass string TimelineIconClass string
ClosedInfoTitle template.HTML
ClosedInfoBody template.HTML
enableStatusCheck bool
StatusCheckData *pullCommitStatusCheckData
ShowStatusCheck bool
HasOverridableBlockers bool HasOverridableBlockers bool
CanMergeNow bool // PR is mergeable, either no blocker, or doer is admin and can bypass the blockers CanMergeNow bool // PR is mergeable, either no blocker, or doer is admin and can bypass the blockers
allowMerge bool // doer has permission to merge allowMerge bool // doer has permission to merge
@@ -283,7 +291,7 @@ type pullMergeBoxData struct {
// don't expose unneeded fields to templates, need more refactoring changes // don't expose unneeded fields to templates, need more refactoring changes
hasStatusCheckBlocker bool hasStatusCheckBlocker bool
isPullBranchDeletable bool IsPullBranchDeletable bool
isBlockedByApprovals bool isBlockedByApprovals bool
isBlockedByRejection bool isBlockedByRejection bool
@@ -291,6 +299,13 @@ type pullMergeBoxData struct {
isBlockedByOutdatedBranch bool isBlockedByOutdatedBranch bool
isBlockedByChangedProtectedFiles bool isBlockedByChangedProtectedFiles bool
requireSigned, willSign bool requireSigned, willSign bool
signingKeyMergeDisplay string
infoCommitBlockers pullMergeBoxInfoItemCollection
infoProtectionBlockers pullMergeBoxInfoItemCollection
infoMergePrompts pullMergeBoxInfoItemCollection
InfoSections []*pullInfoSection
} }
// pullRequestViewInfo is a structured type for viewing pull request // pullRequestViewInfo is a structured type for viewing pull request
@@ -306,11 +321,8 @@ type pullRequestViewInfo struct {
CompareInfo git_service.CompareInfo CompareInfo git_service.CompareInfo
ProtectedBranchRule *git_model.ProtectedBranch ProtectedBranchRule *git_model.ProtectedBranch
StatusCheckData *pullCommitStatusCheckData
CommitStatuses []*git_model.CommitStatus
MergeBoxData *pullMergeBoxData MergeBoxData *pullMergeBoxData
enableStatusCheck bool
workInProgressPrefix string workInProgressPrefix string
} }
@@ -349,7 +361,6 @@ func (prInfo *pullRequestViewInfo) prepareViewFillInfo(ctx *context.Context, bas
if ctx.Written() { if ctx.Written() {
return return
} }
prInfo.prepareViewFillCommitStatusInfo(ctx)
} }
func (prInfo *pullRequestViewInfo) prepareViewFillCompareInfo(ctx *context.Context, baseRef git.RefName) { func (prInfo *pullRequestViewInfo) prepareViewFillCompareInfo(ctx *context.Context, baseRef git.RefName) {
@@ -379,56 +390,47 @@ func (prInfo *pullRequestViewInfo) prepareViewFillCompareInfo(ctx *context.Conte
prInfo.IsPullRequestBroken = true prInfo.IsPullRequestBroken = true
} }
ctx.Data["IsPullRequestBroken"] = prInfo.IsPullRequestBroken
ctx.Data["NumCommits"] = len(prInfo.CompareInfo.Commits) ctx.Data["NumCommits"] = len(prInfo.CompareInfo.Commits)
ctx.Data["NumFiles"] = prInfo.CompareInfo.NumFiles ctx.Data["NumFiles"] = prInfo.CompareInfo.NumFiles
prInfo.setTemplateDataMergeTarget(ctx) prInfo.setTemplateDataMergeTarget(ctx)
} }
func (prInfo *pullRequestViewInfo) prepareViewFillCommitStatusInfo(ctx *context.Context) { func (prInfo *pullRequestViewInfo) prepareMergeBoxStatusCheckData(ctx *context.Context) {
headCommitID := prInfo.CompareInfo.HeadCommitID headCommitID := prInfo.CompareInfo.HeadCommitID
if headCommitID == "" { if headCommitID == "" || prInfo.issue.IsClosed {
return return
} }
repo := ctx.Repo.Repository data := prInfo.MergeBoxData
var pbRequiredContexts []string
data.enableStatusCheck = prInfo.ProtectedBranchRule != nil && prInfo.ProtectedBranchRule.EnableStatusCheck
if prInfo.ProtectedBranchRule != nil {
pbRequiredContexts = prInfo.ProtectedBranchRule.StatusCheckContexts
}
statusCheckData := &pullCommitStatusCheckData{} statusCheckData := &pullCommitStatusCheckData{}
prInfo.StatusCheckData = statusCheckData data.StatusCheckData = statusCheckData
commitStatuses, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, prInfo.CompareInfo.HeadCommitID, db.ListOptionsAll) commitStatuses, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, prInfo.CompareInfo.HeadCommitID, db.ListOptionsAll)
if err != nil { if err != nil {
ctx.ServerError("GetLatestCommitStatus", err) log.Error("GetLatestCommitStatus: %v", err)
return
} }
if !ctx.Repo.Permission.CanRead(unit.TypeActions) { if !ctx.Repo.Permission.CanRead(unit.TypeActions) {
git_model.CommitStatusesHideActionsURL(ctx, commitStatuses) git_model.CommitStatusesHideActionsURL(ctx, commitStatuses)
} }
combinedCommitStatus := git_model.CalcCommitStatus(commitStatuses)
prInfo.CommitStatuses = commitStatuses statusCheckData.ApproveLink = fmt.Sprintf("%s/actions/approve-all-checks?commit_id=%s", ctx.Repo.Repository.Link(), headCommitID)
statusCheckData.ApproveLink = fmt.Sprintf("%s/actions/approve-all-checks?commit_id=%s", repo.Link(), headCommitID) statusCheckData.PullCommitStatuses = commitStatuses
statusCheckData.LatestCommitStatus = git_model.CalcCommitStatus(commitStatuses) if combinedCommitStatus != nil {
ctx.Data["LatestCommitStatuses"] = commitStatuses statusCheckData.pullCommitStatusState = combinedCommitStatus.State
ctx.Data["LatestCommitStatus"] = statusCheckData.LatestCommitStatus
ctx.Data["StatusCheckData"] = prInfo.StatusCheckData
prInfo.ProtectedBranchRule, err = git_model.GetFirstMatchProtectedBranchRule(ctx, ctx.Repo.Repository.ID, prInfo.issue.PullRequest.BaseBranch)
if err != nil {
ctx.ServerError("GetFirstMatchProtectedBranchRule", err)
return
} }
if !prInfo.issue.IsClosed { data.ShowStatusCheck = data.enableStatusCheck || len(statusCheckData.PullCommitStatuses) > 0
prInfo.prepareViewFillCommitStatusInfoForOpen(ctx)
}
}
func (prInfo *pullRequestViewInfo) prepareViewFillCommitStatusInfoForOpen(ctx *context.Context) {
statusCheckData := prInfo.StatusCheckData
commitStatuses := prInfo.CommitStatuses
runs, err := actions_service.GetRunsFromCommitStatuses(ctx, commitStatuses) runs, err := actions_service.GetRunsFromCommitStatuses(ctx, commitStatuses)
if err != nil { if err != nil {
ctx.ServerError("GetRunsFromCommitStatuses", err) log.Error("GetRunsFromCommitStatuses: %v", err)
return
} }
for _, run := range runs { for _, run := range runs {
if run.NeedApproval { if run.NeedApproval {
@@ -439,14 +441,8 @@ func (prInfo *pullRequestViewInfo) prepareViewFillCommitStatusInfoForOpen(ctx *c
statusCheckData.CanApprove = ctx.Repo.Permission.CanWrite(unit.TypeActions) statusCheckData.CanApprove = ctx.Repo.Permission.CanWrite(unit.TypeActions)
} }
pb := prInfo.ProtectedBranchRule
prInfo.enableStatusCheck = pb != nil && pb.EnableStatusCheck
if !prInfo.enableStatusCheck {
return
}
var missingRequiredChecks []string var missingRequiredChecks []string
for _, requiredContext := range pb.StatusCheckContexts { for _, requiredContext := range pbRequiredContexts {
contextFound := false contextFound := false
matchesRequiredContext := createRequiredContextMatcher(requiredContext) matchesRequiredContext := createRequiredContextMatcher(requiredContext)
for _, presentStatus := range commitStatuses { for _, presentStatus := range commitStatuses {
@@ -463,7 +459,7 @@ func (prInfo *pullRequestViewInfo) prepareViewFillCommitStatusInfoForOpen(ctx *c
statusCheckData.MissingRequiredChecks = missingRequiredChecks statusCheckData.MissingRequiredChecks = missingRequiredChecks
statusCheckData.IsContextRequired = func(context string) bool { statusCheckData.IsContextRequired = func(context string) bool {
for _, c := range pb.StatusCheckContexts { for _, c := range pbRequiredContexts {
if c == context { if c == context {
return true return true
} }
@@ -478,7 +474,21 @@ func (prInfo *pullRequestViewInfo) prepareViewFillCommitStatusInfoForOpen(ctx *c
} }
return false return false
} }
statusCheckData.RequiredChecksState = pull_service.MergeRequiredContextsCommitStatus(commitStatuses, pb.StatusCheckContexts) statusCheckData.RequiredChecksState = pull_service.MergeRequiredContextsCommitStatus(commitStatuses, pbRequiredContexts)
if data.enableStatusCheck {
if statusCheckData.RequiredChecksState.IsError() || statusCheckData.RequiredChecksState.IsFailure() {
data.infoProtectionBlockers.AddErrorItem(
svg.RenderHTML("octicon-x"),
ctx.Locale.Tr("repo.pulls.required_status_check_failed"),
)
} else if !statusCheckData.RequiredChecksState.IsSuccess() {
data.infoProtectionBlockers.AddErrorItem(
svg.RenderHTML("octicon-x"),
ctx.Locale.Tr("repo.pulls.required_status_check_missing"),
)
}
}
} }
// prepareViewMergedPullInfo show meta information for a merged pull request view page // prepareViewMergedPullInfo show meta information for a merged pull request view page
@@ -495,14 +505,16 @@ type pullCommitStatusCheckData struct {
CanApprove bool // whether the user can approve workflow runs CanApprove bool // whether the user can approve workflow runs
ApproveLink string // link to approve all checks ApproveLink string // link to approve all checks
RequiredChecksState commitstatus.CommitStatusState RequiredChecksState commitstatus.CommitStatusState
LatestCommitStatus *git_model.CommitStatus
pullCommitStatusState commitstatus.CommitStatusState
PullCommitStatuses []*git_model.CommitStatus
} }
func (d *pullCommitStatusCheckData) CommitStatusCheckPrompt(locale translation.Locale) string { func (d *pullCommitStatusCheckData) CommitStatusCheckPrompt(locale translation.Locale) string {
if d.RequiredChecksState.IsPending() || len(d.MissingRequiredChecks) > 0 { if d.RequiredChecksState.IsPending() || len(d.MissingRequiredChecks) > 0 {
return locale.TrString("repo.pulls.status_checking") return locale.TrString("repo.pulls.status_checking")
} else if d.RequiredChecksState.IsSuccess() { } else if d.RequiredChecksState.IsSuccess() {
if d.LatestCommitStatus != nil && d.LatestCommitStatus.State.IsFailure() { if d.pullCommitStatusState.IsFailure() {
return locale.TrString("repo.pulls.status_checks_failure_optional") return locale.TrString("repo.pulls.status_checks_failure_optional")
} }
return locale.TrString("repo.pulls.status_checks_success") return locale.TrString("repo.pulls.status_checks_success")
@@ -558,7 +570,7 @@ func (prInfo *pullRequestViewInfo) prepareViewOpenPullInfo(ctx *context.Context)
ctx.Data["IsNothingToCompare"] = true ctx.Data["IsNothingToCompare"] = true
} }
// this one is used by both sidebar and merge-box // this one is used by: title edit, sidebar toggle, and merge-box toggle
prInfo.workInProgressPrefix = pull.GetWorkInProgressPrefix(ctx) prInfo.workInProgressPrefix = pull.GetWorkInProgressPrefix(ctx)
if pull.IsWorkInProgress(ctx) { if pull.IsWorkInProgress(ctx) {
ctx.Data["IsPullWorkInProgress"] = prInfo.workInProgressPrefix != "" ctx.Data["IsPullWorkInProgress"] = prInfo.workInProgressPrefix != ""
+172 -13
View File
@@ -3,31 +3,190 @@
package repo package repo
import (
"html/template"
"code.gitea.io/gitea/modules/htmlutil"
"code.gitea.io/gitea/modules/svg"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/context"
)
type pullMergeBoxInfoItem struct {
ItemClass string
SvgIconHTML template.HTML
InfoHTML template.HTML
ListItems []template.HTML
}
type pullMergeBoxInfoItemCollection struct {
items []*pullMergeBoxInfoItem
}
type pullInfoSection struct {
InfoItems []*pullMergeBoxInfoItem
}
func escapeStringSliceToHTML(s []string) (ret []template.HTML) {
for _, v := range s {
ret = append(ret, template.HTML(template.HTMLEscapeString(v)))
}
return ret
}
func (c *pullMergeBoxInfoItemCollection) AddInfoItem(svg, info template.HTML, optItems ...[]template.HTML) {
c.items = append(c.items, &pullMergeBoxInfoItem{
SvgIconHTML: svg,
InfoHTML: info,
ListItems: util.OptionalArg(optItems),
})
}
func (c *pullMergeBoxInfoItemCollection) AddErrorItem(svg, info template.HTML, optItems ...[]template.HTML) {
c.items = append(c.items, &pullMergeBoxInfoItem{
ItemClass: "tw-text-red",
SvgIconHTML: svg,
InfoHTML: info,
ListItems: util.OptionalArg(optItems),
})
}
func (prInfo *pullRequestViewInfo) prepareMergeBoxIconColor() { func (prInfo *pullRequestViewInfo) prepareMergeBoxIconColor() {
pull := prInfo.issue.PullRequest pull := prInfo.issue.PullRequest
mergeBoxData := prInfo.MergeBoxData mergeBoxData := prInfo.MergeBoxData
statusCheckData := prInfo.StatusCheckData
showAsNormalColor := prInfo.issue.IsClosed || prInfo.workInProgressPrefix != "" || pull.IsEmpty() || pull.IsFilesConflicted()
showAsErrorColor := false
showAsWarningColor := pull.IsChecking()
if statusCheckData := mergeBoxData.StatusCheckData; statusCheckData != nil {
showAsErrorColor = statusCheckData.pullCommitStatusState.IsError() || statusCheckData.pullCommitStatusState.IsFailure() ||
statusCheckData.RequiredChecksState.IsError() || statusCheckData.RequiredChecksState.IsFailure()
showAsWarningColor = showAsWarningColor ||
statusCheckData.pullCommitStatusState.IsWarning() || statusCheckData.pullCommitStatusState.IsPending() ||
(mergeBoxData.enableStatusCheck && (statusCheckData.RequiredChecksState.IsWarning() || statusCheckData.RequiredChecksState.IsPending()))
}
hasBlockers := len(mergeBoxData.infoCommitBlockers.items) > 0 || len(mergeBoxData.infoProtectionBlockers.items) > 0
switch { switch {
case pull.HasMerged: case pull.HasMerged:
prInfo.MergeBoxData.TimelineIconClass = "tw-text-purple" prInfo.MergeBoxData.TimelineIconClass = "tw-text-purple"
case prInfo.issue.IsClosed, prInfo.workInProgressPrefix != "", pull.IsFilesConflicted(): case showAsNormalColor:
prInfo.MergeBoxData.TimelineIconClass = "tw-text-text-light" prInfo.MergeBoxData.TimelineIconClass = "tw-text-text-light"
case prInfo.IsPullRequestBroken, mergeBoxData.isBlockedByApprovals, mergeBoxData.isBlockedByRejection, case showAsErrorColor:
mergeBoxData.isBlockedByOfficialReviewRequests, mergeBoxData.isBlockedByOutdatedBranch, mergeBoxData.isBlockedByChangedProtectedFiles:
prInfo.MergeBoxData.TimelineIconClass = "tw-text-red" prInfo.MergeBoxData.TimelineIconClass = "tw-text-red"
case prInfo.enableStatusCheck && (statusCheckData.RequiredChecksState.IsFailure() || statusCheckData.RequiredChecksState.IsError()): case showAsWarningColor:
prInfo.MergeBoxData.TimelineIconClass = "tw-text-red"
case prInfo.enableStatusCheck && (statusCheckData.LatestCommitStatus == nil || statusCheckData.RequiredChecksState.IsPending() || statusCheckData.RequiredChecksState.IsWarning()):
prInfo.MergeBoxData.TimelineIconClass = "tw-text-yellow" prInfo.MergeBoxData.TimelineIconClass = "tw-text-yellow"
case mergeBoxData.allowMerge && mergeBoxData.requireSigned && !mergeBoxData.willSign: case hasBlockers:
prInfo.MergeBoxData.TimelineIconClass = "tw-text-red" 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(): case pull.IsStatusMergeable():
prInfo.MergeBoxData.TimelineIconClass = "tw-text-green" prInfo.MergeBoxData.TimelineIconClass = "tw-text-green"
default: default:
prInfo.MergeBoxData.TimelineIconClass = "tw-text-red" prInfo.MergeBoxData.TimelineIconClass = "tw-text-text-light"
} }
} }
func (prInfo *pullRequestViewInfo) prepareMergeBoxInfoItems(ctx *context.Context) {
pull := prInfo.issue.PullRequest
data := prInfo.MergeBoxData
if pull.HasMerged && data.IsPullBranchDeletable {
data.ClosedInfoTitle = ctx.Locale.Tr("repo.pulls.merged_success")
data.ClosedInfoBody = ctx.Locale.Tr("repo.pulls.merged_info_text", htmlutil.HTMLFormat("<code>%s</code>", prInfo.headTarget))
return
} else if prInfo.issue.IsClosed {
data.ClosedInfoTitle = ctx.Locale.Tr("repo.pulls.closed")
if prInfo.IsPullRequestBroken {
data.ClosedInfoBody = ctx.Locale.Tr("repo.pulls.cant_reopen_deleted_branch")
} else {
data.ClosedInfoBody = ctx.Locale.Tr("repo.pulls.reopen_to_merge")
}
return
}
if pull.IsFilesConflicted() {
detailItems := escapeStringSliceToHTML(pull.ConflictedFiles)
if len(detailItems) == 0 {
detailItems = append(detailItems, ctx.Locale.Tr("repo.pulls.files_conflicted_no_listed_files"))
}
if len(detailItems) > 10 {
detailItems = detailItems[:10]
detailItems = append(detailItems, "...")
}
prInfo.MergeBoxData.infoCommitBlockers.AddInfoItem(
svg.RenderHTML("octicon-x"),
ctx.Locale.Tr("repo.pulls.files_conflicted"),
detailItems,
)
}
if prInfo.IsPullRequestBroken {
prInfo.MergeBoxData.infoCommitBlockers.AddInfoItem(
svg.RenderHTML("octicon-x"),
ctx.Locale.Tr("repo.pulls.data_broken"),
)
}
if pull.IsChecking() {
prInfo.MergeBoxData.infoCommitBlockers.AddInfoItem(
svg.RenderHTML("gitea-running", 16, "rotate-clockwise"),
ctx.Locale.Tr("repo.pulls.is_checking"),
)
}
if pull.IsAncestor() {
prInfo.MergeBoxData.infoCommitBlockers.AddInfoItem(
svg.RenderHTML("octicon-alert"),
ctx.Locale.Tr("repo.pulls.is_ancestor"),
)
}
if !pull.IsStatusMergeable() {
// it is only a "protection" level blocker, it can be bypassed by admin (e.g.: manually merged)
if pull.IsEmpty() {
prInfo.MergeBoxData.infoProtectionBlockers.AddInfoItem(
svg.RenderHTML("octicon-alert"),
ctx.Locale.Tr("repo.pulls.is_empty"),
)
} else {
prInfo.MergeBoxData.infoProtectionBlockers.AddErrorItem(
svg.RenderHTML("octicon-x"),
ctx.Locale.Tr("repo.pulls.cannot_auto_merge_desc"),
)
prInfo.MergeBoxData.infoProtectionBlockers.AddInfoItem(
svg.RenderHTML("octicon-info"),
ctx.Locale.Tr("repo.pulls.cannot_auto_merge_helper"),
)
}
}
if !data.allowMerge {
prInfo.MergeBoxData.infoProtectionBlockers.AddInfoItem(
svg.RenderHTML("octicon-info"),
ctx.Locale.Tr("repo.pulls.no_merge_access"),
)
}
if data.CanMergeNow {
if data.HasOverridableBlockers {
prInfo.MergeBoxData.infoMergePrompts.AddInfoItem(
svg.RenderHTML("octicon-dot-fill"),
ctx.Locale.Tr("repo.pulls.required_status_check_administrator"),
)
} else if pull.IsStatusMergeable() || pull.IsEmpty() {
prInfo.MergeBoxData.infoMergePrompts.AddInfoItem(
svg.RenderHTML("octicon-check"),
ctx.Locale.Tr("repo.pulls.can_auto_merge_desc"),
)
}
}
if len(data.infoCommitBlockers.items) > 0 {
data.InfoSections = append(data.InfoSections, &pullInfoSection{data.infoCommitBlockers.items})
} else {
data.InfoSections = append(data.InfoSections, &pullInfoSection{data.infoProtectionBlockers.items})
}
data.InfoSections = append(data.InfoSections, &pullInfoSection{data.infoMergePrompts.items})
}
+14 -8
View File
@@ -9,6 +9,7 @@ import (
pull_model "code.gitea.io/gitea/models/pull" pull_model "code.gitea.io/gitea/models/pull"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/svg"
"code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
pull_service "code.gitea.io/gitea/services/pull" pull_service "code.gitea.io/gitea/services/pull"
@@ -91,7 +92,7 @@ func (prInfo *pullRequestViewInfo) prepareMergeBoxFormProps(ctx *context.Context
"allOverridableChecksOk": allOverridableChecksOk, "allOverridableChecksOk": allOverridableChecksOk,
"emptyCommit": pull.IsEmpty(), "emptyCommit": pull.IsEmpty(),
"pullHeadCommitID": prInfo.CompareInfo.HeadCommitID, "pullHeadCommitID": prInfo.CompareInfo.HeadCommitID,
"isPullBranchDeletable": prInfo.MergeBoxData.isPullBranchDeletable, "isPullBranchDeletable": prInfo.MergeBoxData.IsPullBranchDeletable,
"defaultMergeStyle": mergeStyle, "defaultMergeStyle": mergeStyle,
"defaultDeleteBranchAfterMerge": prConfig.DefaultDeleteBranchAfterMerge, "defaultDeleteBranchAfterMerge": prConfig.DefaultDeleteBranchAfterMerge,
"mergeMessageFieldPlaceHolder": ctx.Locale.Tr("repo.editor.commit_message_desc"), "mergeMessageFieldPlaceHolder": ctx.Locale.Tr("repo.editor.commit_message_desc"),
@@ -148,12 +149,6 @@ func (prInfo *pullRequestViewInfo) prepareMergeBoxFormProps(ctx *context.Context
} }
} }
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 // 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: // To test it:
// Enable "Manually Merged" feature in the Repository Settings // Enable "Manually Merged" feature in the Repository Settings
@@ -161,7 +156,8 @@ func (prInfo *pullRequestViewInfo) prepareMergeBoxFormProps(ctx *context.Context
// - Merge the pull request branch locally and push the merged commit to Gitea // - 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 // - Make some conflicts between the base branch and the pull request branch
// Then the Manually Merged form will be shown in the merge form // Then the Manually Merged form will be shown in the merge form
if canUseManualMerge() { canUseManualMerge := !pull.IsWorkInProgress(ctx) && !pull.IsChecking() && prConfig.AllowManualMerge
if canUseManualMerge {
mergeStyles = append(mergeStyles, map[string]any{ mergeStyles = append(mergeStyles, map[string]any{
"name": "manually-merged", "name": "manually-merged",
"allowed": prConfig.AllowManualMerge, "allowed": prConfig.AllowManualMerge,
@@ -174,5 +170,15 @@ func (prInfo *pullRequestViewInfo) prepareMergeBoxFormProps(ctx *context.Context
if len(mergeStyles) > 0 { if len(mergeStyles) > 0 {
mergeFormProps["mergeStyles"] = mergeStyles mergeFormProps["mergeStyles"] = mergeStyles
prInfo.MergeBoxData.MergeFormProps = mergeFormProps prInfo.MergeBoxData.MergeFormProps = mergeFormProps
} else if pull.IsStatusMergeable() {
// no merge style was set in repo setting
prInfo.MergeBoxData.infoCommitBlockers.AddInfoItem(
svg.RenderHTML("octicon-x", 16, "tw-text-red"),
ctx.Locale.Tr("repo.pulls.no_merge_desc"),
)
prInfo.MergeBoxData.infoCommitBlockers.AddInfoItem(
svg.RenderHTML("octicon-info"),
ctx.Locale.Tr("repo.pulls.no_merge_helper"),
)
} }
} }
@@ -7,219 +7,67 @@
data-pull-link="{{$.Issue.Link}}" data-pull-link="{{$.Issue.Link}}"
{{end}} {{end}}
> >
{{$statusCheckData := .StatusCheckData}}
{{$requiredStatusCheckState := $statusCheckData.RequiredChecksState}}
<div class="timeline-avatar {{$data.TimelineIconClass}}">{{svg "octicon-git-merge" 40}}</div> <div class="timeline-avatar {{$data.TimelineIconClass}}">{{svg "octicon-git-merge" 40}}</div>
<div class="content"> <div class="content">
<div class="ui segment fitted avatar-content-left-arrow"> <div class="ui segment fitted avatar-content-left-arrow">
<div class="merge-section flex-divided-list flex-items-block items-px-default"> <div class="merge-section flex-divided-list items-px-default">
{{if .LatestCommitStatus}} {{if $data.ShowStatusCheck}}
{{template "repo/issue/view_content/pull_merge_status_checks" (dict {{template "repo/issue/view_content/pull_merge_status_checks" (dict "StatusCheckData" $data.StatusCheckData)}}
"CommitStatuses" .LatestCommitStatuses
"StatusCheckData" $statusCheckData
)}}
{{end}} {{end}}
{{if .Issue.PullRequest.HasMerged}} {{if $data.ClosedInfoTitle}}
{{if .IsPullBranchDeletable}} <div class="item flex-left-right">
<div class="item item-section text tw-flex-1"> <div>
<div class="item-section-left"> <h3 class="tw-mb-2">{{$data.ClosedInfoTitle}}</h3>
<h3 class="tw-mb-2"> <div>{{$data.ClosedInfoBody}}</div>
{{ctx.Locale.Tr "repo.pulls.merged_success"}} </div>
</h3> {{if $data.IsPullBranchDeletable}}
<div class="merge-section-info"> <div>
{{ctx.Locale.Tr "repo.pulls.merged_info_text" (HTMLFormat "<code>%s</code>" .HeadTarget)}}
</div>
</div>
<div class="item-section-right">
<button class="ui button link-action delete-branch-after-merge" data-url="{{.DeleteBranchLink}}">{{ctx.Locale.Tr "repo.branch.delete_html"}}</button> <button class="ui button link-action delete-branch-after-merge" data-url="{{.DeleteBranchLink}}">{{ctx.Locale.Tr "repo.branch.delete_html"}}</button>
</div> </div>
</div> {{end}}
</div>
{{end}}
{{range $infoSection := $data.InfoSections}}
{{if $infoSection.InfoItems}}
<div class="item">
{{range $infoItem := $infoSection.InfoItems}}
<div class="flex-text-block {{$infoItem.ItemClass}}">{{$infoItem.SvgIconHTML}} {{$infoItem.InfoHTML}}</div>
{{if $infoItem.ListItems}}
<ul class="tw-pl-[36px]">{{/* align with the info icon and text */}}
{{range $listItem := $infoItem.ListItems}}
<li>{{$listItem}}</li>
{{end}}
</ul>
{{end}}
{{end}}
</div>
{{end}} {{end}}
{{else if .Issue.IsClosed}} {{end}}
<div class="item item-section text tw-flex-1"> {{if $data.ShowUpdatePullInfo}}
<div class="item-section-left">
<h3 class="tw-mb-2">{{ctx.Locale.Tr "repo.pulls.closed"}}</h3>
<div class="merge-section-info">
{{if .IsPullRequestBroken}}
{{ctx.Locale.Tr "repo.pulls.cant_reopen_deleted_branch"}}
{{else}}
{{ctx.Locale.Tr "repo.pulls.reopen_to_merge"}}
{{end}}
</div>
</div>
{{if and .IsPullBranchDeletable (not .IsPullRequestBroken)}}
<div class="item-section-right">
<button class="ui button link-action delete-branch-after-merge" data-url="{{.DeleteBranchLink}}">{{ctx.Locale.Tr "repo.branch.delete_html"}}</button>
</div>
{{end}}
</div>
{{else if .IsPullFilesConflicted}}
<div class="item"> <div class="item">
{{svg "octicon-x"}} {{template "repo/issue/view_content/update_branch_by_merge" (dict "MergeBoxData" $data "Issue" $.Issue)}}
{{ctx.Locale.Tr "repo.pulls.files_conflicted"}}
</div> </div>
<ul class="item"> {{end}}
{{range .ConflictedFiles}}
<li>{{.}}</li> {{if $.IsPullWorkInProgress}}
{{else}} <div class="item flex-left-right">
<li>{{ctx.Locale.Tr "repo.pulls.files_conflicted_no_listed_files"}}</li> <div class="flex-text-block">
{{end}} {{svg "octicon-x"}} {{ctx.Locale.Tr "repo.pulls.cannot_merge_work_in_progress"}}
</ul>
{{else if .IsPullRequestBroken}}
<div class="item">
{{svg "octicon-x"}}
{{ctx.Locale.Tr "repo.pulls.data_broken"}}
</div>
{{else if .IsPullWorkInProgress}}
<div class="item">
<div class="item-section-left flex-text-inline tw-flex-1">
{{svg "octicon-x"}}
{{ctx.Locale.Tr "repo.pulls.cannot_merge_work_in_progress"}}
</div> </div>
{{if or .HasIssuesOrPullsWritePermission .IsIssuePoster}} {{if or .HasIssuesOrPullsWritePermission .IsIssuePoster}}
<button class="ui compact button" data-global-init="initPullRequestWipToggle" data-title="{{.Issue.Title}}" data-wip-prefix="{{.WorkInProgressPrefix}}" data-update-url="{{.Issue.Link}}/title"> <button class="ui compact button" data-global-init="initPullRequestWipToggle" data-title="{{.Issue.Title}}" data-wip-prefix="{{.WorkInProgressPrefix}}" data-update-url="{{.Issue.Link}}/title">
{{ctx.Locale.Tr "repo.pulls.remove_prefix" .WorkInProgressPrefix}} {{ctx.Locale.Tr "repo.pulls.remove_prefix" .WorkInProgressPrefix}}
</button> </button>
{{end}} {{end}}
</div> </div>
{{template "repo/issue/view_content/update_branch_by_merge" $}} {{end}}
{{else if .Issue.PullRequest.IsChecking}}
<div class="item">
{{svg "gitea-running" 16 "rotate-clockwise"}}
{{ctx.Locale.Tr "repo.pulls.is_checking"}}
</div>
{{else if .Issue.PullRequest.IsAncestor}}
<div class="item">
{{svg "octicon-alert"}}
{{ctx.Locale.Tr "repo.pulls.is_ancestor"}}
</div>
{{else}}
{{if .IsBlockedByApprovals}}
<div class="item">
{{svg "octicon-x"}}
{{if .RequireApprovalsWhitelist}}
{{ctx.Locale.Tr "repo.pulls.blocked_by_approvals_whitelisted" .GrantedApprovals .ProtectedBranch.RequiredApprovals}}
{{else}}
{{ctx.Locale.Tr "repo.pulls.blocked_by_approvals" .GrantedApprovals .ProtectedBranch.RequiredApprovals}}
{{end}}
</div>
{{else if .IsBlockedByRejection}}
<div class="item">
{{svg "octicon-x"}}
{{ctx.Locale.Tr "repo.pulls.blocked_by_rejection"}}
</div>
{{else if .IsBlockedByOfficialReviewRequests}}
<div class="item">
{{svg "octicon-x"}}
{{ctx.Locale.Tr "repo.pulls.blocked_by_official_review_requests"}}
</div>
{{else if .IsBlockedByOutdatedBranch}}
<div class="item">
{{svg "octicon-x"}}
{{ctx.Locale.Tr "repo.pulls.blocked_by_outdated_branch"}}
</div>
{{else if .IsBlockedByChangedProtectedFiles}}
<div class="item">
{{svg "octicon-x"}}
{{ctx.Locale.TrN $.ChangedProtectedFilesNum "repo.pulls.blocked_by_changed_protected_files_1" "repo.pulls.blocked_by_changed_protected_files_n"}}
</div>
<ul class="item">
{{range .ChangedProtectedFiles}}
<li>{{.}}</li>
{{end}}
</ul>
{{else if and .EnableStatusCheck (or $requiredStatusCheckState.IsError $requiredStatusCheckState.IsFailure)}}
<div class="item">
{{svg "octicon-x"}}
{{ctx.Locale.Tr "repo.pulls.required_status_check_failed"}}
</div>
{{else if and .EnableStatusCheck (not $requiredStatusCheckState.IsSuccess)}}
<div class="item">
{{svg "octicon-x"}}
{{ctx.Locale.Tr "repo.pulls.required_status_check_missing"}}
</div>
{{else if and .AllowMerge .RequireSigned (not .WillSign)}}
<div class="item">
{{svg "octicon-x"}}
{{ctx.Locale.Tr "repo.pulls.require_signed_wont_sign"}}
</div>
<div class="item">
{{svg "octicon-unlock"}}
{{ctx.Locale.Tr (printf "repo.signing.wont_sign.%s" .WontSignReason)}}
</div>
{{else if not .Issue.PullRequest.IsStatusMergeable}}
<div class="item tw-text-red">
{{svg "octicon-x"}}
{{ctx.Locale.Tr "repo.pulls.cannot_auto_merge_desc"}}
</div>
<div class="item">
{{svg "octicon-info"}}
{{ctx.Locale.Tr "repo.pulls.cannot_auto_merge_helper"}}
</div>
{{end}}
{{$notAllOverridableChecksOk := $data.HasOverridableBlockers}}
{{$canMergeNow := $data.CanMergeNow}}
{{if $canMergeNow}}
{{if $notAllOverridableChecksOk}}
<div class="item">
{{svg "octicon-dot-fill"}}
{{ctx.Locale.Tr "repo.pulls.required_status_check_administrator"}}
</div>
{{else}}
<div class="item">
{{svg "octicon-check"}}
{{ctx.Locale.Tr "repo.pulls.can_auto_merge_desc"}}
</div>
{{end}}
{{if .WillSign}}
<div class="item">
{{svg "octicon-lock" 16 "tw-text-green"}}
{{ctx.Locale.Tr "repo.signing.will_sign" .SigningKeyMergeDisplay}}
</div>
{{else if .IsSigned}}
<div class="item">
{{svg "octicon-unlock"}}
{{ctx.Locale.Tr (printf "repo.signing.wont_sign.%s" .WontSignReason)}}
</div>
{{end}}
{{end}}
{{if .Issue.PullRequest.IsEmpty}}
<div class="item">
{{svg "octicon-alert"}}
{{ctx.Locale.Tr "repo.pulls.is_empty"}}
</div>
{{end}}
{{template "repo/issue/view_content/update_branch_by_merge" (dict "MergeBoxData" $data "Issue" $.Issue)}}
{{if not .AllowMerge}} {{/* user is allowed to merge */}}
{{/* user is not allowed to merge */}}
<div class="item">
{{svg "octicon-info"}}
{{ctx.Locale.Tr "repo.pulls.no_merge_access"}}
</div>
{{end}}
{{end}}{{/* end if: pull request status */}}
{{if $data.MergeFormProps}} {{if $data.MergeFormProps}}
{{/* The merge form is a Vue component. After mounted, it has a button for choosing merge style, so make it have min-height to avoid layout shifting */}} {{/* The merge form is a Vue component. After mounted, it has a button for choosing merge style, so make it have min-height to avoid layout shifting */}}
<div class="item"> <div class="item">
<div id="pull-request-merge-form" class="tw-min-h-[40px] tw-w-full" data-merge-form-props="{{JsonUtils.EncodeToString $data.MergeFormProps}}"></div> <div id="pull-request-merge-form" class="tw-min-h-[40px] tw-w-full" data-merge-form-props="{{JsonUtils.EncodeToString $data.MergeFormProps}}"></div>
</div> </div>
{{else if and .AllowMerge .Issue.PullRequest.IsStatusMergeable}}
{{/* no merge style was set in repo setting */}}
<div class="item tw-text-red">
{{svg "octicon-x"}}
{{ctx.Locale.Tr "repo.pulls.no_merge_desc"}}
</div>
<div class="item">
{{svg "octicon-info"}}
{{ctx.Locale.Tr "repo.pulls.no_merge_helper"}}
</div>
{{end}} {{end}}
{{if $data.ShowPullCommands}} {{if $data.ShowPullCommands}}
@@ -1,10 +1,9 @@
{{/* Template Attributes: {{/* Template Attributes:
* CommitStatuses: all commit status elements * StatusCheckData: see backend pullCommitStatusCheckData struct
* StatusCheckData: additional status check data, see backend pullCommitStatusCheckData struct
*/}} */}}
{{$commitStatuses := $.CommitStatuses}}
{{$statusCheckData := $.StatusCheckData}} {{$statusCheckData := $.StatusCheckData}}
{{if $statusCheckData}} {{if $statusCheckData}}
{{$commitStatuses := $statusCheckData.PullCommitStatuses}}
<div class="item flex-left-right commit-status-toggle"> <div class="item flex-left-right commit-status-toggle">
<div>{{$statusCheckData.CommitStatusCheckPrompt ctx.Locale}}</div> <div>{{$statusCheckData.CommitStatusCheckPrompt ctx.Locale}}</div>
<button data-global-click="onCommitStatusChecksToggle" class="btn interact-fg" <button data-global-click="onCommitStatusChecksToggle" class="btn interact-fg"
@@ -1,35 +1,33 @@
{{$data := $.MergeBoxData}} {{$data := $.MergeBoxData}}
{{$issue := $.Issue}} {{$issue := $.Issue}}
{{if $data.ShowUpdatePullInfo}} <div class="tw-w-full flex-left-right">
<div class="item flex-left-right"> <div class="flex-text-block">
<div class="flex-text-block"> {{svg "octicon-alert"}}
{{svg "octicon-alert"}} {{ctx.Locale.Tr "repo.pulls.outdated_with_base_branch"}}
{{ctx.Locale.Tr "repo.pulls.outdated_with_base_branch"}} </div>
</div> <div>
<div> {{if and $data.UpdateAllowed $data.UpdateByRebaseAllowed}}
{{if and $data.UpdateAllowed $data.UpdateByRebaseAllowed}} <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="{{$issue.Link}}/update">
<button class="ui button" data-do="{{$issue.Link}}/update"> <span class="button-text">
<span class="button-text"> {{ctx.Locale.Tr "repo.pulls.update_branch"}}
{{ctx.Locale.Tr "repo.pulls.update_branch"}} </span>
</span> </button>
</button> <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="{{$issue.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="{{$issue.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>
{{end}} </div>
{{if and $data.UpdateAllowed (not $data.UpdateByRebaseAllowed)}} {{end}}
<form action="{{$issue.Link}}/update" method="post"> {{if and $data.UpdateAllowed (not $data.UpdateByRebaseAllowed)}}
<button class="ui compact button"> <form action="{{$issue.Link}}/update" method="post">
<span class="ui text">{{ctx.Locale.Tr "repo.pulls.update_branch"}}</span> <button class="ui compact button">
</button> <span class="ui text">{{ctx.Locale.Tr "repo.pulls.update_branch"}}</span>
</form> </button>
{{end}} </form>
</div> {{end}}
</div> </div>
{{end}} </div>
+4 -6
View File
@@ -31,8 +31,7 @@ func TestPullCreate_CommitStatus(t *testing.T) {
testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "") testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
testEditFileToNewBranch(t, session, "user1", "repo1", "master", "status1", "README.md", "status1") testEditFileToNewBranch(t, session, "user1", "repo1", "master", "status1", "README.md", "status1")
url := "/" + path.Join("user1", "repo1", "compare", "master...status1") req := NewRequestWithValues(t, "POST", "/user1/repo1/compare/master...status1",
req := NewRequestWithValues(t, "POST", url,
map[string]string{ map[string]string{
"title": "pull request from status1", "title": "pull request from status1",
}, },
@@ -121,8 +120,7 @@ func TestPullCreate_EmptyChangesWithDifferentCommits(t *testing.T) {
testEditFileToNewBranch(t, session, "user1", "repo1", "master", "status1", "README.md", "status1") testEditFileToNewBranch(t, session, "user1", "repo1", "master", "status1", "README.md", "status1")
testEditFile(t, session, "user1", "repo1", "status1", "README.md", "# repo1\n\nDescription for repo1") testEditFile(t, session, "user1", "repo1", "status1", "README.md", "# repo1\n\nDescription for repo1")
url := "/" + path.Join("user1", "repo1", "compare", "master...status1") req := NewRequestWithValues(t, "POST", "/user1/repo1/compare/master...status1",
req := NewRequestWithValues(t, "POST", url,
map[string]string{ map[string]string{
"title": "pull request from status1", "title": "pull request from status1",
}, },
@@ -134,6 +132,7 @@ func TestPullCreate_EmptyChangesWithDifferentCommits(t *testing.T) {
doc := NewHTMLParser(t, resp.Body) doc := NewHTMLParser(t, resp.Body)
text := strings.TrimSpace(doc.doc.Find(".merge-section").Text()) text := strings.TrimSpace(doc.doc.Find(".merge-section").Text())
assert.Contains(t, text, "The changes on this branch are already on the target branch. This will be an empty commit.")
assert.Contains(t, text, "This pull request can be merged automatically.") assert.Contains(t, text, "This pull request can be merged automatically.")
}) })
} }
@@ -143,8 +142,7 @@ func TestPullCreate_EmptyChangesWithSameCommits(t *testing.T) {
session := loginUser(t, "user1") session := loginUser(t, "user1")
testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "") testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
testCreateBranch(t, session, "user1", "repo1", "branch/master", "status1", http.StatusSeeOther) testCreateBranch(t, session, "user1", "repo1", "branch/master", "status1", http.StatusSeeOther)
url := "/" + path.Join("user1", "repo1", "compare", "master...status1") req := NewRequestWithValues(t, "POST", "/user1/repo1/compare/master...status1",
req := NewRequestWithValues(t, "POST", url,
map[string]string{ map[string]string{
"title": "pull request from status1", "title": "pull request from status1",
}, },
-21
View File
@@ -596,27 +596,6 @@ td .commit-summary {
} }
} }
.repository.view.issue .comment-list .comment .merge-section .item + ul.item {
border-top: 0;
padding: 0 1em 0 52px;
margin-top: -0.5em;
}
.repository.view.issue .comment-list .comment .merge-section .item-section {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 0.5em;
}
.merge-section-info code {
border: 1px solid var(--color-light-border);
border-radius: var(--border-radius);
padding: 2px 4px;
background: var(--color-light);
}
.repository.view.issue .comment-list .comment .no-content { .repository.view.issue .comment-list .comment .no-content {
color: var(--color-text-light-2); color: var(--color-text-light-2);
font-style: italic; font-style: italic;