Files
gitea-tea/internal/services/status.go
T

146 lines
5.3 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package services
import (
"context"
"fmt"
"slices"
"strings"
"gitea.dev/tea/internal/transport"
)
// PullStatus groups open pull requests by the selected account's participation. CurrentBranch is
// the pull request of the checked-out branch, which commands fill when they know it.
type PullStatus struct {
CurrentBranch *Pull `json:"current_branch"`
Authored []SearchItem `json:"authored"`
Assigned []SearchItem `json:"assigned"`
ReviewRequested []SearchItem `json:"review_requested"`
}
// AccountStatus is an account summary with optional repository Actions results.
type AccountStatus struct {
Host string `json:"host"`
User string `json:"user"`
// Repository is the repository the summary is scoped to, empty for all repositories.
Repository string `json:"repository"`
AssignedIssues []SearchItem `json:"assigned_issues"`
PullRequests PullStatus `json:"pull_requests"`
// Mentions are the open issues and pull requests that mention the account.
Mentions []SearchItem `json:"mentions"`
// RunsRepository is the repository whose failing runs are listed, when RunsIncluded.
RunsRepository string `json:"runs_repository"`
FailingRuns []Run `json:"failing_runs"`
RunsIncluded bool `json:"runs_included"`
}
// Status reuses search and Actions workflows.
type Status struct {
Client *transport.Client
Repository string
}
// Pulls collects each category independently; limit applies after repository filtering.
func (s Status) Pulls(ctx context.Context, limit int) (PullStatus, error) {
result := PullStatus{Authored: []SearchItem{}, Assigned: []SearchItem{}, ReviewRequested: []SearchItem{}}
if limit < 1 || limit > 100 {
return result, fmt.Errorf("status limit must be 1..100")
}
scoped := Issues{Client: s.Client, Repository: s.Repository}
opts := DiscussionSearchOptions{Pulls: true, State: "open", Limit: limit, Page: 1, Authored: true}
var err error
result.Authored, err = scoped.Search(ctx, opts)
if err != nil {
return result, err
}
opts.Authored, opts.Assigned = false, true
result.Assigned, err = scoped.Search(ctx, opts)
if err != nil {
return result, err
}
opts.Assigned, opts.ReviewRequested = false, true
if s.Repository == "" {
result.ReviewRequested, err = scoped.Search(ctx, opts)
return result, err
}
// Gitea exposes review filters only on instance-wide search. Filter after
// pagination so requests from other repositories do not consume the limit.
opts.Owner = strings.SplitN(s.Repository, "/", 2)[0]
opts.Limit = 100
for opts.Page = 1; opts.Page <= maxListPages; opts.Page++ {
items, err := (Issues{Client: s.Client}).Search(ctx, opts)
if err != nil {
return result, err
}
if len(items) == 0 {
return result, nil
}
for _, item := range items {
if strings.EqualFold(item.Repository, s.Repository) {
result.ReviewRequested = append(result.ReviewRequested, item)
if len(result.ReviewRequested) == limit {
return result, nil
}
}
}
}
return result, fmt.Errorf("review requests exceeded %d pages", maxListPages)
}
// Summary reports open assigned issues and pull requests and mentions. Failing Actions runs of
// runsRepository are included when it is set and has Actions enabled. A runsRepository that was
// only discovered from a clone is left out when the account cannot read it, instead of failing.
func (s Status) Summary(ctx context.Context, limit int, runsRepository string, discovered bool) (AccountStatus, error) {
result := AccountStatus{Host: s.Client.BaseURL, Repository: s.Repository, AssignedIssues: []SearchItem{}, Mentions: []SearchItem{}, FailingRuns: []Run{}}
if limit < 1 || limit > 100 {
return result, fmt.Errorf("status limit must be 1..100")
}
account, err := CurrentAccount(ctx, s.Client)
if err != nil {
return result, err
}
result.User = account
result.PullRequests, err = s.Pulls(ctx, limit)
if err != nil {
return result, err
}
scoped := Issues{Client: s.Client, Repository: s.Repository}
result.AssignedIssues, err = scoped.Search(ctx, DiscussionSearchOptions{State: "open", Assigned: true, Limit: limit, Page: 1})
if err != nil {
return result, err
}
for _, pulls := range []bool{false, true} {
mentions, err := scoped.Search(ctx, DiscussionSearchOptions{State: "open", Mentioned: true, Pulls: pulls, Limit: limit, Page: 1})
if err != nil {
return result, err
}
result.Mentions = append(result.Mentions, mentions...)
}
slices.SortStableFunc(result.Mentions, func(x, y SearchItem) int { return strings.Compare(y.UpdatedAt, x.UpdatedAt) })
result.Mentions = result.Mentions[:min(len(result.Mentions), limit)]
if runsRepository != "" {
repository, err := (Repositories{Client: s.Client}).View(ctx, runsRepository)
if err != nil {
if discovered && (IsNotFound(err) || IsForbidden(err)) {
return result, nil
}
return result, err
}
if !repository.ActionsEnabled {
return result, nil
}
failing, err := (Actions{Client: s.Client, Repository: runsRepository}).Runs(ctx, RunListOptions{Limit: limit, Page: 1, Statuses: []string{"failure", "timed_out"}})
if err != nil {
if discovered && (IsNotFound(err) || IsForbidden(err)) {
return result, nil
}
return result, err
}
result.RunsRepository, result.FailingRuns, result.RunsIncluded = runsRepository, failing, true
}
return result, nil
}