diff --git a/internal/app/action_runs.go b/internal/app/action_runs.go index 0b3a588e..7afff430 100644 --- a/internal/app/action_runs.go +++ b/internal/app/action_runs.go @@ -617,11 +617,8 @@ $ tea run cancel 34 --force`, if err != nil { return err } - if a.options.json != "" { - return a.writeJSON(cmd, run) - } a.done(cmd, "Requested cancellation of run %d", id) - return nil + return a.jsonResult(cmd, run) }, } cmd.Flags().BoolVar(&force, "force", false, "Cancel without waiting for the runner to acknowledge") @@ -678,11 +675,8 @@ $ tea run rerun 34 --job build`, if run, err = service.Run(cmd.Context(), id); err != nil { return err } - if a.options.json != "" { - return a.writeJSON(cmd, run) - } a.done(cmd, "Requested rerun of run %d (attempt %d); watch it with: tea run watch %d%s", id, run.Attempt, id, a.suggestedRepo(t)) - return nil + return a.jsonResult(cmd, run) }, } cmd.Flags().BoolVar(&failed, "failed", false, "Rerun only the failed jobs") diff --git a/internal/app/actions.go b/internal/app/actions.go index 956a10e2..a1abb9a7 100644 --- a/internal/app/actions.go +++ b/internal/app/actions.go @@ -243,7 +243,7 @@ $ tea workflow run nightly --follow`, if !follow { a.nextStep(cmd, "Watch it: tea run watch %d%s", result.ID, a.suggestedRepo(t)) } - if err := a.printURL(cmd, result.URL); err != nil || !follow { + if err := a.created(cmd, result, result.URL); err != nil || !follow { return err } ctx, cancel := context.WithTimeout(cmd.Context(), timeout) @@ -474,10 +474,7 @@ $ tea workflow %s Nightly`, verb, verb), } a.done(cmd, "%s %s", done, workflow.Label()) } - if a.options.json != "" { - return a.writeJSON(cmd, workflow) - } - return nil + return a.jsonResult(cmd, workflow) }, } } diff --git a/internal/app/admin_users.go b/internal/app/admin_users.go index 4ea5ffcf..8ad258f2 100644 --- a/internal/app/admin_users.go +++ b/internal/app/admin_users.go @@ -280,10 +280,7 @@ $ tea admin user create ci-bot --email ci@example.com --password-file pw.txt --m return err } a.done(cmd, "Created user %s", user.Username) - if err := a.printURL(cmd, user.URL); err != nil { - return err - } - return a.jsonResult(cmd, user) + return a.created(cmd, user, user.URL) } return command } diff --git a/internal/app/branches.go b/internal/app/branches.go index 4fc86c24..f2621da3 100644 --- a/internal/app/branches.go +++ b/internal/app/branches.go @@ -196,7 +196,7 @@ $ tea branch create bisect --source 3f2a9c1`, } else { a.done(cmd, "Created branch %s from %s in %s at %s", item.Name, source, target.Repo, shortSHA(item.SHA)) } - return a.writeMutationJSON(cmd, item) + return a.jsonResult(cmd, item) }, } command.Flags().StringVarP(&source, "source", "s", "", "Branch, tag or commit to start from (default: the default branch)") diff --git a/internal/app/checkout.go b/internal/app/checkout.go index 535a27de..1a2795f6 100644 --- a/internal/app/checkout.go +++ b/internal/app/checkout.go @@ -143,15 +143,12 @@ argument select it, and the branch tracks the head branch, so that git push upda undoRemote() return err } - if a.options.json != "" { - return a.writeJSON(cmd, result) - } if result.Detached { a.done(cmd, "Checked out #%d at %s (detached HEAD)", n, shortSHA(result.Commit)) } else { a.done(cmd, "Checked out #%d into %s", n, result.Branch) } - return nil + return a.jsonResult(cmd, result) }} cmd.Flags().BoolVar(&update, "update", false, "Fast-forward an existing branch to the PR head; reject divergence") cmd.Flags().BoolVar(&detached, "detach", false, "Check out the PR commit without creating or changing a branch") diff --git a/internal/app/comments.go b/internal/app/comments.go index 32e97a07..a0e87977 100644 --- a/internal/app/comments.go +++ b/internal/app/comments.go @@ -142,11 +142,8 @@ func (a *application) commentOn(cmd *cobra.Command, t target, n int64, kind refK if err != nil { return err } - if a.options.json != "" { - return a.writeJSON(cmd, comment) - } a.done(cmd, "Commented on %s", refLabel(t.Repo, n, item.Title)) - return a.printURL(cmd, comment.URL) + return a.created(cmd, comment, comment.URL) } // lastComment returns the newest comment on n by the account tea acts as. @@ -185,11 +182,8 @@ func (a *application) editComment(cmd *cobra.Command, t target, comment services return err } edited.IssueNumber = comment.IssueNumber - if a.options.json != "" { - return a.writeJSON(cmd, edited) - } a.done(cmd, "Edited %s", commentName(t, edited)) - return nil + return a.jsonResult(cmd, edited) } // deleteComment deletes comment after a confirmation that defaults to no. diff --git a/internal/app/human_output.go b/internal/app/human_output.go index 62e06077..fbe406ee 100644 --- a/internal/app/human_output.go +++ b/internal/app/human_output.go @@ -149,12 +149,20 @@ func (a *application) feedback(cmd *cobra.Command, icon, color, format string, a fmt.Fprintln(cmd.ErrOrStderr(), terminal.Colorize(color, icon, a.colorEnabled())+" "+terminal.SafeLine(fmt.Sprintf(format, args...))) } -// printURL writes the URL of a created resource to stdout unless --json is set, so scripts can -// capture it: url=$(tea issue create …). -func (a *application) printURL(cmd *cobra.Command, url string) error { - if a.options.json != "" { +// jsonResult writes the result of a mutation when --json asks for it; people get a done line instead. +func (a *application) jsonResult(cmd *cobra.Command, item any) error { + if a.options.json == "" { return nil } + return a.writeJSON(cmd, item) +} + +// created writes a created resource as JSON with --json, else its URL to stdout, so scripts can +// capture it: url=$(tea issue create …). +func (a *application) created(cmd *cobra.Command, item any, url string) error { + if a.options.json != "" { + return a.writeJSON(cmd, item) + } _, err := fmt.Fprintln(cmd.OutOrStdout(), terminal.SafeLine(url)) return err } @@ -170,7 +178,7 @@ type abortBatch struct{ error } func (e abortBatch) Unwrap() error { return e.error } // applyBatch applies run to each selection and continues past individual failures, which it -// returns prefixed with the selection's label. succeeded sees each result as it arrives. +// returns prefixed with the selection's label. succeeded, when set, sees each result as it arrives. func applyBatch[K, T any](cmd *cobra.Command, keys []K, label func(K) string, run func(K) (T, error), succeeded func(T)) ([]T, []error) { results := []T{} var failures []error @@ -189,14 +197,18 @@ func applyBatch[K, T any](cmd *cobra.Command, keys []K, label func(K) string, ru continue } results = append(results, item) - succeeded(item) + if succeeded != nil { + succeeded(item) + } } return results, failures } -// writeBatchJSON writes an object for one selection (none if it failed), otherwise an array. +// writeBatchJSON writes, with --json, an object for one selection (none if it failed), otherwise an array. func writeBatchJSON[T any](a *application, cmd *cobra.Command, selections int, results []T) error { switch { + case a.options.json == "": + return nil case selections != 1: return a.writeJSON(cmd, results) case len(results) == 1: @@ -209,11 +221,7 @@ func writeBatchJSON[T any](a *application, cmd *cobra.Command, selections int, r // line from report, such as "Closed issue alice/x#5 (Fix login)", and the results as JSON. func batchDone[K, T any](a *application, cmd *cobra.Command, keys []K, label func(K) string, run func(K) (T, error), report func(T) string) error { results, failures := applyBatch(cmd, keys, label, run, func(item T) { a.done(cmd, "%s", report(item)) }) - var err error - if a.options.json != "" { - err = writeBatchJSON(a, cmd, len(keys), results) - } - return errors.Join(append(failures, err)...) + return errors.Join(append(failures, writeBatchJSON(a, cmd, len(keys), results))...) } // stateColors colors issue, pull request and milestone states with the terminal's own palette. diff --git a/internal/app/human_output_test.go b/internal/app/human_output_test.go index cd423349..ae58c20a 100644 --- a/internal/app/human_output_test.go +++ b/internal/app/human_output_test.go @@ -379,8 +379,12 @@ func TestMutationFeedback(t *testing.T) { ref := refLabel("alice/x", 12, "Fix\x1b[2J login") a.done(cmd, "Created issue %s", ref) a.warn(cmd, "Issue %s is already closed", ref) - require.NoError(t, a.printURL(cmd, "https://example.org/12")) - require.Equal(t, tc.out, out.String()) + require.NoError(t, a.created(cmd, map[string]int{"number": 12}, "https://example.org/12")) + if tc.json != "" { + require.JSONEq(t, `{"number":12}`, out.String()) + } else { + require.Equal(t, tc.out, out.String()) + } require.Equal(t, tc.stderr, diagnostics.String()) }) } diff --git a/internal/app/issue_attachments.go b/internal/app/issue_attachments.go index c4dc1f39..3ad176b5 100644 --- a/internal/app/issue_attachments.go +++ b/internal/app/issue_attachments.go @@ -111,11 +111,8 @@ $ tea %[1]s attachment upload %[2]s screenshot.png --name before.png`, words.gro if err != nil { return err } - if a.options.json != "" { - return a.writeJSON(cmd, item) - } a.done(cmd, "Uploaded %s (%s) to %s#%d", item.Name, terminal.Bytes(item.Size), t.Repo, n) - return a.printURL(cmd, item.URL) + return a.created(cmd, item, item.URL) }} upload.Flags().StringVar(&uploadName, "name", "", "File `name` of the attachment (default: the local file name)") @@ -132,11 +129,8 @@ $ tea %[1]s attachment upload %[2]s screenshot.png --name before.png`, words.gro if err != nil { return err } - if a.options.json != "" { - return a.writeJSON(cmd, item) - } a.done(cmd, "Renamed %s", attachmentName(t, n, item)) - return nil + return a.jsonResult(cmd, item) }} edit.Flags().StringVar(&newName, "name", "", "New file `name`") diff --git a/internal/app/issues.go b/internal/app/issues.go index e22fdc20..4b52bfd6 100644 --- a/internal/app/issues.go +++ b/internal/app/issues.go @@ -317,7 +317,7 @@ $ tea issue create --title "Idea" --web`, Args: argsExact(0), RunE: func(cmd *co failures = append(failures, fmt.Errorf("issue %d created; attachment %s upload failed: %w", item.Number, filepath.Base(file.Name()), err)) } } - return errors.Join(append(failures, a.created(cmd, target, item))...) + return errors.Join(append(failures, a.issueCreated(cmd, target, item))...) }} flags := cmd.Flags() flags.StringVarP(&title, "title", "t", "", "Issue title") @@ -340,13 +340,10 @@ $ tea issue create --title "Idea" --web`, Args: argsExact(0), RunE: func(cmd *co return cmd } -// created reports a new issue: as JSON, or its URL on stdout after a ✓ line for people. -func (a *application) created(cmd *cobra.Command, t target, item services.Issue) error { - if a.options.json != "" { - return a.writeJSON(cmd, item) - } +// issueCreated reports a new issue: as JSON, or its URL on stdout after a ✓ line for people. +func (a *application) issueCreated(cmd *cobra.Command, t target, item services.Issue) error { a.done(cmd, "Created issue %s", refLabel(t.Repo, item.Number, item.Title)) - return a.printURL(cmd, item.URL) + return a.created(cmd, item, item.URL) } func (a *application) createIssueInteractively(cmd *cobra.Command) (runErr error) { @@ -379,7 +376,7 @@ func (a *application) createIssueInteractively(cmd *cobra.Command) (runErr error if err != nil { return err } - return a.created(cmd, target, item) + return a.issueCreated(cmd, target, item) } // issueTemplateBody seeds a body from markdown content, or from the description of form templates. @@ -445,12 +442,8 @@ $ tea issue %[1]s 12 13 --comment "Done in #14"`, verb), Annotations: map[string } a.done(cmd, "%s %s %s", past, discussionNoun(item.Kind), refLabel(target.Repo, n, item.Title)) return item, nil - }, func(services.Issue) {}) - var err2 error - if a.options.json != "" { - err2 = writeBatchJSON(a, cmd, len(numbers), results) - } - return errors.Join(append(failures, err2)...) + }, nil) + return errors.Join(append(failures, writeBatchJSON(a, cmd, len(numbers), results))...) }} cmd.Flags().StringVarP(&comment, "comment", "c", "", "Leave a comment before the issue is "+state) cmd.ValidArgsFunction = a.completeIssueArgs(completeState, true) diff --git a/internal/app/labels.go b/internal/app/labels.go index c4e73761..769d3685 100644 --- a/internal/app/labels.go +++ b/internal/app/labels.go @@ -226,15 +226,12 @@ func (a *application) askLabelDetails(cmd *cobra.Command, opts *services.LabelOp // 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 { - if a.options.json != "" { - return a.writeJSON(cmd, item) - } verb := "Created" if updated { verb = "Updated" } a.done(cmd, "%s %s", verb, labelName(s, item)) - return nil + return a.jsonResult(cmd, item) } func (a *application) labelEditCommand(scope labelScope, complete cobra.CompletionFunc) *cobra.Command { @@ -262,11 +259,8 @@ $ tea label edit old-stuff --archived`, Annotations: map[string]string{"tea.argu if err != nil { return err } - if a.options.json != "" { - return a.writeJSON(cmd, item) - } a.done(cmd, "Edited %s", labelName(s, item)) - return nil + return a.jsonResult(cmd, item) }} fields.register(cmd, "Rename the label") cmd.Flags().BoolVar(&byName, "by-name", false, "Match a numeric argument as a label name rather than an ID") @@ -359,10 +353,7 @@ func (a *application) createLabels(cmd *cobra.Command, s services.Labels, rows [ }, func(item services.Label) { a.done(cmd, "%s %s", outcome[item.Name], labelName(s, item)) }) - if a.options.json != "" { - failures = append(failures, writeBatchJSON(a, cmd, len(rows), results)) - } - return errors.Join(failures...) + return errors.Join(append(failures, writeBatchJSON(a, cmd, len(rows), results))...) } // completeScopedLabels completes the label names of the repository or, with --org, of the diff --git a/internal/app/metadata.go b/internal/app/metadata.go index e2a3f28b..d267d66e 100644 --- a/internal/app/metadata.go +++ b/internal/app/metadata.go @@ -274,11 +274,8 @@ $ tea milestone create --title v2.0 --editor`, Args: usageArgs(cobra.MaximumNArg if err != nil { return err } - if a.options.json != "" { - return a.writeJSON(cmd, item) - } a.done(cmd, "Created %s", milestoneName(t, item)) - return a.printURL(cmd, milestoneURL(t, item)) + return a.created(cmd, item, milestoneURL(t, item)) }} fields.register(cmd) enumFlag(cmd, &state, "state", "", "open", "State of the new milestone", "open", "closed") @@ -338,11 +335,8 @@ $ tea milestone edit 3 --editor`, Annotations: map[string]string{"tea.arguments" if err != nil { return err } - if a.options.json != "" { - return a.writeJSON(cmd, item) - } a.done(cmd, "Edited %s", milestoneName(t, item)) - return nil + return a.jsonResult(cmd, item) }} fields.register(cmd) cmd.Flags().BoolVar(&byName, "by-name", false, "Match a numeric argument as a milestone title rather than an ID") @@ -369,11 +363,8 @@ $ tea milestone delete 7 --yes`, Annotations: map[string]string{"tea.confirm": " if err := s.Delete(cmd.Context(), item.ID); err != nil { return err } - if a.options.json != "" { - return a.writeJSON(cmd, item) - } a.done(cmd, "Deleted milestone %q from %s", item.Title, t.Repo) - return nil + return a.jsonResult(cmd, item) }} cmd.Flags().BoolVar(&byName, "by-name", false, "Match a numeric argument as a milestone title rather than an ID") cmd.ValidArgsFunction = a.completeMilestoneArgs(false) diff --git a/internal/app/milestone_issues.go b/internal/app/milestone_issues.go index 182a055e..cff6f190 100644 --- a/internal/app/milestone_issues.go +++ b/internal/app/milestone_issues.go @@ -66,7 +66,7 @@ $ tea milestone issue %[1]s v1.0 12 13 '#14'`, verb), Annotations: map[string]st item, err = service.Edit(cmd.Context(), n, services.IssueEdit{Milestone: &milestone.ID}) } return item, itemError(err, target.Repo) - }, func(services.Issue) {}) + }, nil) if len(results) > 0 { changed := make([]string, len(results)) for i, item := range results { @@ -74,10 +74,7 @@ $ tea milestone issue %[1]s v1.0 12 13 '#14'`, verb), Annotations: map[string]st } a.done(cmd, "%s %s %s %s", past, strings.Join(changed, ", "), preposition, milestoneName(target, milestone)) } - if a.options.json != "" { - failures = append(failures, writeBatchJSON(a, cmd, len(numbers), results)) - } - return errors.Join(failures...) + return errors.Join(append(failures, writeBatchJSON(a, cmd, len(numbers), results))...) }} command.Flags().BoolVar(&byName, "by-name", false, "Match a numeric milestone argument as a title rather than an ID") milestones, issues := a.completeMilestones(), a.completeIssues("open") diff --git a/internal/app/milestone_state.go b/internal/app/milestone_state.go index 06d0e7ca..f3a6e700 100644 --- a/internal/app/milestone_state.go +++ b/internal/app/milestone_state.go @@ -50,11 +50,8 @@ $ tea milestone %[1]s v1.0 v1.1`, verb), Annotations: map[string]string{"tea.arg a.done(cmd, "%s %s", past, milestoneName(t, item)) } return item, err - }, func(services.Milestone) {}) - if a.options.json != "" { - failures = append(failures, writeBatchJSON(a, cmd, len(selected), results)) - } - return errors.Join(failures...) + }, nil) + return errors.Join(append(failures, writeBatchJSON(a, cmd, len(selected), results))...) }} cmd.Flags().BoolVar(&byName, "by-name", false, "Match numeric arguments as milestone titles rather than IDs") cmd.ValidArgsFunction = a.completeMilestoneArgs(true) diff --git a/internal/app/organizations.go b/internal/app/organizations.go index 602b8ca4..9a05b17a 100644 --- a/internal/app/organizations.go +++ b/internal/app/organizations.go @@ -23,14 +23,6 @@ var visibilities = []string{"public", "limited", "private"} // visibilityColors color the visibility of organizations and accounts. var visibilityColors = map[string]string{"public": "green", "limited": "yellow", "private": "red"} -// jsonResult writes the result of a mutation when --json asks for it; people get a done line instead. -func (a *application) jsonResult(cmd *cobra.Command, item any) error { - if a.options.json == "" { - return nil - } - return a.writeJSON(cmd, item) -} - // timeField shows an API timestamp relative to now on a TTY, and as RFC3339 for scripts. func timeField(label, raw string, d terminal.Display) terminal.Field { t := terminal.ParseTime(raw) @@ -250,10 +242,7 @@ $ tea org create acme-internal --visibility private`, return err } a.done(cmd, "Created organization %s", org.Name) - if err := a.printURL(cmd, webURL(target, org.Name)); err != nil { - return err - } - return a.jsonResult(cmd, org) + return a.created(cmd, org, webURL(target, org.Name)) } return cmd } diff --git a/internal/app/pull_clean.go b/internal/app/pull_clean.go index 1c7c8259..9ba42b8c 100644 --- a/internal/app/pull_clean.go +++ b/internal/app/pull_clean.go @@ -103,9 +103,6 @@ func (a *application) pullCleanCommand() *cobra.Command { if err != nil { return err } - if a.options.json != "" { - return a.writeJSON(cmd, result) - } a.done(cmd, "Deleted local branch %s", result.LocalBranch) switch { case result.RemoteDeleted: @@ -115,7 +112,7 @@ func (a *application) pullCleanCommand() *cobra.Command { case !pull.HeadCanPush: a.warn(cmd, "Kept remote branch %s: you cannot push to %s", pull.Head, pull.HeadRepo) } - return nil + return a.jsonResult(cmd, result) }} cmd.Flags().BoolVar(&ignoreSHA, "ignore-sha", false, "Find the local branch by name instead of commit hash") cmd.ValidArgsFunction = a.completePulls("closed") diff --git a/internal/app/pull_create.go b/internal/app/pull_create.go index 19218141..5ff16f4b 100644 --- a/internal/app/pull_create.go +++ b/internal/app/pull_create.go @@ -371,8 +371,5 @@ func (a *application) createPullInteractively(cmd *cobra.Command, snapshot setti // reportCreatedPull reports a new pull request and prints its URL, or the pull request as JSON. func (a *application) reportCreatedPull(cmd *cobra.Command, t target, item services.Pull) error { a.done(cmd, "Created pull request %s", refLabel(t.Repo, item.Number, item.Title)) - if a.options.json != "" { - return a.writeJSON(cmd, item) - } - return a.printURL(cmd, item.URL) + return a.created(cmd, item, item.URL) } diff --git a/internal/app/pull_edit.go b/internal/app/pull_edit.go index bd2b7dfb..d0ec3ec7 100644 --- a/internal/app/pull_edit.go +++ b/internal/app/pull_edit.go @@ -125,10 +125,7 @@ pull request, or mark it as a draft or ready for review.`, Annotations: pullArgu return err } a.done(cmd, "Edited pull request %s", refLabel(t.Repo, n, item.Title)) - if a.options.json != "" { - return a.writeJSON(cmd, item) - } - return nil + return a.jsonResult(cmd, item) }} cmd.Flags().StringVarP(&title, "title", "t", "", "Pull request title") cmd.Flags().StringVar(&deadline, "deadline", "", "Due date: YYYY-MM-DD, RFC3339, or Unix timestamp; empty clears") @@ -206,10 +203,7 @@ removes or adds that marker.`, Annotations: pullArgumentsAnnotation(nil), Args: a.done(cmd, "Marked pull request %s as ready for review", refLabel(t.Repo, n, item.Title)) } } - if a.options.json != "" { - return a.writeJSON(cmd, item) - } - return nil + return a.jsonResult(cmd, item) }} cmd.Flags().BoolVar(&undo, "undo", false, "Turn the pull request back into a draft") cmd.ValidArgsFunction = a.completePulls("open") diff --git a/internal/app/pull_mutations.go b/internal/app/pull_mutations.go index 8a9567f2..9a4b8312 100644 --- a/internal/app/pull_mutations.go +++ b/internal/app/pull_mutations.go @@ -109,10 +109,7 @@ func (a *application) pullStateCommand(closing bool) *cobra.Command { if deleteBranch { a.deleteHead(cmd, service, item, false) } - if a.options.json != "" { - return a.writeJSON(cmd, item) - } - return nil + return a.jsonResult(cmd, item) }} cmd.Flags().StringVarP(&comment, "comment", "c", "", "Leave a comment before changing the state") cmd.ValidArgsFunction = a.completePulls(map[bool]string{true: "open", false: "closed"}[closing]) @@ -302,10 +299,7 @@ the base branch.`, Annotations: pullArgumentsAnnotation(map[string]string{"tea.c switch { case item.Merged: a.warn(cmd, "Pull request %s is already merged", ref) - if a.options.json != "" { - return a.writeJSON(cmd, item) - } - return nil + return a.jsonResult(cmd, item) case item.State == "closed": return fmt.Errorf("pull request %s is closed; reopen it with: tea pr reopen %d", ref, n) case item.Draft: @@ -362,10 +356,7 @@ the base branch.`, Annotations: pullArgumentsAnnotation(map[string]string{"tea.c a.deleteHead(cmd, service, merged, true) } } - if a.options.json != "" { - return a.writeJSON(cmd, merged) - } - return nil + return a.jsonResult(cmd, merged) }} enumFlag(cmd, &method, "method", "s", "", "Merge method (default: the repository's default merge style)", mergeMethods...) cmd.Flags().StringVar(&method, "style", "", "Alias of --method") diff --git a/internal/app/pull_review.go b/internal/app/pull_review.go index 77b23726..2aa7ec7a 100644 --- a/internal/app/pull_review.go +++ b/internal/app/pull_review.go @@ -268,13 +268,10 @@ func ownReview(pull services.Pull, t target, state string) error { // reportReview reports a submitted review, then prints its URL, or the review as JSON. func (a *application) reportReview(cmd *cobra.Command, t target, pull services.Pull, item services.Review) error { a.done(cmd, "%s pull request %s", reviewVerbs[item.State], refLabel(t.Repo, pull.Number, pull.Title)) - switch { - case a.options.json != "": - return a.writeJSON(cmd, item) - case item.URL != "": - return a.printURL(cmd, item.URL) + if a.options.json == "" && item.URL == "" { + return nil } - return nil + return a.created(cmd, item, item.URL) } // readInlineComments reads the inline comments of --comments-file, a JSON array. @@ -625,10 +622,7 @@ func (a *application) pullReviewReplyCommand() *cobra.Command { return err } a.done(cmd, "Replied to review comment %d on %s#%d", id, t.Repo, n) - if a.options.json != "" { - return a.writeJSON(cmd, item) - } - return a.printURL(cmd, item.URL) + return a.created(cmd, item, item.URL) }} cmd.Flags().StringVarP(&body, "body", "b", "", "Reply body") cmd.Flags().StringVarP(&file, "body-file", "F", "", "Read the body from `file`, or - for stdin") diff --git a/internal/app/pull_review_lifecycle.go b/internal/app/pull_review_lifecycle.go index 6e16b49f..2e12186f 100644 --- a/internal/app/pull_review_lifecycle.go +++ b/internal/app/pull_review_lifecycle.go @@ -204,10 +204,7 @@ func (a *application) pullReviewDismissCommand(dismiss bool) *cobra.Command { } else { a.done(cmd, "Restored %s", subject) } - if a.options.json != "" { - return a.writeJSON(cmd, item) - } - return nil + return a.jsonResult(cmd, item) }} if dismiss { cmd.Annotations["tea.confirm"] = "true" diff --git a/internal/app/pull_review_requests.go b/internal/app/pull_review_requests.go index 15a08f7b..45f423b0 100644 --- a/internal/app/pull_review_requests.go +++ b/internal/app/pull_review_requests.go @@ -42,10 +42,7 @@ func (a *application) pullReviewRequestCommand(add bool) *cobra.Command { names = append(names, "team "+team) } a.done(cmd, "%s %s on %s", verb, strings.Join(names, ", "), refLabel(t.Repo, item.Number, item.Title)) - if a.options.json != "" { - return a.writeJSON(cmd, item) - } - return nil + return a.jsonResult(cmd, item) }} cmd.Flags().StringSliceVarP(&input.Users, "reviewer", "r", nil, "Reviewer usernames or email addresses; @me for yourself") cmd.Flags().StringSliceVar(&input.Teams, "team", nil, "Teams of the repository's organization") diff --git a/internal/app/release_assets.go b/internal/app/release_assets.go index 885517aa..37a1c88b 100644 --- a/internal/app/release_assets.go +++ b/internal/app/release_assets.go @@ -352,7 +352,7 @@ func (a *application) assetEditCommand() *cobra.Command { return err } a.done(cmd, "Renamed asset %s to %s in release %s", asset.Name, item.Name, release.Tag) - return a.writeMutationJSON(cmd, item) + return a.jsonResult(cmd, item) }, } command.Flags().StringVar(&name, "name", "", "New asset name") diff --git a/internal/app/releases.go b/internal/app/releases.go index 08743d76..3226fa46 100644 --- a/internal/app/releases.go +++ b/internal/app/releases.go @@ -300,12 +300,7 @@ $ tea release create v1.1.0-rc1 --target release/1.1 --draft --prerelease --titl item.Assets = append(item.Assets, asset) a.done(cmd, "Uploaded %s (%s)", asset.Name, terminal.Bytes(asset.Size)) } - if a.options.json != "" { - failures = append(failures, a.writeJSON(cmd, item)) - } else { - failures = append(failures, a.printURL(cmd, item.URL)) - } - return errors.Join(failures...) + return errors.Join(append(failures, a.created(cmd, item, item.URL))...) }, } fields.register(a, command) @@ -402,11 +397,8 @@ func (a *application) releasePublishCommand() *cobra.Command { a.done(cmd, "Published release %s in %s", item.Tag, target.Repo) } return item, err - }, func(services.Release) {}) - if a.options.json != "" { - failures = append(failures, writeBatchJSON(a, cmd, len(args), results)) - } - return errors.Join(failures...) + }, nil) + return errors.Join(append(failures, writeBatchJSON(a, cmd, len(args), results))...) }, } command.ValidArgsFunction = a.completeReleases() diff --git a/internal/app/repo_mutations.go b/internal/app/repo_mutations.go index 538e3b85..abd9a913 100644 --- a/internal/app/repo_mutations.go +++ b/internal/app/repo_mutations.go @@ -278,7 +278,7 @@ $ tea repo clone alice/project -- --branch develop --recurse-submodules`, if _, err := a.cloneRepository(cmd, t, item, opts); err != nil { return err } - return a.writeMutationJSON(cmd, item) + return a.jsonResult(cmd, item) }, } command.Flags().IntVarP(&opts.depth, "depth", "d", 0, "Number of commits to fetch (0: full history)") @@ -352,10 +352,7 @@ $ tea repo fork alice/project --clone -- --depth 1`, return err } } - if a.options.json != "" { - return a.writeJSON(cmd, fork) - } - return a.printURL(cmd, fork.URL) + return a.created(cmd, fork, fork.URL) }, } command.Flags().StringVar(&opts.Organization, "org", "", "Fork into this organization") @@ -552,7 +549,7 @@ $ tea repo archive alice/old-project --yes`, } else { a.warn(cmd, "%s is not archived", item.FullName) } - return a.writeMutationJSON(cmd, item) + return a.jsonResult(cmd, item) } if archive { if err := a.confirmAction(cmd, terminal.Confirmation{Prompt: "Archive " + t.Repo + inferred + "? It becomes read-only.", NonTTY: "archive " + t.Repo}); err != nil { @@ -568,20 +565,12 @@ $ tea repo archive alice/old-project --yes`, } else { a.done(cmd, "Unarchived repository %s", item.FullName) } - return a.writeMutationJSON(cmd, item) + return a.jsonResult(cmd, item) } command.ValidArgsFunction = a.completeRepos() return command } -// writeMutationJSON writes the result of a mutation with --json; people get done lines instead. -func (a *application) writeMutationJSON(cmd *cobra.Command, item any) error { - if a.options.json == "" { - return nil - } - return a.writeJSON(cmd, item) -} - func (a *application) repoRenameCommand() *cobra.Command { command := &cobra.Command{ Use: "rename ", @@ -624,7 +613,7 @@ $ tea repo rename new-name -R alice/old-name`, if remote, ok := strings.CutPrefix(t.RepoSource, "remote "); ok { a.warn(cmd, "Git remote %s still points to %s; update it with git remote set-url", remote, t.Repo) } - return a.writeMutationJSON(cmd, item) + return a.jsonResult(cmd, item) }, } return command diff --git a/internal/app/repository_create.go b/internal/app/repository_create.go index fc956afd..5ec68c69 100644 --- a/internal/app/repository_create.go +++ b/internal/app/repository_create.go @@ -159,10 +159,7 @@ $ tea repo create project --source . --push`, if err != nil { return err } - if a.options.json != "" { - return a.writeJSON(cmd, item) - } - return a.printURL(cmd, item.URL) + return a.created(cmd, item, item.URL) }, } flags := command.Flags() diff --git a/internal/app/repository_migrate.go b/internal/app/repository_migrate.go index 95257b1e..0a3081ff 100644 --- a/internal/app/repository_migrate.go +++ b/internal/app/repository_migrate.go @@ -143,10 +143,7 @@ $ tea repo migrate https://github.com/acme/tool --service github --issues --pull detail = " (pull mirror)" } a.done(cmd, "Imported %s into %s%s", opts.CloneURL, item.FullName, detail) - if a.options.json != "" { - return a.writeJSON(cmd, item) - } - return a.printURL(cmd, item.URL) + return a.created(cmd, item, item.URL) }, } flags := command.Flags() diff --git a/internal/app/repository_sync.go b/internal/app/repository_sync.go index ede1aff8..659bf18e 100644 --- a/internal/app/repository_sync.go +++ b/internal/app/repository_sync.go @@ -91,11 +91,8 @@ $ tea repo sync bob/project`, if err != nil { return err } - if a.options.json != "" { - return a.writeJSON(cmd, result) - } a.reportSync(cmd, result, remote+"/"+result.Branch) - return nil + return a.jsonResult(cmd, result) }, } command.Flags().StringVarP(&branch, "branch", "b", "", "Branch to sync (default: the repository's default branch)") @@ -118,7 +115,7 @@ func (a *application) syncOnServer(cmd *cobra.Command, t target, branch string) return err } a.done(cmd, "Started updating the mirror %s from its source", item.FullName) - return a.writeMutationJSON(cmd, git.SyncResult{Remote: mirrorSourceURL(item)}) + return a.jsonResult(cmd, git.SyncResult{Remote: mirrorSourceURL(item)}) case !item.Fork || item.Parent == nil: return clierr.Wrap(clierr.Usage, fmt.Errorf("%s is neither a fork nor a mirror, and has no Git remote here; run tea repo sync inside a clone of it", item.FullName)) } @@ -141,7 +138,7 @@ func (a *application) syncOnServer(cmd *cobra.Command, t target, branch string) } else { a.done(cmd, "Synced %s %s from %s (fast-forward %s..%s)", item.FullName, branch, item.Parent.FullName, shortSHA(result.Previous), shortSHA(result.Commit)) } - return a.writeMutationJSON(cmd, result) + return a.jsonResult(cmd, result) } // reportSync tells how a sync from source, such as origin/main, changed the branch. diff --git a/internal/app/teams.go b/internal/app/teams.go index 2cb1dd95..995d8c05 100644 --- a/internal/app/teams.go +++ b/internal/app/teams.go @@ -380,10 +380,7 @@ $ tea team create acme ops --permission admin --all-repositories`, return err } a.done(cmd, "Created team %s (ID %d) with %s access", team.Slug(), team.ID, team.Access()) - if err := a.printURL(cmd, teamURL(target, team)); err != nil { - return err - } - return a.jsonResult(cmd, team) + return a.created(cmd, team, teamURL(target, team)) }, } flags.register(cmd, true) diff --git a/internal/app/time.go b/internal/app/time.go index 0ab1a530..6dbf8d6c 100644 --- a/internal/app/time.go +++ b/internal/app/time.go @@ -174,11 +174,8 @@ $ tea time log https://gitea.com/OWNER/REPO/issues/12 45m`, Annotations: map[str if err != nil { return err } - if a.options.json != "" { - return a.writeJSON(cmd, item) - } a.done(cmd, "Logged %s on %s", spent(item.Seconds), strings.TrimSuffix(refLabel(target.Repo, n, item.IssueTitle), " ()")) - return nil + return a.jsonResult(cmd, item) }} } diff --git a/internal/app/webhooks.go b/internal/app/webhooks.go index d9806e4e..19f41ef5 100644 --- a/internal/app/webhooks.go +++ b/internal/app/webhooks.go @@ -431,10 +431,7 @@ $ tea webhook create https://hooks.slack.com/services/T0/B0/X --org acme --type return err } a.done(cmd, "Created %s in %s for %s", webhookLabel(hook), webhookScopeName(service.Scope), collapsedEvents(hook.Events)) - if err := a.printURL(cmd, webhookSettingsURL(target, service.Scope, hook.ID)); err != nil { - return err - } - return a.jsonResult(cmd, hook) + return a.created(cmd, hook, webhookSettingsURL(target, service.Scope, hook.ID)) } return command } diff --git a/internal/app/wiki.go b/internal/app/wiki.go index f99e50ca..1748672b 100644 --- a/internal/app/wiki.go +++ b/internal/app/wiki.go @@ -213,10 +213,7 @@ $ tea wiki create Home --editor`, return err } a.done(cmd, "Created wiki page %q in %s", item.Title, target.Repo) - if a.options.json != "" { - return a.writeJSON(cmd, item) - } - return a.printURL(cmd, item.URL) + return a.created(cmd, item, item.URL) }, } content.register(command) @@ -298,7 +295,7 @@ $ tea wiki edit Home --editor`, } else { a.done(cmd, "Edited wiki page %q in %s", item.Title, target.Repo) } - return a.writeMutationJSON(cmd, item) + return a.jsonResult(cmd, item) }, } content.register(command)