diff --git a/internal/app/checkout.go b/internal/app/checkout.go index 28584326..f89db34e 100644 --- a/internal/app/checkout.go +++ b/internal/app/checkout.go @@ -75,12 +75,8 @@ var errNoMatchingRemote = errors.New("no Git remote matches the selected reposit func (a *application) pullCheckoutCommand() *cobra.Command { var branch string var detached, update bool - cmd := &cobra.Command{Use: "checkout { | | }", Aliases: []string{"co"}, Short: "Check out a pull request", Long: `Check out the head of a pull request in a local branch. - -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 { + cmd := &cobra.Command{Use: "checkout { | | }", 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 { if detached && (cmd.Flags().Changed("branch") || update) { 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.Example = `$ tea pr checkout 13 $ tea pr co feature/login -$ tea pr checkout 13 --branch review-13 $ tea pr checkout 13 --update` return cmd } diff --git a/internal/app/comments.go b/internal/app/comments.go index 4fd5e924..3e23eed5 100644 --- a/internal/app/comments.go +++ b/internal/app/comments.go @@ -16,7 +16,6 @@ import ( "github.com/spf13/cobra" ) -// commentTitle is the prompt for a comment body. const commentTitle = "Comment (markdown):" // 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) } -// commentFlags are the body flags of commands that write a comment. type commentFlags struct { body, file string 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) } -// 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) { items, err := (services.Comments{Client: t.client, Repository: t.Repo}).List(cmd.Context(), n) 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) } -// comment returns comment id, which a command of kind may change: pr commands refuse comments on -// issues. -func (a *application) comment(cmd *cobra.Command, t target, id int64, kind refKind) (services.Comment, error) { +// comment returns the comment that the ID argument names, which a command of kind may change: pr +// commands refuse comments on issues. +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) 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) } - return current, err + return t, current, err } // 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. func (a *application) deleteComment(cmd *cobra.Command, t target, comment services.Comment) error { - name := commentName(t, comment) - return a.deleteConfirmed(cmd, name, func() error { + return a.deleteConfirmed(cmd, commentName(t, comment), func() error { 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 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 --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 { if len(args) == 0 { 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) view := &cobra.Command{Use: "view ", 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]) - 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) + t, comment, err := a.comment(cmd, args[0], kind) if err != nil { 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 { return err } - id, err := number(args[0]) - 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) + t, comment, err := a.comment(cmd, args[0], kind) if err != nil { return err } @@ -382,15 +367,7 @@ $ tea %[1]s comment edit 81`, words.group), Args: argsExact(1), RunE: func(cmd * editFlags.register(edit) remove := &cobra.Command{Use: "delete ", 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]) - 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) + t, comment, err := a.comment(cmd, args[0], kind) if err != nil { 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 { if kind == pullRef { return firstArg(a.completePulls("open")) diff --git a/internal/app/discussion_search.go b/internal/app/discussion_search.go index 08487051..da769c22 100644 --- a/internal/app/discussion_search.go +++ b/internal/app/discussion_search.go @@ -24,7 +24,6 @@ func (a *application) discussionSearchCommand(pulls bool) *cobra.Command { var updated timeRange opts.Pulls = pulls 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 $ tea search %[1]s crash -R alice/project --label bug --milestone v1.0`, noun) if pulls { diff --git a/internal/app/discussion_view.go b/internal/app/discussion_view.go index 78f674b0..a65a09f0 100644 --- a/internal/app/discussion_view.go +++ b/internal/app/discussion_view.go @@ -37,12 +37,10 @@ type discussionComment struct { meta [2]string } -// stateBadge shows a discussion state, such as Open or Merged, in its color. func stateBadge(state string) terminal.Cell { return terminal.Cell{Text: titleCase(state), Color: stateColors[state]} } -// labelCells shows labels in their colors. func labelCells(labels []services.Label) []terminal.Cell { cells := make([]terminal.Cell, len(labels)) for i, label := range labels { @@ -151,7 +149,6 @@ func pullView(item services.Pull, repo string, details pullDetails) discussionVi if checks != "" { checksHint = fmt.Sprintf("%s (tea pr checks %d)", checks, item.Number) } - reviews := details.reviews return discussionView{ number: item.Number, comments: item.Comments, summary: summary, 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) { - return pullReviewComments(cmd, t, n, reviews) + return pullReviewComments(cmd, t, n, details.reviews) }, } } diff --git a/internal/app/issue_attachments.go b/internal/app/issue_attachments.go index 3c274c06..c2821db7 100644 --- a/internal/app/issue_attachments.go +++ b/internal/app/issue_attachments.go @@ -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 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} - // discussionTarget resolves the issue or pull request argument of an attachment command. discussionTarget := func(cmd *cobra.Command, raw string) (target, int64, error) { t, n, err := a.resolveRef(cmd, raw, kind) 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 } + // 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) 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) view := &cobra.Command{Use: "view { | } ", 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 { return err } @@ -121,7 +129,7 @@ $ tea %[1]s attachment upload %[2]s screenshot.png --name before.png`, words.gro if strings.TrimSpace(newName) == "" { 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 { 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`") remove := &cobra.Command{Use: "delete { | } ", 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 { return err } @@ -144,8 +152,7 @@ $ tea %[1]s attachment upload %[2]s screenshot.png --name before.png`, words.gro if err != nil { return err } - name := attachmentName(t, n, item) - return a.deleteConfirmed(cmd, name, func() error { return service.DeleteAttachment(cmd.Context(), n, id) }) + return a.deleteConfirmed(cmd, attachmentName(t, n, item), func() error { return service.DeleteAttachment(cmd.Context(), n, id) }) }} 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 # 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 { - t, n, id, err := a.attachmentArgs(cmd, args, discussionTarget) + t, n, id, err := attachmentArgs(cmd, args) if err != nil { 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) 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 -} diff --git a/internal/app/issue_edit.go b/internal/app/issue_edit.go index 5eaae88a..96875e3e 100644 --- a/internal/app/issue_edit.go +++ b/internal/app/issue_edit.go @@ -14,7 +14,6 @@ import ( "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)")) 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 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 --remove-label wontfix --remove-milestone -$ 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) { +$ 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) { var draft terminal.Draft defer finishDraft(&draft, &runErr) if err := validateBodySources(cmd); err != nil { return err } - prompt := a.canPrompt() if !localFlagsChanged(cmd) { - if !prompt || a.deps.IssueEditForm == nil { + if !a.canPrompt() || a.deps.IssueEditForm == nil { return errNothingToEdit } 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 } -// editedIssue words the done line of an issue edit. func editedIssue(t target) func(services.Issue) string { return func(item services.Issue) string { return "Edited " + discussionNoun(item.Kind) + " " + refLabel(t.Repo, item.Number, item.Title) diff --git a/internal/app/issues.go b/internal/app/issues.go index 70261653..cd34cf3e 100644 --- a/internal/app/issues.go +++ b/internal/app/issues.go @@ -20,7 +20,6 @@ import ( "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: - by number, e.g. "123" or "#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 $ tea issue list --assignee @me --json number,title # 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 --search "login" --web`, Args: argsExact(0), RunE: func(cmd *cobra.Command, _ []string) error { +$ tea issue list --kind all --milestone v1.0 | cut -f1,2`, Args: argsExact(0), RunE: func(cmd *cobra.Command, _ []string) error { var err error if opts.Since, opts.Before, err = updated.parse(a.deps.Now()); err != nil { return err @@ -194,13 +192,9 @@ func (a *application) issueCreateCommand() *cobra.Command { var useEditor, web bool 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 -body, then optionally for assignees, a milestone, labels and a due date. The new issue's URL -is printed, so scripts can capture it.`, Example: `$ tea issue create +body, then optionally for assignees, a milestone, labels and a due date.`, Example: `$ tea issue create $ 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 -# 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) { +$ tea issue create --template "Bug report"`, Args: argsExact(0), RunE: func(cmd *cobra.Command, _ []string) (runErr error) { var draft terminal.Draft defer finishDraft(&draft, &runErr) 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 } } - prompt := a.canPrompt() - if !localFlagsChanged(cmd) && prompt && a.deps.IssueForm != nil { + if !localFlagsChanged(cmd) && a.canPrompt() && a.deps.IssueForm != nil { return a.createIssueInteractively(cmd) } var target target @@ -461,7 +454,6 @@ func milestoneFlag(cmd *cobra.Command, value string) (selected string, remove bo 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) { milestone, err := (services.Milestones{Client: t.client, Repository: t.Repo}).FindOne(cmd.Context(), raw, false) 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") } -// completeMetadataFlags completes the label and assignee flags of metadataFlags. func (a *application) completeMetadataFlags(cmd *cobra.Command) { for _, name := range []string{"add-label", "remove-label", "set-labels"} { _ = cmd.RegisterFlagCompletionFunc(name, a.completeLabels()) diff --git a/internal/app/labels.go b/internal/app/labels.go index cb8e9453..e77509e9 100644 --- a/internal/app/labels.go +++ b/internal/app/labels.go @@ -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. 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 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()) } -// labelFields are the fields of label create and edit. type labelFields struct { name, color, description string exclusive, archived bool @@ -184,7 +182,12 @@ $ tea label create bug -c ee0701 --force`, Args: usageArgs(cobra.MaximumNArgs(1) if err != nil { 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`") _ = cmd.Flags().MarkHidden("name") @@ -228,16 +231,6 @@ func forceHint(err error) error { 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 { var fields labelFields var byName bool @@ -317,7 +310,6 @@ $ tea label export --repo alice/old | tea label import - --repo alice/new`, Args 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 { data, err := readFileOrStdin(cmd.InOrStdin(), file) if err != nil { @@ -331,12 +323,7 @@ func (a *application) importLabels(cmd *cobra.Command, scope labelScope, file st if err != nil { return err } - return a.createLabels(cmd, s, rows, force) -} - -// 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 { + // Each row is created, or updated with force, continuing past failures. existing, err := s.All(cmd.Context()) if err != nil { return err diff --git a/internal/app/metadata.go b/internal/app/metadata.go index 0e3dcc30..547d49b8 100644 --- a/internal/app/metadata.go +++ b/internal/app/metadata.go @@ -18,7 +18,6 @@ import ( "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 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 } -// milestoneFields are the fields of milestone create and edit. type milestoneFields struct { title, description, descriptionFile, deadline string useEditor bool @@ -211,7 +209,6 @@ func (a *application) milestoneOptions(cmd *cobra.Command, f *milestoneFields) ( return opts, nil } -// formOptions turns milestone form values into options. func formOptions(values terminal.MilestoneFormValues) services.MilestoneOptions { deadline := time.Time{} if values.Deadline != nil { @@ -225,7 +222,7 @@ func (a *application) milestoneCreateCommand(service milestoneService) *cobra.Co var fields milestoneFields var state string cmd := &cobra.Command{Use: "create []", 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) { if err := validateTextSources(cmd, "description", "description-file"); err != nil { return err @@ -238,12 +235,11 @@ $ tea milestone create --title v2.0 --editor`, Args: usageArgs(cobra.MaximumNArg return err } } - prompt := a.canPrompt() var opts services.MilestoneOptions var draft terminal.Draft var err error 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{}) if err != nil { 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 { return err } - prompt := a.canPrompt() - form := !localFlagsChanged(cmd) && prompt && a.deps.MilestoneForm != nil + form := !localFlagsChanged(cmd) && a.canPrompt() && a.deps.MilestoneForm != nil if !form && !localFlagsChanged(cmd) { return errNothingToEdit } @@ -370,7 +365,6 @@ $ tea milestone delete 7 --yes`, Annotations: map[string]string{"tea.confirm": " 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) { if strings.TrimSpace(raw) == "" { return services.Milestone{}, clierr.Wrap(clierr.Usage, errors.New("milestone title or ID cannot be empty")) diff --git a/internal/app/pull_create.go b/internal/app/pull_create.go index 37861f0d..cf74284c 100644 --- a/internal/app/pull_create.go +++ b/internal/app/pull_create.go @@ -220,8 +220,7 @@ $ tea pr create # Take the title and body from the commits, without questions $ tea pr create --fill -$ tea pr create -t "Add login" -b "Closes #1" -B develop -r carol -l enhancement -$ tea pr create --draft --fill` +$ tea pr create -t "Add login" -b "Closes #1" -B develop -r carol -l enhancement` return create } @@ -316,13 +315,14 @@ func (a *application) createPullInteractively(cmd *cobra.Command, snapshot setti if err != nil { 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 { return err } baseOwner, _, _ := strings.Cut(target.Repo, "/") 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 { 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 { 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 { return err } diff --git a/internal/app/pull_mutations.go b/internal/app/pull_mutations.go index 599de686..79be53c0 100644 --- a/internal/app/pull_mutations.go +++ b/internal/app/pull_mutations.go @@ -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()) } -// pullStateCommand builds tea pr close, or tea pr reopen when closing is false. func (a *application) pullStateCommand(closing bool) *cobra.Command { verb, done, state := "reopen", "Reopened", "open" 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", } -// mergeMethods are the merge styles in Gitea's order. var mergeMethods = []string{"merge", "rebase", "rebase-merge", "squash", "fast-forward-only"} func (a *application) pullMergeCommand() *cobra.Command { diff --git a/internal/app/pull_review.go b/internal/app/pull_review.go index c40db618..7ce6049e 100644 --- a/internal/app/pull_review.go +++ b/internal/app/pull_review.go @@ -19,7 +19,6 @@ import ( "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: - by number, such as "13" or "#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.` -// pullArgumentsAnnotation adds the ARGUMENTS section to a pr command's help. func pullArgumentsAnnotation(annotations map[string]string) map[string]string { if annotations == nil { annotations = map[string]string{} @@ -43,7 +41,6 @@ var reviewStates = map[string]string{ "comment": "COMMENT", "pending": "PENDING", } -// reviewType holds the flags that choose the type of a review. type reviewType struct { approve, requestChanges, comment, pending bool // 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 $ 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 $ tea pr review 13 --pending --comments-file inline.json $ 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.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 -type with --approve, --request-changes or --comment. On a terminal, leaving the type out -opens an interactive review that also offers to annotate the diff in your editor.` +Choose the review type with --approve, --request-changes or --comment. On a terminal, leaving +the type out opens an interactive review that also offers to annotate the diff in your editor.` review.Example = reviewExamples review.SuggestionsMinimumDistance = 2 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 } -// reviewCreateCommand builds tea pr review and its explicit form tea pr review create. func (a *application) reviewCreateCommand(use, short string) *cobra.Command { var kind reviewType 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) } -// readInlineComments reads the inline comments of --comments-file, a JSON array. func readInlineComments(path string) ([]services.InlineComment, error) { if path == "" { return nil, nil @@ -296,7 +284,6 @@ func readInlineComments(path string) ([]services.InlineComment, error) { 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 { t, n, err := a.pullSelector(cmd, args) if err != nil { diff --git a/internal/app/pulls.go b/internal/app/pulls.go index 0a0990c3..8cf81061 100644 --- a/internal/app/pulls.go +++ b/internal/app/pulls.go @@ -23,8 +23,6 @@ one select the pull request of the current branch.`} group.Annotations = pullArgumentsAnnotation(nil) group.Example = `$ tea pr list --author @me $ tea pr create --fill -$ tea pr checkout 13 -$ tea pr review --approve $ tea pr merge --squash --delete-branch` group.AddCommand(a.pullChecksCommand(), a.pullCheckoutCommand(), a.pullCleanCommand(), a.statusCommand(true)) 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") { 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]{ - 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 { state := pullState(item) 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.Example = `$ tea pr list $ 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")'` return cmd } @@ -180,7 +175,6 @@ $ tea pr view # View #13 with its comments and reviews $ 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` return cmd }