mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 19:11:25 +01:00
refactor(services): leave CLI flag and command names to the app layer
Services report ErrRunScanExhausted, ErrMaxPages and plain conflicts, and the commands add the --branch/--max-pages/--force hints. Unknown label names use the shared NotFoundError, and the repository-scoped review and team search refusal moves into tea search's own usage check. Signed-off-by: bircni <bircni@icloud.com>
This commit is contained in:
10 files changed
+37
-18
No files matched your search
@@ -78,6 +78,9 @@ $ tea run list --since 24h --json id,status,conclusion,workflow`,
|
|||||||
opts.Workflow = workflow.ID
|
opts.Workflow = workflow.ID
|
||||||
}
|
}
|
||||||
page, err := service.RunsPage(cmd.Context(), opts)
|
page, err := service.RunsPage(cmd.Context(), opts)
|
||||||
|
if errors.Is(err, services.ErrRunScanExhausted) {
|
||||||
|
return fmt.Errorf("%w; narrow the search with --branch, --workflow or --status", err)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-1
@@ -256,7 +256,7 @@ func (a *application) apiCommand() *cobra.Command {
|
|||||||
response.tty = a.deps.Streams.OutTTY
|
response.tty = a.deps.Streams.OutTTY
|
||||||
response.color = response.tty && a.colorEnabled()
|
response.color = response.tty && a.colorEnabled()
|
||||||
}
|
}
|
||||||
return (services.API{Client: target.client}).Visit(cmd.Context(), services.APIOptions{Endpoint: endpoint, Method: method, Body: body, Headers: headers, Paginate: paginate, MaxPages: maxPages}, func(r *http.Response) error {
|
err = (services.API{Client: target.client}).Visit(cmd.Context(), services.APIOptions{Endpoint: endpoint, Method: method, Body: body, Headers: headers, Paginate: paginate, MaxPages: maxPages}, func(r *http.Response) error {
|
||||||
if verbose {
|
if verbose {
|
||||||
if err := writeRequestHead(cmd.ErrOrStderr(), r.Request); err != nil {
|
if err := writeRequestHead(cmd.ErrOrStderr(), r.Request); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -273,6 +273,10 @@ func (a *application) apiCommand() *cobra.Command {
|
|||||||
}
|
}
|
||||||
return response.write(destination, r)
|
return response.write(destination, r)
|
||||||
})
|
})
|
||||||
|
if errors.Is(err, services.ErrMaxPages) {
|
||||||
|
return errors.New("API pagination exceeded --max-pages")
|
||||||
|
}
|
||||||
|
return err
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
cmd.Flags().StringVarP(&method, "method", "X", "GET", "HTTP `method`")
|
cmd.Flags().StringVarP(&method, "method", "X", "GET", "HTTP `method`")
|
||||||
|
|||||||
@@ -56,8 +56,8 @@ repository with --repo. Words of the query match titles and bodies.`, nouns), Ex
|
|||||||
if opts.Team != "" && opts.Owner == "" {
|
if opts.Team != "" && opts.Owner == "" {
|
||||||
return clierr.Wrap(clierr.Usage, errors.New("--team requires --owner"))
|
return clierr.Wrap(clierr.Usage, errors.New("--team requires --owner"))
|
||||||
}
|
}
|
||||||
if opts.Team != "" && target.Repo != "" {
|
if (opts.Team != "" || opts.ReviewRequested || opts.Reviewed) && target.Repo != "" {
|
||||||
return clierr.Wrap(clierr.Usage, errors.New("--team requires instance-wide search; omit --repo or TEA_REPO"))
|
return clierr.Wrap(clierr.Usage, errors.New("--team, --review-requested and --reviewed require instance-wide search; omit --repo or TEA_REPO"))
|
||||||
}
|
}
|
||||||
opts.Author = target.expandMe(opts.Author)
|
opts.Author = target.expandMe(opts.Author)
|
||||||
if target.Repo != "" {
|
if target.Repo != "" {
|
||||||
|
|||||||
+10
-1
@@ -182,7 +182,7 @@ $ tea label create bug -c ee0701 --force`, Args: usageArgs(cobra.MaximumNArgs(1)
|
|||||||
}
|
}
|
||||||
item, updated, err := s.CreateUnique(cmd.Context(), existing, opts, force)
|
item, updated, err := s.CreateUnique(cmd.Context(), existing, opts, force)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return forceHint(err)
|
||||||
}
|
}
|
||||||
return a.reportLabel(cmd, s, item, updated)
|
return a.reportLabel(cmd, s, item, updated)
|
||||||
}}
|
}}
|
||||||
@@ -220,6 +220,14 @@ func (a *application) askLabelDetails(cmd *cobra.Command, opts *services.LabelOp
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// forceHint points at --force when a label exists already.
|
||||||
|
func forceHint(err error) error {
|
||||||
|
if conflict, ok := errors.AsType[*services.ConflictError](err); ok && conflict.Reason == services.ReasonExists {
|
||||||
|
conflict.Hint = "pass --force to update it"
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// reportLabel reports a created or updated label as JSON or with a ✓ line.
|
// reportLabel reports a created or updated label as JSON or with a ✓ line.
|
||||||
func (a *application) reportLabel(cmd *cobra.Command, s services.Labels, item services.Label, updated bool) error {
|
func (a *application) reportLabel(cmd *cobra.Command, s services.Labels, item services.Label, updated bool) error {
|
||||||
verb := "Created"
|
verb := "Created"
|
||||||
@@ -336,6 +344,7 @@ func (a *application) createLabels(cmd *cobra.Command, s services.Labels, rows [
|
|||||||
outcome := map[string]string{}
|
outcome := map[string]string{}
|
||||||
results, failures := applyBatch(cmd, rows, func(row services.LabelOptions) string { return "label " + strconv.Quote(*row.Name) }, func(row services.LabelOptions) (services.Label, error) {
|
results, failures := applyBatch(cmd, rows, func(row services.LabelOptions) string { return "label " + strconv.Quote(*row.Name) }, func(row services.LabelOptions) (services.Label, error) {
|
||||||
item, updated, err := s.CreateUnique(cmd.Context(), existing, row, force)
|
item, updated, err := s.CreateUnique(cmd.Context(), existing, row, force)
|
||||||
|
err = forceHint(err)
|
||||||
switch {
|
switch {
|
||||||
case err != nil:
|
case err != nil:
|
||||||
return item, err
|
return item, err
|
||||||
|
|||||||
@@ -100,6 +100,9 @@ type RunListOptions struct {
|
|||||||
// RunStatuses are the values Gitea accepts as run status filters.
|
// RunStatuses are the values Gitea accepts as run status filters.
|
||||||
var RunStatuses = []string{"queued", "in_progress", "waiting", "requested", "pending", "action_required", "completed", "success", "failure", "cancelled", "skipped", "timed_out", "neutral"} //nolint:misspell // Gitea API spelling.
|
var RunStatuses = []string{"queued", "in_progress", "waiting", "requested", "pending", "action_required", "completed", "success", "failure", "cancelled", "skipped", "timed_out", "neutral"} //nolint:misspell // Gitea API spelling.
|
||||||
|
|
||||||
|
// ErrRunScanExhausted reports time filters that matched too few of the runs a scan may read.
|
||||||
|
var ErrRunScanExhausted = fmt.Errorf("time filters scanned %d runs without exhausting the history", maxRunScanPages*100)
|
||||||
|
|
||||||
// Validate rejects unsupported status filters before performing API requests.
|
// Validate rejects unsupported status filters before performing API requests.
|
||||||
func (o RunListOptions) Validate() error {
|
func (o RunListOptions) Validate() error {
|
||||||
if o.Limit < 1 || o.Limit > 1000 || o.Page < 1 || o.Page > int(^uint(0)>>1)/o.Limit {
|
if o.Limit < 1 || o.Limit > 1000 || o.Page < 1 || o.Page > int(^uint(0)>>1)/o.Limit {
|
||||||
@@ -107,7 +110,7 @@ func (o RunListOptions) Validate() error {
|
|||||||
}
|
}
|
||||||
for _, status := range o.Statuses {
|
for _, status := range o.Statuses {
|
||||||
if !slices.Contains(RunStatuses, status) {
|
if !slices.Contains(RunStatuses, status) {
|
||||||
return fmt.Errorf("unsupported run status %q; see tea run list --help", status)
|
return fmt.Errorf("unsupported run status %q", status)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -173,7 +176,7 @@ func (s Actions) runs(ctx context.Context, opts RunListOptions) (Page[Run], erro
|
|||||||
return Page[Run]{Items: result}, nil
|
return Page[Run]{Items: result}, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Page[Run]{}, fmt.Errorf("time filters scanned %d runs without exhausting the history; narrow the search with --branch, --workflow or --status", maxRunScanPages*100)
|
return Page[Run]{}, ErrRunScanExhausted
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s Actions) runsPage(ctx context.Context, opts RunListOptions) (Page[Run], error) {
|
func (s Actions) runsPage(ctx context.Context, opts RunListOptions) (Page[Run], error) {
|
||||||
|
|||||||
@@ -275,7 +275,7 @@ func TestRunTimeFilteringStopsAtScanCap(t *testing.T) {
|
|||||||
defer server.Close()
|
defer server.Close()
|
||||||
service := Actions{Client: transport.New(server.URL, "", false), Repository: "owner/repo"}
|
service := Actions{Client: transport.New(server.URL, "", false), Repository: "owner/repo"}
|
||||||
_, err := service.Runs(context.Background(), RunListOptions{Limit: 1, Page: 1, Until: &until})
|
_, err := service.Runs(context.Background(), RunListOptions{Limit: 1, Page: 1, Until: &until})
|
||||||
require.ErrorContains(t, err, "narrow the search")
|
require.ErrorIs(t, err, ErrRunScanExhausted)
|
||||||
require.Equal(t, maxRunScanPages, pages)
|
require.Equal(t, maxRunScanPages, pages)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ type APIOptions struct {
|
|||||||
MaxPages int
|
MaxPages int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ErrMaxPages reports a paginated request that has more pages than APIOptions.MaxPages allows.
|
||||||
|
var ErrMaxPages = errors.New("API pagination exceeded the page limit")
|
||||||
|
|
||||||
func nextPageLinks(headers []string) ([]string, error) {
|
func nextPageLinks(headers []string) ([]string, error) {
|
||||||
result := []string{}
|
result := []string{}
|
||||||
for _, header := range headers {
|
for _, header := range headers {
|
||||||
@@ -141,7 +144,7 @@ func (s API) Visit(ctx context.Context, options APIOptions, visit func(*http.Res
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if page+1 >= options.MaxPages {
|
if page+1 >= options.MaxPages {
|
||||||
return errors.New("API pagination exceeded --max-pages")
|
return ErrMaxPages
|
||||||
}
|
}
|
||||||
if _, err := s.Client.URL(next); err != nil {
|
if _, err := s.Client.URL(next); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ func (s Labels) CreateUnique(ctx context.Context, existing []Label, opts LabelOp
|
|||||||
return Label{}, false, err
|
return Label{}, false, err
|
||||||
}
|
}
|
||||||
if !update {
|
if !update {
|
||||||
return Label{}, false, &ConflictError{Reason: ReasonExists, Kind: "label", Key: strconv.Quote(current.Name), Scope: s.Scope(), Hint: "pass --force to update it"}
|
return Label{}, false, &ConflictError{Reason: ReasonExists, Kind: "label", Key: strconv.Quote(current.Name), Scope: s.Scope()}
|
||||||
}
|
}
|
||||||
opts.Name = nil
|
opts.Name = nil
|
||||||
label, err := s.Edit(ctx, current.ID, opts)
|
label, err := s.Edit(ctx, current.ID, opts)
|
||||||
@@ -68,7 +68,7 @@ func selectLabelNames(scope string, names []string, matches map[string][]int64)
|
|||||||
for _, name := range names {
|
for _, name := range names {
|
||||||
candidates := matches[name]
|
candidates := matches[name]
|
||||||
if len(candidates) == 0 {
|
if len(candidates) == 0 {
|
||||||
return nil, fmt.Errorf("label %q does not exist; use tea label list", name)
|
return nil, NotFound("label", strconv.Quote(name), scope)
|
||||||
}
|
}
|
||||||
if len(candidates) > 1 {
|
if len(candidates) > 1 {
|
||||||
return nil, &AmbiguousError{Kind: "label", Key: strconv.Quote(name), Scope: scope, IDs: candidates}
|
return nil, &AmbiguousError{Kind: "label", Key: strconv.Quote(name), Scope: scope, IDs: candidates}
|
||||||
@@ -148,7 +148,7 @@ func (s Labels) RequireFilterNames(ctx context.Context, names []string) error {
|
|||||||
}
|
}
|
||||||
for _, name := range names {
|
for _, name := range names {
|
||||||
if !known[name] {
|
if !known[name] {
|
||||||
return fmt.Errorf("label %q does not exist in %s; issue filters match repository labels only, see tea label list", name, s.Repository)
|
return fmt.Errorf("label %q does not exist in %s; issue filters match repository labels only", name, s.Repository)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ func TestLabelSelectionReadsCappedPagesAndPreservesOrder(t *testing.T) {
|
|||||||
require.Equal(t, []int64{2, 1}, ids)
|
require.Equal(t, []int64{2, 1}, ids)
|
||||||
require.Equal(t, 3, reads)
|
require.Equal(t, 3, reads)
|
||||||
_, err = service.ResolveIssueNames(context.Background(), []string{"missing"})
|
_, err = service.ResolveIssueNames(context.Background(), []string{"missing"})
|
||||||
require.ErrorContains(t, err, "does not exist")
|
require.ErrorContains(t, err, "not found")
|
||||||
before := reads
|
before := reads
|
||||||
_, err = service.ResolveIssueNames(context.Background(), []string{" "})
|
_, err = service.ResolveIssueNames(context.Background(), []string{" "})
|
||||||
require.ErrorContains(t, err, "cannot be empty")
|
require.ErrorContains(t, err, "cannot be empty")
|
||||||
@@ -89,5 +89,5 @@ func TestScopedLabelNamesDoNotCollideWithLiteralPrefixes(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, []int64{1, 2, 3}, ids)
|
require.Equal(t, []int64{1, 2, 3}, ids)
|
||||||
_, err = service.ResolveIssueNames(context.Background(), []string{"org:bug"})
|
_, err = service.ResolveIssueNames(context.Background(), []string{"org:bug"})
|
||||||
require.ErrorContains(t, err, "does not exist")
|
require.ErrorContains(t, err, "not found")
|
||||||
}
|
}
|
||||||
@@ -75,11 +75,8 @@ func (s Issues) SearchPage(ctx context.Context, opts DiscussionSearchOptions) (P
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s Issues) searchRepository(ctx context.Context, opts DiscussionSearchOptions, kind gitea.IssueType) (Page[*gitea.Issue], error) {
|
func (s Issues) searchRepository(ctx context.Context, opts DiscussionSearchOptions, kind gitea.IssueType) (Page[*gitea.Issue], error) {
|
||||||
if opts.ReviewRequested || opts.Reviewed {
|
if opts.ReviewRequested || opts.Reviewed || opts.Team != "" {
|
||||||
return Page[*gitea.Issue]{}, &CapabilityError{Feature: "repository-scoped review search", Reason: "Gitea supports review filters on instance-wide search only; omit --repo or TEA_REPO"}
|
return Page[*gitea.Issue]{}, &CapabilityError{Feature: "repository-scoped review or team search", Reason: "Gitea supports review and team filters on instance-wide search only"}
|
||||||
}
|
|
||||||
if opts.Team != "" {
|
|
||||||
return Page[*gitea.Issue]{}, &CapabilityError{Feature: "repository-scoped team search", Reason: "Gitea supports team filters on instance-wide search only; omit --repo or TEA_REPO"}
|
|
||||||
}
|
}
|
||||||
owner, repo, err := splitRepository(s.Repository)
|
owner, repo, err := splitRepository(s.Repository)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Reference in new issue
Block a user