mirror of
https://gitea.com/gitea/tea.git
synced 2026-06-05 18:58:43 +02:00
Merge branch 'main' into lunny/add_reply_code_review
This commit is contained in:
+1
-1
@@ -41,7 +41,7 @@ func App() *cli.Command {
|
||||
&CmdActions,
|
||||
&CmdWiki,
|
||||
&CmdWebhooks,
|
||||
&CmdAddComment,
|
||||
&CmdComments,
|
||||
|
||||
&CmdOpen,
|
||||
&CmdNotifications,
|
||||
|
||||
+20
-85
@@ -4,93 +4,28 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
stdctx "context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"gitea.dev/tea/cmd/comments"
|
||||
|
||||
gitea "gitea.dev/sdk"
|
||||
|
||||
"gitea.dev/tea/cmd/flags"
|
||||
"gitea.dev/tea/modules/config"
|
||||
"gitea.dev/tea/modules/context"
|
||||
"gitea.dev/tea/modules/interact"
|
||||
"gitea.dev/tea/modules/print"
|
||||
"gitea.dev/tea/modules/theme"
|
||||
"gitea.dev/tea/modules/utils"
|
||||
|
||||
"charm.land/huh/v2"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
// CmdAddComment is the main command to operate with notifications
|
||||
var CmdAddComment = cli.Command{
|
||||
Name: "comment",
|
||||
Aliases: []string{"c"},
|
||||
Category: catEntities,
|
||||
Usage: "Add a comment to an issue / pr",
|
||||
Description: "Add a comment to an issue / pr",
|
||||
ArgsUsage: "<issue / pr index> [<comment body>]",
|
||||
Action: runAddComment,
|
||||
Flags: flags.AllDefaultFlags,
|
||||
}
|
||||
|
||||
func runAddComment(requestCtx stdctx.Context, cmd *cli.Command) error {
|
||||
ctx, err := context.InitCommand(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
args := ctx.Args()
|
||||
if args.Len() == 0 {
|
||||
return fmt.Errorf("please specify issue / pr index")
|
||||
}
|
||||
|
||||
idx, err := utils.ArgToIndex(ctx.Args().First())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
body := strings.Join(ctx.Args().Tail(), " ")
|
||||
if interact.IsStdinPiped() {
|
||||
// custom solution until https://github.com/AlecAivazis/survey/issues/328 is fixed
|
||||
if bodyStdin, err := io.ReadAll(ctx.Reader); err != nil {
|
||||
return err
|
||||
} else if len(bodyStdin) != 0 {
|
||||
body = strings.Join([]string{body, string(bodyStdin)}, "\n\n")
|
||||
}
|
||||
} else if len(body) == 0 {
|
||||
if err := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewText().
|
||||
Title("Comment(markdown):").
|
||||
ExternalEditor(config.GetPreferences().Editor).
|
||||
EditorExtension("md").
|
||||
Value(&body),
|
||||
),
|
||||
).WithTheme(theme.GetTheme()).
|
||||
Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(body) == 0 {
|
||||
return errors.New("no comment content provided")
|
||||
}
|
||||
|
||||
client := ctx.Login.Client()
|
||||
comment, _, err := client.Issues.CreateIssueComment(requestCtx, ctx.Owner, ctx.Repo, idx, gitea.CreateIssueCommentOption{
|
||||
Body: body,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
print.Comment(comment)
|
||||
|
||||
return nil
|
||||
// CmdComments is the top-level command for managing comments on issues and pull requests.
|
||||
var CmdComments = cli.Command{
|
||||
Name: "comments",
|
||||
Aliases: []string{"comment", "c"},
|
||||
Category: catEntities,
|
||||
Usage: "Manage comments on issues and pull requests",
|
||||
Description: `Manage comments on issues and pull requests.
|
||||
|
||||
When invoked with an issue/PR index and an optional body, behaves like 'tea comments add'
|
||||
(this preserves the historical 'tea comment <idx> "<body>"' shorthand).`,
|
||||
ArgsUsage: "<issue / pr index> [<comment body>]",
|
||||
Action: comments.RunCommentsAdd,
|
||||
Flags: comments.CmdCommentsAdd.Flags,
|
||||
Commands: []*cli.Command{
|
||||
&comments.CmdCommentsAdd,
|
||||
&comments.CmdCommentsList,
|
||||
&comments.CmdCommentsEdit,
|
||||
&comments.CmdCommentsDelete,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package comments
|
||||
|
||||
import (
|
||||
stdctx "context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
gitea "gitea.dev/sdk"
|
||||
|
||||
"gitea.dev/tea/cmd/flags"
|
||||
"gitea.dev/tea/modules/config"
|
||||
"gitea.dev/tea/modules/context"
|
||||
"gitea.dev/tea/modules/interact"
|
||||
"gitea.dev/tea/modules/print"
|
||||
"gitea.dev/tea/modules/theme"
|
||||
"gitea.dev/tea/modules/utils"
|
||||
|
||||
"charm.land/huh/v2"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
// CmdCommentsAdd adds a comment to an issue or pull request.
|
||||
var CmdCommentsAdd = cli.Command{
|
||||
Name: "add",
|
||||
Aliases: []string{"a"},
|
||||
Usage: "Add a comment to an issue or pull request",
|
||||
Description: "Add a comment to an issue or pull request.",
|
||||
ArgsUsage: "<issue / pr index> [<comment body>]",
|
||||
Action: RunCommentsAdd,
|
||||
Flags: flags.AllDefaultFlags,
|
||||
}
|
||||
|
||||
// RunCommentsAdd creates a new comment.
|
||||
func RunCommentsAdd(requestCtx stdctx.Context, cmd *cli.Command) error {
|
||||
ctx, err := context.InitCommand(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ctx.Args().Len() == 0 {
|
||||
return fmt.Errorf("please specify issue / pr index")
|
||||
}
|
||||
|
||||
idx, err := utils.ArgToIndex(ctx.Args().First())
|
||||
if err != nil {
|
||||
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 {
|
||||
if err := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewText().
|
||||
Title("Comment(markdown):").
|
||||
ExternalEditor(config.GetPreferences().Editor).
|
||||
EditorExtension("md").
|
||||
Value(&body),
|
||||
),
|
||||
).WithTheme(theme.GetTheme()).
|
||||
Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(body) == 0 {
|
||||
return errors.New("no comment content provided")
|
||||
}
|
||||
|
||||
client := ctx.Login.Client()
|
||||
comment, _, err := client.Issues.CreateIssueComment(requestCtx, ctx.Owner, ctx.Repo, idx, gitea.CreateIssueCommentOption{
|
||||
Body: body,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
print.Comment(comment)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package comments
|
||||
|
||||
import (
|
||||
stdctx "context"
|
||||
"fmt"
|
||||
|
||||
"gitea.dev/tea/cmd/flags"
|
||||
"gitea.dev/tea/modules/context"
|
||||
"gitea.dev/tea/modules/utils"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
// CmdCommentsDelete deletes one or more comments by ID.
|
||||
var CmdCommentsDelete = cli.Command{
|
||||
Name: "delete",
|
||||
Aliases: []string{"rm"},
|
||||
Usage: "Delete one or more comments by ID",
|
||||
Description: "Delete one or more comments by their comment ID. Use 'tea comments list <issue>' to find IDs.",
|
||||
ArgsUsage: "<comment id> [<comment id>...]",
|
||||
Action: RunCommentsDelete,
|
||||
Flags: flags.AllDefaultFlags,
|
||||
}
|
||||
|
||||
// RunCommentsDelete removes one or more comments.
|
||||
func RunCommentsDelete(requestCtx stdctx.Context, cmd *cli.Command) error {
|
||||
ctx, err := context.InitCommand(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ctx.Args().Len() == 0 {
|
||||
return fmt.Errorf("please specify at least one comment id")
|
||||
}
|
||||
|
||||
ids, err := utils.ArgsToIndices(ctx.Args().Slice())
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid comment id: %s", err)
|
||||
}
|
||||
|
||||
client := ctx.Login.Client()
|
||||
for _, id := range ids {
|
||||
if _, err := client.Issues.DeleteIssueComment(requestCtx, ctx.Owner, ctx.Repo, id); err != nil {
|
||||
return fmt.Errorf("could not delete comment %d: %s", id, err)
|
||||
}
|
||||
fmt.Printf("Deleted comment %d\n", id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package comments
|
||||
|
||||
import (
|
||||
stdctx "context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
gitea "gitea.dev/sdk"
|
||||
|
||||
"gitea.dev/tea/cmd/flags"
|
||||
"gitea.dev/tea/modules/config"
|
||||
"gitea.dev/tea/modules/context"
|
||||
"gitea.dev/tea/modules/interact"
|
||||
"gitea.dev/tea/modules/print"
|
||||
"gitea.dev/tea/modules/theme"
|
||||
"gitea.dev/tea/modules/utils"
|
||||
|
||||
"charm.land/huh/v2"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
// CmdCommentsEdit edits an existing comment.
|
||||
var CmdCommentsEdit = cli.Command{
|
||||
Name: "edit",
|
||||
Aliases: []string{"e"},
|
||||
Usage: "Edit the body of an existing comment",
|
||||
Description: `Edit the body of an existing comment by its comment ID. Use 'tea comments list <issue>' 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.`,
|
||||
ArgsUsage: "<comment id> [<new body>]",
|
||||
Action: RunCommentsEdit,
|
||||
Flags: flags.AllDefaultFlags,
|
||||
}
|
||||
|
||||
// RunCommentsEdit updates the body of an existing comment.
|
||||
func RunCommentsEdit(requestCtx stdctx.Context, cmd *cli.Command) error {
|
||||
ctx, err := context.InitCommand(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ctx.Args().Len() == 0 {
|
||||
return fmt.Errorf("please specify comment id")
|
||||
}
|
||||
|
||||
id, err := utils.ArgToIndex(ctx.Args().First())
|
||||
if err != nil {
|
||||
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 {
|
||||
// Fetch current body to pre-populate the editor.
|
||||
client := ctx.Login.Client()
|
||||
current, _, fetchErr := client.Issues.GetIssueComment(requestCtx, ctx.Owner, ctx.Repo, id)
|
||||
if fetchErr != nil {
|
||||
return fmt.Errorf("could not fetch comment %d: %s", id, fetchErr)
|
||||
}
|
||||
body = current.Body
|
||||
if err := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewText().
|
||||
Title("Comment(markdown):").
|
||||
ExternalEditor(config.GetPreferences().Editor).
|
||||
EditorExtension("md").
|
||||
Value(&body),
|
||||
),
|
||||
).WithTheme(theme.GetTheme()).
|
||||
Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(body) == 0 {
|
||||
return errors.New("no comment content provided")
|
||||
}
|
||||
|
||||
client := ctx.Login.Client()
|
||||
comment, _, err := client.Issues.EditIssueComment(requestCtx, ctx.Owner, ctx.Repo, id, gitea.EditIssueCommentOption{
|
||||
Body: body,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
print.Comment(comment)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package comments
|
||||
|
||||
import (
|
||||
stdctx "context"
|
||||
"fmt"
|
||||
|
||||
gitea "gitea.dev/sdk"
|
||||
|
||||
"gitea.dev/tea/cmd/flags"
|
||||
"gitea.dev/tea/modules/context"
|
||||
"gitea.dev/tea/modules/print"
|
||||
"gitea.dev/tea/modules/utils"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
// CmdCommentsList lists comments on an issue or pull request.
|
||||
var CmdCommentsList = cli.Command{
|
||||
Name: "list",
|
||||
Aliases: []string{"ls"},
|
||||
Usage: "List comments on an issue or pull request",
|
||||
Description: "List comments on an issue or pull request. Comment IDs returned here are the IDs accepted by 'tea comments edit' and 'tea comments delete'.",
|
||||
ArgsUsage: "<issue / pr index>",
|
||||
Action: RunCommentsList,
|
||||
Flags: append([]cli.Flag{
|
||||
&flags.PaginationPageFlag,
|
||||
&flags.PaginationLimitFlag,
|
||||
}, flags.AllDefaultFlags...),
|
||||
}
|
||||
|
||||
// RunCommentsList lists comments on the given issue/PR.
|
||||
func RunCommentsList(requestCtx stdctx.Context, cmd *cli.Command) error {
|
||||
ctx, err := context.InitCommand(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ctx.Args().Len() == 0 {
|
||||
return fmt.Errorf("please specify issue / pr index")
|
||||
}
|
||||
|
||||
idx, err := utils.ArgToIndex(ctx.Args().First())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client := ctx.Login.Client()
|
||||
comments, _, err := client.Issues.ListIssueComments(requestCtx, ctx.Owner, ctx.Repo, idx, gitea.ListIssueCommentOptions{
|
||||
ListOptions: flags.GetListOptions(cmd),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return print.CommentsList(comments, ctx.Output)
|
||||
}
|
||||
+19
-4
@@ -13,6 +13,7 @@ import (
|
||||
"gitea.dev/tea/modules/context"
|
||||
"gitea.dev/tea/modules/interact"
|
||||
"gitea.dev/tea/modules/task"
|
||||
"gitea.dev/tea/modules/utils"
|
||||
)
|
||||
|
||||
// CmdPullsCreate creates a pull request
|
||||
@@ -46,6 +47,10 @@ var CmdPullsCreate = cli.Command{
|
||||
Name: "topic",
|
||||
Usage: "Topic name for agit flow pull request",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "draft",
|
||||
Usage: "Create as a draft (prepends \"WIP: \" to the title; Gitea treats WIP-prefixed PRs as drafts)",
|
||||
},
|
||||
}, flags.IssuePRCreateFlags...),
|
||||
}
|
||||
|
||||
@@ -54,10 +59,16 @@ func runPullsCreate(requestCtx stdctx.Context, cmd *cli.Command) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ctx.Ensure(context.CtxRequirement{
|
||||
LocalRepo: true,
|
||||
RemoteRepo: true,
|
||||
}); err != nil {
|
||||
// Interactive mode and head-branch defaulting both need a local repo.
|
||||
// When --head is given explicitly the user can target a cross-fork PR
|
||||
// from outside a working tree (e.g. with --repo <owner>/<repo>);
|
||||
// task.CreatePull only consults ctx.LocalRepo when head is empty.
|
||||
needsLocalRepo := ctx.IsInteractiveMode() || len(ctx.String("head")) == 0
|
||||
requirement := context.CtxRequirement{RemoteRepo: true}
|
||||
if needsLocalRepo {
|
||||
requirement.LocalRepo = true
|
||||
}
|
||||
if err := ctx.Ensure(requirement); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -75,6 +86,10 @@ func runPullsCreate(requestCtx stdctx.Context, cmd *cli.Command) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if ctx.Bool("draft") {
|
||||
opts.Title = utils.AddDraftPrefix(opts.Title)
|
||||
}
|
||||
|
||||
if ctx.Bool("agit") {
|
||||
return task.CreateAgitFlowPull(
|
||||
requestCtx,
|
||||
|
||||
@@ -18,6 +18,42 @@ import (
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
// applyDraftFlag mutates opts.Title according to --draft / --ready.
|
||||
// If a flag is set but --title isn't, it fetches the current title from the server.
|
||||
// Returns an error if both --draft and --ready are set, or on a server error.
|
||||
func applyDraftFlag(requestCtx stdctx.Context, ctx *context.TeaContext, client *gitea.Client, idx int64, opts *task.EditIssueOption) error {
|
||||
draft := ctx.Bool("draft")
|
||||
ready := ctx.Bool("ready")
|
||||
if !draft && !ready {
|
||||
return nil
|
||||
}
|
||||
if draft && ready {
|
||||
return fmt.Errorf("--draft and --ready are mutually exclusive")
|
||||
}
|
||||
|
||||
var current string
|
||||
if opts.Title != nil {
|
||||
current = *opts.Title
|
||||
} else {
|
||||
pr, _, err := client.PullRequests.GetPullRequest(requestCtx, ctx.Owner, ctx.Repo, idx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not fetch pull request #%d: %s", idx, err)
|
||||
}
|
||||
current = pr.Title
|
||||
}
|
||||
|
||||
var next string
|
||||
if draft {
|
||||
next = utils.AddDraftPrefix(current)
|
||||
} else {
|
||||
next = utils.StripDraftPrefix(current)
|
||||
}
|
||||
if next != current || opts.Title != nil {
|
||||
opts.Title = &next
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CmdPullsEdit is the subcommand of pulls to edit pull requests
|
||||
var CmdPullsEdit = cli.Command{
|
||||
Name: "edit",
|
||||
@@ -37,6 +73,14 @@ use an empty string (eg. --milestone "").`,
|
||||
Name: "remove-reviewers",
|
||||
Usage: "Comma-separated list of usernames to remove from reviewers",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "draft",
|
||||
Usage: "Mark as draft by prepending \"WIP: \" to the title (idempotent)",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "ready",
|
||||
Usage: "Mark as ready for review by stripping any leading \"WIP: \" or \"[WIP]\" prefix",
|
||||
},
|
||||
),
|
||||
}
|
||||
|
||||
@@ -72,6 +116,9 @@ func runPullsEdit(requestCtx stdctx.Context, cmd *cli.Command) error {
|
||||
|
||||
client := ctx.Login.Client()
|
||||
for _, opts.Index = range indices {
|
||||
if err := applyDraftFlag(requestCtx, ctx, client, opts.Index, opts); err != nil {
|
||||
return err
|
||||
}
|
||||
pr, err := task.EditPull(requestCtx, ctx, client, *opts)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
+60
-2
@@ -375,6 +375,8 @@ Create a pull-request
|
||||
|
||||
**--description, -d**="":
|
||||
|
||||
**--draft**: Create as a draft (prepends "WIP: " to the title; Gitea treats WIP-prefixed PRs as drafts)
|
||||
|
||||
**--head**="": Branch name of the PR source (default is current one). To specify a different head repo, use <user>:<branch>
|
||||
|
||||
**--labels, -L**="": Comma-separated list of labels to assign
|
||||
@@ -431,10 +433,14 @@ Edit one or more pull requests
|
||||
|
||||
**--description, -d**="":
|
||||
|
||||
**--draft**: Mark as draft by prepending "WIP: " to the title (idempotent)
|
||||
|
||||
**--login, -l**="": Use a different Gitea Login. Optional
|
||||
|
||||
**--milestone, -m**="": Milestone to assign
|
||||
|
||||
**--ready**: Mark as ready for review by stripping any leading "WIP: " or "[WIP]" prefix
|
||||
|
||||
**--referenced-version, -v**="": commit-hash or tag name to assign
|
||||
|
||||
**--remote, -R**="": Discover Gitea login from remote. Optional
|
||||
@@ -1909,9 +1915,61 @@ Update a webhook
|
||||
|
||||
**--url**="": webhook URL
|
||||
|
||||
## comment, c
|
||||
## comments, comment, c
|
||||
|
||||
Add a comment to an issue / pr
|
||||
Manage comments on issues and pull requests
|
||||
|
||||
**--login, -l**="": Use a different Gitea Login. Optional
|
||||
|
||||
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
|
||||
|
||||
**--remote, -R**="": Discover Gitea login from remote. Optional
|
||||
|
||||
**--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional
|
||||
|
||||
### add, a
|
||||
|
||||
Add a comment to an issue or pull request
|
||||
|
||||
**--login, -l**="": Use a different Gitea Login. Optional
|
||||
|
||||
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
|
||||
|
||||
**--remote, -R**="": Discover Gitea login from remote. Optional
|
||||
|
||||
**--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional
|
||||
|
||||
### list, ls
|
||||
|
||||
List comments on an issue or pull request
|
||||
|
||||
**--limit, --lm**="": specify limit of items per page (default: 30)
|
||||
|
||||
**--login, -l**="": Use a different Gitea Login. Optional
|
||||
|
||||
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
|
||||
|
||||
**--page, -p**="": specify page (default: 1)
|
||||
|
||||
**--remote, -R**="": Discover Gitea login from remote. Optional
|
||||
|
||||
**--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional
|
||||
|
||||
### edit, e
|
||||
|
||||
Edit the body of an existing comment
|
||||
|
||||
**--login, -l**="": Use a different Gitea Login. Optional
|
||||
|
||||
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
|
||||
|
||||
**--remote, -R**="": Discover Gitea login from remote. Optional
|
||||
|
||||
**--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional
|
||||
|
||||
### delete, rm
|
||||
|
||||
Delete one or more comments by ID
|
||||
|
||||
**--login, -l**="": Use a different Gitea Login. Optional
|
||||
|
||||
|
||||
@@ -29,6 +29,56 @@ func Comments(comments []*gitea.Comment) {
|
||||
), baseURL)
|
||||
}
|
||||
|
||||
// CommentsList prints comments in tabular form, including IDs so they can be
|
||||
// passed to 'tea comments edit' / 'tea comments delete'.
|
||||
func CommentsList(comments []*gitea.Comment, output string) error {
|
||||
if len(comments) == 0 {
|
||||
fmt.Println("No comments found")
|
||||
return nil
|
||||
}
|
||||
|
||||
t := tableWithHeader(
|
||||
"ID",
|
||||
"Author",
|
||||
"Created",
|
||||
"Updated",
|
||||
"Body",
|
||||
)
|
||||
|
||||
for _, c := range comments {
|
||||
updated := ""
|
||||
if c.Updated.After(c.Created) {
|
||||
updated = FormatTime(c.Updated, false)
|
||||
}
|
||||
t.addRow(
|
||||
fmt.Sprintf("%d", c.ID),
|
||||
"@"+c.Poster.UserName,
|
||||
FormatTime(c.Created, false),
|
||||
updated,
|
||||
summarizeBody(c.Body),
|
||||
)
|
||||
}
|
||||
|
||||
return t.print(output)
|
||||
}
|
||||
|
||||
func summarizeBody(body string) string {
|
||||
const max = 80
|
||||
var b strings.Builder
|
||||
for _, r := range body {
|
||||
if r == '\n' || r == '\r' {
|
||||
b.WriteByte(' ')
|
||||
} else {
|
||||
b.WriteRune(r)
|
||||
}
|
||||
}
|
||||
s := b.String()
|
||||
if len(s) > max {
|
||||
return s[:max-1] + "…"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Comment renders a comment to stdout
|
||||
func Comment(c *gitea.Comment) {
|
||||
_ = outputMarkdown(formatComment(c), getRepoURL(c.HTMLURL))
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package utils
|
||||
|
||||
import "regexp"
|
||||
|
||||
// draftPrefixRe matches the prefixes Gitea recognizes as marking a pull
|
||||
// request as a draft: a leading "WIP:" or "[WIP]" (case-insensitive),
|
||||
// followed by any whitespace.
|
||||
var draftPrefixRe = regexp.MustCompile(`(?i)^(wip:\s*|\[wip\]\s*)`)
|
||||
|
||||
// HasDraftPrefix reports whether title already starts with a draft marker.
|
||||
func HasDraftPrefix(title string) bool {
|
||||
return draftPrefixRe.MatchString(title)
|
||||
}
|
||||
|
||||
// AddDraftPrefix returns title with a "WIP: " prefix, or title unchanged
|
||||
// if it already carries a recognized draft prefix.
|
||||
func AddDraftPrefix(title string) string {
|
||||
if HasDraftPrefix(title) {
|
||||
return title
|
||||
}
|
||||
return "WIP: " + title
|
||||
}
|
||||
|
||||
// StripDraftPrefix returns title with any recognized leading draft prefix
|
||||
// removed, or title unchanged if no prefix is present.
|
||||
func StripDraftPrefix(title string) string {
|
||||
return draftPrefixRe.ReplaceAllString(title, "")
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package utils
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestDraftPrefix(t *testing.T) {
|
||||
cases := []struct {
|
||||
in string
|
||||
has bool
|
||||
stripped string
|
||||
withDraft string
|
||||
}{
|
||||
{"plain title", false, "plain title", "WIP: plain title"},
|
||||
{"WIP: already", true, "already", "WIP: already"},
|
||||
{"wip: lowercase", true, "lowercase", "wip: lowercase"},
|
||||
{"[WIP] bracketed", true, "bracketed", "[WIP] bracketed"},
|
||||
{"[wip] extra space", true, "extra space", "[wip] extra space"},
|
||||
{"Draft: not recognized", false, "Draft: not recognized", "WIP: Draft: not recognized"},
|
||||
{"", false, "", "WIP: "},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := HasDraftPrefix(c.in); got != c.has {
|
||||
t.Errorf("HasDraftPrefix(%q) = %v, want %v", c.in, got, c.has)
|
||||
}
|
||||
if got := StripDraftPrefix(c.in); got != c.stripped {
|
||||
t.Errorf("StripDraftPrefix(%q) = %q, want %q", c.in, got, c.stripped)
|
||||
}
|
||||
if got := AddDraftPrefix(c.in); got != c.withDraft {
|
||||
t.Errorf("AddDraftPrefix(%q) = %q, want %q", c.in, got, c.withDraft)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.dev/sdk"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestCommentsCommandLifecycle exercises `tea comments add`, `list`, `edit`,
|
||||
// and `delete` end-to-end against a live Gitea instance. It creates a throwaway
|
||||
// repo with one issue, then drives the comment subcommands and verifies state
|
||||
// via the SDK after each step.
|
||||
func TestCommentsCommandLifecycle(t *testing.T) {
|
||||
login := createIntegrationLogin(t)
|
||||
client := login.Client()
|
||||
ctx := context.Background()
|
||||
|
||||
repoName := fmt.Sprintf("tea-comments-integration-%d", time.Now().UnixNano())
|
||||
_, _, err := client.CreateRepo(ctx, gitea.CreateRepoOption{Name: repoName, AutoInit: true})
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
if _, delErr := client.DeleteRepo(ctx, login.User, repoName); delErr != nil {
|
||||
t.Logf("failed to delete integration test repo %q: %v", repoName, delErr)
|
||||
}
|
||||
})
|
||||
|
||||
repoSlug := fmt.Sprintf("%s/%s", login.User, repoName)
|
||||
|
||||
issue, _, err := client.Issues.CreateIssue(ctx, login.User, repoName, gitea.CreateIssueOption{
|
||||
Title: "comment test issue",
|
||||
Body: "scratch issue for tea comment integration tests",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
issueIdx := strconv.FormatInt(issue.Index, 10)
|
||||
|
||||
// add via positional body
|
||||
addOutput := runTeaCommand(t,
|
||||
"comments", "add",
|
||||
"--login", login.Name,
|
||||
"--repo", repoSlug,
|
||||
issueIdx, "first comment",
|
||||
)
|
||||
assert.Contains(t, addOutput, "first comment")
|
||||
|
||||
// add via the historical shorthand (parent command, no "add" subcommand)
|
||||
runTeaCommand(t,
|
||||
"comments",
|
||||
"--login", login.Name,
|
||||
"--repo", repoSlug,
|
||||
issueIdx, "second comment",
|
||||
)
|
||||
|
||||
// list should now show both, in tabular form with IDs
|
||||
listOutput := runTeaCommand(t,
|
||||
"comments", "list",
|
||||
"--login", login.Name,
|
||||
"--repo", repoSlug,
|
||||
issueIdx,
|
||||
)
|
||||
assert.Contains(t, listOutput, "first comment")
|
||||
assert.Contains(t, listOutput, "second comment")
|
||||
|
||||
// pull the real IDs from the server so edit/delete have something to act on
|
||||
apiComments, _, err := client.Issues.ListIssueComments(ctx, login.User, repoName, issue.Index, gitea.ListIssueCommentOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, apiComments, 2)
|
||||
|
||||
var firstID, secondID int64
|
||||
for _, c := range apiComments {
|
||||
switch {
|
||||
case strings.Contains(c.Body, "first comment"):
|
||||
firstID = c.ID
|
||||
case strings.Contains(c.Body, "second comment"):
|
||||
secondID = c.ID
|
||||
}
|
||||
}
|
||||
require.NotZero(t, firstID, "first comment id not found in listing")
|
||||
require.NotZero(t, secondID, "second comment id not found in listing")
|
||||
|
||||
// edit the first comment via positional body
|
||||
editOutput := runTeaCommand(t,
|
||||
"comments", "edit",
|
||||
"--login", login.Name,
|
||||
"--repo", repoSlug,
|
||||
strconv.FormatInt(firstID, 10), "first comment (edited)",
|
||||
)
|
||||
assert.Contains(t, editOutput, "first comment (edited)")
|
||||
|
||||
edited, _, err := client.Issues.GetIssueComment(ctx, login.User, repoName, firstID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "first comment (edited)", edited.Body)
|
||||
|
||||
// delete both comments in one batch
|
||||
deleteOutput := runTeaCommand(t,
|
||||
"comments", "delete",
|
||||
"--login", login.Name,
|
||||
"--repo", repoSlug,
|
||||
strconv.FormatInt(firstID, 10),
|
||||
strconv.FormatInt(secondID, 10),
|
||||
)
|
||||
assert.Contains(t, deleteOutput, fmt.Sprintf("Deleted comment %d", firstID))
|
||||
assert.Contains(t, deleteOutput, fmt.Sprintf("Deleted comment %d", secondID))
|
||||
|
||||
remaining, _, err := client.Issues.ListIssueComments(ctx, login.User, repoName, issue.Index, gitea.ListIssueCommentOptions{})
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, remaining, "expected all comments to be deleted")
|
||||
}
|
||||
Reference in New Issue
Block a user