feat(status): show the current branch's pull request, mentions and runs from the clone

tea pr status starts with the pull request of the checked-out branch,
"[feat/one] - Checks: 2 pending · Review: Approved", then lists the
pull requests created by you, assigned to you and requesting your
review under bold headings, as linked #N or OWNER/REPO#N refs with
truncated titles instead of URL columns, and "Nothing here" for an
empty section. Its JSON adds current_branch.

tea status uses the same layout, adds a Mentions section (open issues
and pull requests that mention you; JSON "mentions"), and lists the
failing runs of the clone's repository without -R, naming the remote
it came from. Piped output keeps the URLs.

Signed-off-by: bircni <bircni@icloud.com>
This commit is contained in:
bircni committed 2026-10-11 23:55:13 +02:00
1 parent 4895aa089d
commit 78f2ef5280
5 files changed
+211 -48

No files matched your search

+143 -32
View File
@@ -6,6 +6,7 @@ package app
import (
"fmt"
"strconv"
"strings"
"gitea.dev/tea/internal/services"
"gitea.dev/tea/internal/terminal"
@@ -15,67 +16,177 @@ import (
func (a *application) statusCommand(pullsOnly bool) *cobra.Command {
var limit int
var runs bool
short := "Show assigned issues, pull requests, and selected repository failures"
short, example := "Show your work across repositories", `$ tea status
$ tea status -R alice/project --runs=false`
long := `Show the open issues and pull requests assigned to you, the reviews requested from you, and
the open discussions that mention you. In a clone, it also lists the failing Actions runs of
the clone's repository; -R scopes everything to one repository.`
if pullsOnly {
short = "Show your open authored, assigned, and requested-review pull requests"
short, example = "Show the status of your pull requests", `$ tea pr status
$ tea pr status -R alice/project`
long = `Show the pull request of the current branch with its checks and review state, then the open
pull requests you created, those assigned to you, and those that request your review.`
}
command := &cobra.Command{Use: "status", Short: short, Args: argsExact(0), RunE: func(cmd *cobra.Command, _ []string) error {
target, err := a.resolve(cmd.Context(), false)
command := &cobra.Command{Use: "status", Short: short, Long: long, Example: example, Args: argsExact(0), RunE: func(cmd *cobra.Command, _ []string) error {
t, err := a.resolve(cmd.Context(), false)
if err != nil {
return err
}
service := services.Status{Client: target.client, Repository: target.Repo}
service := services.Status{Client: t.client, Repository: t.Repo}
d := a.display()
if pullsOnly {
result, err := service.Pulls(cmd.Context(), limit)
if err != nil {
return err
}
current, details, branch := a.currentPullStatus(cmd)
result.CurrentBranch = current
if a.options.json != "" {
return a.writeJSON(cmd, result)
}
return a.writeHuman(cmd, pullStatusText(result, a.colorEnabled()))
return a.writeHuman(cmd, currentBranchSection(d, branch, current, details)+pullStatusText(d, result, t.Repo))
}
result, err := service.Summary(cmd.Context(), limit, runs)
runsRepo, runsSource := t.Repo, t.RepoSource
if runs && runsRepo == "" {
if discovered, err := a.resolveTarget(cmd.Context(), true, false); err == nil && discovered.Host == t.Host {
runsRepo, runsSource = discovered.Repo, discovered.RepoSource
}
}
if !runs {
runsRepo = ""
}
result, err := service.Summary(cmd.Context(), limit, runsRepo)
if err != nil {
return err
}
if a.options.json != "" {
return a.writeJSON(cmd, result)
}
color := a.colorEnabled()
text := fmt.Sprintf("%s on %s\n", result.User, result.Host) + statusSection("Assigned issues", result.AssignedIssues, statusRow, color) + pullStatusText(result.PullRequests, color)
text := fmt.Sprintf("%s on %s\n\n", terminal.Emphasis(terminal.SafeLine(result.User), d.Color), terminal.SafeLine(result.Host)) +
statusSection(d, "Assigned issues", result.AssignedIssues, t.Repo) +
statusSection(d, "Assigned pull requests", result.PullRequests.Assigned, t.Repo) +
statusSection(d, "Review requests", result.PullRequests.ReviewRequested, t.Repo) +
statusSection(d, "Mentions", result.Mentions, t.Repo)
if result.RunsIncluded {
text += statusSection("Failing runs in "+result.Repository, result.FailingRuns, func(run services.Run) []terminal.Cell {
return terminal.Row(strconv.FormatInt(run.ID, 10), run.Title, run.Conclusion, run.URL)
}, color)
heading := "Failing runs in " + result.Repository
if runsSource != "" && t.Repo == "" {
heading += " (from " + strings.Replace(runsSource, "remote ", "git remote ", 1) + ")"
}
text += section(d, heading, result.FailingRuns, func(run services.Run) []terminal.Cell {
return []terminal.Cell{{Text: strconv.FormatInt(run.ID, 10), Link: run.URL}, terminal.StatusIcon(run.Status, run.Conclusion), {Text: run.Title}, {Text: run.Conclusion}, {Text: run.URL}}
})
}
return a.writeHuman(cmd, text)
return a.writeHuman(cmd, strings.TrimSuffix(text, "\n"))
}}
positiveIntVar(command, &limit, "limit", 30, 100, "Maximum results in each category (1..100)")
positiveIntVar(command, &limit, "limit", 30, 100, "Maximum results in each section (1..100)")
if !pullsOnly {
command.Flags().BoolVar(&runs, "runs", true, "Include failed or timed-out Actions runs when --repo or TEA_REPO is selected")
command.Flags().BoolVar(&runs, "runs", true, "Include the failing Actions runs of the selected or discovered repository")
}
return command
}
func pullStatusText(result services.PullStatus, color bool) string {
return statusSection("Authored pull requests", result.Authored, statusRow, color) +
statusSection("Assigned pull requests", result.Assigned, statusRow, color) +
statusSection("Review requested", result.ReviewRequested, statusRow, color)
}
// statusSection renders a heading over an indented table, or None when items is empty.
func statusSection[T any](name string, items []T, row func(T) []terminal.Cell, color bool) string {
if len(items) == 0 {
return name + "\n None\n"
// currentPullStatus reads the pull request of the checked-out branch with its checks and reviews.
// Outside a checkout, or with --repo, it reports none; branch names the checked-out branch.
func (a *application) currentPullStatus(cmd *cobra.Command) (*services.Pull, pullDetails, string) {
var details pullDetails
if a.options.repo != "" {
return nil, details, ""
}
return name + "\n" + table(items, func(item T) []terminal.Cell {
cells := row(item)
cells[0].Text = " " + cells[0].Text
return cells
}, color)
branch, err := a.currentBranch(cmd.Context())
if err != nil {
return nil, details, ""
}
t, n, err := a.pullSelector(cmd, nil)
if err != nil {
return nil, details, branch
}
service := services.Pulls{Client: t.client, Repository: t.Repo}
pull, err := service.View(cmd.Context(), n)
if err != nil {
return nil, details, branch
}
if details.reviews, err = (services.Reviews{Client: t.client, Repository: t.Repo}).All(cmd.Context(), n); err == nil {
pull.ReviewDecision = services.ReviewDecision(services.LatestReviews(details.reviews), len(pull.Reviewers)+len(pull.ReviewerTeams) > 0)
}
if pull.HeadSHA != "" {
details.checks, _ = service.CommitChecks(cmd.Context(), pull.HeadSHA)
}
return &pull, details, branch
}
func statusRow(item services.SearchItem) []terminal.Cell {
return terminal.Row(fmt.Sprintf("%s#%d", item.Repository, item.Number), item.Title, item.URL)
// currentBranchSection shows the pull request of the checked-out branch and how it stands:
// "[feat/one] - Checks: 2 pending · Review: Approved".
func currentBranchSection(d terminal.Display, branch string, pull *services.Pull, details pullDetails) string {
if branch == "" {
return ""
}
heading := terminal.Emphasis("Current branch", d.Color && d.TTY) + "\n"
if pull == nil {
return heading + " " + terminal.Colorize("muted", "There is no pull request associated with ["+terminal.SafeLine(branch)+"]", d.Color) + "\n\n"
}
state := pullState(*pull)
row := terminal.Table([]terminal.Column{{Prefix: "#"}, {Flex: true}, {}}, [][]terminal.Cell{{
{Text: strconv.FormatInt(pull.Number, 10), Link: pull.URL, Color: stateColors[state]}, {Text: pull.Title}, {Text: "[" + pull.Head + "]", Color: "cyan"},
}}, indented(d))
facts := []string{}
if checks := checkSummary(details.checks); checks != "" {
facts = append(facts, "Checks: "+checks)
}
decision := map[string]string{"approved": "Approved", "changes_requested": "Changes requested", "review_required": "Review required"}[pull.ReviewDecision]
if decision != "" {
facts = append(facts, "Review: "+decision)
}
if state != "open" {
facts = append(facts, titleCase(state))
}
text := heading + indent(row)
if len(facts) > 0 {
text += " - " + strings.Join(facts, " · ") + "\n"
}
return text + "\n"
}
func pullStatusText(d terminal.Display, result services.PullStatus, repo string) string {
return statusSection(d, "Created by you", result.Authored, repo) +
statusSection(d, "Assigned to you", result.Assigned, repo) +
statusSection(d, "Requesting a code review from you", result.ReviewRequested, repo)
}
// statusSection lists issues or pull requests under a heading, as #N within repo and OWNER/REPO#N
// elsewhere.
func statusSection(d terminal.Display, heading string, items []services.SearchItem, repo string) string {
return section(d, heading, items, func(item services.SearchItem) []terminal.Cell {
ref := fmt.Sprintf("%s#%d", item.Repository, item.Number)
if repo != "" && strings.EqualFold(item.Repository, repo) {
ref = fmt.Sprintf("#%d", item.Number)
}
return []terminal.Cell{{Text: ref, Link: item.URL}, {Text: item.Title}, {Text: item.URL}}
})
}
// section renders a bold heading over an indented table without header, or "Nothing here". The
// last cell of a row is a URL that only piped output keeps.
func section[T any](d terminal.Display, heading string, items []T, row func(T) []terminal.Cell) string {
text := terminal.Emphasis(heading, d.Color && d.TTY) + "\n"
if len(items) == 0 {
return text + " " + terminal.Colorize("muted", "Nothing here", d.Color) + "\n\n"
}
rows := make([][]terminal.Cell, len(items))
columns := []terminal.Column{{}}
for i, item := range items {
rows[i] = row(item)
}
for range len(rows[0]) - 2 {
columns = append(columns, terminal.Column{Flex: true})
}
columns = append(columns, terminal.Column{PipedOnly: true})
return text + indent(terminal.Table(columns, rows, indented(d))) + "\n"
}
// indented narrows the display by the two-space indent of status sections.
func indented(d terminal.Display) terminal.Display {
if d.Width > 2 {
d.Width -= 2
}
return d
}