mirror of
https://gitea.com/gitea/tea.git
synced 2026-09-10 07:26:33 -04:00
Implements #1050. Prompt to delete the source branch after a successful tea pr merge, displaying the branch name because deletion cannot be undone. --delete-branch skips the prompt for scripts; this flag was requested in #810 for gh parity before being closed as an upstream Gitea bug. PullDeleteBranch takes a confirmation callback rather than prompting directly, fetching the pull request once and passing the branch name to the caller. The callback runs after state and permission checks so the user is not prompted for a deletion that would fail anyway. Deletion uses the API against the head repository, so it works for pull requests opened from a fork never checked out locally. tea pr clean is unsuitable because it requires a local repo and fails when the branch is not found locally. A matching local branch is still removed as a best effort, matched by commit rather than name so a branch with unpushed commits is left alone; PullClean makes the same choice and only accepts name matching behind --ignore-sha. The prompt is gated on stdin and stdout both being terminals, following the existing check in modules/context. TeaContext.IsInteractiveMode could not be used: despite its doc comment, it only reports whether any flag was passed, so a script running a bare tea pr merge <idx> would have blocked on a prompt it could not answer. Branches already deleted server-side, PRs still open, and head repositories the user cannot push to are each handled explicitly rather than surfacing a raw API error. Signed-off-by: Jan Baer <jan.s.baer@googlemail.com>
140 lines
3.6 KiB
Go
140 lines
3.6 KiB
Go
// Copyright 2024 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package interact
|
|
|
|
import (
|
|
stdctx "context"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
gitea "gitea.dev/sdk"
|
|
|
|
"gitea.dev/tea/cmd/flags"
|
|
"gitea.dev/tea/modules/context"
|
|
"gitea.dev/tea/modules/task"
|
|
"gitea.dev/tea/modules/theme"
|
|
"gitea.dev/tea/modules/utils"
|
|
|
|
"charm.land/huh/v2"
|
|
"golang.org/x/term"
|
|
)
|
|
|
|
// MergePull interactively creates a PR
|
|
func MergePull(requestCtx stdctx.Context, ctx *context.TeaContext) error {
|
|
if ctx.LocalRepo == nil {
|
|
return fmt.Errorf("pull request index is required")
|
|
}
|
|
|
|
branch, _, err := ctx.LocalRepo.TeaGetCurrentBranchNameAndSHA()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
idx, err := getPullIndex(requestCtx, ctx, branch)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := task.PullMerge(requestCtx, ctx.Login, ctx.Owner, ctx.Repo, idx, gitea.MergePullRequestOption{
|
|
Style: gitea.MergeStyle(ctx.String("style")),
|
|
Title: ctx.String("title"),
|
|
Message: ctx.String("message"),
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
|
|
return MaybeDeleteSourceBranch(requestCtx, ctx, idx, ctx.Bool("delete-branch"))
|
|
}
|
|
|
|
// getPullIndex interactively determines the PR index
|
|
func getPullIndex(requestCtx stdctx.Context, ctx *context.TeaContext, branch string) (int64, error) {
|
|
c := ctx.Login.Client()
|
|
opts := gitea.ListPullRequestsOptions{
|
|
State: gitea.StateOpen,
|
|
ListOptions: flags.GetListOptions(ctx.Command),
|
|
}
|
|
selected := ""
|
|
loadMoreOption := "PR not found? Load more PRs..."
|
|
|
|
// paginated fetch
|
|
var prs []*gitea.PullRequest
|
|
for {
|
|
var err error
|
|
prs, _, err = c.PullRequests.ListRepoPullRequests(requestCtx, ctx.Owner, ctx.Repo, opts)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if len(prs) == 0 {
|
|
return 0, fmt.Errorf("no open PRs found")
|
|
}
|
|
opts.ListOptions.Page++
|
|
prOptions := make([]string, 0)
|
|
|
|
// get the PR indexes where head branch is the current branch
|
|
for _, pr := range prs {
|
|
if pr.Head.Ref == branch {
|
|
prOptions = append(prOptions, fmt.Sprintf("#%d: %s", pr.Index, pr.Title))
|
|
}
|
|
}
|
|
|
|
// then get the PR indexes where base branch is the current branch
|
|
for _, pr := range prs {
|
|
// don't add the same PR twice, so `pr.Head.Ref != branch`
|
|
if pr.Base.Ref == branch && pr.Head.Ref != branch {
|
|
prOptions = append(prOptions, fmt.Sprintf("#%d: %s", pr.Index, pr.Title))
|
|
}
|
|
}
|
|
|
|
prOptions = append(prOptions, loadMoreOption)
|
|
|
|
if err := huh.NewSelect[string]().
|
|
Title("Select a PR to merge:").
|
|
Options(huh.NewOptions(prOptions...)...).
|
|
Value(&selected).
|
|
Filtering(true).
|
|
Run(); err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
if selected != loadMoreOption {
|
|
break
|
|
}
|
|
}
|
|
|
|
// get the index from the selected option
|
|
before, _, _ := strings.Cut(selected, ":")
|
|
before = strings.TrimPrefix(before, "#")
|
|
idx, err := utils.ArgToIndex(before)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return idx, nil
|
|
}
|
|
|
|
// MaybeDeleteSourceBranch deletes the source branch on --delete-branch, else
|
|
// asks. Needs a real terminal: IsInteractiveMode only counts flags.
|
|
func MaybeDeleteSourceBranch(requestCtx stdctx.Context, ctx *context.TeaContext, index int64, force bool) error {
|
|
confirm := confirmDeleteBranch
|
|
if force {
|
|
confirm = nil
|
|
} else if !term.IsTerminal(int(os.Stdin.Fd())) || !term.IsTerminal(int(os.Stdout.Fd())) {
|
|
return nil
|
|
}
|
|
|
|
return task.PullDeleteBranch(requestCtx, ctx.Login, ctx.Owner, ctx.Repo, index, confirm)
|
|
}
|
|
|
|
// confirmDeleteBranch names the branch, since the delete is irreversible.
|
|
func confirmDeleteBranch(branch string) (bool, error) {
|
|
var confirmed bool
|
|
err := huh.NewConfirm().
|
|
Title(fmt.Sprintf("Delete the source branch '%s'?", branch)).
|
|
Value(&confirmed).
|
|
WithTheme(theme.GetTheme()).
|
|
Run()
|
|
return confirmed, err
|
|
}
|