Add reply to code review

This commit is contained in:
Lunny Xiao
2026-05-05 21:21:44 -07:00
parent e686e8d0bd
commit 6af01bb13d
8 changed files with 181 additions and 17 deletions

View File

@@ -77,6 +77,7 @@ var CmdPulls = cli.Command{
&pulls.CmdPullsApprove,
&pulls.CmdPullsReject,
&pulls.CmdPullsMerge,
&pulls.CmdPullsReply,
&pulls.CmdPullsReviewComments,
&pulls.CmdPullsResolve,
&pulls.CmdPullsUnresolve,

29
cmd/pulls/reply.go Normal file
View File

@@ -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: "<pull index> <comment id> [<reply>]",
Action: func(_ stdctx.Context, cmd *cli.Command) error {
ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
return runPullReviewReply(ctx)
},
Flags: flags.AllDefaultFlags,
}

55
cmd/pulls/reply_test.go Normal file
View File

@@ -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
}
})
}
}

View File

@@ -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
}

View File

@@ -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

2
go.mod
View File

@@ -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

18
go.sum
View File

@@ -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=

View File

@@ -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()