From 6af01bb13d6a4c0391e864a26e30fe9a40e3b877 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Tue, 5 May 2026 21:21:44 -0700 Subject: [PATCH] Add reply to code review --- cmd/pulls.go | 1 + cmd/pulls/reply.go | 29 +++++++++++++ cmd/pulls/reply_test.go | 55 ++++++++++++++++++++++++ cmd/pulls/review_helpers.go | 66 +++++++++++++++++++++++++++++ docs/CLI.md | 12 ++++++ go.mod | 2 +- go.sum | 18 +------- modules/task/pull_review_comment.go | 15 +++++++ 8 files changed, 181 insertions(+), 17 deletions(-) create mode 100644 cmd/pulls/reply.go create mode 100644 cmd/pulls/reply_test.go diff --git a/cmd/pulls.go b/cmd/pulls.go index 5abbc71..bd28d16 100644 --- a/cmd/pulls.go +++ b/cmd/pulls.go @@ -77,6 +77,7 @@ var CmdPulls = cli.Command{ &pulls.CmdPullsApprove, &pulls.CmdPullsReject, &pulls.CmdPullsMerge, + &pulls.CmdPullsReply, &pulls.CmdPullsReviewComments, &pulls.CmdPullsResolve, &pulls.CmdPullsUnresolve, diff --git a/cmd/pulls/reply.go b/cmd/pulls/reply.go new file mode 100644 index 0000000..d3608a4 --- /dev/null +++ b/cmd/pulls/reply.go @@ -0,0 +1,29 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package pulls + +import ( + stdctx "context" + + "code.gitea.io/tea/cmd/flags" + "code.gitea.io/tea/modules/context" + + "github.com/urfave/cli/v3" +) + +// CmdPullsReply replies to a review comment on a pull request. +var CmdPullsReply = cli.Command{ + Name: "reply", + Usage: "Reply to a pull request review comment", + Description: "Reply to a pull request review comment", + ArgsUsage: " []", + Action: func(_ stdctx.Context, cmd *cli.Command) error { + ctx, err := context.InitCommand(cmd) + if err != nil { + return err + } + return runPullReviewReply(ctx) + }, + Flags: flags.AllDefaultFlags, +} diff --git a/cmd/pulls/reply_test.go b/cmd/pulls/reply_test.go new file mode 100644 index 0000000..2e8ccb2 --- /dev/null +++ b/cmd/pulls/reply_test.go @@ -0,0 +1,55 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package pulls + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestReply(t *testing.T) { + tests := []struct { + name string + args []string + wantErr bool + errContains string + }{ + { + name: "no arguments", + args: []string{}, + wantErr: true, + errContains: "pull request index and comment ID are required", + }, + { + name: "missing comment id", + args: []string{"1"}, + wantErr: true, + errContains: "pull request index and comment ID are required", + }, + { + name: "pull index and comment id", + args: []string{"1", "2"}, + wantErr: true, + errContains: "no reply content provided", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := CmdPullsReply + args := append([]string{"reply"}, tt.args...) + args = append(args, "--repo", "user/repo") + err := cmd.Run(context.Background(), args) + if tt.wantErr { + assert.Error(t, err) + if tt.errContains != "" { + assert.Contains(t, err.Error(), tt.errContains) + } + return + } + }) + } +} diff --git a/cmd/pulls/review_helpers.go b/cmd/pulls/review_helpers.go index ba844a6..94b2c97 100644 --- a/cmd/pulls/review_helpers.go +++ b/cmd/pulls/review_helpers.go @@ -4,13 +4,20 @@ package pulls import ( + "errors" "fmt" + "io" "strings" "code.gitea.io/sdk/gitea" + "code.gitea.io/tea/modules/config" "code.gitea.io/tea/modules/context" + "code.gitea.io/tea/modules/interact" "code.gitea.io/tea/modules/task" + "code.gitea.io/tea/modules/theme" "code.gitea.io/tea/modules/utils" + + "charm.land/huh/v2" ) // runPullReview handles the common logic for approving/rejecting pull requests @@ -58,3 +65,62 @@ func runResolveComment(ctx *context.TeaContext, action func(*context.TeaContext, return action(ctx, commentID) } + +// runPullReviewReply handles replying to a specific review comment on a pull request. +func runPullReviewReply(ctx *context.TeaContext) error { + if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil { + return err + } + + if ctx.Args().Len() < 2 { + return fmt.Errorf("pull request index and comment ID are required") + } + + idx, err := utils.ArgToIndex(ctx.Args().First()) + if err != nil { + return err + } + + commentID, err := utils.ArgToIndex(ctx.Args().Get(1)) + if err != nil { + return err + } + + body, err := getCommentBody(ctx, ctx.Args().Slice()[2:], "Reply(markdown):", "reply") + if err != nil { + return err + } + + return task.ReplyToPullReviewComment(ctx, idx, commentID, body) +} + +func getCommentBody(ctx *context.TeaContext, extraArgs []string, promptTitle, noun string) (string, error) { + body := strings.Join(extraArgs, " ") + if interact.IsStdinPiped() { + bodyStdin, err := io.ReadAll(ctx.Reader) + if err != nil { + return "", err + } + 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(promptTitle). + ExternalEditor(config.GetPreferences().Editor). + EditorExtension("md"). + Value(&body), + ), + ).WithTheme(theme.GetTheme()).Run(); err != nil { + return "", err + } + } + + if len(strings.TrimSpace(body)) == 0 { + return "", errors.New("no " + noun + " content provided") + } + + return body, nil +} diff --git a/docs/CLI.md b/docs/CLI.md index 94474b8..feeff7d 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -483,6 +483,18 @@ Merge a pull request **--title, -t**="": Merge commit title +### reply + +Reply to a pull request review 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 + ### review-comments, rc List review comments on a pull request diff --git a/go.mod b/go.mod index 178ac45..c5ef4c3 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( charm.land/huh/v2 v2.0.3 charm.land/lipgloss/v2 v2.0.3 code.gitea.io/gitea-vet v0.2.3 - code.gitea.io/sdk/gitea v0.24.1 + code.gitea.io/sdk/gitea v0.25.0 gitea.com/noerw/unidiff-comments v0.0.0-20220822113322-50f4daa0e35c github.com/adrg/xdg v0.5.3 github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de diff --git a/go.sum b/go.sum index 2bc49ac..6cdf58f 100644 --- a/go.sum +++ b/go.sum @@ -6,14 +6,12 @@ charm.land/glamour/v2 v2.0.0 h1:IDBoqLEy7Hdpb9VOXN+khLP/XSxtJy1VsHuW/yF87+U= charm.land/glamour/v2 v2.0.0/go.mod h1:kjq9WB0s8vuUYZNYey2jp4Lgd9f4cKdzAw88FZtpj/w= charm.land/huh/v2 v2.0.3 h1:2cJsMqEPwSywGHvdlKsJyQKPtSJLVnFKyFbsYZTlLkU= charm.land/huh/v2 v2.0.3/go.mod h1:93eEveeeqn47MwiC3tf+2atZ2l7Is88rAtmZNZ8x9Wc= -charm.land/lipgloss/v2 v2.0.2 h1:xFolbF8JdpNkM2cEPTfXEcW1p6NRzOWTSamRfYEw8cs= -charm.land/lipgloss/v2 v2.0.2/go.mod h1:KjPle2Qd3YmvP1KL5OMHiHysGcNwq6u83MUjYkFvEkM= charm.land/lipgloss/v2 v2.0.3 h1:yM2zJ4Cf5Y51b7RHIwioil4ApI/aypFXXVHSwlM6RzU= charm.land/lipgloss/v2 v2.0.3/go.mod h1:7myLU9iG/3xluAWzpY/fSxYYHCgoKTie7laxk6ATwXA= code.gitea.io/gitea-vet v0.2.3 h1:gdFmm6WOTM65rE8FUBTRzeQZYzXePKSSB1+r574hWwI= code.gitea.io/gitea-vet v0.2.3/go.mod h1:zcNbT/aJEmivCAhfmkHOlT645KNOf9W2KnkLgFjGGfE= -code.gitea.io/sdk/gitea v0.24.1 h1:hpaqcdGcBmfMpV7JSbBJVwE99qo+WqGreJYKrDKEyW8= -code.gitea.io/sdk/gitea v0.24.1/go.mod h1:5/77BL3sHneCMEiZaMT9lfTvnnibsYxyO48mceCF3qA= +code.gitea.io/sdk/gitea v0.25.0 h1:wSJlL0Qv+ODY2OdF0L7fwt86wgf1C/0g3xIXZ6eC5zI= +code.gitea.io/sdk/gitea v0.25.0/go.mod h1:uDFWYBU8dgZsgOHwe6C/6olxvf8FHguNB3wW1i83fgg= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= gitea.com/noerw/unidiff-comments v0.0.0-20220822113322-50f4daa0e35c h1:8fTkq2UaVkLHZCF+iB4wTxINmVAToe2geZGayk9LMbA= @@ -57,8 +55,6 @@ github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q= github.com/charmbracelet/ultraviolet v0.0.0-20260330092749-0f94982c930b h1:ASDO9RT6SNKTQN87jO2bRfxHFJq8cgeYdFzivY2gCeM= github.com/charmbracelet/ultraviolet v0.0.0-20260330092749-0f94982c930b/go.mod h1:Vo8TffMf0q7Uho/n8e6XpBZvOWtd3g39yX+9P5rRutA= -github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8= -github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ= github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI= github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ= github.com/charmbracelet/x/conpty v0.1.1 h1:s1bUxjoi7EpqiXysVtC+a8RrvPPNcNvAjfi4jxsAuEs= @@ -114,14 +110,6 @@ github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= -github.com/go-authgate/sdk-go v0.6.1 h1:oQREINU63YckTRdJ+0VBmN6ewFSMXa0D862w8624/jw= -github.com/go-authgate/sdk-go v0.6.1/go.mod h1:55PLAPuu8GDK0omOwG6lx4c+9/T6dJwZd8kecUueLEk= -github.com/go-authgate/sdk-go v0.7.0 h1:hUqUMzsDkb+l5EiL+aX2LaFon/3mbjHmxm97qHHHL3k= -github.com/go-authgate/sdk-go v0.7.0/go.mod h1:Afx/Dbyvf8pw4YeOqVEVdDW2WHhn534Sb2/TaFQktuU= -github.com/go-authgate/sdk-go v0.8.0 h1:uYJMOv//qwMEJeiFTUvXGXozEHGUOsS6zfOVXxEwat4= -github.com/go-authgate/sdk-go v0.8.0/go.mod h1:Afx/Dbyvf8pw4YeOqVEVdDW2WHhn534Sb2/TaFQktuU= -github.com/go-authgate/sdk-go v0.9.0 h1:VgQNjcKXtMONNiVf4coC/J69H78CkTt3CJ8maiQSf6Y= -github.com/go-authgate/sdk-go v0.9.0/go.mod h1:Afx/Dbyvf8pw4YeOqVEVdDW2WHhn534Sb2/TaFQktuU= github.com/go-authgate/sdk-go v0.10.0 h1:MNcfV6XSPs63SWPDdLqoJ9CFiKlXIue1RmiAbTXDAEI= github.com/go-authgate/sdk-go v0.10.0/go.mod h1:Afx/Dbyvf8pw4YeOqVEVdDW2WHhn534Sb2/TaFQktuU= github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI= @@ -132,8 +120,6 @@ github.com/go-git/go-billy/v5 v5.8.0 h1:I8hjc3LbBlXTtVuFNJuwYuMiHvQJDq1AT6u4DwDz github.com/go-git/go-billy/v5 v5.8.0/go.mod h1:RpvI/rw4Vr5QA+Z60c6d6LXH0rYJo0uD5SqfmrrheCY= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= -github.com/go-git/go-git/v5 v5.17.2 h1:B+nkdlxdYrvyFK4GPXVU8w1U+YkbsgciIR7f2sZJ104= -github.com/go-git/go-git/v5 v5.17.2/go.mod h1:pW/VmeqkanRFqR6AljLcs7EA7FbZaN5MQqO7oZADXpo= github.com/go-git/go-git/v5 v5.18.0 h1:O831KI+0PR51hM2kep6T8k+w0/LIAD490gvqMCvL5hM= github.com/go-git/go-git/v5 v5.18.0/go.mod h1:pW/VmeqkanRFqR6AljLcs7EA7FbZaN5MQqO7oZADXpo= github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= diff --git a/modules/task/pull_review_comment.go b/modules/task/pull_review_comment.go index 1dea8fd..f1e541b 100644 --- a/modules/task/pull_review_comment.go +++ b/modules/task/pull_review_comment.go @@ -54,6 +54,21 @@ func ResolvePullReviewComment(ctx *context.TeaContext, commentID int64) error { return nil } +// ReplyToPullReviewComment replies to a review comment on a pull request. +func ReplyToPullReviewComment(ctx *context.TeaContext, idx, commentID int64, body string) error { + c := ctx.Login.Client() + + comment, _, err := c.CreatePullReviewCommentReply(ctx.Owner, ctx.Repo, idx, commentID, gitea.CreatePullReviewCommentReplyOptions{ + Body: body, + }) + if err != nil { + return err + } + + fmt.Println(comment.HTMLURL) + return nil +} + // UnresolvePullReviewComment unresolves a review comment func UnresolvePullReviewComment(ctx *context.TeaContext, commentID int64) error { c := ctx.Login.Client()