revert: tea 1.0 rewrite (#1122–#1126) (#1146)

Reverts #1126, #1125, #1124, #1123 and #1122.

`go.mod` keeps the Go 1.26 line with toolchain `go1.26.9` (instead of the original `go1.26.6`) so the security fix from #1131 isn't lost.

reopens #69
reopens #141
reopens #162
reopens #172
reopens #197
reopens #232
reopens #240
reopens #246
reopens #323
reopens #325
reopens #326
reopens #328
reopens #344
reopens #351
reopens #373
reopens #403
reopens #406
reopens #417
reopens #430
reopens #457
reopens #480
reopens #482
reopens #489
reopens #490
reopens #498
reopens #500
reopens #504
reopens #510
reopens #516
reopens #523
reopens #556
reopens #560
reopens #569
reopens #571
reopens #602
reopens #632
reopens #633
reopens #643
reopens #666
reopens #670
reopens #672
reopens #677
reopens #683
reopens #697
reopens #726
reopens #737
reopens #770
reopens #779
reopens #804
reopens #817
reopens #831
reopens #833
reopens #834
reopens #908
reopens #966
reopens #1023
reopens #1029
reopens #1032
reopens #1050
reopens #1052
reopens #1069
reopens #1073
reopens #1087
reopens #1091
reopens #1094
reopens #1110
reopens #1115
reopens #1116
reopens #1118

Reviewed-on: https://gitea.com/gitea/tea/pulls/1146
Reviewed-by: TheFox0x7 <95654+thefox0x7@noreply.gitea.com>
This commit is contained in:
bircni committed 2026-10-18 20:56:33 +00:00
1 parent 33bb12729e
commit c5f674111a
604 files changed
+33711 -35545

No files matched your search

+31
View File
@@ -0,0 +1,31 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package pulls
import (
stdctx "context"
gitea "gitea.dev/sdk"
"github.com/urfave/cli/v3"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/context"
)
// CmdPullsApprove approves a PR
var CmdPullsApprove = cli.Command{
Name: "approve",
Aliases: []string{"lgtm", "a"},
Usage: "Approve a pull request",
Description: "Approve a pull request",
ArgsUsage: "<pull index> [<comment>]",
Action: func(requestCtx stdctx.Context, cmd *cli.Command) error {
ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
return runPullReview(requestCtx, ctx, gitea.ReviewStateApproved, false)
},
Flags: flags.AllDefaultFlags,
}
+59
View File
@@ -0,0 +1,59 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package pulls
import (
stdctx "context"
"fmt"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/context"
"gitea.dev/tea/modules/interact"
"gitea.dev/tea/modules/task"
"gitea.dev/tea/modules/utils"
"github.com/urfave/cli/v3"
)
// CmdPullsCheckout is a command to locally checkout the given PR
var CmdPullsCheckout = cli.Command{
Name: "checkout",
Aliases: []string{"co"},
Usage: "Locally check out the given PR",
Description: `Locally check out the given PR`,
Action: runPullsCheckout,
ArgsUsage: "<pull index>",
Flags: append([]cli.Flag{
&cli.BoolFlag{
Name: "branch",
Aliases: []string{"b"},
Usage: "Create a local branch if it doesn't exist yet",
},
}, flags.AllDefaultFlags...),
}
func runPullsCheckout(requestCtx stdctx.Context, cmd *cli.Command) error {
ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{
LocalRepo: true,
RemoteRepo: true,
}); err != nil {
return err
}
if ctx.Args().Len() != 1 {
return fmt.Errorf("pull request index is required")
}
idx, err := utils.ArgToIndex(ctx.Args().First())
if err != nil {
return err
}
if err := task.PullCheckout(requestCtx, ctx.Login, ctx.Owner, ctx.Repo, ctx.Bool("branch"), idx, interact.PromptPassword); err != nil && !interact.IsQuitting(err) {
return err
}
return nil
}
+56
View File
@@ -0,0 +1,56 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package pulls
import (
"fmt"
stdctx "context"
"github.com/urfave/cli/v3"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/context"
"gitea.dev/tea/modules/interact"
"gitea.dev/tea/modules/task"
"gitea.dev/tea/modules/utils"
)
// CmdPullsClean removes the remote and local feature branches, if a PR is merged.
var CmdPullsClean = cli.Command{
Name: "clean",
Usage: "Deletes local & remote feature-branches for a closed pull request",
Description: `Deletes local & remote feature-branches for a closed pull request`,
ArgsUsage: "<pull index>",
Action: runPullsClean,
Flags: append([]cli.Flag{
&cli.BoolFlag{
Name: "ignore-sha",
Usage: "Find the local branch by name instead of commit hash (less precise)",
},
}, flags.AllDefaultFlags...),
}
func runPullsClean(requestCtx stdctx.Context, cmd *cli.Command) error {
ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{LocalRepo: true}); err != nil {
return err
}
if ctx.Args().Len() != 1 {
return fmt.Errorf("pull request index is required")
}
idx, err := utils.ArgToIndex(ctx.Args().First())
if err != nil {
return err
}
if err := task.PullClean(requestCtx, ctx.Login, ctx.Owner, ctx.Repo, idx, ctx.Bool("ignore-sha"), interact.PromptPassword); err != nil && !interact.IsQuitting(err) {
return err
}
return nil
}
+26
View File
@@ -0,0 +1,26 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package pulls
import (
"context"
"gitea.dev/sdk"
"gitea.dev/tea/cmd/flags"
"github.com/urfave/cli/v3"
)
// CmdPullsClose closes a given open pull request
var CmdPullsClose = cli.Command{
Name: "close",
Usage: "Change state of one or more pull requests to 'closed'",
Description: `Change state of one or more pull requests to 'closed'`,
ArgsUsage: "<pull index> [<pull index>...]",
Action: func(ctx context.Context, cmd *cli.Command) error {
s := gitea.StateClosed
return editPullState(ctx, cmd, gitea.EditPullRequestOption{State: &s})
},
Flags: flags.AllDefaultFlags,
}
+166
View File
@@ -0,0 +1,166 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package pulls
import (
stdctx "context"
"encoding/json"
"fmt"
"io"
gitea "gitea.dev/sdk"
"github.com/urfave/cli/v3"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/context"
"gitea.dev/tea/modules/interact"
"gitea.dev/tea/modules/print"
"gitea.dev/tea/modules/task"
"gitea.dev/tea/modules/utils"
)
// CmdPullsCreate creates a pull request
var CmdPullsCreate = cli.Command{
Name: "create",
Aliases: []string{"c"},
Usage: "Create a pull-request",
Description: "Create a pull-request in the current repo",
Action: runPullsCreate,
Flags: append([]cli.Flag{
&cli.StringFlag{
Name: "head",
Usage: "Branch name of the PR source (default is current one). To specify a different head repo, use <user>:<branch>",
},
&cli.StringFlag{
Name: "base",
Aliases: []string{"b"},
Usage: "Branch name of the PR target (default is repos default branch)",
},
&cli.BoolFlag{
Name: "allow-maintainer-edits",
Aliases: []string{"edits"},
Usage: "Enable maintainers to push to the base branch of created pull",
Value: true,
},
&cli.BoolFlag{
Name: "agit",
Usage: "Create an agit flow pull request",
},
&cli.StringFlag{
Name: "topic",
Usage: "Topic name for agit flow pull request",
},
&cli.BoolFlag{
Name: "draft",
Usage: "Create as a draft (prepends \"WIP: \" to the title; Gitea treats WIP-prefixed PRs as drafts)",
},
}, flags.IssuePRCreateFlags...),
}
func runPullsCreate(requestCtx stdctx.Context, cmd *cli.Command) error {
ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
// Interactive mode and head-branch defaulting both need a local repo.
// When --head is given explicitly the user can target a cross-fork PR
// from outside a working tree (e.g. with --repo <owner>/<repo>);
// task.CreatePull only consults ctx.LocalRepo when head is empty.
needsLocalRepo := ctx.IsInteractiveMode() || len(ctx.String("head")) == 0
requirement := context.CtxRequirement{RemoteRepo: true}
if needsLocalRepo {
requirement.LocalRepo = true
}
if err := ctx.Ensure(requirement); err != nil {
return err
}
// no args -> interactive mode
if ctx.IsInteractiveMode() {
if err := interact.CreatePull(requestCtx, ctx); err != nil && !interact.IsQuitting(err) {
return err
}
return nil
}
// agit flow creates the PR via git push and returns no PR object, so
// --output cannot be honored there; fail fast before any API calls
if ctx.Bool("agit") && ctx.IsSet("output") {
return fmt.Errorf("--output cannot be combined with --agit: the PR is created via git push, so no pull request object is available to print")
}
// else use args to create PR
opts, err := flags.GetIssuePRCreateFlags(requestCtx, ctx)
if err != nil {
return err
}
if ctx.Bool("draft") {
opts.Title = utils.AddDraftPrefix(opts.Title)
}
if ctx.Bool("agit") {
return task.CreateAgitFlowPull(
requestCtx,
ctx,
ctx.String("remote"),
ctx.String("head"),
ctx.String("base"),
ctx.String("topic"),
opts,
interact.PromptPassword,
)
}
var allowMaintainerEdits *bool
if ctx.IsSet("allow-maintainer-edits") {
allowMaintainerEdits = gitea.OptionalBool(ctx.Bool("allow-maintainer-edits"))
}
pr, err := task.CreatePull(
requestCtx,
ctx,
ctx.String("base"),
ctx.String("head"),
allowMaintainerEdits,
opts,
)
if err != nil {
return err
}
if ctx.IsSet("output") {
switch ctx.String("output") {
case "json":
return writeCreatedPullAsJSON(ctx.Writer, pr)
}
}
print.PullDetails(pr, nil, nil)
return nil
}
// createdPullJSON is the machine-readable representation of a freshly
// created pull request. A new PR has no reviews, comments or CI yet, so
// this is intentionally leaner than the detail view's pullData (cmd/pulls.go).
type createdPullJSON struct {
Index int64 `json:"index"`
Title string `json:"title"`
URL string `json:"url"`
State gitea.StateType `json:"state"`
Base string `json:"base"`
Head string `json:"head"`
}
func writeCreatedPullAsJSON(w io.Writer, pr *gitea.PullRequest) error {
return json.NewEncoder(w).Encode(createdPullJSON{
Index: pr.Index,
Title: pr.Title,
URL: pr.HTMLURL,
State: pr.State,
Base: pr.Base.Ref,
Head: pr.Head.Ref,
})
}
+48
View File
@@ -0,0 +1,48 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package pulls_test
import (
"context"
"testing"
"gitea.dev/tea/cmd"
"gitea.dev/tea/modules/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestPullsCreateAgitOutputRejected verifies that --output (parsed via the
// urfave/cli v3 ancestor-flag cascade, since create itself does not declare
// it) is rejected for the agit flow before any API call or git push happens.
func TestPullsCreateAgitOutputRejected(t *testing.T) {
config.SetConfigForTesting(config.LocalConfig{
Logins: []config.Login{{
Name: "testLogin",
URL: "https://gitea.example.com",
Token: "test-token",
User: "testUser",
Default: true,
}},
})
t.Cleanup(func() {
config.SetConfigForTesting(config.LocalConfig{})
})
app := cmd.App()
args := []string{
"tea", "pulls", "create",
"--agit",
"--output", "json",
"--head", "topic-branch",
"--title", "test",
"--login", "testLogin",
"--repo", "user/repo",
}
err := app.Run(context.Background(), args)
require.Error(t, err)
assert.Contains(t, err.Error(), "--output cannot be combined with --agit")
}
+45
View File
@@ -0,0 +1,45 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package pulls
import (
"bytes"
"encoding/json"
"testing"
gitea "gitea.dev/sdk"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestWriteCreatedPullAsJSON(t *testing.T) {
pr := &gitea.PullRequest{
Index: 33,
Title: "test title",
HTMLURL: "https://gitea.example.com/owner/repo/pulls/33",
State: gitea.StateOpen,
Base: &gitea.PRBranchInfo{Ref: "main"},
Head: &gitea.PRBranchInfo{Ref: "feature"},
}
var buf bytes.Buffer
require.NoError(t, writeCreatedPullAsJSON(&buf, pr))
var got map[string]any
require.NoError(t, json.Unmarshal(buf.Bytes(), &got))
assert.Equal(t, float64(33), got["index"])
assert.Equal(t, "test title", got["title"])
assert.Equal(t, "https://gitea.example.com/owner/repo/pulls/33", got["url"])
assert.Equal(t, "open", got["state"])
assert.Equal(t, "main", got["base"])
assert.Equal(t, "feature", got["head"])
// exactly the lean field set, nothing extra
assert.Len(t, got, 6)
// machine-readable output must not contain terminal escape sequences
assert.NotContains(t, buf.String(), "\x1b")
}
+168
View File
@@ -0,0 +1,168 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package pulls
import (
stdctx "context"
"fmt"
"strings"
gitea "gitea.dev/sdk"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/context"
"gitea.dev/tea/modules/print"
"gitea.dev/tea/modules/task"
"gitea.dev/tea/modules/utils"
"github.com/urfave/cli/v3"
)
// applyDraftFlag mutates opts.Title according to --draft / --ready.
// If a flag is set but --title isn't, it fetches the current title from the server.
// Returns an error if both --draft and --ready are set, or on a server error.
func applyDraftFlag(requestCtx stdctx.Context, ctx *context.TeaContext, client *gitea.Client, idx int64, opts *task.EditIssueOption) error {
draft := ctx.Bool("draft")
ready := ctx.Bool("ready")
if !draft && !ready {
return nil
}
if draft && ready {
return fmt.Errorf("--draft and --ready are mutually exclusive")
}
var current string
if opts.Title != nil {
current = *opts.Title
} else {
pr, _, err := client.PullRequests.GetPullRequest(requestCtx, ctx.Owner, ctx.Repo, idx)
if err != nil {
return fmt.Errorf("could not fetch pull request #%d: %s", idx, err)
}
current = pr.Title
}
var next string
if draft {
next = utils.AddDraftPrefix(current)
} else {
next = utils.StripDraftPrefix(current)
}
if next != current || opts.Title != nil {
opts.Title = &next
}
return nil
}
// CmdPullsEdit is the subcommand of pulls to edit pull requests
var CmdPullsEdit = cli.Command{
Name: "edit",
Aliases: []string{"e"},
Usage: "Edit one or more pull requests",
Description: `Edit one or more pull requests. To unset a property again,
use an empty string (eg. --milestone "").`,
ArgsUsage: "<idx> [<idx>...]",
Action: runPullsEdit,
Flags: append(flags.IssuePREditFlags,
&cli.StringFlag{
Name: "add-reviewers",
Aliases: []string{"r"},
Usage: "Comma-separated list of usernames to request review from",
},
&cli.StringFlag{
Name: "remove-reviewers",
Usage: "Comma-separated list of usernames to remove from reviewers",
},
&cli.BoolFlag{
Name: "draft",
Usage: "Mark as draft by prepending \"WIP: \" to the title (idempotent)",
},
&cli.BoolFlag{
Name: "ready",
Usage: "Mark as ready for review by stripping any leading \"WIP: \" or \"[WIP]\" prefix",
},
),
}
func runPullsEdit(requestCtx stdctx.Context, cmd *cli.Command) error {
ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
if !cmd.Args().Present() {
return fmt.Errorf("must specify at least one pull request index")
}
opts, err := flags.GetIssuePREditFlags(ctx)
if err != nil {
return err
}
if cmd.IsSet("add-reviewers") {
opts.AddReviewers = strings.Split(cmd.String("add-reviewers"), ",")
}
if cmd.IsSet("remove-reviewers") {
opts.RemoveReviewers = strings.Split(cmd.String("remove-reviewers"), ",")
}
indices, err := utils.ArgsToIndices(ctx.Args().Slice())
if err != nil {
return err
}
client := ctx.Login.Client()
for _, opts.Index = range indices {
if err := applyDraftFlag(requestCtx, ctx, client, opts.Index, opts); err != nil {
return err
}
pr, err := task.EditPull(requestCtx, ctx, client, *opts)
if err != nil {
return err
}
if ctx.Args().Len() > 1 {
fmt.Println(pr.HTMLURL)
} else {
print.PullDetails(pr, nil, nil)
}
}
return nil
}
// editPullState abstracts the arg parsing to edit the given pull request
func editPullState(requestCtx stdctx.Context, cmd *cli.Command, opts gitea.EditPullRequestOption) error {
ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
if ctx.Args().Len() == 0 {
return fmt.Errorf("pull request index is required")
}
indices, err := utils.ArgsToIndices(ctx.Args().Slice())
if err != nil {
return err
}
client := ctx.Login.Client()
for _, index := range indices {
pr, _, err := client.PullRequests.EditPullRequest(requestCtx, ctx.Owner, ctx.Repo, index, opts)
if err != nil {
return err
}
if len(indices) > 1 {
fmt.Println(pr.HTMLURL)
} else {
print.PullDetails(pr, nil, nil)
}
}
return nil
}
+80
View File
@@ -0,0 +1,80 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package pulls
import (
stdctx "context"
"fmt"
"slices"
"gitea.dev/sdk"
"github.com/urfave/cli/v3"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/context"
"gitea.dev/tea/modules/print"
)
var pullFieldsFlag = flags.FieldsFlag(print.PullFields, []string{
"index", "title", "state", "author", "milestone", "updated", "labels",
})
// CmdPullsList represents a sub command of issues to list pulls
var CmdPullsList = cli.Command{
Name: "list",
Aliases: []string{"ls"},
Usage: "List pull requests of the repository",
Description: `List pull requests of the repository`,
ArgsUsage: " ", // command does not accept arguments
Action: RunPullsList,
Flags: append([]cli.Flag{pullFieldsFlag}, flags.PRListingFlags...),
}
// RunPullsList return list of pulls
func RunPullsList(requestCtx stdctx.Context, cmd *cli.Command) error {
ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
state, err := flags.ParseState(ctx.String("state"))
if err != nil {
return err
}
client := ctx.Login.Client()
prs, _, err := client.PullRequests.ListRepoPullRequests(requestCtx, ctx.Owner, ctx.Repo, gitea.ListPullRequestsOptions{
ListOptions: flags.GetListOptions(cmd),
State: state,
})
if err != nil {
return err
}
fields, err := pullFieldsFlag.GetValues(cmd)
if err != nil {
return err
}
var ciStatuses map[int64]*gitea.CombinedStatus
if slices.Contains(fields, "ci") {
ciStatuses = map[int64]*gitea.CombinedStatus{}
for _, pr := range prs {
if pr.Head == nil || pr.Head.Sha == "" {
continue
}
ci, _, err := client.Repositories.GetCombinedStatus(requestCtx, ctx.Owner, ctx.Repo, pr.Head.Sha)
if err != nil {
fmt.Printf("error fetching CI status for PR #%d: %v\n", pr.Index, err)
continue
}
ciStatuses[pr.Index] = ci
}
}
return print.PullsList(prs, ctx.Output, fields, ciStatuses)
}
+72
View File
@@ -0,0 +1,72 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package pulls
import (
stdctx "context"
gitea "gitea.dev/sdk"
"github.com/urfave/cli/v3"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/context"
"gitea.dev/tea/modules/interact"
"gitea.dev/tea/modules/task"
"gitea.dev/tea/modules/utils"
)
// CmdPullsMerge merges a PR
var CmdPullsMerge = cli.Command{
Name: "merge",
Aliases: []string{"m"},
Usage: "Merge a pull request",
Description: "Merge a pull request",
ArgsUsage: "<pull index>",
Flags: append([]cli.Flag{
&cli.StringFlag{
Name: "style",
Aliases: []string{"s"},
Usage: "Kind of merge to perform: merge, rebase, squash, rebase-merge",
Value: "merge",
},
&cli.StringFlag{
Name: "title",
Aliases: []string{"t"},
Usage: "Merge commit title",
},
&cli.StringFlag{
Name: "message",
Aliases: []string{"m"},
Usage: "Merge commit message",
},
}, flags.AllDefaultFlags...),
Action: func(requestCtx stdctx.Context, cmd *cli.Command) error {
ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
if ctx.Args().Len() != 1 {
// If no PR index is provided, try interactive mode
if err := interact.MergePull(requestCtx, ctx); err != nil && !interact.IsQuitting(err) {
return err
}
return nil
}
idx, err := utils.ArgToIndex(ctx.Args().First())
if err != nil {
return err
}
return 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"),
})
},
}
+30
View File
@@ -0,0 +1,30 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package pulls
import (
stdctx "context"
gitea "gitea.dev/sdk"
"github.com/urfave/cli/v3"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/context"
)
// CmdPullsReject requests changes to a PR
var CmdPullsReject = cli.Command{
Name: "reject",
Usage: "Request changes to a pull request",
Description: "Request changes to a pull request",
ArgsUsage: "<pull index> <reason>",
Action: func(requestCtx stdctx.Context, cmd *cli.Command) error {
ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
return runPullReview(requestCtx, ctx, gitea.ReviewStateRequestChanges, true)
},
Flags: flags.AllDefaultFlags,
}
+27
View File
@@ -0,0 +1,27 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package pulls
import (
"context"
"gitea.dev/sdk"
"gitea.dev/tea/cmd/flags"
"github.com/urfave/cli/v3"
)
// CmdPullsReopen reopens a given closed pull request
var CmdPullsReopen = cli.Command{
Name: "reopen",
Aliases: []string{"open"},
Usage: "Change state of one or more pull requests to 'open'",
Description: `Change state of one or more pull requests to 'open'`,
ArgsUsage: "<pull index> [<pull index>...]",
Action: func(ctx context.Context, cmd *cli.Command) error {
s := gitea.StateOpen
return editPullState(ctx, cmd, gitea.EditPullRequestOption{State: &s})
},
Flags: flags.AllDefaultFlags,
}
+29
View File
@@ -0,0 +1,29 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package pulls
import (
stdctx "context"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/context"
"github.com/urfave/cli/v3"
)
// CmdPullsReply replies to a review comment on a pull request.
var CmdPullsReply = cli.Command{
Name: "reply",
Usage: "Reply to a pull request review comment",
Description: "Reply to a pull request review comment",
ArgsUsage: "<pull index> <comment id> [<reply>]",
Action: func(requestCtx stdctx.Context, cmd *cli.Command) error {
ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
return runPullReviewReply(requestCtx, ctx)
},
Flags: flags.AllDefaultFlags,
}
+70
View File
@@ -0,0 +1,70 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package pulls
import (
"context"
"testing"
"gitea.dev/tea/modules/config"
"github.com/stretchr/testify/assert"
)
func TestReply(t *testing.T) {
config.SetConfigForTesting(config.LocalConfig{
Logins: []config.Login{{
Name: "testLogin",
URL: "https://gitea.example.com",
Token: "test-token",
User: "testUser",
Default: true,
}},
})
t.Cleanup(func() {
config.SetConfigForTesting(config.LocalConfig{})
})
tests := []struct {
name string
args []string
wantErr bool
errContains string
}{
{
name: "no arguments",
args: []string{},
wantErr: true,
errContains: "pull request index and comment ID are required",
},
{
name: "missing comment id",
args: []string{"1"},
wantErr: true,
errContains: "pull request index and comment ID are required",
},
{
name: "pull index and comment id",
args: []string{"1", "2"},
wantErr: true,
errContains: "no reply content provided",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cmd := CmdPullsReply
args := append([]string{"reply"}, tt.args...)
args = append(args, "--login", "testLogin", "--repo", "user/repo")
err := cmd.Run(context.Background(), args)
if tt.wantErr {
assert.Error(t, err)
if tt.errContains != "" {
assert.Contains(t, err.Error(), tt.errContains)
}
return
}
})
}
}
+30
View File
@@ -0,0 +1,30 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package pulls
import (
stdctx "context"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/context"
"gitea.dev/tea/modules/task"
"github.com/urfave/cli/v3"
)
// CmdPullsResolve resolves a review comment on a pull request
var CmdPullsResolve = cli.Command{
Name: "resolve",
Usage: "Resolve a review comment on a pull request",
Description: "Resolve a review comment on a pull request",
ArgsUsage: "<comment id>",
Action: func(requestCtx stdctx.Context, cmd *cli.Command) error {
ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
return runResolveComment(requestCtx, ctx, task.ResolvePullReviewComment)
},
Flags: flags.AllDefaultFlags,
}
+59
View File
@@ -0,0 +1,59 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package pulls
import (
stdctx "context"
"fmt"
"os"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/context"
"gitea.dev/tea/modules/interact"
"gitea.dev/tea/modules/print"
"gitea.dev/tea/modules/utils"
"github.com/urfave/cli/v3"
)
// CmdPullsReview starts an interactive review session
var CmdPullsReview = cli.Command{
Name: "review",
Usage: "Interactively review a pull request",
Description: "Interactively review a pull request",
ArgsUsage: "<pull index>",
Action: func(requestCtx stdctx.Context, cmd *cli.Command) error {
ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
if !ctx.Args().Present() {
return fmt.Errorf("must specify at least one PR index")
}
// This command is intentionally interactive. Fail early in CI / non-TTY
// contexts rather than hanging on prompts.
if os.Getenv("CI") != "" || !print.IsInteractive() || interact.IsStdinPiped() {
return fmt.Errorf("pull review requires an interactive terminal")
}
for _, arg := range ctx.Args().Slice() {
idx, err := utils.ArgToIndex(arg)
if err != nil {
return err
}
if err := interact.ReviewPull(requestCtx, ctx, idx); err != nil && !interact.IsQuitting(err) {
return err
}
}
return nil
},
Flags: flags.AllDefaultFlags,
}
+63
View File
@@ -0,0 +1,63 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package pulls
import (
stdctx "context"
"fmt"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/context"
"gitea.dev/tea/modules/print"
"gitea.dev/tea/modules/task"
"gitea.dev/tea/modules/utils"
"github.com/urfave/cli/v3"
)
var reviewCommentFieldsFlag = flags.FieldsFlag(print.PullReviewCommentFields, []string{
"id", "path", "line", "body", "reviewer", "resolver",
})
// CmdPullsReviewComments lists review comments on a pull request
var CmdPullsReviewComments = cli.Command{
Name: "review-comments",
Aliases: []string{"rc"},
Usage: "List review comments on a pull request",
Description: "List review comments on a pull request",
ArgsUsage: "<pull index>",
Action: runPullsReviewComments,
Flags: append([]cli.Flag{reviewCommentFieldsFlag}, flags.AllDefaultFlags...),
}
func runPullsReviewComments(requestCtx stdctx.Context, cmd *cli.Command) error {
ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
if ctx.Args().Len() < 1 {
return fmt.Errorf("pull request index is required")
}
idx, err := utils.ArgToIndex(ctx.Args().First())
if err != nil {
return err
}
comments, err := task.ListPullReviewComments(requestCtx, ctx, idx)
if err != nil {
return err
}
fields, err := reviewCommentFieldsFlag.GetValues(cmd)
if err != nil {
return err
}
return print.PullReviewCommentsList(comments, ctx.Output, fields)
}
+128
View File
@@ -0,0 +1,128 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package pulls
import (
stdctx "context"
"errors"
"fmt"
"io"
"strings"
gitea "gitea.dev/sdk"
"gitea.dev/tea/modules/config"
"gitea.dev/tea/modules/context"
"gitea.dev/tea/modules/interact"
"gitea.dev/tea/modules/task"
"gitea.dev/tea/modules/theme"
"gitea.dev/tea/modules/utils"
"charm.land/huh/v2"
)
// runPullReview handles the common logic for approving/rejecting pull requests
func runPullReview(requestCtx stdctx.Context, ctx *context.TeaContext, state gitea.ReviewStateType, requireComment bool) error {
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
minArgs := 1
if requireComment {
minArgs = 2
}
if ctx.Args().Len() < minArgs {
if requireComment {
return fmt.Errorf("pull request index and comment are required")
}
return fmt.Errorf("pull request index is required")
}
idx, err := utils.ArgToIndex(ctx.Args().First())
if err != nil {
return err
}
comment := strings.Join(ctx.Args().Tail(), " ")
return task.CreatePullReview(requestCtx, ctx, idx, state, comment, nil)
}
// runResolveComment handles the common logic for resolving/unresolving review comments
func runResolveComment(requestCtx stdctx.Context, ctx *context.TeaContext, action func(stdctx.Context, *context.TeaContext, int64) error) error {
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
if ctx.Args().Len() < 1 {
return fmt.Errorf("comment ID is required")
}
commentID, err := utils.ArgToIndex(ctx.Args().First())
if err != nil {
return err
}
return action(requestCtx, ctx, commentID)
}
// runPullReviewReply handles replying to a specific review comment on a pull request.
func runPullReviewReply(requestCtx stdctx.Context, ctx *context.TeaContext) error {
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
if ctx.Args().Len() < 2 {
return fmt.Errorf("pull request index and comment ID are required")
}
idx, err := utils.ArgToIndex(ctx.Args().First())
if err != nil {
return err
}
commentID, err := utils.ArgToIndex(ctx.Args().Get(1))
if err != nil {
return err
}
body, err := getCommentBody(ctx, ctx.Args().Slice()[2:], "Reply(markdown):", "reply")
if err != nil {
return err
}
return task.ReplyToPullReviewComment(requestCtx, ctx, idx, commentID, body)
}
func getCommentBody(ctx *context.TeaContext, extraArgs []string, promptTitle, noun string) (string, error) {
body := strings.Join(extraArgs, " ")
if interact.IsStdinPiped() {
bodyStdin, err := io.ReadAll(ctx.Reader)
if err != nil {
return "", err
}
if len(bodyStdin) != 0 {
body = strings.Join([]string{body, string(bodyStdin)}, "\n\n")
}
} else if len(body) == 0 {
if err := huh.NewForm(
huh.NewGroup(
huh.NewText().
Title(promptTitle).
ExternalEditor(config.GetPreferences().Editor).
EditorExtension("md").
Value(&body),
),
).WithTheme(theme.GetTheme()).Run(); err != nil {
return "", err
}
}
if len(strings.TrimSpace(body)) == 0 {
return "", errors.New("no " + noun + " content provided")
}
return body, nil
}
+61
View File
@@ -0,0 +1,61 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package pulls
import (
"os"
"testing"
"github.com/stretchr/testify/assert"
)
func TestReview(t *testing.T) {
if os.Getenv("GITEA_TEA_TEST_URL") == "" {
t.Skip("GITEA_TEA_TEST_URL is not set, skipping test")
}
tests := []struct {
name string
args []string
wantErr bool
errContains string
}{
{
name: "no arguments",
args: []string{},
wantErr: true,
errContains: "must specify at least one PR index",
},
{
name: "one argument",
args: []string{"1"},
wantErr: false,
},
{
name: "two arguments",
args: []string{"1", "2"},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cmd := CmdPullsReview
args := append(tt.args, "--repo", "user/repo")
err := cmd.Run(t.Context(), args)
if tt.wantErr {
assert.Error(t, err)
if tt.errContains != "" {
assert.Contains(t, err.Error(), tt.errContains)
}
return
}
// Don't assert no error, because we expect an error about the missing
// remote. Just assert that the error is not the one we're looking for.
if err != nil {
assert.NotContains(t, err.Error(), "must specify at least one PR index")
}
})
}
}
+30
View File
@@ -0,0 +1,30 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package pulls
import (
stdctx "context"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/context"
"gitea.dev/tea/modules/task"
"github.com/urfave/cli/v3"
)
// CmdPullsUnresolve unresolves a review comment on a pull request
var CmdPullsUnresolve = cli.Command{
Name: "unresolve",
Usage: "Unresolve a review comment on a pull request",
Description: "Unresolve a review comment on a pull request",
ArgsUsage: "<comment id>",
Action: func(requestCtx stdctx.Context, cmd *cli.Command) error {
ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
return runResolveComment(requestCtx, ctx, task.UnresolvePullReviewComment)
},
Flags: flags.AllDefaultFlags,
}