mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 11:01:25 +01:00
196 lines
8.1 KiB
Go
196 lines
8.1 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package app
|
|
|
|
import (
|
|
"errors"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"gitea.dev/tea/internal/clierr"
|
|
"gitea.dev/tea/internal/services"
|
|
"gitea.dev/tea/internal/terminal"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
func (a *application) prCommand() *cobra.Command {
|
|
group := &cobra.Command{Use: "pr", Short: "Manage pull requests", Long: `Work with Gitea pull requests: list, view, create, review, check out and merge them.
|
|
|
|
Commands that act on one pull request take a number, a URL or a branch name, and without
|
|
one select the pull request of the current branch.`}
|
|
group.Annotations = pullArgumentsAnnotation(nil)
|
|
group.Example = `$ tea pr list --author @me
|
|
$ tea pr create --fill
|
|
$ tea pr merge --squash --delete-branch`
|
|
group.AddCommand(a.pullChecksCommand(), a.pullCheckoutCommand(), a.pullCleanCommand(), a.statusCommand(true))
|
|
group.AddCommand(a.pullListCommand(), a.pullViewCommand())
|
|
a.addPullMutations(group)
|
|
group.AddCommand(a.pullEditCommand(), a.pullReadyCommand())
|
|
group.AddCommand(a.issueAttachmentCommand(pullRef), a.commentCommand(pullRef))
|
|
return group
|
|
}
|
|
|
|
func (a *application) pullListCommand() *cobra.Command {
|
|
var limit, page int
|
|
var state, milestone string
|
|
var options services.PullListOptions
|
|
var labels []string
|
|
var web bool
|
|
cmd := &cobra.Command{Use: "list", Short: "List pull requests", Long: `List the pull requests of a repository, newest first.
|
|
|
|
The reviewers, reviewer_teams and merged_by JSON fields come from separate requests for
|
|
each pull request, which tea only makes when --json names them.`, Args: argsExact(0), RunE: func(cmd *cobra.Command, _ []string) error {
|
|
if id, err := strconv.ParseInt(milestone, 10, 64); err == nil && id < 1 {
|
|
return clierr.Wrap(clierr.Usage, errors.New("--milestone must be a milestone title or a positive ID"))
|
|
}
|
|
if options.NoAssignee && options.Assignee != "" {
|
|
return clierr.Wrap(clierr.Usage, errors.New("--assignee and --no-assignee are mutually exclusive"))
|
|
}
|
|
t, err := a.resolve(cmd.Context(), true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if web {
|
|
link := repositoryURL(t) + "/pulls"
|
|
switch state {
|
|
case "closed", "merged":
|
|
link += "?state=closed"
|
|
case "all":
|
|
link += "?state=all"
|
|
}
|
|
return a.openBrowser(cmd, link)
|
|
}
|
|
options.Author, options.Assignee = t.expandMe(options.Author), t.expandMe(options.Assignee)
|
|
options.State, options.Limit, options.Page = state, limit, page
|
|
if options.Labels, err = (services.Labels{Client: t.client, Repository: t.Repo}).ResolveIssueNames(cmd.Context(), labels); err != nil {
|
|
return err
|
|
}
|
|
if milestone != "" {
|
|
if options.Milestone, err = milestoneID(cmd, t, milestone); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
service := services.Pulls{Client: t.client, Repository: t.Repo}
|
|
window, err := service.ListPage(cmd.Context(), options)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
items := window.Items
|
|
if a.jsonSelects("reviewers", "reviewer_teams", "merged_by") {
|
|
if items, err = service.ViewAll(cmd.Context(), items); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
noun := map[string]string{"open": "open pull requests", "closed": "closed pull requests", "merged": "merged pull requests", "all": "pull requests"}[state]
|
|
what := noun + " in " + t.Repo
|
|
empty := "no " + what
|
|
if localFlagsChanged(cmd, "state", "limit", "page", "sort") {
|
|
empty = "no " + noun + " match your filters in " + t.Repo
|
|
}
|
|
return writeTable(a, cmd, items, listSpec[services.Pull]{
|
|
Columns: []terminal.Column{{Header: "ID", Prefix: "#"}, {Header: "Title", Flex: true}, {Header: "Branch", Flex: true, Min: 12}, {Header: "State", PipedOnly: state == "open"}, {Header: "Author"}, {Header: "Created"}},
|
|
Row: func(item services.Pull) []terminal.Cell {
|
|
state := pullState(item)
|
|
return []terminal.Cell{
|
|
{Text: strconv.FormatInt(item.Number, 10), Link: item.URL, Color: stateColors[state]},
|
|
{Text: item.Title},
|
|
{Text: pullHead(item, t.Repo)},
|
|
{Text: state, Color: stateColors[state]},
|
|
{Text: item.Author},
|
|
{Time: terminal.ParseTime(item.CreatedAt)},
|
|
}
|
|
},
|
|
Empty: empty, Context: what,
|
|
}, servicePage(window, page))
|
|
}}
|
|
enumFlag(cmd, &state, "state", "s", "open", "Filter by state", "open", "closed", "merged", "all")
|
|
pageFlags(cmd, &limit, &page)
|
|
cmd.Flags().StringVarP(&options.Author, "author", "A", "", "Filter by author; @me for yourself")
|
|
cmd.Flags().StringVarP(&options.Assignee, "assignee", "a", "", "Filter by assignee; @me for yourself")
|
|
cmd.Flags().BoolVar(&options.NoAssignee, "no-assignee", false, "Only pull requests without an assignee")
|
|
cmd.Flags().StringVarP(&options.Base, "base", "B", "", "Filter by base branch")
|
|
cmd.Flags().StringVarP(&options.Head, "head", "H", "", "Filter by head branch, or OWNER:BRANCH for a fork")
|
|
cmd.Flags().BoolVarP(&options.Draft, "draft", "d", false, "Show only drafts")
|
|
cmd.Flags().StringVarP(&options.Search, "search", "S", "", "Filter by text in the title or body")
|
|
cmd.Flags().StringSliceVarP(&labels, "label", "l", nil, "Filter by labels; repo: or org: picks between same-named labels")
|
|
cmd.Flags().StringVarP(&milestone, "milestone", "m", "", "Filter by milestone title or ID")
|
|
enumFlag(cmd, &options.Sort, "sort", "", "", "Order", "oldest", "recentupdate", "recentclose", "leastupdate", "mostcomment", "leastcomment", "priority")
|
|
cmd.Flags().BoolVarP(&web, "web", "w", false, "Open the pull request list in the browser")
|
|
_ = cmd.RegisterFlagCompletionFunc("author", a.completeUsers())
|
|
_ = cmd.RegisterFlagCompletionFunc("assignee", a.completeUsers())
|
|
_ = cmd.RegisterFlagCompletionFunc("base", a.completeBranches())
|
|
_ = cmd.RegisterFlagCompletionFunc("head", a.completeBranches())
|
|
_ = cmd.RegisterFlagCompletionFunc("label", a.completeLabels())
|
|
_ = cmd.RegisterFlagCompletionFunc("milestone", a.completeMilestones())
|
|
cmd.Example = `$ tea pr list
|
|
$ tea pr list --author @me --state all
|
|
$ tea pr list --json number,title,head --jq '.[] | select(.head == "feature/login")'`
|
|
return cmd
|
|
}
|
|
|
|
// jsonSelects reports whether --json names one of fields, rather than selecting every field.
|
|
func (a *application) jsonSelects(fields ...string) bool {
|
|
for field := range strings.SplitSeq(a.options.json, ",") {
|
|
if slices.Contains(fields, strings.TrimSpace(field)) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (a *application) pullViewCommand() *cobra.Command {
|
|
var web, comments bool
|
|
cmd := &cobra.Command{Use: "view [<number> | <url> | <branch>]", Short: "View a pull request", Annotations: pullArgumentsAnnotation(nil), Args: usageArgs(cobra.MaximumNArgs(1)), RunE: func(cmd *cobra.Command, args []string) error {
|
|
t, n, err := a.pullSelector(cmd, args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
service := services.Pulls{Client: t.client, Repository: t.Repo}
|
|
item, err := service.View(cmd.Context(), n)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if web {
|
|
return a.openBrowser(cmd, item.URL)
|
|
}
|
|
var details pullDetails
|
|
if details.reviews, err = (services.Reviews{Client: t.client, Repository: t.Repo}).All(cmd.Context(), n); err != nil {
|
|
return err
|
|
}
|
|
item.ReviewDecision = services.ReviewDecision(services.LatestReviews(details.reviews), len(item.Reviewers)+len(item.ReviewerTeams) > 0)
|
|
if a.options.json != "" {
|
|
return a.writeJSON(cmd, item)
|
|
}
|
|
if item.State == "open" && item.HeadSHA != "" {
|
|
if details.checks, err = service.CommitChecks(cmd.Context(), item.HeadSHA); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return a.writeDiscussion(cmd, t, pullView(item, t.Repo, details), comments)
|
|
}}
|
|
cmd.Flags().BoolVarP(&web, "web", "w", false, "Open the pull request in the browser")
|
|
cmd.Flags().BoolVarP(&comments, "comments", "c", false, "Show comments and reviews")
|
|
cmd.ValidArgsFunction = a.completePulls("all")
|
|
cmd.Example = `# View the pull request of the current branch
|
|
$ tea pr view
|
|
|
|
# View #13 with its comments and reviews
|
|
$ tea pr view 13 --comments
|
|
|
|
$ tea pr view feature/login --json number,state,review_decision`
|
|
return cmd
|
|
}
|
|
|
|
// pullState reports merged pull requests as merged, and open drafts as draft.
|
|
func pullState(item services.Pull) string {
|
|
switch {
|
|
case item.Merged:
|
|
return "merged"
|
|
case item.Draft && item.State == "open":
|
|
return "draft"
|
|
}
|
|
return item.State
|
|
}
|