refactor(issue,pr): trim help, restating comments and single-use helpers

Cut issue, pr, label, milestone and comment examples to at most three, drop
doc comments that only restate the name, fold the comment and attachment
argument lookups into one closure each, and inline label helpers with one
caller.

Signed-off-by: bircni <bircni@icloud.com>
This commit is contained in:
bircni committed 2026-10-12 20:48:00 +02:00
1 parent 4a85bb3d40
commit 2826726557
13 files changed
+57 -147

No files matched your search

+2 -7
View File
@@ -75,12 +75,8 @@ var errNoMatchingRemote = errors.New("no Git remote matches the selected reposit
func (a *application) pullCheckoutCommand() *cobra.Command { func (a *application) pullCheckoutCommand() *cobra.Command {
var branch string var branch string
var detached, update bool var detached, update bool
cmd := &cobra.Command{Use: "checkout {<number> | <url> | <branch>}", Aliases: []string{"co"}, Short: "Check out a pull request", Long: `Check out the head of a pull request in a local branch. cmd := &cobra.Command{Use: "checkout {<number> | <url> | <branch>}", Aliases: []string{"co"}, Short: "Check out a pull request", Long: `Check out the head of a pull request in a local branch that tracks the head branch, so
that git push updates it and pr commands without an argument select the pull request.`, Annotations: pullArgumentsAnnotation(nil), Args: argsExact(1), RunE: func(cmd *cobra.Command, args []string) error {
The branch takes the name of the pull request's head branch when that name is free or
already points at the head; otherwise it is OWNER/BRANCH for a fork, or pr/NUMBER. tea
records the pull request in the branch's Git configuration, so that pr commands without an
argument select it, and the branch tracks the head branch, so that git push updates it.`, Annotations: pullArgumentsAnnotation(nil), Args: argsExact(1), RunE: func(cmd *cobra.Command, args []string) error {
if detached && (cmd.Flags().Changed("branch") || update) { if detached && (cmd.Flags().Changed("branch") || update) {
return clierr.Wrap(clierr.Usage, errors.New("--detach and --branch/--update are mutually exclusive")) return clierr.Wrap(clierr.Usage, errors.New("--detach and --branch/--update are mutually exclusive"))
} }
@@ -156,7 +152,6 @@ argument select it, and the branch tracks the head branch, so that git push upda
cmd.ValidArgsFunction = a.completePulls("open") cmd.ValidArgsFunction = a.completePulls("open")
cmd.Example = `$ tea pr checkout 13 cmd.Example = `$ tea pr checkout 13
$ tea pr co feature/login $ tea pr co feature/login
$ tea pr checkout 13 --branch review-13
$ tea pr checkout 13 --update` $ tea pr checkout 13 --update`
return cmd return cmd
} }
+16 -40
View File
@@ -16,7 +16,6 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
// commentTitle is the prompt for a comment body.
const commentTitle = "Comment (markdown):" const commentTitle = "Comment (markdown):"
// commentNouns are the words for the commands of an issue or pull request comment group. // commentNouns are the words for the commands of an issue or pull request comment group.
@@ -65,7 +64,6 @@ func commentName(t target, comment services.Comment) string {
return fmt.Sprintf("comment %d on %s#%d", comment.ID, t.Repo, comment.IssueNumber) return fmt.Sprintf("comment %d on %s#%d", comment.ID, t.Repo, comment.IssueNumber)
} }
// commentFlags are the body flags of commands that write a comment.
type commentFlags struct { type commentFlags struct {
body, file string body, file string
useEditor bool useEditor bool
@@ -142,7 +140,6 @@ func (a *application) commentOn(cmd *cobra.Command, t target, n int64, kind refK
return a.created(cmd, comment, comment.URL) return a.created(cmd, comment, comment.URL)
} }
// lastComment returns the newest comment on n by the account tea acts as.
func lastComment(cmd *cobra.Command, t target, n int64) (services.Comment, error) { func lastComment(cmd *cobra.Command, t target, n int64) (services.Comment, error) {
items, err := (services.Comments{Client: t.client, Repository: t.Repo}).List(cmd.Context(), n) items, err := (services.Comments{Client: t.client, Repository: t.Repo}).List(cmd.Context(), n)
if err != nil { if err != nil {
@@ -156,14 +153,22 @@ func lastComment(cmd *cobra.Command, t target, n int64) (services.Comment, error
return services.Comment{}, fmt.Errorf("%s has no comments on %s#%d", t.login(), t.Repo, n) return services.Comment{}, fmt.Errorf("%s has no comments on %s#%d", t.login(), t.Repo, n)
} }
// comment returns comment id, which a command of kind may change: pr commands refuse comments on // comment returns the comment that the ID argument names, which a command of kind may change: pr
// issues. // commands refuse comments on issues.
func (a *application) comment(cmd *cobra.Command, t target, id int64, kind refKind) (services.Comment, error) { func (a *application) comment(cmd *cobra.Command, arg string, kind refKind) (target, services.Comment, error) {
id, err := number(arg)
if err != nil {
return target{}, services.Comment{}, err
}
t, err := a.resolve(cmd.Context(), true)
if err != nil {
return t, services.Comment{}, err
}
current, err := (services.Comments{Client: t.client, Repository: t.Repo}).View(cmd.Context(), id) current, err := (services.Comments{Client: t.client, Repository: t.Repo}).View(cmd.Context(), id)
if err == nil && kind == pullRef && !current.Pull { if err == nil && kind == pullRef && !current.Pull {
err = fmt.Errorf("comment %d in %s is on an issue, not a pull request; use %s %d", id, t.Repo, issueCommandPath(cmd), id) err = fmt.Errorf("comment %d in %s is on an issue, not a pull request; use %s %d", id, t.Repo, issueCommandPath(cmd), id)
} }
return current, err return t, current, err
} }
// editComment replaces the text of comment, prefilling the prompt or editor with it. // editComment replaces the text of comment, prefilling the prompt or editor with it.
@@ -184,8 +189,7 @@ func (a *application) editComment(cmd *cobra.Command, t target, comment services
// deleteComment deletes comment after a confirmation that defaults to no. // deleteComment deletes comment after a confirmation that defaults to no.
func (a *application) deleteComment(cmd *cobra.Command, t target, comment services.Comment) error { func (a *application) deleteComment(cmd *cobra.Command, t target, comment services.Comment) error {
name := commentName(t, comment) return a.deleteConfirmed(cmd, commentName(t, comment), func() error {
return a.deleteConfirmed(cmd, name, func() error {
return (services.Comments{Client: t.client, Repository: t.Repo}).Delete(cmd.Context(), comment.ID) return (services.Comments{Client: t.client, Repository: t.Repo}).Delete(cmd.Context(), comment.ID)
}) })
} }
@@ -262,10 +266,7 @@ func (a *application) commentCommand(kind refKind) *cobra.Command {
Without --body, --body-file or --editor, the configured editor opens, or a multi-line prompt Without --body, --body-file or --editor, the configured editor opens, or a multi-line prompt
asks for the comment. The subcommands list, view, edit and delete comments by their ID.`, kind.article()), Example: fmt.Sprintf(`$ tea %[1]s comment %[2]s --body "Fixed in #14" asks for the comment. The subcommands list, view, edit and delete comments by their ID.`, kind.article()), Example: fmt.Sprintf(`$ tea %[1]s comment %[2]s --body "Fixed in #14"
$ tea %[1]s comment %[2]s --body-file notes.md
# Change or remove your last comment
$ tea %[1]s comment %[2]s --edit-last --body "Fixed in #15" $ tea %[1]s comment %[2]s --edit-last --body "Fixed in #15"
$ tea %[1]s comment %[2]s --delete-last --yes
$ tea %[1]s comment list %[2]s`, words.group, words.example), Annotations: map[string]string{"tea.arguments": words.arguments, "tea.confirm": "true"}, Args: commentArgs(kind), RunE: func(cmd *cobra.Command, args []string) error { $ tea %[1]s comment list %[2]s`, words.group, words.example), Annotations: map[string]string{"tea.arguments": words.arguments, "tea.confirm": "true"}, Args: commentArgs(kind), RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 { if len(args) == 0 {
if localFlagsChanged(cmd) { if localFlagsChanged(cmd) {
@@ -329,15 +330,7 @@ $ tea %[1]s comment list %[2]s --json id,author,body`, words.group, words.exampl
list.ValidArgsFunction = a.completeDiscussions(kind) list.ValidArgsFunction = a.completeDiscussions(kind)
view := &cobra.Command{Use: "view <comment-id>", Short: "View a comment", Example: fmt.Sprintf("$ tea %s comment view 81", words.group), Args: argsExact(1), RunE: func(cmd *cobra.Command, args []string) error { view := &cobra.Command{Use: "view <comment-id>", Short: "View a comment", Example: fmt.Sprintf("$ tea %s comment view 81", words.group), Args: argsExact(1), RunE: func(cmd *cobra.Command, args []string) error {
id, err := number(args[0]) t, comment, err := a.comment(cmd, args[0], kind)
if err != nil {
return err
}
t, err := a.resolve(cmd.Context(), true)
if err != nil {
return err
}
comment, err := a.comment(cmd, t, id, kind)
if err != nil { if err != nil {
return err return err
} }
@@ -365,15 +358,7 @@ $ tea %[1]s comment edit 81`, words.group), Args: argsExact(1), RunE: func(cmd *
if err := editFlags.validate(cmd); err != nil { if err := editFlags.validate(cmd); err != nil {
return err return err
} }
id, err := number(args[0]) t, comment, err := a.comment(cmd, args[0], kind)
if err != nil {
return err
}
t, err := a.resolve(cmd.Context(), true)
if err != nil {
return err
}
comment, err := a.comment(cmd, t, id, kind)
if err != nil { if err != nil {
return err return err
} }
@@ -382,15 +367,7 @@ $ tea %[1]s comment edit 81`, words.group), Args: argsExact(1), RunE: func(cmd *
editFlags.register(edit) editFlags.register(edit)
remove := &cobra.Command{Use: "delete <comment-id>", Short: "Delete a comment", Example: fmt.Sprintf("$ tea %s comment delete 81 --yes", words.group), Annotations: map[string]string{"tea.text": "comment deletion does not return a resource", "tea.confirm": "true"}, Args: argsExact(1), RunE: func(cmd *cobra.Command, args []string) error { remove := &cobra.Command{Use: "delete <comment-id>", Short: "Delete a comment", Example: fmt.Sprintf("$ tea %s comment delete 81 --yes", words.group), Annotations: map[string]string{"tea.text": "comment deletion does not return a resource", "tea.confirm": "true"}, Args: argsExact(1), RunE: func(cmd *cobra.Command, args []string) error {
id, err := number(args[0]) t, comment, err := a.comment(cmd, args[0], kind)
if err != nil {
return err
}
t, err := a.resolve(cmd.Context(), true)
if err != nil {
return err
}
comment, err := a.comment(cmd, t, id, kind)
if err != nil { if err != nil {
return err return err
} }
@@ -415,7 +392,6 @@ func commentArgs(kind refKind) cobra.PositionalArgs {
} }
} }
// completeDiscussions completes the open issues or pull requests that a command of kind takes.
func (a *application) completeDiscussions(kind refKind) cobra.CompletionFunc { func (a *application) completeDiscussions(kind refKind) cobra.CompletionFunc {
if kind == pullRef { if kind == pullRef {
return firstArg(a.completePulls("open")) return firstArg(a.completePulls("open"))
-1
View File
@@ -24,7 +24,6 @@ func (a *application) discussionSearchCommand(pulls bool) *cobra.Command {
var updated timeRange var updated timeRange
opts.Pulls = pulls opts.Pulls = pulls
example := fmt.Sprintf(`$ tea search %[1]s "login error" example := fmt.Sprintf(`$ tea search %[1]s "login error"
$ tea search %[1]s --author @me --state all
# Search one repository; its labels and milestones are checked # Search one repository; its labels and milestones are checked
$ tea search %[1]s crash -R alice/project --label bug --milestone v1.0`, noun) $ tea search %[1]s crash -R alice/project --label bug --milestone v1.0`, noun)
if pulls { if pulls {
+1 -4
View File
@@ -37,12 +37,10 @@ type discussionComment struct {
meta [2]string meta [2]string
} }
// stateBadge shows a discussion state, such as Open or Merged, in its color.
func stateBadge(state string) terminal.Cell { func stateBadge(state string) terminal.Cell {
return terminal.Cell{Text: titleCase(state), Color: stateColors[state]} return terminal.Cell{Text: titleCase(state), Color: stateColors[state]}
} }
// labelCells shows labels in their colors.
func labelCells(labels []services.Label) []terminal.Cell { func labelCells(labels []services.Label) []terminal.Cell {
cells := make([]terminal.Cell, len(labels)) cells := make([]terminal.Cell, len(labels))
for i, label := range labels { for i, label := range labels {
@@ -151,7 +149,6 @@ func pullView(item services.Pull, repo string, details pullDetails) discussionVi
if checks != "" { if checks != "" {
checksHint = fmt.Sprintf("%s (tea pr checks %d)", checks, item.Number) checksHint = fmt.Sprintf("%s (tea pr checks %d)", checks, item.Number)
} }
reviews := details.reviews
return discussionView{ return discussionView{
number: item.Number, comments: item.Comments, summary: summary, number: item.Number, comments: item.Comments, summary: summary,
view: terminal.View{ view: terminal.View{
@@ -176,7 +173,7 @@ func pullView(item services.Pull, repo string, details pullDetails) discussionVi
} }
}, },
extraComments: func(cmd *cobra.Command, t target, n int64) ([]discussionComment, error) { extraComments: func(cmd *cobra.Command, t target, n int64) ([]discussionComment, error) {
return pullReviewComments(cmd, t, n, reviews) return pullReviewComments(cmd, t, n, details.reviews)
}, },
} }
} }
+14 -18
View File
@@ -48,7 +48,6 @@ func (a *application) issueAttachmentCommand(kind refKind) *cobra.Command {
group := &cobra.Command{Use: "attachment", Short: "Manage the attachments of " + kind.noun() + "s", Long: fmt.Sprintf(`List, view, upload, download, rename and delete the files attached to %s. Attachments group := &cobra.Command{Use: "attachment", Short: "Manage the attachments of " + kind.noun() + "s", Long: fmt.Sprintf(`List, view, upload, download, rename and delete the files attached to %s. Attachments
are addressed by the %s number and the attachment ID that attachment list shows.`, kind.article(), kind.noun())} are addressed by the %s number and the attachment ID that attachment list shows.`, kind.article(), kind.noun())}
annotations := map[string]string{"tea.arguments": words.arguments} annotations := map[string]string{"tea.arguments": words.arguments}
// discussionTarget resolves the issue or pull request argument of an attachment command.
discussionTarget := func(cmd *cobra.Command, raw string) (target, int64, error) { discussionTarget := func(cmd *cobra.Command, raw string) (target, int64, error) {
t, n, err := a.resolveRef(cmd, raw, kind) t, n, err := a.resolveRef(cmd, raw, kind)
if err == nil && kind == pullRef { if err == nil && kind == pullRef {
@@ -56,6 +55,15 @@ are addressed by the %s number and the attachment ID that attachment list shows.
} }
return t, n, err return t, n, err
} }
// attachmentArgs checks the attachment ID before any request.
attachmentArgs := func(cmd *cobra.Command, args []string) (target, int64, int64, error) {
id, err := number(args[1])
if err != nil {
return target{}, 0, 0, err
}
t, n, err := discussionTarget(cmd, args[0])
return t, n, id, err
}
completeFirst := a.completeDiscussions(kind) completeFirst := a.completeDiscussions(kind)
var limit, page int var limit, page int
@@ -77,7 +85,7 @@ are addressed by the %s number and the attachment ID that attachment list shows.
pageFlags(list, &limit, &page) pageFlags(list, &limit, &page)
view := &cobra.Command{Use: "view {<number> | <url>} <attachment-id>", Short: "View an attachment", Example: fmt.Sprintf("$ tea %s attachment view %s 9", words.group, words.example), Annotations: annotations, Args: argsExact(2), ValidArgsFunction: completeFirst, RunE: func(cmd *cobra.Command, args []string) error { view := &cobra.Command{Use: "view {<number> | <url>} <attachment-id>", Short: "View an attachment", Example: fmt.Sprintf("$ tea %s attachment view %s 9", words.group, words.example), Annotations: annotations, Args: argsExact(2), ValidArgsFunction: completeFirst, RunE: func(cmd *cobra.Command, args []string) error {
t, n, id, err := a.attachmentArgs(cmd, args, discussionTarget) t, n, id, err := attachmentArgs(cmd, args)
if err != nil { if err != nil {
return err return err
} }
@@ -121,7 +129,7 @@ $ tea %[1]s attachment upload %[2]s screenshot.png --name before.png`, words.gro
if strings.TrimSpace(newName) == "" { if strings.TrimSpace(newName) == "" {
return clierr.Wrap(clierr.Usage, errors.New("supply a nonempty --name")) return clierr.Wrap(clierr.Usage, errors.New("supply a nonempty --name"))
} }
t, n, id, err := a.attachmentArgs(cmd, args, discussionTarget) t, n, id, err := attachmentArgs(cmd, args)
if err != nil { if err != nil {
return err return err
} }
@@ -135,7 +143,7 @@ $ tea %[1]s attachment upload %[2]s screenshot.png --name before.png`, words.gro
edit.Flags().StringVar(&newName, "name", "", "New file `name`") edit.Flags().StringVar(&newName, "name", "", "New file `name`")
remove := &cobra.Command{Use: "delete {<number> | <url>} <attachment-id>", Short: "Delete an attachment", Example: fmt.Sprintf("$ tea %s attachment delete %s 9 --yes", words.group, words.example), Annotations: map[string]string{"tea.arguments": words.arguments, "tea.confirm": "true", "tea.text": "attachment deletion does not return a resource"}, Args: argsExact(2), ValidArgsFunction: completeFirst, RunE: func(cmd *cobra.Command, args []string) error { remove := &cobra.Command{Use: "delete {<number> | <url>} <attachment-id>", Short: "Delete an attachment", Example: fmt.Sprintf("$ tea %s attachment delete %s 9 --yes", words.group, words.example), Annotations: map[string]string{"tea.arguments": words.arguments, "tea.confirm": "true", "tea.text": "attachment deletion does not return a resource"}, Args: argsExact(2), ValidArgsFunction: completeFirst, RunE: func(cmd *cobra.Command, args []string) error {
t, n, id, err := a.attachmentArgs(cmd, args, discussionTarget) t, n, id, err := attachmentArgs(cmd, args)
if err != nil { if err != nil {
return err return err
} }
@@ -144,8 +152,7 @@ $ tea %[1]s attachment upload %[2]s screenshot.png --name before.png`, words.gro
if err != nil { if err != nil {
return err return err
} }
name := attachmentName(t, n, item) return a.deleteConfirmed(cmd, attachmentName(t, n, item), func() error { return service.DeleteAttachment(cmd.Context(), n, id) })
return a.deleteConfirmed(cmd, name, func() error { return service.DeleteAttachment(cmd.Context(), n, id) })
}} }}
var destination downloadFlags var destination downloadFlags
@@ -155,7 +162,7 @@ skips the download.`, Example: fmt.Sprintf(`$ tea %[1]s attachment download %[2]
$ tea %[1]s attachment download %[2]s 9 --output crash.log --clobber $ tea %[1]s attachment download %[2]s 9 --output crash.log --clobber
# Write the file to standard output # Write the file to standard output
$ tea %[1]s attachment download %[2]s 9 -O - | less`, words.group, words.example), Annotations: map[string]string{"tea.arguments": words.arguments, "tea.text": "attachment downloads are binary"}, Args: argsExact(2), ValidArgsFunction: completeFirst, RunE: func(cmd *cobra.Command, args []string) error { $ tea %[1]s attachment download %[2]s 9 -O - | less`, words.group, words.example), Annotations: map[string]string{"tea.arguments": words.arguments, "tea.text": "attachment downloads are binary"}, Args: argsExact(2), ValidArgsFunction: completeFirst, RunE: func(cmd *cobra.Command, args []string) error {
t, n, id, err := a.attachmentArgs(cmd, args, discussionTarget) t, n, id, err := attachmentArgs(cmd, args)
if err != nil { if err != nil {
return err return err
} }
@@ -184,14 +191,3 @@ $ tea %[1]s attachment download %[2]s 9 -O - | less`, words.group, words.example
group.AddCommand(list, view, upload, edit, remove, download) group.AddCommand(list, view, upload, edit, remove, download)
return group return group
} }
// attachmentArgs resolves the issue or pull request and attachment ID arguments of an attachment
// command, checking the ID before any request.
func (a *application) attachmentArgs(cmd *cobra.Command, args []string, resolve func(*cobra.Command, string) (target, int64, error)) (target, int64, int64, error) {
id, err := number(args[1])
if err != nil {
return target{}, 0, 0, err
}
t, n, err := resolve(cmd, args[0])
return t, n, id, err
}
+2 -6
View File
@@ -14,7 +14,6 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
// errNothingToEdit is the shared answer to edit commands without anything to change.
var errNothingToEdit = clierr.Wrap(clierr.Usage, errors.New("nothing to edit; pass a flag such as --title (see --help)")) var errNothingToEdit = clierr.Wrap(clierr.Usage, errors.New("nothing to edit; pass a flag such as --title (see --help)"))
func (a *application) issueEditCommand() *cobra.Command { func (a *application) issueEditCommand() *cobra.Command {
@@ -25,16 +24,14 @@ func (a *application) issueEditCommand() *cobra.Command {
starting from its current values. Label and assignee flags add to or remove from each issue's starting from its current values. Label and assignee flags add to or remove from each issue's
current values; the --set flags replace them.`, Example: `$ tea issue edit 12 --title "Crash on start" --add-label bug current values; the --set flags replace them.`, Example: `$ tea issue edit 12 --title "Crash on start" --add-label bug
$ tea issue edit 12 13 --add-assignee @me --milestone v1.0 $ tea issue edit 12 13 --add-assignee @me --milestone v1.0
$ tea issue edit 12 --remove-label wontfix --remove-milestone $ tea issue edit 12 --remove-label wontfix --remove-milestone`, Annotations: map[string]string{"tea.arguments": issueArguments}, Args: usageArgs(cobra.MinimumNArgs(1)), RunE: func(cmd *cobra.Command, args []string) (runErr error) {
$ tea issue edit 12 --body-file notes.md`, Annotations: map[string]string{"tea.arguments": issueArguments}, Args: usageArgs(cobra.MinimumNArgs(1)), RunE: func(cmd *cobra.Command, args []string) (runErr error) {
var draft terminal.Draft var draft terminal.Draft
defer finishDraft(&draft, &runErr) defer finishDraft(&draft, &runErr)
if err := validateBodySources(cmd); err != nil { if err := validateBodySources(cmd); err != nil {
return err return err
} }
prompt := a.canPrompt()
if !localFlagsChanged(cmd) { if !localFlagsChanged(cmd) {
if !prompt || a.deps.IssueEditForm == nil { if !a.canPrompt() || a.deps.IssueEditForm == nil {
return errNothingToEdit return errNothingToEdit
} }
target, numbers, err := a.resolveRefs(cmd, args, issueRef) target, numbers, err := a.resolveRefs(cmd, args, issueRef)
@@ -128,7 +125,6 @@ $ tea issue edit 12 --body-file notes.md`, Annotations: map[string]string{"tea.a
return cmd return cmd
} }
// editedIssue words the done line of an issue edit.
func editedIssue(t target) func(services.Issue) string { func editedIssue(t target) func(services.Issue) string {
return func(item services.Issue) string { return func(item services.Issue) string {
return "Edited " + discussionNoun(item.Kind) + " " + refLabel(t.Repo, item.Number, item.Title) return "Edited " + discussionNoun(item.Kind) + " " + refLabel(t.Repo, item.Number, item.Title)
+4 -13
View File
@@ -20,7 +20,6 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
// issueArguments is the ARGUMENTS help of commands that take issue references.
const issueArguments = `An issue can be supplied as argument in any of the following formats: const issueArguments = `An issue can be supplied as argument in any of the following formats:
- by number, e.g. "123" or "#123" - by number, e.g. "123" or "#123"
- by URL, e.g. "https://gitea.com/OWNER/REPO/issues/123" - by URL, e.g. "https://gitea.com/OWNER/REPO/issues/123"
@@ -44,8 +43,7 @@ func (a *application) issueListCommand() *cobra.Command {
Labels and milestones must exist in the repository.`, Example: `$ tea issue list --label bug --state all Labels and milestones must exist in the repository.`, Example: `$ tea issue list --label bug --state all
$ tea issue list --assignee @me --json number,title $ tea issue list --assignee @me --json number,title
# Issues and pull requests in a milestone, as tab-separated values # Issues and pull requests in a milestone, as tab-separated values
$ tea issue list --kind all --milestone v1.0 | cut -f1,2 $ tea issue list --kind all --milestone v1.0 | cut -f1,2`, Args: argsExact(0), RunE: func(cmd *cobra.Command, _ []string) error {
$ tea issue list --search "login" --web`, Args: argsExact(0), RunE: func(cmd *cobra.Command, _ []string) error {
var err error var err error
if opts.Since, opts.Before, err = updated.parse(a.deps.Now()); err != nil { if opts.Since, opts.Before, err = updated.parse(a.deps.Now()); err != nil {
return err return err
@@ -194,13 +192,9 @@ func (a *application) issueCreateCommand() *cobra.Command {
var useEditor, web bool var useEditor, web bool
var assignees, labelNames, attachmentPaths []string var assignees, labelNames, attachmentPaths []string
cmd := &cobra.Command{Use: "create", Aliases: []string{"new"}, Short: "Create an issue", Long: `Create an issue in a repository. Without flags on a terminal, a form asks for the title and cmd := &cobra.Command{Use: "create", Aliases: []string{"new"}, Short: "Create an issue", Long: `Create an issue in a repository. Without flags on a terminal, a form asks for the title and
body, then optionally for assignees, a milestone, labels and a due date. The new issue's URL body, then optionally for assignees, a milestone, labels and a due date.`, Example: `$ tea issue create
is printed, so scripts can capture it.`, Example: `$ tea issue create
$ tea issue create --title "Crash on start" --body "Steps to reproduce…" --label bug --milestone v1.0 $ tea issue create --title "Crash on start" --body "Steps to reproduce…" --label bug --milestone v1.0
$ tea issue create -t "Update docs" -F notes.md -a @me $ tea issue create --template "Bug report"`, Args: argsExact(0), RunE: func(cmd *cobra.Command, _ []string) (runErr error) {
# Start from a repository issue template, or continue in the browser
$ tea issue create --template "Bug report"
$ tea issue create --title "Idea" --web`, Args: argsExact(0), RunE: func(cmd *cobra.Command, _ []string) (runErr error) {
var draft terminal.Draft var draft terminal.Draft
defer finishDraft(&draft, &runErr) defer finishDraft(&draft, &runErr)
if err := validateBodySources(cmd); err != nil { if err := validateBodySources(cmd); err != nil {
@@ -230,8 +224,7 @@ $ tea issue create --title "Idea" --web`, Args: argsExact(0), RunE: func(cmd *co
return err return err
} }
} }
prompt := a.canPrompt() if !localFlagsChanged(cmd) && a.canPrompt() && a.deps.IssueForm != nil {
if !localFlagsChanged(cmd) && prompt && a.deps.IssueForm != nil {
return a.createIssueInteractively(cmd) return a.createIssueInteractively(cmd)
} }
var target target var target target
@@ -461,7 +454,6 @@ func milestoneFlag(cmd *cobra.Command, value string) (selected string, remove bo
return strings.TrimSpace(value), false, nil return strings.TrimSpace(value), false, nil
} }
// milestoneID resolves a milestone title or ID in the target repository.
func milestoneID(cmd *cobra.Command, t target, raw string) (int64, error) { func milestoneID(cmd *cobra.Command, t target, raw string) (int64, error) {
milestone, err := (services.Milestones{Client: t.client, Repository: t.Repo}).FindOne(cmd.Context(), raw, false) milestone, err := (services.Milestones{Client: t.client, Repository: t.Repo}).FindOne(cmd.Context(), raw, false)
return milestone.ID, err return milestone.ID, err
@@ -539,7 +531,6 @@ func (f *metadataFlags) register(cmd *cobra.Command) {
flags.StringSliceVar(&f.setAssignees, "set-assignees", nil, "Replace all assignees; an empty value clears them") flags.StringSliceVar(&f.setAssignees, "set-assignees", nil, "Replace all assignees; an empty value clears them")
} }
// completeMetadataFlags completes the label and assignee flags of metadataFlags.
func (a *application) completeMetadataFlags(cmd *cobra.Command) { func (a *application) completeMetadataFlags(cmd *cobra.Command) {
for _, name := range []string{"add-label", "remove-label", "set-labels"} { for _, name := range []string{"add-label", "remove-label", "set-labels"} {
_ = cmd.RegisterFlagCompletionFunc(name, a.completeLabels()) _ = cmd.RegisterFlagCompletionFunc(name, a.completeLabels())
+7 -20
View File
@@ -23,7 +23,6 @@ type labelScope func(*cobra.Command) (services.Labels, error)
// labelColors are the colors of new labels created without --color, from Gitea's default labels. // labelColors are the colors of new labels created without --color, from Gitea's default labels.
var labelColors = []string{"ee0701", "fbca04", "84b6eb", "cc317c", "c2e0c6", "0e8a16", "1d76db", "5319e7", "d4c5f9", "e99695", "bfd4f2", "006b75"} var labelColors = []string{"ee0701", "fbca04", "84b6eb", "cc317c", "c2e0c6", "0e8a16", "1d76db", "5319e7", "d4c5f9", "e99695", "bfd4f2", "006b75"}
// labelArguments is the ARGUMENTS help of commands that take labels.
const labelArguments = `A label can be supplied by its name, such as "bug" or "kind/feature", or by its ID. A const labelArguments = `A label can be supplied by its name, such as "bug" or "kind/feature", or by its ID. A
number is an ID first, then a name.` number is an ID first, then a name.`
@@ -90,7 +89,6 @@ func labelName(s services.Labels, item services.Label) string {
return fmt.Sprintf("label %q in %s", item.Name, s.Scope()) return fmt.Sprintf("label %q in %s", item.Name, s.Scope())
} }
// labelFields are the fields of label create and edit.
type labelFields struct { type labelFields struct {
name, color, description string name, color, description string
exclusive, archived bool exclusive, archived bool
@@ -184,7 +182,12 @@ $ tea label create bug -c ee0701 --force`, Args: usageArgs(cobra.MaximumNArgs(1)
if err != nil { if err != nil {
return forceHint(err) return forceHint(err)
} }
return a.reportLabel(cmd, s, item, updated) verb := "Created"
if updated {
verb = "Updated"
}
a.done(cmd, "%s %s", verb, labelName(s, item))
return a.jsonResult(cmd, item)
}} }}
fields.register(cmd, "Label `name`") fields.register(cmd, "Label `name`")
_ = cmd.Flags().MarkHidden("name") _ = cmd.Flags().MarkHidden("name")
@@ -228,16 +231,6 @@ func forceHint(err error) error {
return err return err
} }
// reportLabel reports a created or updated label as JSON or with a ✓ line.
func (a *application) reportLabel(cmd *cobra.Command, s services.Labels, item services.Label, updated bool) error {
verb := "Created"
if updated {
verb = "Updated"
}
a.done(cmd, "%s %s", verb, labelName(s, item))
return a.jsonResult(cmd, item)
}
func (a *application) labelEditCommand(scope labelScope, complete cobra.CompletionFunc) *cobra.Command { func (a *application) labelEditCommand(scope labelScope, complete cobra.CompletionFunc) *cobra.Command {
var fields labelFields var fields labelFields
var byName bool var byName bool
@@ -317,7 +310,6 @@ $ tea label export --repo alice/old | tea label import - --repo alice/new`, Args
return cmd return cmd
} }
// importLabels creates the labels of a label file, or of stdin for "-".
func (a *application) importLabels(cmd *cobra.Command, scope labelScope, file string, force bool) error { func (a *application) importLabels(cmd *cobra.Command, scope labelScope, file string, force bool) error {
data, err := readFileOrStdin(cmd.InOrStdin(), file) data, err := readFileOrStdin(cmd.InOrStdin(), file)
if err != nil { if err != nil {
@@ -331,12 +323,7 @@ func (a *application) importLabels(cmd *cobra.Command, scope labelScope, file st
if err != nil { if err != nil {
return err return err
} }
return a.createLabels(cmd, s, rows, force) // Each row is created, or updated with force, continuing past failures.
}
// createLabels creates each label in rows, continuing past failures. A label that exists already
// is updated with force and fails otherwise.
func (a *application) createLabels(cmd *cobra.Command, s services.Labels, rows []services.LabelOptions, force bool) error {
existing, err := s.All(cmd.Context()) existing, err := s.All(cmd.Context())
if err != nil { if err != nil {
return err return err
+3 -9
View File
@@ -18,7 +18,6 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
// milestoneArguments is the ARGUMENTS help of commands that take milestones.
const milestoneArguments = `A milestone can be supplied by its title, such as "v1.0", or by its ID. A number is an ID const milestoneArguments = `A milestone can be supplied by its title, such as "v1.0", or by its ID. A number is an ID
first, then a title; --by-name matches it as a title only.` first, then a title; --by-name matches it as a title only.`
@@ -164,7 +163,6 @@ $ tea milestone view 3 --web`, Annotations: map[string]string{"tea.arguments": m
return cmd return cmd
} }
// milestoneFields are the fields of milestone create and edit.
type milestoneFields struct { type milestoneFields struct {
title, description, descriptionFile, deadline string title, description, descriptionFile, deadline string
useEditor bool useEditor bool
@@ -211,7 +209,6 @@ func (a *application) milestoneOptions(cmd *cobra.Command, f *milestoneFields) (
return opts, nil return opts, nil
} }
// formOptions turns milestone form values into options.
func formOptions(values terminal.MilestoneFormValues) services.MilestoneOptions { func formOptions(values terminal.MilestoneFormValues) services.MilestoneOptions {
deadline := time.Time{} deadline := time.Time{}
if values.Deadline != nil { if values.Deadline != nil {
@@ -225,7 +222,7 @@ func (a *application) milestoneCreateCommand(service milestoneService) *cobra.Co
var fields milestoneFields var fields milestoneFields
var state string var state string
cmd := &cobra.Command{Use: "create [<title>]", Aliases: []string{"new"}, Short: "Create a milestone", Long: `Create a milestone. Without flags on a terminal, a form asks for the title, description cmd := &cobra.Command{Use: "create [<title>]", Aliases: []string{"new"}, Short: "Create a milestone", Long: `Create a milestone. Without flags on a terminal, a form asks for the title, description
and due date. The new milestone's URL is printed, so scripts can capture it.`, Example: `$ tea milestone create v1.0 --deadline 2026-12-31 --description "First release" and due date.`, Example: `$ tea milestone create v1.0 --deadline 2026-12-31 --description "First release"
$ tea milestone create --title v2.0 --editor`, Args: usageArgs(cobra.MaximumNArgs(1)), RunE: func(cmd *cobra.Command, args []string) (runErr error) { $ tea milestone create --title v2.0 --editor`, Args: usageArgs(cobra.MaximumNArgs(1)), RunE: func(cmd *cobra.Command, args []string) (runErr error) {
if err := validateTextSources(cmd, "description", "description-file"); err != nil { if err := validateTextSources(cmd, "description", "description-file"); err != nil {
return err return err
@@ -238,12 +235,11 @@ $ tea milestone create --title v2.0 --editor`, Args: usageArgs(cobra.MaximumNArg
return err return err
} }
} }
prompt := a.canPrompt()
var opts services.MilestoneOptions var opts services.MilestoneOptions
var draft terminal.Draft var draft terminal.Draft
var err error var err error
defer finishDraft(&draft, &runErr) defer finishDraft(&draft, &runErr)
if !localFlagsChanged(cmd) && prompt && a.deps.MilestoneForm != nil { if !localFlagsChanged(cmd) && a.canPrompt() && a.deps.MilestoneForm != nil {
values, err := a.deps.MilestoneForm.EditMilestone(cmd.Context(), "", terminal.MilestoneFormValues{}) values, err := a.deps.MilestoneForm.EditMilestone(cmd.Context(), "", terminal.MilestoneFormValues{})
if err != nil { if err != nil {
return err return err
@@ -291,8 +287,7 @@ $ tea milestone edit 3 --editor`, Annotations: map[string]string{"tea.arguments"
if err := validateTextSources(cmd, "description", "description-file"); err != nil { if err := validateTextSources(cmd, "description", "description-file"); err != nil {
return err return err
} }
prompt := a.canPrompt() form := !localFlagsChanged(cmd) && a.canPrompt() && a.deps.MilestoneForm != nil
form := !localFlagsChanged(cmd) && prompt && a.deps.MilestoneForm != nil
if !form && !localFlagsChanged(cmd) { if !form && !localFlagsChanged(cmd) {
return errNothingToEdit return errNothingToEdit
} }
@@ -370,7 +365,6 @@ $ tea milestone delete 7 --yes`, Annotations: map[string]string{"tea.confirm": "
return cmd return cmd
} }
// findMilestone resolves a milestone argument, a title or an ID.
func findMilestone(cmd *cobra.Command, s services.Milestones, raw string, byName bool) (services.Milestone, error) { func findMilestone(cmd *cobra.Command, s services.Milestones, raw string, byName bool) (services.Milestone, error) {
if strings.TrimSpace(raw) == "" { if strings.TrimSpace(raw) == "" {
return services.Milestone{}, clierr.Wrap(clierr.Usage, errors.New("milestone title or ID cannot be empty")) return services.Milestone{}, clierr.Wrap(clierr.Usage, errors.New("milestone title or ID cannot be empty"))
+5 -5
View File
@@ -220,8 +220,7 @@ $ tea pr create
# Take the title and body from the commits, without questions # Take the title and body from the commits, without questions
$ tea pr create --fill $ tea pr create --fill
$ tea pr create -t "Add login" -b "Closes #1" -B develop -r carol -l enhancement $ tea pr create -t "Add login" -b "Closes #1" -B develop -r carol -l enhancement`
$ tea pr create --draft --fill`
return create return create
} }
@@ -316,13 +315,14 @@ func (a *application) createPullInteractively(cmd *cobra.Command, snapshot setti
if err != nil { if err != nil {
return err return err
} }
selectables, err := (services.Pulls{Client: target.client, Repository: target.Repo}).CreateSelectables(cmd.Context()) service := services.Pulls{Client: target.client, Repository: target.Repo}
selectables, err := service.CreateSelectables(cmd.Context())
if err != nil { if err != nil {
return err return err
} }
baseOwner, _, _ := strings.Cut(target.Repo, "/") baseOwner, _, _ := strings.Cut(target.Repo, "/")
headOwner, headBranch := a.defaultPullHead(cmd.Context()) headOwner, headBranch := a.defaultPullHead(cmd.Context())
template, err := (services.Pulls{Client: target.client, Repository: target.Repo}).Template(cmd.Context(), repo.DefaultBranch) template, err := service.Template(cmd.Context(), repo.DefaultBranch)
if err != nil { if err != nil {
return err return err
} }
@@ -361,7 +361,7 @@ func (a *application) createPullInteractively(cmd *cobra.Command, snapshot setti
if err := a.ensureHeadPublished(cmd, target, snapshot, options.Head); err != nil { if err := a.ensureHeadPublished(cmd, target, snapshot, options.Head); err != nil {
return err return err
} }
item, err := (services.Pulls{Client: target.client, Repository: target.Repo}).Create(cmd.Context(), options) item, err := service.Create(cmd.Context(), options)
if err != nil { if err != nil {
return err return err
} }
-2
View File
@@ -71,7 +71,6 @@ func (a *application) addPullMutations(group *cobra.Command) {
group.AddCommand(a.pullCreateCommand(), a.pullStateCommand(true), a.pullStateCommand(false), a.pullDiffCommand(), a.pullReviewCommand(), a.pullMergeCommand()) group.AddCommand(a.pullCreateCommand(), a.pullStateCommand(true), a.pullStateCommand(false), a.pullDiffCommand(), a.pullReviewCommand(), a.pullMergeCommand())
} }
// pullStateCommand builds tea pr close, or tea pr reopen when closing is false.
func (a *application) pullStateCommand(closing bool) *cobra.Command { func (a *application) pullStateCommand(closing bool) *cobra.Command {
verb, done, state := "reopen", "Reopened", "open" verb, done, state := "reopen", "Reopened", "open"
if closing { if closing {
@@ -252,7 +251,6 @@ var mergeVerbs = map[string]string{
"merge": "Merged", "rebase": "Rebased and merged", "rebase-merge": "Rebased and merged", "squash": "Squashed and merged", "fast-forward-only": "Merged", "merge": "Merged", "rebase": "Rebased and merged", "rebase-merge": "Rebased and merged", "squash": "Squashed and merged", "fast-forward-only": "Merged",
} }
// mergeMethods are the merge styles in Gitea's order.
var mergeMethods = []string{"merge", "rebase", "rebase-merge", "squash", "fast-forward-only"} var mergeMethods = []string{"merge", "rebase", "rebase-merge", "squash", "fast-forward-only"}
func (a *application) pullMergeCommand() *cobra.Command { func (a *application) pullMergeCommand() *cobra.Command {
+2 -15
View File
@@ -19,7 +19,6 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
// pullArguments documents the pull request argument of pr commands in their ARGUMENTS section.
const pullArguments = `A pull request can be supplied as argument in any of the following formats: const pullArguments = `A pull request can be supplied as argument in any of the following formats:
- by number, such as "13" or "#13" - by number, such as "13" or "#13"
- by URL, such as "https://gitea.com/OWNER/REPO/pulls/13" - by URL, such as "https://gitea.com/OWNER/REPO/pulls/13"
@@ -27,7 +26,6 @@ const pullArguments = `A pull request can be supplied as argument in any of the
Without an argument, the pull request of the current branch is selected.` Without an argument, the pull request of the current branch is selected.`
// pullArgumentsAnnotation adds the ARGUMENTS section to a pr command's help.
func pullArgumentsAnnotation(annotations map[string]string) map[string]string { func pullArgumentsAnnotation(annotations map[string]string) map[string]string {
if annotations == nil { if annotations == nil {
annotations = map[string]string{} annotations = map[string]string{}
@@ -43,7 +41,6 @@ var reviewStates = map[string]string{
"comment": "COMMENT", "pending": "PENDING", "comment": "COMMENT", "pending": "PENDING",
} }
// reviewType holds the flags that choose the type of a review.
type reviewType struct { type reviewType struct {
approve, requestChanges, comment, pending bool approve, requestChanges, comment, pending bool
// event is the hidden spelling of the type that tea 0.x and scripts use. // event is the hidden spelling of the type that tea 0.x and scripts use.
@@ -138,12 +135,6 @@ $ tea pr review --approve
# Request changes on #13 # Request changes on #13
$ tea pr review 13 -r -b "Please hash the password" $ tea pr review 13 -r -b "Please hash the password"
# Comment with a body read from a file
$ tea pr review 13 -c -F notes.md
# Review interactively: pick the type, write the body, optionally annotate the diff
$ tea pr review 13
# Start a pending review with inline comments, then submit it # Start a pending review with inline comments, then submit it
$ tea pr review 13 --pending --comments-file inline.json $ tea pr review 13 --pending --comments-file inline.json
$ tea pr review submit 13 --approve` $ tea pr review submit 13 --approve`
@@ -152,9 +143,8 @@ func (a *application) pullReviewCommand() *cobra.Command {
review := a.reviewCreateCommand("review [<number> | <url> | <branch>]", "Review a pull request or manage its reviews") review := a.reviewCreateCommand("review [<number> | <url> | <branch>]", "Review a pull request or manage its reviews")
review.Long = `Submit a review on a pull request, or manage existing reviews with the commands below. review.Long = `Submit a review on a pull request, or manage existing reviews with the commands below.
Without an argument, the pull request of the current branch is reviewed. Choose the review Choose the review type with --approve, --request-changes or --comment. On a terminal, leaving
type with --approve, --request-changes or --comment. On a terminal, leaving the type out the type out opens an interactive review that also offers to annotate the diff in your editor.`
opens an interactive review that also offers to annotate the diff in your editor.`
review.Example = reviewExamples review.Example = reviewExamples
review.SuggestionsMinimumDistance = 2 review.SuggestionsMinimumDistance = 2
create := a.reviewCreateCommand("create [<number> | <url> | <branch>]", `Submit a review (same as "tea pr review")`) create := a.reviewCreateCommand("create [<number> | <url> | <branch>]", `Submit a review (same as "tea pr review")`)
@@ -169,7 +159,6 @@ $ tea pr review create --comment -b "Looks good overall"`
return review return review
} }
// reviewCreateCommand builds tea pr review and its explicit form tea pr review create.
func (a *application) reviewCreateCommand(use, short string) *cobra.Command { func (a *application) reviewCreateCommand(use, short string) *cobra.Command {
var kind reviewType var kind reviewType
var body, file, commentsFile, commit string var body, file, commentsFile, commit string
@@ -263,7 +252,6 @@ func (a *application) reportReview(cmd *cobra.Command, t target, pull services.P
return a.created(cmd, item, item.URL) return a.created(cmd, item, item.URL)
} }
// readInlineComments reads the inline comments of --comments-file, a JSON array.
func readInlineComments(path string) ([]services.InlineComment, error) { func readInlineComments(path string) ([]services.InlineComment, error) {
if path == "" { if path == "" {
return nil, nil return nil, nil
@@ -296,7 +284,6 @@ func readInlineComments(path string) ([]services.InlineComment, error) {
return comments, nil return comments, nil
} }
// reviewInteractively asks for the review type, body and inline comments, then submits the review.
func (a *application) reviewInteractively(cmd *cobra.Command, args []string) error { func (a *application) reviewInteractively(cmd *cobra.Command, args []string) error {
t, n, err := a.pullSelector(cmd, args) t, n, err := a.pullSelector(cmd, args)
if err != nil { if err != nil {
+1 -7
View File
@@ -23,8 +23,6 @@ one select the pull request of the current branch.`}
group.Annotations = pullArgumentsAnnotation(nil) group.Annotations = pullArgumentsAnnotation(nil)
group.Example = `$ tea pr list --author @me group.Example = `$ tea pr list --author @me
$ tea pr create --fill $ tea pr create --fill
$ tea pr checkout 13
$ tea pr review --approve
$ tea pr merge --squash --delete-branch` $ tea pr merge --squash --delete-branch`
group.AddCommand(a.pullChecksCommand(), a.pullCheckoutCommand(), a.pullCleanCommand(), a.statusCommand(true)) group.AddCommand(a.pullChecksCommand(), a.pullCheckoutCommand(), a.pullCleanCommand(), a.statusCommand(true))
group.AddCommand(a.pullListCommand(), a.pullViewCommand()) group.AddCommand(a.pullListCommand(), a.pullViewCommand())
@@ -88,9 +86,8 @@ each pull request, which tea only makes when --json names them.`, Args: argsExac
if localFlagsChanged(cmd, "state", "limit", "page", "sort") { if localFlagsChanged(cmd, "state", "limit", "page", "sort") {
empty = "no " + noun + " match your filters in " + t.Repo empty = "no " + noun + " match your filters in " + t.Repo
} }
columns := []terminal.Column{{Header: "ID", Prefix: "#"}, {Header: "Title", Flex: true}, {Header: "Branch", Flex: true, Min: 12}, {Header: "State", PipedOnly: state == "open"}, {Header: "Author"}, {Header: "Created"}}
return writeTable(a, cmd, items, listSpec[services.Pull]{ return writeTable(a, cmd, items, listSpec[services.Pull]{
Columns: columns, Columns: []terminal.Column{{Header: "ID", Prefix: "#"}, {Header: "Title", Flex: true}, {Header: "Branch", Flex: true, Min: 12}, {Header: "State", PipedOnly: state == "open"}, {Header: "Author"}, {Header: "Created"}},
Row: func(item services.Pull) []terminal.Cell { Row: func(item services.Pull) []terminal.Cell {
state := pullState(item) state := pullState(item)
return []terminal.Cell{ return []terminal.Cell{
@@ -125,8 +122,6 @@ each pull request, which tea only makes when --json names them.`, Args: argsExac
_ = cmd.RegisterFlagCompletionFunc("milestone", a.completeMilestones()) _ = cmd.RegisterFlagCompletionFunc("milestone", a.completeMilestones())
cmd.Example = `$ tea pr list cmd.Example = `$ tea pr list
$ tea pr list --author @me --state all $ tea pr list --author @me --state all
$ tea pr list -l bug -B main --draft
$ tea pr list --state merged -L 10
$ tea pr list --json number,title,head --jq '.[] | select(.head == "feature/login")'` $ tea pr list --json number,title,head --jq '.[] | select(.head == "feature/login")'`
return cmd return cmd
} }
@@ -180,7 +175,6 @@ $ tea pr view
# View #13 with its comments and reviews # View #13 with its comments and reviews
$ tea pr view 13 --comments $ tea pr view 13 --comments
$ tea pr view https://gitea.com/owner/repo/pulls/13 --web
$ tea pr view feature/login --json number,state,review_decision` $ tea pr view feature/login --json number,state,review_decision`
return cmd return cmd
} }