Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions models/pull/automerge.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,23 @@ func GetScheduledMergeByPullID(ctx context.Context, pullID int64) (bool, *AutoMe
return true, scheduledPRM, err
}

// GetScheduledMergeByPullIDs returns the scheduled auto merges for the given pull request IDs, keyed by pull ID.
// The returned entries do not have their Doer loaded.
func GetScheduledMergeByPullIDs(ctx context.Context, pullIDs []int64) (map[int64]*AutoMerge, error) {
if len(pullIDs) == 0 {
return map[int64]*AutoMerge{}, nil
}
merges := make([]*AutoMerge, 0, len(pullIDs))
if err := db.GetEngine(ctx).In("pull_id", pullIDs).Find(&merges); err != nil {
return nil, err
}
result := make(map[int64]*AutoMerge, len(merges))
for _, m := range merges {
result[m.PullID] = m
}
return result, nil
}

func GetScheduledMergePullIDsSince(ctx context.Context, since timeutil.TimeStamp) ([]int64, error) {
var pullIDs []int64
err := db.GetEngine(ctx).Table(&AutoMerge{}).Where("created_unix >= ?", since).Cols("pull_id").Find(&pullIDs)
Expand Down
14 changes: 14 additions & 0 deletions modules/structs/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ type PullRequest struct {

// Whether the pull request can be merged
Mergeable bool `json:"mergeable"`
// The scheduled auto merge, null if the pull request is not scheduled to auto merge
AutoMerge *PullRequestAutoMerge `json:"auto_merge"`
// Whether the pull request has been merged
HasMerged bool `json:"merged"`
// swagger:strfmt date-time
Expand Down Expand Up @@ -95,6 +97,18 @@ type PullRequest struct {
ContentVersion int `json:"content_version"`
}

// PullRequestAutoMerge represents a pull request scheduled to auto merge when all checks succeed
type PullRequestAutoMerge struct {
// The user who scheduled the auto merge
EnabledBy *User `json:"enabled_by"`
// The merge method that will be used, eg: "merge", "rebase", "rebase-merge", "squash", "fast-forward-only"
MergeMethod string `json:"merge_method"`
// The title of the resulting merge commit
CommitTitle string `json:"commit_title"`
// The message of the resulting merge commit
CommitMessage string `json:"commit_message"`
}

// PRBranchInfo information about a branch
type PRBranchInfo struct {
// The display name of the branch
Expand Down
17 changes: 11 additions & 6 deletions options/locale/locale_en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -1839,8 +1839,8 @@
"repo.pulls.is_empty": "The changes on this branch are already on the target branch. This will be an empty commit.",
"repo.pulls.required_status_check_failed": "Some required checks were not successful.",
"repo.pulls.required_status_check_missing": "Some required checks are missing.",
"repo.pulls.required_status_check_administrator": "As an administrator, you may still merge this pull request.",
"repo.pulls.required_status_check_bypass_allowlist": "You are allowed to bypass branch protection rules for this merge.",
"repo.pulls.merging_is_blocked": "Merging is blocked",
"repo.pulls.merge_bypass_rules": "Merge without waiting for requirements to be met (bypass rules)",
"repo.pulls.blocked_by_approvals": "This pull request doesn't have enough required approvals yet. %d of %d official approvals granted.",
"repo.pulls.blocked_by_approvals_whitelisted": "This pull request doesn't have enough required approvals yet. %d of %d approvals granted from users or teams on the allowlist.",
"repo.pulls.blocked_by_rejection": "This pull request has changes requested by an official reviewer.",
Expand Down Expand Up @@ -1910,18 +1910,23 @@
"repo.pulls.reopen": "Reopen Pull Request",
"repo.pulls.closed_at": "closed this pull request <a id=\"%[1]s\" href=\"#%[1]s\">%[2]s</a>",
"repo.pulls.reopened_at": "reopened this pull request <a id=\"%[1]s\" href=\"#%[1]s\">%[2]s</a>",
"repo.pulls.cmd_instruction_hint": "View command line instructions",
"repo.pulls.cmd_instruction_hint": "Show command line instructions",
"repo.pulls.cmd_instruction_modal_title": "Checkout via the command line",
"repo.pulls.cmd_instruction_checkout_title": "Checkout",
"repo.pulls.cmd_instruction_checkout_desc": "From your project repository, check out a new branch and test the changes.",
"repo.pulls.cmd_instruction_merge_title": "Merge",
"repo.pulls.cmd_instruction_merge_desc": "Merge the changes and update on Gitea.",
"repo.pulls.cmd_instruction_merge_warning": "Warning: This operation cannot merge pull request because \"autodetect manual merge\" is not enabled.",
"repo.pulls.clear_merge_message": "Clear merge message",
"repo.pulls.clear_merge_message_hint": "Clearing the merge message will only remove the commit message content and keep generated git trailers such as \"Co-Authored-By…\".",
"repo.pulls.auto_merge_button_when_succeed": "(When checks succeed)",
"repo.pulls.auto_merge_when_succeed": "Auto merge when all checks succeed",
"repo.pulls.enable_auto_merge": "Enable auto merge (%s)",
"repo.pulls.merge_style_short_merge": "merge commit",
"repo.pulls.merge_style_short_rebase": "rebase",
"repo.pulls.merge_style_short_rebase_merge": "rebase and merge",
"repo.pulls.merge_style_short_squash": "squash",
"repo.pulls.merge_style_short_fast_forward_only": "fast-forward",
"repo.pulls.auto_merge_newly_scheduled": "The pull request was scheduled to merge when all checks succeed.",
"repo.pulls.auto_merge_has_pending_schedule": "%[1]s scheduled this pull request to auto merge when all checks succeed %[2]s.",
"repo.pulls.auto_merge_has_pending_schedule": "%[1]s scheduled this pull request to auto merge (%[3]s) when all checks succeed %[2]s.",
"repo.pulls.auto_merge_cancel_schedule": "Cancel auto merge",
"repo.pulls.auto_merge_not_scheduled": "This pull request is not scheduled to auto merge.",
"repo.pulls.auto_merge_canceled_schedule": "The auto merge was canceled for this pull request.",
Expand Down
2 changes: 0 additions & 2 deletions routers/web/repo/issue_view.go
Original file line number Diff line number Diff line change
Expand Up @@ -951,10 +951,8 @@ func (prInfo *pullRequestViewInfo) prepareMergeBox(ctx *context.Context, issue *
data.hasStatusCheckBlocker

data.canBypassProtection = isRepoAdmin
data.canBypassProtectionAsAdmin = isRepoAdmin
if ctx.IsSigned && prInfo.ProtectedBranchRule != nil {
data.canBypassProtection = git_model.CanBypassBranchProtection(ctx, prInfo.ProtectedBranchRule, ctx.Doer, isRepoAdmin)
data.canBypassProtectionAsAdmin = isRepoAdmin && !prInfo.ProtectedBranchRule.BlockAdminMergeOverride
}

// CanMergeNow means: if the doer has write permission, whether the PR can be merged now
Expand Down
9 changes: 4 additions & 5 deletions routers/web/repo/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -274,11 +274,10 @@ type pullMergeBoxData struct {
// The latter gate the merge even when the rule's own status check is disabled.
hasRequiredStatusContexts bool

hasOverridableBlockers bool
canMergeNow bool // PR is mergeable, either no blocker, or doer can bypass the blockers
hasPermToMerge bool // doer has permission to merge
canBypassProtection bool
canBypassProtectionAsAdmin bool
hasOverridableBlockers bool
canMergeNow bool // PR is mergeable, either no blocker, or doer can bypass the blockers
hasPermToMerge bool // doer has permission to merge
canBypassProtection bool

ShowUpdatePullInfo bool
UpdatePrimaryAction *pullUpdateAction
Expand Down
42 changes: 23 additions & 19 deletions routers/web/repo/pull_merge_box.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@ type pullMergeBoxInfoItem struct {
SvgIconHTML template.HTML
InfoHTML template.HTML
ListItems []template.HTML
ExtraClass string
}

type pullMergeBoxInfoItemCollection struct {
items []*pullMergeBoxInfoItem
items []*pullMergeBoxInfoItem
errorCount int
}

type pullInfoSection struct {
Expand All @@ -41,12 +43,15 @@ func (c *pullMergeBoxInfoItemCollection) AddInfoItem(svg, info template.HTML, op
})
}

// AddErrorItem adds a blocking reason, rendered as a muted sub-row of the "merging is blocked" heading
func (c *pullMergeBoxInfoItemCollection) AddErrorItem(info template.HTML, optItems ...[]template.HTML) {
c.items = append(c.items, &pullMergeBoxInfoItem{
SvgIconHTML: svg.RenderHTML("octicon-x", 16, "tw-text-red"),
SvgIconHTML: svg.RenderHTML("octicon-dot-fill", 16, "tw-text-text-light"),
InfoHTML: info,
ListItems: util.OptionalArg(optItems),
ExtraClass: "tw-pl-6 tw-text-text-light",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it is a right design.

The pullMergeBoxInfoItemCollection is designed to be a flat list. Now, you just added the hacky patches to make it "indented" and "grouped".

Image

It is very fragile.

I believe that "don't try to teach a duck to bark to make it become a dog".

If you need a new layout, design a proper data structure for it. Complex changes can be in a separate and dedicated PR.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems working because at the moment all callers to AddErrorItem are on infoProtectionBlockers, then the design is abused by this PR.

But indeed AddInfoItem / AddErrorItem are similar functions, the layout will break if AddInfoItem / AddErrorItem are both called for the same section.

We should avoid the unclear and fragile behaviors when designing the functions and methods. Do not bring surprises to developers.

})
c.errorCount++
}

func (prInfo *pullRequestViewInfo) prepareMergeBoxIconColor() {
Expand Down Expand Up @@ -164,28 +169,27 @@ func (prInfo *pullRequestViewInfo) prepareMergeBoxInfoItems(ctx *context.Context
)
}

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"),
)
}
// when the doer can bypass overridable blockers, the merge form offers switch-to-force-merge,
// so no separate admin/allowlist prompt is needed here; only show the positive "can be merged" hint when clear
if data.canMergeNow && !data.hasOverridableBlockers && (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})
items := data.infoProtectionBlockers.items
if data.infoProtectionBlockers.errorCount > 0 {
heading := &pullMergeBoxInfoItem{
SvgIconHTML: svg.RenderHTML("octicon-x", 16, "tw-text-red"),
InfoHTML: htmlutil.HTMLFormat("<strong>%s</strong>", ctx.Locale.Tr("repo.pulls.merging_is_blocked")),
}
items = append([]*pullMergeBoxInfoItem{heading}, items...)
}
data.InfoSections = append(data.InfoSections, &pullInfoSection{items})
}
data.InfoSections = append(data.InfoSections, &pullInfoSection{data.infoMergePrompts.items})
}
42 changes: 32 additions & 10 deletions routers/web/repo/pull_merge_form.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ import (
pull_service "gitea.dev/services/pull"
)

// mergeStyleShortLocaleKeys maps a merge style to the locale key of its short, human-readable label.
// Only auto-merge-capable styles have an entry; callers fall back to the raw style for anything else.
var mergeStyleShortLocaleKeys = map[repo_model.MergeStyle]string{
repo_model.MergeStyleMerge: "repo.pulls.merge_style_short_merge",
repo_model.MergeStyleRebase: "repo.pulls.merge_style_short_rebase",
repo_model.MergeStyleRebaseMerge: "repo.pulls.merge_style_short_rebase_merge",
repo_model.MergeStyleSquash: "repo.pulls.merge_style_short_squash",
repo_model.MergeStyleFastForwardOnly: "repo.pulls.merge_style_short_fast_forward_only",
}

func (prInfo *pullRequestViewInfo) prepareMergeBoxFormProps(ctx *context.Context) {
pull := prInfo.issue.PullRequest
if pull.HasMerged || prInfo.issue.IsClosed {
Expand Down Expand Up @@ -61,7 +71,11 @@ func (prInfo *pullRequestViewInfo) prepareMergeBoxFormProps(ctx *context.Context
var hasPendingPullRequestMergeTip template.HTML
if hasPendingPullRequestMerge {
createdPRMergeStr := templates.TimeSince(pendingPullRequestMerge.CreatedUnix)
hasPendingPullRequestMergeTip = ctx.Locale.Tr("repo.pulls.auto_merge_has_pending_schedule", pendingPullRequestMerge.Doer.Name, createdPRMergeStr)
styleShort := any(string(pendingPullRequestMerge.MergeStyle))
if key, ok := mergeStyleShortLocaleKeys[pendingPullRequestMerge.MergeStyle]; ok {
styleShort = ctx.Locale.Tr(key)
}
hasPendingPullRequestMergeTip = ctx.Locale.Tr("repo.pulls.auto_merge_has_pending_schedule", pendingPullRequestMerge.Doer.Name, createdPRMergeStr, styleShort)
}

var defaultMergeTitle, defaultMergeBody string
Expand All @@ -85,18 +99,18 @@ func (prInfo *pullRequestViewInfo) prepareMergeBoxFormProps(ctx *context.Context

allOverridableChecksOk := !prInfo.MergeBoxData.hasOverridableBlockers
mergeFormProps := map[string]any{
"baseLink": prInfo.issue.Link(),
"textCancel": ctx.Locale.Tr("cancel"),
"textDeleteBranch": ctx.Locale.Tr("repo.branch.delete", prInfo.headTarget),
"textAutoMergeButtonWhenSucceed": ctx.Locale.Tr("repo.pulls.auto_merge_button_when_succeed"),
"textAutoMergeWhenSucceed": ctx.Locale.Tr("repo.pulls.auto_merge_when_succeed"),
"textAutoMergeCancelSchedule": ctx.Locale.Tr("repo.pulls.auto_merge_cancel_schedule"),
"textClearMergeMessage": ctx.Locale.Tr("repo.pulls.clear_merge_message"),
"textClearMergeMessageHint": ctx.Locale.Tr("repo.pulls.clear_merge_message_hint"),
"textMergeCommitId": ctx.Locale.Tr("repo.pulls.merge_commit_id"),
"baseLink": prInfo.issue.Link(),
"textCancel": ctx.Locale.Tr("cancel"),
"textDeleteBranch": ctx.Locale.Tr("repo.branch.delete", prInfo.headTarget),
"textAutoMergeCancelSchedule": ctx.Locale.Tr("repo.pulls.auto_merge_cancel_schedule"),
"textClearMergeMessage": ctx.Locale.Tr("repo.pulls.clear_merge_message"),
"textClearMergeMessageHint": ctx.Locale.Tr("repo.pulls.clear_merge_message_hint"),
"textMergeCommitId": ctx.Locale.Tr("repo.pulls.merge_commit_id"),

"canMergeNow": prInfo.MergeBoxData.canMergeNow,
"allOverridableChecksOk": allOverridableChecksOk,
"canBypassProtection": prInfo.MergeBoxData.canBypassProtection,
"textBypassRules": ctx.Locale.Tr("repo.pulls.merge_bypass_rules"),
"emptyCommit": pull.IsEmpty(),
"pullHeadCommitID": prInfo.CompareInfo.HeadCommitID,
"isPullBranchDeletable": prInfo.MergeBoxData.IsPullBranchDeletable,
Expand All @@ -107,6 +121,9 @@ func (prInfo *pullRequestViewInfo) prepareMergeBoxFormProps(ctx *context.Context

"hasPendingPullRequestMerge": hasPendingPullRequestMerge,
"hasPendingPullRequestMergeTip": hasPendingPullRequestMergeTip,

"showPullCommands": prInfo.MergeBoxData.ShowPullCommands,
"textCmdHint": ctx.Locale.Tr("repo.pulls.cmd_instruction_hint"),
}

// if this pr can be merged now, then hide the auto merge
Expand All @@ -118,6 +135,7 @@ func (prInfo *pullRequestViewInfo) prepareMergeBoxFormProps(ctx *context.Context
"name": "merge",
"allowed": prConfig.AllowMerge,
"textDoMerge": ctx.Locale.Tr("repo.pulls.merge_pull_request"),
"textAutoMerge": ctx.Locale.Tr("repo.pulls.enable_auto_merge", ctx.Locale.Tr("repo.pulls.merge_style_short_merge")),
"mergeTitleFieldText": defaultMergeTitle,
"mergeMessageFieldText": defaultMergeBody,
"hideAutoMerge": generalHideAutoMerge,
Expand All @@ -126,13 +144,15 @@ func (prInfo *pullRequestViewInfo) prepareMergeBoxFormProps(ctx *context.Context
"name": "rebase",
"allowed": prConfig.AllowRebase,
"textDoMerge": ctx.Locale.Tr("repo.pulls.rebase_merge_pull_request"),
"textAutoMerge": ctx.Locale.Tr("repo.pulls.enable_auto_merge", ctx.Locale.Tr("repo.pulls.merge_style_short_rebase")),
"hideMergeMessageTexts": true,
"hideAutoMerge": generalHideAutoMerge,
},
map[string]any{
"name": "rebase-merge",
"allowed": prConfig.AllowRebaseMerge,
"textDoMerge": ctx.Locale.Tr("repo.pulls.rebase_merge_commit_pull_request"),
"textAutoMerge": ctx.Locale.Tr("repo.pulls.enable_auto_merge", ctx.Locale.Tr("repo.pulls.merge_style_short_rebase_merge")),
"mergeTitleFieldText": defaultMergeTitle,
"mergeMessageFieldText": defaultMergeBody,
"hideAutoMerge": generalHideAutoMerge,
Expand All @@ -141,6 +161,7 @@ func (prInfo *pullRequestViewInfo) prepareMergeBoxFormProps(ctx *context.Context
"name": "squash",
"allowed": prConfig.AllowSquash,
"textDoMerge": ctx.Locale.Tr("repo.pulls.squash_merge_pull_request"),
"textAutoMerge": ctx.Locale.Tr("repo.pulls.enable_auto_merge", ctx.Locale.Tr("repo.pulls.merge_style_short_squash")),
"mergeTitleFieldText": defaultSquashMergeTitle,
"mergeMessageFieldText": git.CommitMessageMerge(defaultSquashMergeCommitMessages, defaultSquashMergeBody),
"hideAutoMerge": generalHideAutoMerge,
Expand All @@ -149,6 +170,7 @@ func (prInfo *pullRequestViewInfo) prepareMergeBoxFormProps(ctx *context.Context
"name": "fast-forward-only",
"allowed": prConfig.AllowFastForwardOnly && pull.CommitsBehind == 0,
"textDoMerge": ctx.Locale.Tr("repo.pulls.fast_forward_only_merge_pull_request"),
"textAutoMerge": ctx.Locale.Tr("repo.pulls.enable_auto_merge", ctx.Locale.Tr("repo.pulls.merge_style_short_fast_forward_only")),
"hideMergeMessageTexts": true,
"hideAutoMerge": generalHideAutoMerge,
},
Expand Down
Loading
Loading