From 12947f068aa9f5827d05381caccb6d330fbe42f1 Mon Sep 17 00:00:00 2001 From: Brien Coffield Date: Mon, 29 Jun 2026 00:52:24 +0000 Subject: [PATCH] feat(comments): accept -d/--description for comment body (#1043) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #1042. ### What `tea issue create`, `tea issue edit` and `tea pr create` all take the body via `-d` / `--description`, but `tea comments add` and `tea comments edit` only accepted it positionally — passing `-d` errored with `flag provided but not defined: -d`. This adds `-d` / `--description` to both `comments` subcommands so every body-bearing command shares one flag. ### Behaviour / precedence Unchanged for existing usage. Body resolution is now: 1. positional argument (kept first for back-compat), 2. `-d` / `--description`, 3. piped stdin, 4. `$EDITOR` (interactive only). ### Testing - `go build`, `go vet ./...`, `go test -short ./cmd/... ./modules/...` all pass. - Manually verified `tea comments add --help` / `edit --help` list the flag, and that `-d` resolves the body (reaches the API, no parse error, no editor prompt, no stdin block). --------- Co-authored-by: Lunny Xiao Reviewed-on: https://gitea.com/gitea/tea/pulls/1043 Reviewed-by: Lunny Xiao Co-authored-by: Brien Coffield Co-committed-by: Brien Coffield --- cmd/comments/add.go | 27 +++++----- cmd/comments/body.go | 37 ++++++++++++++ cmd/comments/body_test.go | 101 ++++++++++++++++++++++++++++++++++++++ cmd/comments/edit.go | 25 +++++----- docs/CLI.md | 6 +++ 5 files changed, 171 insertions(+), 25 deletions(-) create mode 100644 cmd/comments/body.go create mode 100644 cmd/comments/body_test.go diff --git a/cmd/comments/add.go b/cmd/comments/add.go index aff2d04a..601006c7 100644 --- a/cmd/comments/add.go +++ b/cmd/comments/add.go @@ -7,7 +7,6 @@ import ( stdctx "context" "errors" "fmt" - "io" "strings" gitea "gitea.dev/sdk" @@ -32,7 +31,13 @@ var CmdCommentsAdd = cli.Command{ Description: "Add a comment to an issue or pull request.", ArgsUsage: " []", Action: RunCommentsAdd, - Flags: flags.AllDefaultFlags, + Flags: append([]cli.Flag{ + &cli.StringFlag{ + Name: "description", + Aliases: []string{"d"}, + Usage: "comment body (alternative to the positional argument)", + }, + }, flags.AllDefaultFlags...), } // RunCommentsAdd creates a new comment. @@ -54,18 +59,12 @@ func RunCommentsAdd(requestCtx stdctx.Context, cmd *cli.Command) error { return err } - body := strings.Join(ctx.Args().Tail(), " ") - // Only consume stdin if no positional body was given. interact.IsStdinPiped() - // is true for any non-TTY stdin (CI, subshells, agent harnesses) — not just - // piped data — so reading unconditionally would block forever in those - // contexts when the body is supplied via args. - if len(body) == 0 && interact.IsStdinPiped() { - if bodyStdin, err := io.ReadAll(ctx.Reader); err != nil { - return err - } else if len(bodyStdin) != 0 { - body = string(bodyStdin) - } - } else if len(body) == 0 { + stdinPiped := interact.IsStdinPiped() + body, err := resolveBody(strings.Join(ctx.Args().Tail(), " "), ctx.String("description"), stdinPiped, ctx.Reader) + if err != nil { + return err + } + if len(body) == 0 && !stdinPiped { if err := huh.NewForm( huh.NewGroup( huh.NewText(). diff --git a/cmd/comments/body.go b/cmd/comments/body.go new file mode 100644 index 00000000..dcda8314 --- /dev/null +++ b/cmd/comments/body.go @@ -0,0 +1,37 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package comments + +import "io" + +// resolveBody picks the comment body from the non-interactive sources, in +// precedence order: +// +// 1. the positional argument (kept first for back-compat with the historical +// 'tea comment ""' shorthand), +// 2. the -d/--description flag (mirrors the body flag on 'issue create', +// 'issue edit' and 'pr create'), +// 3. piped stdin. +// +// stdin is only read when stdinPiped is true (a non-TTY stdin, e.g. CI, +// subshells or agent harnesses) and no body was supplied otherwise, so the +// command never blocks reading an interactive terminal when a body is already +// given. An empty result means the caller should fall back to the editor (when +// interactive) or error out. +func resolveBody(positional, description string, stdinPiped bool, stdin io.Reader) (string, error) { + if len(positional) != 0 { + return positional, nil + } + if len(description) != 0 { + return description, nil + } + if stdinPiped { + stdinBytes, err := io.ReadAll(stdin) + if err != nil { + return "", err + } + return string(stdinBytes), nil + } + return "", nil +} diff --git a/cmd/comments/body_test.go b/cmd/comments/body_test.go new file mode 100644 index 00000000..4c3ab1e3 --- /dev/null +++ b/cmd/comments/body_test.go @@ -0,0 +1,101 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package comments + +import ( + "io" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestResolveBody(t *testing.T) { + testCases := []struct { + name string + positional string + description string + stdinPiped bool + stdin string + expected string + }{ + { + name: "positional only", + positional: "from positional", + expected: "from positional", + }, + { + name: "description flag only", + description: "from -d", + expected: "from -d", + }, + { + name: "positional wins over description for back-compat", + positional: "from positional", + description: "from -d", + expected: "from positional", + }, + { + name: "description wins over piped stdin", + description: "from -d", + stdinPiped: true, + stdin: "from stdin", + expected: "from -d", + }, + { + name: "piped stdin used when nothing else given", + stdinPiped: true, + stdin: "from stdin", + expected: "from stdin", + }, + { + name: "stdin ignored when not piped (interactive terminal)", + stdinPiped: false, + stdin: "should never be read", + expected: "", + }, + { + name: "empty when no source provided", + stdinPiped: false, + expected: "", + }, + { + name: "piped but empty stdin yields empty body", + stdinPiped: true, + stdin: "", + expected: "", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + body, err := resolveBody(tc.positional, tc.description, tc.stdinPiped, strings.NewReader(tc.stdin)) + require.NoError(t, err) + assert.Equal(t, tc.expected, body) + }) + } +} + +// TestResolveBodyDoesNotReadStdinWhenBodyGiven guards the original bug: when a +// body is supplied positionally (or via -d), stdin must not be consumed, so the +// command can never block on a non-TTY stdin under CI / agent harnesses. +func TestResolveBodyDoesNotReadStdinWhenBodyGiven(t *testing.T) { + reader := &trackingReader{} + + body, err := resolveBody("positional body", "", true, reader) + require.NoError(t, err) + assert.Equal(t, "positional body", body) + assert.False(t, reader.read, "stdin must not be read when a body is supplied") +} + +// trackingReader records whether Read was ever called. +type trackingReader struct { + read bool +} + +func (r *trackingReader) Read(p []byte) (int, error) { + r.read = true + return 0, io.EOF +} diff --git a/cmd/comments/edit.go b/cmd/comments/edit.go index bf5a8763..45da15d3 100644 --- a/cmd/comments/edit.go +++ b/cmd/comments/edit.go @@ -7,7 +7,6 @@ import ( stdctx "context" "errors" "fmt" - "io" "strings" gitea "gitea.dev/sdk" @@ -31,10 +30,16 @@ var CmdCommentsEdit = cli.Command{ Usage: "Edit the body of an existing comment", Description: `Edit the body of an existing comment by its comment ID. Use 'tea comments list ' to find IDs. -The new body can be supplied as a positional argument, piped on stdin, or (if neither is given and stdin is a terminal) entered in your $EDITOR.`, +The new body can be supplied as a positional argument, via -d/--description, piped on stdin, or (if none is given and stdin is a terminal) entered in your $EDITOR.`, ArgsUsage: " []", Action: RunCommentsEdit, - Flags: flags.AllDefaultFlags, + Flags: append([]cli.Flag{ + &cli.StringFlag{ + Name: "description", + Aliases: []string{"d"}, + Usage: "new comment body (alternative to the positional argument)", + }, + }, flags.AllDefaultFlags...), } // RunCommentsEdit updates the body of an existing comment. @@ -56,14 +61,12 @@ func RunCommentsEdit(requestCtx stdctx.Context, cmd *cli.Command) error { return fmt.Errorf("invalid comment id %q: %s", ctx.Args().First(), err) } - body := strings.Join(ctx.Args().Tail(), " ") - if len(body) == 0 && interact.IsStdinPiped() { - if bodyStdin, err := io.ReadAll(ctx.Reader); err != nil { - return err - } else if len(bodyStdin) != 0 { - body = string(bodyStdin) - } - } else if len(body) == 0 { + stdinPiped := interact.IsStdinPiped() + body, err := resolveBody(strings.Join(ctx.Args().Tail(), " "), ctx.String("description"), stdinPiped, ctx.Reader) + if err != nil { + return err + } + if len(body) == 0 && !stdinPiped { // Fetch current body to pre-populate the editor. client := ctx.Login.Client() current, _, fetchErr := client.Issues.GetIssueComment(requestCtx, ctx.Owner, ctx.Repo, id) diff --git a/docs/CLI.md b/docs/CLI.md index 1454623f..6946c5e2 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -1927,6 +1927,8 @@ Update a webhook Manage comments on issues and pull requests +**--description, -d**="": comment body (alternative to the positional argument) + **--login, -l**="": Use a different Gitea Login. Optional **--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json) @@ -1939,6 +1941,8 @@ Manage comments on issues and pull requests Add a comment to an issue or pull request +**--description, -d**="": comment body (alternative to the positional argument) + **--login, -l**="": Use a different Gitea Login. Optional **--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json) @@ -1967,6 +1971,8 @@ List comments on an issue or pull request Edit the body of an existing comment +**--description, -d**="": new comment body (alternative to the positional argument) + **--login, -l**="": Use a different Gitea Login. Optional **--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)