// Copyright 2026 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package repo import ( "html/template" "gitea.dev/modules/htmlutil" "gitea.dev/modules/svg" "gitea.dev/modules/util" "gitea.dev/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 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 showAsNormalColor: prInfo.MergeBoxData.TimelineIconClass = "tw-text-text-light" case showAsErrorColor: prInfo.MergeBoxData.TimelineIconClass = "tw-text-red" case showAsWarningColor: prInfo.MergeBoxData.TimelineIconClass = "tw-text-yellow" case hasBlockers: prInfo.MergeBoxData.TimelineIconClass = "tw-text-red" case pull.IsStatusMergeable(): prInfo.MergeBoxData.TimelineIconClass = "tw-text-green" default: 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.hasPermToMerge { prInfo.MergeBoxData.infoProtectionBlockers.AddInfoItem( svg.RenderHTML("octicon-info"), ctx.Locale.Tr("repo.pulls.no_merge_access"), ) } if data.canMergeNow { if data.hasOverridableBlockers { prompt := ctx.Locale.Tr("repo.pulls.required_status_check_bypass_allowlist") if data.canBypassProtectionAsAdmin { prompt = ctx.Locale.Tr("repo.pulls.required_status_check_administrator") } prInfo.MergeBoxData.infoMergePrompts.AddInfoItem( svg.RenderHTML("octicon-dot-fill"), prompt, ) } 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}) }