feat(pr): merge with the repository's defaults, ready, gh flags and ✓ lines for pr mutations

pr merge takes the method from the repository's default merge style
instead of always merging (D22), accepts gh's --merge, --squash and
--rebase next to -s/--method, refuses methods the repository disables,
and adds -d/--delete-branch (defaulting to the repository setting),
--auto, -t, -b, -F and -e. On a terminal it asks for the method and
branch deletion with the defaults preselected, without a picker when
the current branch has a pull request. It reports "✓ Squashed and
merged pull request alice/x#3 (title)", the deleted remote branch, and
switches off and deletes the local head branch; --json returns the
merged pull request. Merged, closed and draft pull requests are
reported before anything is asked. -s without a value points at
--squash.

pr checkout names the local branch after the head branch when that
name is free or already at the head, falling back to OWNER/BRANCH for
forks and pr/NUMBER (D13), fetches and switches quietly, reports
"✓ Checked out #1 into feat/one", and has the alias co. pr clean
reports what it deleted and that a remote branch was already gone.

pr create asks for the title and body, then "What's next?": submit,
submit as a draft, add metadata or change the branches, without the
AGit question up front or a final confirmation. It pushes an unpushed
head with --set-upstream --quiet ("✓ Pushed feat/one to origin"),
prints "✓ Created pull request alice/x#9 (title)" and the URL, and
adds --fill, --web, the alias new, the gh shorthands, and milestones
by title.

New tea pr ready [--undo]. pr close and reopen take -c/--comment,
close takes -d/--delete-branch, and both say when the pull request
already is in that state. pr edit reports "✓ Edited", takes the
singular gh reviewer flags (the plurals still work), milestones by
title and --remove-milestone. pr diff is colored and paged on a
terminal and byte-exact when piped, with --name-only, --color and
--web. Every pull request argument accepts #N, URLs and branch names
and defaults to the current branch's pull request.

Signed-off-by: bircni <bircni@icloud.com>
This commit is contained in:
bircni committed 2026-10-11 23:55:11 +02:00
1 parent 2c5e005bcf
commit 4895aa089d
28 files changed
+1169 -504

No files matched your search

+73 -2
View File
@@ -5,6 +5,7 @@ package services
import (
"bytes"
"cmp"
"context"
"errors"
"fmt"
@@ -369,10 +370,79 @@ func (s Pulls) Diff(ctx context.Context, n int64) (io.ReadCloser, error) {
return io.NopCloser(bytes.NewReader(diff)), nil
}
// MergeOptions supplies merge strategy and optional commit text.
// DeleteHead deletes the head branch of a pull request in its head repository.
func (s Pulls) DeleteHead(ctx context.Context, item Pull) error {
if item.HeadRemoteDeleted || item.Head == "" {
return nil
}
repo := cmp.Or(item.HeadRepo, s.Repository)
_, err := repoCall(ctx, s.Client, repo, func(ctx context.Context, c *gitea.Client, owner, name string) (bool, *gitea.Response, error) {
return c.Repositories.DeleteRepoBranch(ctx, owner, name, item.Head)
})
return err
}
// Files lists the paths of the files a pull request changes.
func (s Pulls) Files(ctx context.Context, n int64) ([]string, error) {
files, err := listAll(func(page int) ([]*gitea.ChangedFile, error) {
return repoCall(ctx, s.Client, s.Repository, func(ctx context.Context, c *gitea.Client, owner, name string) ([]*gitea.ChangedFile, *gitea.Response, error) {
return c.PullRequests.ListPullRequestFiles(ctx, owner, name, n, gitea.ListPullRequestFilesOptions{ListOptions: listOptions(100, page)})
})
})
if err != nil {
return nil, err
}
paths := make([]string, 0, len(files))
for _, file := range files {
if file != nil {
paths = append(paths, file.Filename)
}
}
return paths, nil
}
// MergeOptions supplies merge strategy and optional commit text. Auto merges once the required
// checks succeed instead of now.
type MergeOptions struct {
Method, Title, Message string
DeleteBranch bool
DeleteBranch, Auto bool
}
// MergeSettings are the merge styles a repository allows, in Gitea's order, and its merge defaults.
type MergeSettings struct {
Styles []string
Default string
DeleteBranch bool
}
// MergeSettings reads the merge styles the repository allows and its default style and branch
// deletion. A default style that is not allowed falls back to the first allowed one.
func (s Pulls) MergeSettings(ctx context.Context) (MergeSettings, error) {
repo, err := repoCall(ctx, s.Client, s.Repository, func(ctx context.Context, c *gitea.Client, owner, name string) (*gitea.Repository, *gitea.Response, error) {
return c.Repositories.GetRepo(ctx, owner, name)
})
if err != nil {
return MergeSettings{}, err
}
settings := MergeSettings{Default: string(repo.DefaultMergeStyle), DeleteBranch: repo.DefaultDeleteBranchAfterMerge}
for _, style := range []struct {
name gitea.MergeStyle
allowed bool
}{
{gitea.MergeStyleMerge, repo.AllowMerge},
{gitea.MergeStyleRebase, repo.AllowRebase},
{gitea.MergeStyleRebaseMerge, repo.AllowRebaseMerge},
{gitea.MergeStyleSquash, repo.AllowSquash},
{gitea.MergeStyleFastForwardOnly, repo.AllowFastForwardOnlyMerge},
} {
if style.allowed {
settings.Styles = append(settings.Styles, string(style.name))
}
}
if !slices.Contains(settings.Styles, settings.Default) && len(settings.Styles) > 0 {
settings.Default = settings.Styles[0]
}
return settings, nil
}
// Merge merges the pull request's current head with the selected strategy; Gitea refuses the
@@ -385,6 +455,7 @@ func (s Pulls) Merge(ctx context.Context, n int64, opts MergeOptions) error {
}
option := gitea.MergePullRequestOption{
Style: gitea.MergeStyle(opts.Method), Title: opts.Title, Message: opts.Message, DeleteBranchAfterMerge: &opts.DeleteBranch,
MergeWhenChecksSucceed: opts.Auto,
}
if pull.Head != nil {
option.HeadCommitId = pull.Head.Sha