diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index 80692acdaf..6f3141885b 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -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.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.not_signed_in": "You are not signed in.", "repo.ext_wiki": "Access to External Wiki", "repo.ext_wiki.desc": "Link to an external wiki.", "repo.wiki": "Wiki", diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go index 83451843ce..2797b0b26d 100644 --- a/routers/web/repo/compare.go +++ b/routers/web/repo/compare.go @@ -624,7 +624,6 @@ func (cpi *comparePageInfoType) prepareCreatePullRequestPage(ctx *context.Contex return } ctx.Data["PullRequest"] = pr - ctx.HTML(http.StatusOK, tplCompareDiff) return } diff --git a/routers/web/repo/issue_view.go b/routers/web/repo/issue_view.go index 28fdfb34fa..c0cb77f497 100644 --- a/routers/web/repo/issue_view.go +++ b/routers/web/repo/issue_view.go @@ -28,6 +28,7 @@ import ( "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/svg" "code.gitea.io/gitea/modules/templates/vars" "code.gitea.io/gitea/modules/util" "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) } -func (prInfo *pullRequestViewInfo) prepareMergeBoxRequireSigning(ctx *context.Context) { +func (prInfo *pullRequestViewInfo) prepareMergeBoxCommitSigning(ctx *context.Context) { pull := prInfo.issue.PullRequest - willSign := false + data := prInfo.MergeBoxData + + pb := prInfo.ProtectedBranchRule + data.requireSigned = pb != nil && pb.RequireSignedCommits + + wontSignReason := "" if ctx.Doer != nil { sign, key, _, err := asymkey_service.SignMerge(ctx, pull, ctx.Doer, ctx.Repo.GitRepo) - willSign = sign - ctx.Data["SigningKeyMergeDisplay"] = asymkey_model.GetDisplaySigningKey(key) + data.willSign = sign + data.signingKeyMergeDisplay = asymkey_model.GetDisplaySigningKey(key) if err != nil { if asymkey_service.IsErrWontSign(err) { - ctx.Data["WontSignReason"] = err.(*asymkey_service.ErrWontSign).Reason + wontSignReason = string(err.(*asymkey_service.ErrWontSign).Reason) } 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) } } - } 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) { @@ -571,8 +603,7 @@ func (prInfo *pullRequestViewInfo) prepareMergeBoxDeleteBranch(ctx *context.Cont isPullBranchDeletable = !exist } - ctx.Data["IsPullBranchDeletable"] = isPullBranchDeletable - prInfo.MergeBoxData.isPullBranchDeletable = isPullBranchDeletable + prInfo.MergeBoxData.IsPullBranchDeletable = isPullBranchDeletable } func prepareIssueViewSidebarPin(ctx *context.Context, issue *issues_model.Issue) { @@ -829,11 +860,6 @@ func (prInfo *pullRequestViewInfo) prepareMergeBox(ctx *context.Context, issue * data := &pullMergeBoxData{} prInfo.MergeBoxData = data - statusCheckData := prInfo.StatusCheckData - if statusCheckData == nil { - statusCheckData = &pullCommitStatusCheckData{} // make the following logic easier, no need to keep checking "nil" - } - canDelete := 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 err := pull.LoadHeadRepo(ctx); err != nil { 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.ShowMergeInstructions = canWriteToHeadRepo data.ShowPullCommands = pull.HeadRepo != nil && !pull.HasMerged && !issue.IsClosed - ctx.Data["AllowMerge"] = data.allowMerge - pb := prInfo.ProtectedBranchRule - if pb != nil { - pb.Repo = pull.BaseRepo - 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.prepareMergeBoxProtectionChecks(ctx) + if ctx.Written() { + return } - prInfo.prepareMergeBoxRequireSigning(ctx) + prInfo.prepareMergeBoxCommitSigning(ctx) if ctx.Written() { return } @@ -947,18 +943,15 @@ func (prInfo *pullRequestViewInfo) prepareMergeBox(ctx *context.Context, issue * prConfig := issue.Repo.MustGetUnit(ctx, unit.TypePullRequests).PullRequestsConfig() 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. // 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) // 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) - data.hasStatusCheckBlocker = enableStatusCheck && !statusCheckData.RequiredChecksState.IsSuccess() + data.hasStatusCheckBlocker = data.enableStatusCheck && !data.StatusCheckData.RequiredChecksState.IsSuccess() // this logic is from: // {{$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)}} // 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 - adminCanOverrideBlockers := (pb == nil || !pb.BlockAdminMergeOverride) && isRepoAdmin + adminCanOverrideBlockers := (prInfo.ProtectedBranchRule == nil || !prInfo.ProtectedBranchRule.BlockAdminMergeOverride) && isRepoAdmin data.CanMergeNow = (!data.HasOverridableBlockers || adminCanOverrideBlockers) && // status checks are satisfied (!data.requireSigned || data.willSign) // signing requirement is satisfied - ctx.Data["PullMergeBoxData"] = prInfo.MergeBoxData prInfo.prepareMergeBoxFormProps(ctx) + prInfo.prepareMergeBoxInfoItems(ctx) 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) { diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index 404f77beb7..eab4df77ad 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -9,6 +9,7 @@ import ( "errors" "fmt" "html" + "html/template" "net/http" "strconv" "strings" @@ -35,6 +36,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/svg" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/util" @@ -56,7 +58,6 @@ import ( ) const ( - tplCompareDiff templates.TplName = "repo/diff/compare" tplPullCommits templates.TplName = "repo/pulls/commits" tplPullFiles templates.TplName = "repo/pulls/files" @@ -268,6 +269,13 @@ type pullMergeBoxData struct { TimelineIconClass string + ClosedInfoTitle template.HTML + ClosedInfoBody template.HTML + + enableStatusCheck bool + StatusCheckData *pullCommitStatusCheckData + ShowStatusCheck bool + HasOverridableBlockers 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 @@ -283,7 +291,7 @@ type pullMergeBoxData struct { // don't expose unneeded fields to templates, need more refactoring changes hasStatusCheckBlocker bool - isPullBranchDeletable bool + IsPullBranchDeletable bool isBlockedByApprovals bool isBlockedByRejection bool @@ -291,6 +299,13 @@ type pullMergeBoxData struct { isBlockedByOutdatedBranch bool isBlockedByChangedProtectedFiles bool requireSigned, willSign bool + signingKeyMergeDisplay string + + infoCommitBlockers pullMergeBoxInfoItemCollection + infoProtectionBlockers pullMergeBoxInfoItemCollection + infoMergePrompts pullMergeBoxInfoItemCollection + + InfoSections []*pullInfoSection } // pullRequestViewInfo is a structured type for viewing pull request @@ -306,11 +321,8 @@ type pullRequestViewInfo struct { CompareInfo git_service.CompareInfo ProtectedBranchRule *git_model.ProtectedBranch - StatusCheckData *pullCommitStatusCheckData - CommitStatuses []*git_model.CommitStatus MergeBoxData *pullMergeBoxData - enableStatusCheck bool workInProgressPrefix string } @@ -349,7 +361,6 @@ func (prInfo *pullRequestViewInfo) prepareViewFillInfo(ctx *context.Context, bas if ctx.Written() { return } - prInfo.prepareViewFillCommitStatusInfo(ctx) } func (prInfo *pullRequestViewInfo) prepareViewFillCompareInfo(ctx *context.Context, baseRef git.RefName) { @@ -379,56 +390,47 @@ func (prInfo *pullRequestViewInfo) prepareViewFillCompareInfo(ctx *context.Conte prInfo.IsPullRequestBroken = true } - ctx.Data["IsPullRequestBroken"] = prInfo.IsPullRequestBroken ctx.Data["NumCommits"] = len(prInfo.CompareInfo.Commits) ctx.Data["NumFiles"] = prInfo.CompareInfo.NumFiles prInfo.setTemplateDataMergeTarget(ctx) } -func (prInfo *pullRequestViewInfo) prepareViewFillCommitStatusInfo(ctx *context.Context) { +func (prInfo *pullRequestViewInfo) prepareMergeBoxStatusCheckData(ctx *context.Context) { headCommitID := prInfo.CompareInfo.HeadCommitID - if headCommitID == "" { + if headCommitID == "" || prInfo.issue.IsClosed { 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{} - prInfo.StatusCheckData = statusCheckData + data.StatusCheckData = statusCheckData commitStatuses, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, prInfo.CompareInfo.HeadCommitID, db.ListOptionsAll) if err != nil { - ctx.ServerError("GetLatestCommitStatus", err) - return + log.Error("GetLatestCommitStatus: %v", err) } if !ctx.Repo.Permission.CanRead(unit.TypeActions) { git_model.CommitStatusesHideActionsURL(ctx, commitStatuses) } - - prInfo.CommitStatuses = commitStatuses - statusCheckData.ApproveLink = fmt.Sprintf("%s/actions/approve-all-checks?commit_id=%s", repo.Link(), headCommitID) - statusCheckData.LatestCommitStatus = git_model.CalcCommitStatus(commitStatuses) - ctx.Data["LatestCommitStatuses"] = commitStatuses - 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 + combinedCommitStatus := git_model.CalcCommitStatus(commitStatuses) + statusCheckData.ApproveLink = fmt.Sprintf("%s/actions/approve-all-checks?commit_id=%s", ctx.Repo.Repository.Link(), headCommitID) + statusCheckData.PullCommitStatuses = commitStatuses + if combinedCommitStatus != nil { + statusCheckData.pullCommitStatusState = combinedCommitStatus.State } - if !prInfo.issue.IsClosed { - prInfo.prepareViewFillCommitStatusInfoForOpen(ctx) - } -} + data.ShowStatusCheck = data.enableStatusCheck || len(statusCheckData.PullCommitStatuses) > 0 -func (prInfo *pullRequestViewInfo) prepareViewFillCommitStatusInfoForOpen(ctx *context.Context) { - statusCheckData := prInfo.StatusCheckData - commitStatuses := prInfo.CommitStatuses runs, err := actions_service.GetRunsFromCommitStatuses(ctx, commitStatuses) if err != nil { - ctx.ServerError("GetRunsFromCommitStatuses", err) - return + log.Error("GetRunsFromCommitStatuses: %v", err) } for _, run := range runs { if run.NeedApproval { @@ -439,14 +441,8 @@ func (prInfo *pullRequestViewInfo) prepareViewFillCommitStatusInfoForOpen(ctx *c statusCheckData.CanApprove = ctx.Repo.Permission.CanWrite(unit.TypeActions) } - pb := prInfo.ProtectedBranchRule - prInfo.enableStatusCheck = pb != nil && pb.EnableStatusCheck - if !prInfo.enableStatusCheck { - return - } - var missingRequiredChecks []string - for _, requiredContext := range pb.StatusCheckContexts { + for _, requiredContext := range pbRequiredContexts { contextFound := false matchesRequiredContext := createRequiredContextMatcher(requiredContext) for _, presentStatus := range commitStatuses { @@ -463,7 +459,7 @@ func (prInfo *pullRequestViewInfo) prepareViewFillCommitStatusInfoForOpen(ctx *c statusCheckData.MissingRequiredChecks = missingRequiredChecks statusCheckData.IsContextRequired = func(context string) bool { - for _, c := range pb.StatusCheckContexts { + for _, c := range pbRequiredContexts { if c == context { return true } @@ -478,7 +474,21 @@ func (prInfo *pullRequestViewInfo) prepareViewFillCommitStatusInfoForOpen(ctx *c } 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 @@ -495,14 +505,16 @@ type pullCommitStatusCheckData struct { CanApprove bool // whether the user can approve workflow runs ApproveLink string // link to approve all checks RequiredChecksState commitstatus.CommitStatusState - LatestCommitStatus *git_model.CommitStatus + + pullCommitStatusState commitstatus.CommitStatusState + PullCommitStatuses []*git_model.CommitStatus } func (d *pullCommitStatusCheckData) CommitStatusCheckPrompt(locale translation.Locale) string { if d.RequiredChecksState.IsPending() || len(d.MissingRequiredChecks) > 0 { return locale.TrString("repo.pulls.status_checking") } 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_success") @@ -558,7 +570,7 @@ func (prInfo *pullRequestViewInfo) prepareViewOpenPullInfo(ctx *context.Context) 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) if pull.IsWorkInProgress(ctx) { ctx.Data["IsPullWorkInProgress"] = prInfo.workInProgressPrefix != "" diff --git a/routers/web/repo/pull_merge_box.go b/routers/web/repo/pull_merge_box.go index 4163d98cbf..27bb8aef28 100644 --- a/routers/web/repo/pull_merge_box.go +++ b/routers/web/repo/pull_merge_box.go @@ -3,31 +3,190 @@ 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() { pull := prInfo.issue.PullRequest 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 { case pull.HasMerged: prInfo.MergeBoxData.TimelineIconClass = "tw-text-purple" - case prInfo.issue.IsClosed, prInfo.workInProgressPrefix != "", pull.IsFilesConflicted(): + case showAsNormalColor: prInfo.MergeBoxData.TimelineIconClass = "tw-text-text-light" - case prInfo.IsPullRequestBroken, mergeBoxData.isBlockedByApprovals, mergeBoxData.isBlockedByRejection, - mergeBoxData.isBlockedByOfficialReviewRequests, mergeBoxData.isBlockedByOutdatedBranch, mergeBoxData.isBlockedByChangedProtectedFiles: + case showAsErrorColor: 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()): + case showAsWarningColor: prInfo.MergeBoxData.TimelineIconClass = "tw-text-yellow" - case mergeBoxData.allowMerge && mergeBoxData.requireSigned && !mergeBoxData.willSign: + case hasBlockers: 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" + 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("%s", 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}) +} diff --git a/routers/web/repo/pull_merge_form.go b/routers/web/repo/pull_merge_form.go index 0cc2bfc27b..27619a8877 100644 --- a/routers/web/repo/pull_merge_form.go +++ b/routers/web/repo/pull_merge_form.go @@ -9,6 +9,7 @@ import ( pull_model "code.gitea.io/gitea/models/pull" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" + "code.gitea.io/gitea/modules/svg" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/services/context" pull_service "code.gitea.io/gitea/services/pull" @@ -91,7 +92,7 @@ func (prInfo *pullRequestViewInfo) prepareMergeBoxFormProps(ctx *context.Context "allOverridableChecksOk": allOverridableChecksOk, "emptyCommit": pull.IsEmpty(), "pullHeadCommitID": prInfo.CompareInfo.HeadCommitID, - "isPullBranchDeletable": prInfo.MergeBoxData.isPullBranchDeletable, + "isPullBranchDeletable": prInfo.MergeBoxData.IsPullBranchDeletable, "defaultMergeStyle": mergeStyle, "defaultDeleteBranchAfterMerge": prConfig.DefaultDeleteBranchAfterMerge, "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 // To test it: // 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 // - 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() { + canUseManualMerge := !pull.IsWorkInProgress(ctx) && !pull.IsChecking() && prConfig.AllowManualMerge + if canUseManualMerge { mergeStyles = append(mergeStyles, map[string]any{ "name": "manually-merged", "allowed": prConfig.AllowManualMerge, @@ -174,5 +170,15 @@ func (prInfo *pullRequestViewInfo) prepareMergeBoxFormProps(ctx *context.Context if len(mergeStyles) > 0 { mergeFormProps["mergeStyles"] = mergeStyles 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"), + ) } } diff --git a/templates/repo/issue/view_content/pull_merge_box.tmpl b/templates/repo/issue/view_content/pull_merge_box.tmpl index 610016e27c..9f6330305a 100644 --- a/templates/repo/issue/view_content/pull_merge_box.tmpl +++ b/templates/repo/issue/view_content/pull_merge_box.tmpl @@ -7,219 +7,67 @@ data-pull-link="{{$.Issue.Link}}" {{end}} > - {{$statusCheckData := .StatusCheckData}} - {{$requiredStatusCheckState := $statusCheckData.RequiredChecksState}}
{{svg "octicon-git-merge" 40}}
-
- {{if .LatestCommitStatus}} - {{template "repo/issue/view_content/pull_merge_status_checks" (dict - "CommitStatuses" .LatestCommitStatuses - "StatusCheckData" $statusCheckData - )}} +
+ {{if $data.ShowStatusCheck}} + {{template "repo/issue/view_content/pull_merge_status_checks" (dict "StatusCheckData" $data.StatusCheckData)}} {{end}} - {{if .Issue.PullRequest.HasMerged}} - {{if .IsPullBranchDeletable}} -
-
-

- {{ctx.Locale.Tr "repo.pulls.merged_success"}} -

- -
-
+ {{if $data.ClosedInfoTitle}} +
+
+

{{$data.ClosedInfoTitle}}

+
{{$data.ClosedInfoBody}}
+
+ {{if $data.IsPullBranchDeletable}} +
-
+ {{end}} +
+ {{end}} + {{range $infoSection := $data.InfoSections}} + {{if $infoSection.InfoItems}} +
+ {{range $infoItem := $infoSection.InfoItems}} +
{{$infoItem.SvgIconHTML}} {{$infoItem.InfoHTML}}
+ {{if $infoItem.ListItems}} +
    {{/* align with the info icon and text */}} + {{range $listItem := $infoItem.ListItems}} +
  • {{$listItem}}
  • + {{end}} +
+ {{end}} + {{end}} +
{{end}} - {{else if .Issue.IsClosed}} -
-
-

{{ctx.Locale.Tr "repo.pulls.closed"}}

- -
- {{if and .IsPullBranchDeletable (not .IsPullRequestBroken)}} -
- -
- {{end}} -
- {{else if .IsPullFilesConflicted}} + {{end}} + {{if $data.ShowUpdatePullInfo}}
- {{svg "octicon-x"}} - {{ctx.Locale.Tr "repo.pulls.files_conflicted"}} + {{template "repo/issue/view_content/update_branch_by_merge" (dict "MergeBoxData" $data "Issue" $.Issue)}}
-
    - {{range .ConflictedFiles}} -
  • {{.}}
  • - {{else}} -
  • {{ctx.Locale.Tr "repo.pulls.files_conflicted_no_listed_files"}}
  • - {{end}} -
- {{else if .IsPullRequestBroken}} -
- {{svg "octicon-x"}} - {{ctx.Locale.Tr "repo.pulls.data_broken"}} -
- {{else if .IsPullWorkInProgress}} -
-
- {{svg "octicon-x"}} - {{ctx.Locale.Tr "repo.pulls.cannot_merge_work_in_progress"}} + {{end}} + + {{if $.IsPullWorkInProgress}} +
+
+ {{svg "octicon-x"}} {{ctx.Locale.Tr "repo.pulls.cannot_merge_work_in_progress"}}
{{if or .HasIssuesOrPullsWritePermission .IsIssuePoster}} - + {{end}}
- {{template "repo/issue/view_content/update_branch_by_merge" $}} - {{else if .Issue.PullRequest.IsChecking}} -
- {{svg "gitea-running" 16 "rotate-clockwise"}} - {{ctx.Locale.Tr "repo.pulls.is_checking"}} -
- {{else if .Issue.PullRequest.IsAncestor}} -
- {{svg "octicon-alert"}} - {{ctx.Locale.Tr "repo.pulls.is_ancestor"}} -
- {{else}} - {{if .IsBlockedByApprovals}} -
- {{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}} -
- {{else if .IsBlockedByRejection}} -
- {{svg "octicon-x"}} - {{ctx.Locale.Tr "repo.pulls.blocked_by_rejection"}} -
- {{else if .IsBlockedByOfficialReviewRequests}} -
- {{svg "octicon-x"}} - {{ctx.Locale.Tr "repo.pulls.blocked_by_official_review_requests"}} -
- {{else if .IsBlockedByOutdatedBranch}} -
- {{svg "octicon-x"}} - {{ctx.Locale.Tr "repo.pulls.blocked_by_outdated_branch"}} -
- {{else if .IsBlockedByChangedProtectedFiles}} -
- {{svg "octicon-x"}} - {{ctx.Locale.TrN $.ChangedProtectedFilesNum "repo.pulls.blocked_by_changed_protected_files_1" "repo.pulls.blocked_by_changed_protected_files_n"}} -
-
    - {{range .ChangedProtectedFiles}} -
  • {{.}}
  • - {{end}} -
- {{else if and .EnableStatusCheck (or $requiredStatusCheckState.IsError $requiredStatusCheckState.IsFailure)}} -
- {{svg "octicon-x"}} - {{ctx.Locale.Tr "repo.pulls.required_status_check_failed"}} -
- {{else if and .EnableStatusCheck (not $requiredStatusCheckState.IsSuccess)}} -
- {{svg "octicon-x"}} - {{ctx.Locale.Tr "repo.pulls.required_status_check_missing"}} -
- {{else if and .AllowMerge .RequireSigned (not .WillSign)}} -
- {{svg "octicon-x"}} - {{ctx.Locale.Tr "repo.pulls.require_signed_wont_sign"}} -
-
- {{svg "octicon-unlock"}} - {{ctx.Locale.Tr (printf "repo.signing.wont_sign.%s" .WontSignReason)}} -
- {{else if not .Issue.PullRequest.IsStatusMergeable}} -
- {{svg "octicon-x"}} - {{ctx.Locale.Tr "repo.pulls.cannot_auto_merge_desc"}} -
-
- {{svg "octicon-info"}} - {{ctx.Locale.Tr "repo.pulls.cannot_auto_merge_helper"}} -
- {{end}} - - {{$notAllOverridableChecksOk := $data.HasOverridableBlockers}} - {{$canMergeNow := $data.CanMergeNow}} - - {{if $canMergeNow}} - {{if $notAllOverridableChecksOk}} -
- {{svg "octicon-dot-fill"}} - {{ctx.Locale.Tr "repo.pulls.required_status_check_administrator"}} -
- {{else}} -
- {{svg "octicon-check"}} - {{ctx.Locale.Tr "repo.pulls.can_auto_merge_desc"}} -
- {{end}} - {{if .WillSign}} -
- {{svg "octicon-lock" 16 "tw-text-green"}} - {{ctx.Locale.Tr "repo.signing.will_sign" .SigningKeyMergeDisplay}} -
- {{else if .IsSigned}} -
- {{svg "octicon-unlock"}} - {{ctx.Locale.Tr (printf "repo.signing.wont_sign.%s" .WontSignReason)}} -
- {{end}} - {{end}} - - {{if .Issue.PullRequest.IsEmpty}} -
- {{svg "octicon-alert"}} - {{ctx.Locale.Tr "repo.pulls.is_empty"}} -
- {{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 */}} -
- {{svg "octicon-info"}} - {{ctx.Locale.Tr "repo.pulls.no_merge_access"}} -
- {{end}} - {{end}}{{/* end if: pull request status */}} + {{end}} {{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 */}}
- {{else if and .AllowMerge .Issue.PullRequest.IsStatusMergeable}} - {{/* no merge style was set in repo setting */}} -
- {{svg "octicon-x"}} - {{ctx.Locale.Tr "repo.pulls.no_merge_desc"}} -
-
- {{svg "octicon-info"}} - {{ctx.Locale.Tr "repo.pulls.no_merge_helper"}} -
{{end}} {{if $data.ShowPullCommands}} diff --git a/templates/repo/issue/view_content/pull_merge_status_checks.tmpl b/templates/repo/issue/view_content/pull_merge_status_checks.tmpl index 24cf9d39e1..5bb48f2009 100644 --- a/templates/repo/issue/view_content/pull_merge_status_checks.tmpl +++ b/templates/repo/issue/view_content/pull_merge_status_checks.tmpl @@ -1,10 +1,9 @@ {{/* Template Attributes: -* CommitStatuses: all commit status elements -* StatusCheckData: additional status check data, see backend pullCommitStatusCheckData struct +* StatusCheckData: see backend pullCommitStatusCheckData struct */}} -{{$commitStatuses := $.CommitStatuses}} {{$statusCheckData := $.StatusCheckData}} {{if $statusCheckData}} + {{$commitStatuses := $statusCheckData.PullCommitStatuses}}
{{$statusCheckData.CommitStatusCheckPrompt ctx.Locale}}
- diff --git a/tests/integration/pull_status_test.go b/tests/integration/pull_status_test.go index b2e6273bad..16b415c364 100644 --- a/tests/integration/pull_status_test.go +++ b/tests/integration/pull_status_test.go @@ -31,8 +31,7 @@ func TestPullCreate_CommitStatus(t *testing.T) { testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "") testEditFileToNewBranch(t, session, "user1", "repo1", "master", "status1", "README.md", "status1") - url := "/" + path.Join("user1", "repo1", "compare", "master...status1") - req := NewRequestWithValues(t, "POST", url, + req := NewRequestWithValues(t, "POST", "/user1/repo1/compare/master...status1", map[string]string{ "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") 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", url, + req := NewRequestWithValues(t, "POST", "/user1/repo1/compare/master...status1", map[string]string{ "title": "pull request from status1", }, @@ -134,6 +132,7 @@ func TestPullCreate_EmptyChangesWithDifferentCommits(t *testing.T) { doc := NewHTMLParser(t, resp.Body) 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.") }) } @@ -143,8 +142,7 @@ func TestPullCreate_EmptyChangesWithSameCommits(t *testing.T) { session := loginUser(t, "user1") testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "") testCreateBranch(t, session, "user1", "repo1", "branch/master", "status1", http.StatusSeeOther) - url := "/" + path.Join("user1", "repo1", "compare", "master...status1") - req := NewRequestWithValues(t, "POST", url, + req := NewRequestWithValues(t, "POST", "/user1/repo1/compare/master...status1", map[string]string{ "title": "pull request from status1", }, diff --git a/web_src/css/repo.css b/web_src/css/repo.css index f1c4678626..fb85dc8e9f 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -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 { color: var(--color-text-light-2); font-style: italic;