feat(search): gh-style issue and pull request search tables

search issue and search pr printed rows without a header, showed merged
pull requests as closed and never said that more results exist. They now
show REPO#ID TITLE STATE AUTHOR UPDATED under a context line that counts
the matches and points at the next page, are also search issues and
search prs, take -s -L -A -l -m and @me, and use --until for --before,
which still works. Search results gain an author JSON field.

Signed-off-by: bircni <bircni@icloud.com>
This commit is contained in:
bircni committed 2026-10-12 00:17:07 +02:00
1 parent e5796af9e9
commit f6a0d18b8a
6 files changed
+120 -58

No files matched your search

+29 -20
View File
@@ -6,8 +6,8 @@ package services
import (
"context"
"fmt"
"net/http"
"net/url"
"strconv"
"time"
gitea "gitea.dev/sdk"
@@ -25,6 +25,7 @@ type SearchItem struct {
State string `json:"state"`
Type string `json:"type"`
Repository string `json:"repository"`
Author string `json:"author"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
@@ -39,12 +40,18 @@ type DiscussionSearchOptions struct {
// Search searches one repository or accessible repositories across the selected instance.
func (s Issues) Search(ctx context.Context, opts DiscussionSearchOptions) ([]SearchItem, error) {
page, err := s.SearchPage(ctx, opts)
return page.Items, err
}
// SearchPage is Search with what the server reports about further results.
func (s Issues) SearchPage(ctx context.Context, opts DiscussionSearchOptions) (Page[SearchItem], error) {
kind, resultType := gitea.IssueTypeIssue, "issue"
if opts.Pulls {
kind, resultType = gitea.IssueTypePull, "pr"
}
if opts.Team != "" && opts.Owner == "" {
return nil, fmt.Errorf("team search requires an organization owner")
return Page[SearchItem]{}, fmt.Errorf("team search requires an organization owner")
}
search := s.searchInstance
if s.Repository != "" {
@@ -52,33 +59,34 @@ func (s Issues) Search(ctx context.Context, opts DiscussionSearchOptions) ([]Sea
}
issues, err := search(ctx, opts, kind)
if err != nil {
return nil, err
return Page[SearchItem]{}, err
}
return convertAll(issues, func(issue *gitea.Issue) SearchItem {
items := convertAll(issues.Items, func(issue *gitea.Issue) SearchItem {
repository := s.Repository
if issue.Repository != nil {
repository = issue.Repository.FullName
}
return SearchItem{
ID: issue.ID, Number: issue.Index, Title: issue.Title, Body: issue.Body, URL: issue.HTMLURL, State: discussionState(issue),
Type: resultType, Repository: repository, CreatedAt: timestamp(issue.Created), UpdatedAt: timestamp(issue.Updated),
Type: resultType, Repository: repository, Author: login(issue.Poster), CreatedAt: timestamp(issue.Created), UpdatedAt: timestamp(issue.Updated),
}
}), nil
})
return Page[SearchItem]{Items: items, More: issues.More, Total: issues.Total, Counted: issues.Counted}, nil
}
func (s Issues) searchRepository(ctx context.Context, opts DiscussionSearchOptions, kind gitea.IssueType) ([]*gitea.Issue, error) {
func (s Issues) searchRepository(ctx context.Context, opts DiscussionSearchOptions, kind gitea.IssueType) (Page[*gitea.Issue], error) {
if opts.ReviewRequested || opts.Reviewed {
return nil, &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 search", Reason: "Gitea supports review filters on instance-wide search only; omit --repo or TEA_REPO"}
}
if opts.Team != "" {
return nil, &CapabilityError{Feature: "repository-scoped team search", Reason: "Gitea supports team filters on instance-wide search only; omit --repo or TEA_REPO"}
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)
if err != nil {
return nil, err
return Page[*gitea.Issue]{}, err
}
query := gitea.ListIssueOption{
ListOptions: listOptions(opts.Limit, opts.Page), State: gitea.StateType(opts.State), Type: kind, KeyWord: opts.Query,
State: gitea.StateType(opts.State), Type: kind, KeyWord: opts.Query,
Labels: filterValues(opts.Labels), Milestones: filterValues(opts.Milestones), CreatedBy: opts.Author, Owner: opts.Owner,
}
if opts.Since != nil {
@@ -90,7 +98,7 @@ func (s Issues) searchRepository(ctx context.Context, opts DiscussionSearchOptio
if opts.Authored || opts.Assigned || opts.Mentioned {
account, err := CurrentAccount(ctx, s.Client)
if err != nil {
return nil, err
return Page[*gitea.Issue]{}, err
}
if opts.Authored {
query.CreatedBy = account
@@ -102,14 +110,17 @@ func (s Issues) searchRepository(ctx context.Context, opts DiscussionSearchOptio
query.MentionedBy = account
}
}
issues, _, err := call(ctx, s.Client, func(ctx context.Context, c *gitea.Client) ([]*gitea.Issue, *gitea.Response, error) {
return c.Issues.ListRepoIssues(ctx, owner, repo, query)
return listPage(opts.Limit, opts.Page, func(limit, page int) ([]*gitea.Issue, http.Header, error) {
query.ListOptions = listOptions(limit, page)
issues, resp, err := call(ctx, s.Client, func(ctx context.Context, c *gitea.Client) ([]*gitea.Issue, *gitea.Response, error) {
return c.Issues.ListRepoIssues(ctx, owner, repo, query)
})
return issues, headerOf(resp), err
})
return issues, err
}
func (s Issues) searchInstance(ctx context.Context, opts DiscussionSearchOptions, kind gitea.IssueType) ([]*gitea.Issue, error) {
query := url.Values{"q": {opts.Query}, "state": {opts.State}, "type": {string(kind)}, "limit": {strconv.Itoa(opts.Limit)}, "page": {strconv.Itoa(opts.Page)}}
func (s Issues) searchInstance(ctx context.Context, opts DiscussionSearchOptions, kind gitea.IssueType) (Page[*gitea.Issue], error) {
query := url.Values{"q": {opts.Query}, "state": {opts.State}, "type": {string(kind)}}
for key, value := range map[string]string{"owner": opts.Owner, "team": opts.Team, "created_by": opts.Author, "labels": opts.Labels, "milestones": opts.Milestones} {
if value != "" {
query.Set(key, value)
@@ -125,8 +136,6 @@ func (s Issues) searchInstance(ctx context.Context, opts DiscussionSearchOptions
query.Set(key, "true")
}
}
var issues []*gitea.Issue
// TODO(sdk): ListIssueOption lacks the assigned, created, mentioned, review_requested and reviewed flags.
err := getJSON(ctx, s.Client, "repos/issues/search?"+query.Encode(), &issues)
return issues, err
return rawPage[*gitea.Issue](ctx, s.Client, "repos/issues/search", query, opts.Limit, opts.Page)
}