Files
gitea-tea/internal/app/pull_review_lifecycle.go
T
bircni 1f4b5f868f refactor(prompts): fold Asker into Prompter and share delete confirmations
FormPrompter is the only production prompter and implements the rich
prompt API, so Ask, AskText and ConfirmDefault become the Prompter
interface and the plain Input/Confirm fallbacks go away.

Confirmations that default to no derive their non-terminal wording
from the prompt, and single deletes share deleteConfirmed for the
prompt, the request and the done line.

Signed-off-by: bircni <bircni@icloud.com>
2026-10-12 10:47:54 +02:00

214 lines
7.3 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package app
import (
"errors"
"fmt"
"strconv"
"strings"
"gitea.dev/tea/internal/clierr"
"gitea.dev/tea/internal/services"
"gitea.dev/tea/internal/terminal"
"github.com/spf13/cobra"
)
func (a *application) pullReviewSubmitCommand() *cobra.Command {
var kind reviewType
var body, file string
var useEditor bool
cmd := &cobra.Command{Use: "submit [<number> | <url> | <branch>] [<review-id>]", Short: "Submit your pending review", Annotations: pullArgumentsAnnotation(nil), Args: usageArgs(cobra.MaximumNArgs(2)), RunE: func(cmd *cobra.Command, args []string) (runErr error) {
var draft terminal.Draft
defer finishDraft(&draft, &runErr)
if err := validateBodySources(cmd); err != nil {
return err
}
state, err := kind.state()
if err != nil {
return err
}
if state == "" {
return errReviewType(cmd)
}
missingBody := clierr.Wrap(clierr.Usage, errors.New("a change request needs a body; pass --body, --body-file or --editor"))
var supplied *string
if cmd.Flags().Changed("body") || cmd.Flags().Changed("body-file") {
if body, err = a.readBody(cmd, body, file); err != nil {
return err
}
if state == "REQUEST_CHANGES" && strings.TrimSpace(body) == "" {
return missingBody
}
supplied = &body
}
var id int64
if len(args) == 2 {
if id, err = number(args[1]); err != nil {
return err
}
}
t, n, err := a.pullSelector(cmd, args[:min(len(args), 1)])
if err != nil {
return err
}
pull, err := (services.Pulls{Client: t.client, Repository: t.Repo}).View(cmd.Context(), n)
if err != nil {
return err
}
if err := ownReview(pull, t, state); err != nil {
return err
}
service := services.Reviews{Client: t.client, Repository: t.Repo}
if id == 0 {
if id, err = a.pendingReview(cmd, service, t, n, state); err != nil {
return err
}
}
if useEditor || (supplied == nil && state == "REQUEST_CHANGES") {
current, err := service.View(cmd.Context(), n, id)
if err != nil {
return err
}
supplied = &current.Body
if useEditor {
if draft, err = a.editDraft(cmd, current.Body); err != nil {
return err
}
supplied = &draft.Body
}
}
if state == "REQUEST_CHANGES" && strings.TrimSpace(*supplied) == "" {
return missingBody
}
item, err := service.Submit(cmd.Context(), n, id, services.ReviewSubmitOptions{Event: state, Body: supplied})
if err != nil {
return err
}
return a.reportReview(cmd, t, pull, item)
}}
kind.register(cmd, false)
cmd.Flags().StringVarP(&body, "body", "b", "", "Review body (default: the body of the pending review)")
cmd.Flags().StringVarP(&file, "body-file", "F", "", "Read the body from `file`, or - for stdin")
cmd.Flags().BoolVarP(&useEditor, "editor", "e", false, "Edit the body of the pending review in your editor")
cmd.Example = `# Start a pending review, then submit it as an approval
$ tea pr review 13 --pending -b "First pass" --comments-file inline.json
$ tea pr review submit 13 --approve
# Submit a pending review by its ID
$ tea pr review submit 13 21 --request-changes -b "See the inline comments"`
cmd.ValidArgsFunction = a.completeReviewArgs(false)
return cmd
}
// pendingReview finds the pending review that the account started on pull request n.
func (a *application) pendingReview(cmd *cobra.Command, service services.Reviews, t target, n int64, state string) (int64, error) {
pending, err := service.Pending(cmd.Context(), n, t.login())
if err != nil {
return 0, err
}
switch len(pending) {
case 0:
flag := map[string]string{"APPROVED": "--approve", "REQUEST_CHANGES": "--request-changes", "COMMENT": "--comment"}[state]
return 0, fmt.Errorf("no pending review by %s on %s#%d; to post a new review run: tea pr review %d %s", t.login(), t.Repo, n, n, flag)
case 1:
return pending[0].ID, nil
}
ids := make([]string, len(pending))
for i, review := range pending {
ids[i] = strconv.FormatInt(review.ID, 10)
}
return 0, clierr.Wrap(clierr.Usage, fmt.Errorf("%s has several pending reviews on %s#%d (%s); pass a review ID", t.login(), t.Repo, n, strings.Join(ids, ", ")))
}
func (a *application) pullReviewDeleteCommand() *cobra.Command {
cmd := &cobra.Command{Use: "delete <number> <review-id>", Short: "Delete a review", Annotations: pullArgumentsAnnotation(textOutput("review deletion does not return a resource")), Args: argsExact(2), RunE: func(cmd *cobra.Command, args []string) error {
t, n, id, err := a.pullAndID(cmd, args)
if err != nil {
return err
}
service := services.Reviews{Client: t.client, Repository: t.Repo}
item, err := service.View(cmd.Context(), n, id)
if err != nil {
return err
}
subject := fmt.Sprintf("review %d by %s on %s#%d", id, item.Author, t.Repo, n)
return a.deleteConfirmed(cmd, subject, func() error { return service.Delete(cmd.Context(), n, id) })
}}
cmd.Annotations["tea.confirm"] = "true"
cmd.Example = "$ tea pr review delete 13 21"
cmd.ValidArgsFunction = a.completeReviewArgs(false)
return cmd
}
func (a *application) pullReviewDismissCommand(dismiss bool) *cobra.Command {
var body, file string
var useEditor bool
use, short := "undismiss <number> <review-id>", "Restore a dismissed review"
if dismiss {
use, short = "dismiss <number> <review-id>", "Dismiss an approval or change request"
}
cmd := &cobra.Command{Use: use, Short: short, Annotations: pullArgumentsAnnotation(nil), Args: argsExact(2), RunE: func(cmd *cobra.Command, args []string) (runErr error) {
var draft terminal.Draft
defer finishDraft(&draft, &runErr)
var err error
if dismiss {
if err := validateBodySources(cmd); err != nil {
return err
}
switch {
case useEditor:
draft, err = a.editDraft(cmd, "")
body = draft.Body
case cmd.Flags().Changed("body") || cmd.Flags().Changed("body-file"):
body, err = a.readBody(cmd, body, file)
default:
_, err = a.requireInput(cmd, &body, "body", "Reason for dismissing the review", nil)
}
if err != nil {
return err
}
if strings.TrimSpace(body) == "" {
return clierr.Wrap(clierr.Usage, errors.New("a dismissal needs a reason; pass --body, --body-file or --editor"))
}
}
t, n, id, err := a.pullAndID(cmd, args)
if err != nil {
return err
}
service := services.Reviews{Client: t.client, Repository: t.Repo}
current, err := service.View(cmd.Context(), n, id)
if err != nil {
return err
}
subject := fmt.Sprintf("%s on %s#%d", reviewPossessive(current), t.Repo, n)
if dismiss {
if err := a.confirmAction(cmd, terminal.Confirmation{Prompt: "Dismiss " + subject + "?"}); err != nil {
return err
}
}
item, err := service.Dismiss(cmd.Context(), n, id, dismiss, body)
if err != nil {
return err
}
if dismiss {
a.done(cmd, "Dismissed %s", subject)
} else {
a.done(cmd, "Restored %s", subject)
}
return a.jsonResult(cmd, item)
}}
if dismiss {
cmd.Annotations["tea.confirm"] = "true"
cmd.Flags().StringVarP(&body, "body", "b", "", "Reason for dismissing the review (required)")
cmd.Flags().StringVarP(&file, "body-file", "F", "", "Read the reason from `file`, or - for stdin")
cmd.Flags().BoolVarP(&useEditor, "editor", "e", false, "Write the reason in your editor")
cmd.Example = `$ tea pr review dismiss 13 6 -b "Approved an outdated revision"`
} else {
cmd.Example = "$ tea pr review undismiss 13 6"
}
cmd.ValidArgsFunction = a.completeReviewArgs(false)
return cmd
}