mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 19:11:25 +01:00
feat(repo): show repository columns, state and README
repo list and search repo printed bare names. They now render a table with NAME, DESCRIPTION, INFO (public or private, fork, mirror, template, archived, empty) and UPDATED under "Showing N of M repositories ...", an empty-state message on stderr, and tab-separated rows with the URL when piped. --type, --sort and --order complete and validate their values. repo view shows the visibility and kind badges, stars, forks, open issues and pull requests, the parent of a fork, the source of a mirror, the default branch and the clone URLs, followed by the README of the default branch (or --branch), rendered on a terminal. The repository JSON gains empty, stars_count, forks_count, open_issues_count, open_pr_count, topics and updated_at. Signed-off-by: bircni <bircni@icloud.com>
This commit is contained in:
10 files changed
+456
-152
No files matched your search
@@ -26,6 +26,8 @@ func repositoryFrom(repo *gitea.Repository) Repository {
|
||||
FullName: repo.FullName, Description: repo.Description, URL: repo.HTMLURL, Private: repo.Private,
|
||||
DefaultBranch: repo.DefaultBranch, CloneURL: repo.CloneURL, SSHURL: repo.SSHURL,
|
||||
Archived: repo.Archived, Template: repo.Template, Fork: repo.Fork, Mirror: repo.Mirror, OriginalURL: repo.OriginalURL,
|
||||
Empty: repo.Empty, Stars: repo.Stars, Forks: repo.Forks, OpenIssues: repo.OpenIssues, OpenPulls: repo.OpenPulls,
|
||||
Topics: repo.Topics, UpdatedAt: timestamp(repo.Updated),
|
||||
}
|
||||
if repo.Parent != nil {
|
||||
parent := repositoryFrom(repo.Parent)
|
||||
|
||||
@@ -39,11 +39,14 @@ func TestRepositoryFromMapsParents(t *testing.T) {
|
||||
"id":2,"name":"fork","full_name":"bob/fork","description":"Copy","html_url":"https://gitea.test/bob/fork",
|
||||
"private":true,"default_branch":"main","clone_url":"https://gitea.test/bob/fork.git","ssh_url":"git@gitea.test:bob/fork.git",
|
||||
"archived":true,"template":true,"fork":true,"mirror":true,"website":"https://example.test","has_actions":true,
|
||||
"original_url":"https://source.test/repo","parent":{"id":1,"name":"project","full_name":"alice/project"}}`)
|
||||
"original_url":"https://source.test/repo","parent":{"id":1,"name":"project","full_name":"alice/project"},
|
||||
"empty":true,"stars_count":3,"forks_count":2,"open_issues_count":5,"open_pr_counter":1,"topics":["cli"],
|
||||
"updated_at":"2026-10-02T12:00:00Z"}`)
|
||||
require.Equal(t, Repository{
|
||||
ActionsEnabled: true, Website: "https://example.test", ID: 2, Name: "fork", FullName: "bob/fork", Description: "Copy",
|
||||
URL: "https://gitea.test/bob/fork", Private: true, DefaultBranch: "main", CloneURL: "https://gitea.test/bob/fork.git",
|
||||
SSHURL: "git@gitea.test:bob/fork.git", Archived: true, Template: true, Fork: true, Mirror: true,
|
||||
Empty: true, Stars: 3, Forks: 2, OpenIssues: 5, OpenPulls: 1, Topics: []string{"cli"}, UpdatedAt: "2026-10-02T12:00:00Z",
|
||||
Parent: &Repository{ID: 1, Name: "project", FullName: "alice/project"}, OriginalURL: "https://source.test/repo",
|
||||
}, repositoryFrom(repo))
|
||||
}
|
||||
|
||||
@@ -5,6 +5,10 @@ package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
gitea "gitea.dev/sdk"
|
||||
"gitea.dev/tea/internal/transport"
|
||||
@@ -30,6 +34,13 @@ type Repository struct {
|
||||
Template bool `json:"template"`
|
||||
Fork bool `json:"fork"`
|
||||
Mirror bool `json:"mirror"`
|
||||
Empty bool `json:"empty"`
|
||||
Stars int `json:"stars_count"`
|
||||
Forks int `json:"forks_count"`
|
||||
OpenIssues int `json:"open_issues_count"`
|
||||
OpenPulls int `json:"open_pr_count"`
|
||||
Topics []string `json:"topics"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
Parent *Repository `json:"parent,omitempty"`
|
||||
// OriginalURL stays out of JSON output: migration sources may embed credentials.
|
||||
OriginalURL string `json:"-"`
|
||||
@@ -78,6 +89,36 @@ func (s Repositories) get(ctx context.Context, repo string) (*gitea.Repository,
|
||||
})
|
||||
}
|
||||
|
||||
// readmeNames are the README file names that Gitea shows, in order of preference.
|
||||
var readmeNames = []string{"readme.md", "readme.markdown", "readme", "readme.txt"}
|
||||
|
||||
// Readme returns the README in the top directory of the repository at ref: its file name and its
|
||||
// content, or "" when there is none.
|
||||
func (s Repositories) Readme(ctx context.Context, repo, ref string) (name, content string, err error) {
|
||||
entries, err := repoCall(ctx, s.Client, repo, func(ctx context.Context, c *gitea.Client, owner, name string) ([]*gitea.ContentsResponse, *gitea.Response, error) {
|
||||
return c.Repositories.ListContents(ctx, owner, name, ref, "")
|
||||
})
|
||||
if err != nil {
|
||||
if apiErr, ok := errors.AsType[*transport.APIError](err); ok && apiErr.Status == http.StatusNotFound {
|
||||
return "", "", nil
|
||||
}
|
||||
return "", "", err
|
||||
}
|
||||
rank := len(readmeNames)
|
||||
for _, entry := range entries {
|
||||
if i := slices.Index(readmeNames, strings.ToLower(entry.Name)); entry.Type == "file" && i >= 0 && i < rank {
|
||||
name, rank = entry.Name, i
|
||||
}
|
||||
}
|
||||
if name == "" {
|
||||
return "", "", nil
|
||||
}
|
||||
data, err := repoCall(ctx, s.Client, repo, func(ctx context.Context, c *gitea.Client, owner, repo string) ([]byte, *gitea.Response, error) {
|
||||
return c.Repositories.GetFile(ctx, owner, repo, ref, name)
|
||||
})
|
||||
return name, string(data), err
|
||||
}
|
||||
|
||||
// RepositoryPermissions holds server-side access flags used only for local decisions.
|
||||
// It is intentionally omitted from the stable Repository JSON schema.
|
||||
type RepositoryPermissions struct {
|
||||
|
||||
@@ -54,24 +54,30 @@ func repositoryKind(item Repository, kind string) bool {
|
||||
// List returns repositories accessible to the account, or its selected collection.
|
||||
// It reports whether more repositories follow the selected page.
|
||||
func (s Repositories) List(ctx context.Context, opts RepositoryListOptions) ([]Repository, bool, error) {
|
||||
page, err := s.ListPage(ctx, opts)
|
||||
return page.Items, page.More, err
|
||||
}
|
||||
|
||||
// ListPage is List that also reports the size of the collection when the server counts it.
|
||||
// A kind filter applies to the page, so the collection is not counted then.
|
||||
func (s Repositories) ListPage(ctx context.Context, opts RepositoryListOptions) (Page[Repository], error) {
|
||||
isOrg := false
|
||||
var err error
|
||||
if opts.Owner != "" {
|
||||
if _, isOrg, err = s.ownerID(ctx, opts.Owner); err != nil {
|
||||
return nil, false, err
|
||||
return Page[Repository]{}, err
|
||||
}
|
||||
}
|
||||
var items []*gitea.Repository
|
||||
var more bool
|
||||
var window Page[*gitea.Repository]
|
||||
if opts.Owner == "" && (opts.Starred || opts.Watched) {
|
||||
// TODO(sdk): GetMyStarredRepos and GetMyWatchedRepos accept no ListOptions.
|
||||
endpoint := "user/subscriptions"
|
||||
if opts.Starred {
|
||||
endpoint = "user/starred"
|
||||
}
|
||||
items, more, err = rawWindow[*gitea.Repository](ctx, s.Client, endpoint, nil, opts.Limit, opts.Page)
|
||||
window, err = rawPage[*gitea.Repository](ctx, s.Client, endpoint, nil, opts.Limit, opts.Page)
|
||||
} else {
|
||||
items, more, err = listWindow(opts.Limit, opts.Page, func(limit, page int) ([]*gitea.Repository, http.Header, error) {
|
||||
window, err = listPage(opts.Limit, opts.Page, func(limit, page int) ([]*gitea.Repository, http.Header, error) {
|
||||
items, resp, err := call(ctx, s.Client, func(ctx context.Context, c *gitea.Client) ([]*gitea.Repository, *gitea.Response, error) {
|
||||
switch {
|
||||
case isOrg:
|
||||
@@ -86,22 +92,22 @@ func (s Repositories) List(ctx context.Context, opts RepositoryListOptions) ([]R
|
||||
})
|
||||
}
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
return Page[Repository]{}, err
|
||||
}
|
||||
result := make([]Repository, 0, len(items))
|
||||
for _, item := range items {
|
||||
result := Page[Repository]{Items: make([]Repository, 0, len(window.Items)), More: window.More, Total: window.Total, Counted: window.Counted && opts.Kind == ""}
|
||||
for _, item := range window.Items {
|
||||
if repo := repositoryFrom(item); repositoryKind(repo, opts.Kind) {
|
||||
result = append(result, repo)
|
||||
result.Items = append(result.Items, repo)
|
||||
}
|
||||
}
|
||||
return result, more, nil
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Search searches repository names, descriptions or topics within one instance.
|
||||
func (s Repositories) Search(ctx context.Context, opts RepositorySearchOptions) ([]Repository, error) {
|
||||
func (s Repositories) Search(ctx context.Context, opts RepositorySearchOptions) (Page[Repository], error) {
|
||||
options := gitea.SearchRepoOptions{
|
||||
ListOptions: listOptions(opts.Limit, opts.Page), Keyword: opts.Query, KeywordIsTopic: opts.Topic,
|
||||
KeywordInDescription: true, IsPrivate: opts.Private, IsArchived: opts.Archived, Type: gitea.RepoType(opts.Kind),
|
||||
Keyword: opts.Query, KeywordIsTopic: opts.Topic, KeywordInDescription: true,
|
||||
IsPrivate: opts.Private, IsArchived: opts.Archived, Type: gitea.RepoType(opts.Kind),
|
||||
}
|
||||
if opts.Sort != "" {
|
||||
options.Sort, options.Order = opts.Sort, opts.Order
|
||||
@@ -109,7 +115,7 @@ func (s Repositories) Search(ctx context.Context, opts RepositorySearchOptions)
|
||||
if opts.Owner != "" {
|
||||
id, _, err := s.ownerID(ctx, opts.Owner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return Page[Repository]{}, err
|
||||
}
|
||||
options.OwnerID = id
|
||||
}
|
||||
@@ -117,14 +123,18 @@ func (s Repositories) Search(ctx context.Context, opts RepositorySearchOptions)
|
||||
return c.Users.GetMyUserInfo(ctx)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return Page[Repository]{}, err
|
||||
}
|
||||
options.PrioritizedByOwnerID = account.ID
|
||||
items, _, err := call(ctx, s.Client, func(ctx context.Context, c *gitea.Client) ([]*gitea.Repository, *gitea.Response, error) {
|
||||
return c.Repositories.SearchRepos(ctx, options)
|
||||
window, err := listPage(opts.Limit, opts.Page, func(limit, page int) ([]*gitea.Repository, http.Header, error) {
|
||||
options.ListOptions = listOptions(limit, page)
|
||||
items, resp, err := call(ctx, s.Client, func(ctx context.Context, c *gitea.Client) ([]*gitea.Repository, *gitea.Response, error) {
|
||||
return c.Repositories.SearchRepos(ctx, options)
|
||||
})
|
||||
return items, headerOf(resp), err
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return Page[Repository]{}, err
|
||||
}
|
||||
return convertAll(items, repositoryFrom), nil
|
||||
return Page[Repository]{Items: convertAll(window.Items, repositoryFrom), More: window.More, Total: window.Total, Counted: window.Counted}, nil
|
||||
}
|
||||
@@ -112,10 +112,10 @@ func TestRepositorySearchSendsExplicitFalseFilters(t *testing.T) {
|
||||
_, _ = w.Write([]byte(`{"data":[]}`))
|
||||
}))
|
||||
value := false
|
||||
items, err := (Repositories{Client: client}).Search(context.Background(), RepositorySearchOptions{Query: "two words", Private: &value, Archived: &value, Topic: true, Limit: 30, Page: 1})
|
||||
page, err := (Repositories{Client: client}).Search(context.Background(), RepositorySearchOptions{Query: "two words", Private: &value, Archived: &value, Topic: true, Limit: 30, Page: 1})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, items)
|
||||
require.Empty(t, items)
|
||||
require.NotNil(t, page.Items)
|
||||
require.Empty(t, page.Items)
|
||||
}
|
||||
|
||||
func TestRepositorySearchScopesOwner(t *testing.T) {
|
||||
@@ -136,7 +136,7 @@ func TestRepositorySearchScopesOwner(t *testing.T) {
|
||||
_, _ = w.Write([]byte(`{"data":[{"id":4,"full_name":"acme/tool"}]}`))
|
||||
}
|
||||
}))
|
||||
items, err := (Repositories{Client: client}).Search(context.Background(), RepositorySearchOptions{Owner: "acme", Kind: "fork", Sort: "updated", Order: "desc", Limit: 30, Page: 1})
|
||||
page, err := (Repositories{Client: client}).Search(context.Background(), RepositorySearchOptions{Owner: "acme", Kind: "fork", Sort: "updated", Order: "desc", Limit: 30, Page: 1})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []Repository{{ID: 4, FullName: "acme/tool"}}, items)
|
||||
require.Equal(t, []Repository{{ID: 4, FullName: "acme/tool"}}, page.Items)
|
||||
}
|
||||
Reference in new issue
Block a user