mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 11:01: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
+437
-133
No files matched your search
@@ -511,13 +511,10 @@ const (
|
|||||||
repo delete
|
repo delete
|
||||||
repo edit
|
repo edit
|
||||||
repo fork
|
repo fork
|
||||||
repo list
|
|
||||||
repo migrate
|
repo migrate
|
||||||
repo set-default
|
repo set-default
|
||||||
repo view
|
|
||||||
search issue
|
search issue
|
||||||
search pr
|
search pr
|
||||||
search repo
|
|
||||||
status
|
status
|
||||||
time create
|
time create
|
||||||
time delete
|
time delete
|
||||||
@@ -543,8 +540,6 @@ const (
|
|||||||
pr comment
|
pr comment
|
||||||
release
|
release
|
||||||
release asset
|
release asset
|
||||||
repo
|
|
||||||
search
|
|
||||||
time
|
time
|
||||||
wiki
|
wiki
|
||||||
`
|
`
|
||||||
|
|||||||
+220
-68
@@ -5,6 +5,7 @@ package app
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gitea.dev/tea/internal/clierr"
|
"gitea.dev/tea/internal/clierr"
|
||||||
@@ -14,6 +15,10 @@ import (
|
|||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// repositoryArguments documents the [<repository>] argument of repository commands.
|
||||||
|
const repositoryArguments = `A repository can be supplied as NAME (one of yours), OWNER/NAME, HOST/OWNER/NAME or a URL.
|
||||||
|
Without an argument, the repository of the current Git clone is used.`
|
||||||
|
|
||||||
// repositoryArgument selects the repository from an optional positional owner/name or URL.
|
// repositoryArgument selects the repository from an optional positional owner/name or URL.
|
||||||
func (a *application) repositoryArgument(args []string) error {
|
func (a *application) repositoryArgument(args []string) error {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
@@ -40,75 +45,13 @@ func (a *application) rejectRepoFlag(cmd *cobra.Command, suggest func(owner stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *application) repoCommand() *cobra.Command {
|
func (a *application) repoCommand() *cobra.Command {
|
||||||
group := &cobra.Command{Use: "repo", Short: "Work with Gitea repositories"}
|
group := &cobra.Command{
|
||||||
var opts services.RepositoryListOptions
|
Use: "repo",
|
||||||
list := &cobra.Command{Use: "list [<owner>]", Short: "List your repositories", Args: usageArgs(cobra.MaximumNArgs(1)), RunE: func(cmd *cobra.Command, args []string) error {
|
Short: "Work with Gitea repositories",
|
||||||
if err := a.rejectRepoFlag(cmd, func(owner string) string { return "tea repo list " + owner }); err != nil {
|
Long: `Work with Gitea repositories: list and view them, create, fork, clone and migrate them, keep
|
||||||
return err
|
local branches and forks in sync, and change their settings.`,
|
||||||
}
|
}
|
||||||
if len(args) > 0 {
|
group.AddCommand(a.repoListCommand(), a.repoViewCommand(), a.repositorySyncCommand())
|
||||||
if cmd.Flags().Changed("owner") {
|
|
||||||
return clierr.Wrap(clierr.Usage, errors.New("supply the owner as an argument or --owner, not both"))
|
|
||||||
}
|
|
||||||
opts.Owner = args[0]
|
|
||||||
}
|
|
||||||
if err := validateRepositoryKind(opts.Kind); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
scopes := 0
|
|
||||||
for _, selected := range []bool{opts.Owner != "", opts.Starred, opts.Watched} {
|
|
||||||
if selected {
|
|
||||||
scopes++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if scopes > 1 {
|
|
||||||
return clierr.Wrap(clierr.Usage, errors.New("--owner, --starred, and --watched select mutually exclusive collections"))
|
|
||||||
}
|
|
||||||
target, err := a.resolve(cmd.Context(), false)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
items, more, err := (services.Repositories{Client: target.client}).List(cmd.Context(), opts)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := writeList(a, cmd, items, func(item services.Repository) []terminal.Cell {
|
|
||||||
return []terminal.Cell{{Text: item.FullName, Link: item.URL}}
|
|
||||||
}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return a.morePagesHint(cmd, more, opts.Page)
|
|
||||||
}}
|
|
||||||
pageFlags(list, &opts.Limit, &opts.Page)
|
|
||||||
list.Flags().StringVar(&opts.Owner, "owner", "", "List repositories belonging to this user or organization")
|
|
||||||
list.Flags().StringVar(&opts.Kind, "type", "", "Repository kind: fork, source, mirror")
|
|
||||||
list.Flags().BoolVar(&opts.Starred, "starred", false, "List your starred repositories")
|
|
||||||
list.Flags().BoolVar(&opts.Watched, "watched", false, "List your watched repositories")
|
|
||||||
group.AddCommand(list, a.repositorySyncCommand())
|
|
||||||
var web bool
|
|
||||||
view := &cobra.Command{Use: "view [repository]", Short: "View the selected repository", Args: usageArgs(cobra.MaximumNArgs(1)), RunE: func(cmd *cobra.Command, args []string) error {
|
|
||||||
if err := a.repositoryArgument(args); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
target, err := a.resolve(cmd.Context(), true)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
item, err := (services.Repositories{Client: target.client}).View(cmd.Context(), target.Repo)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if web {
|
|
||||||
return a.openBrowser(cmd, item.URL)
|
|
||||||
}
|
|
||||||
return writeRecord(a, cmd, item, func(item services.Repository) terminal.Record {
|
|
||||||
record := repositoryRecord(item)
|
|
||||||
record.Lines = append([]string{terminal.SafeLine(item.Description)}, record.Lines...)
|
|
||||||
return record
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
view.Flags().BoolVar(&web, "web", false, "Open the repository in a browser")
|
|
||||||
group.AddCommand(view)
|
|
||||||
group.AddCommand(&cobra.Command{Use: "set-default [repository]", Short: "Save the selected repository in local Git configuration", Annotations: textOutput("set-default configures Git"), Args: usageArgs(cobra.MaximumNArgs(1)), RunE: func(cmd *cobra.Command, args []string) error {
|
group.AddCommand(&cobra.Command{Use: "set-default [repository]", Short: "Save the selected repository in local Git configuration", Annotations: textOutput("set-default configures Git"), Args: usageArgs(cobra.MaximumNArgs(1)), RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
if err := a.repositoryArgument(args); err != nil {
|
if err := a.repositoryArgument(args); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -123,3 +66,212 @@ func (a *application) repoCommand() *cobra.Command {
|
|||||||
a.addRepositoryMutations(group)
|
a.addRepositoryMutations(group)
|
||||||
return group
|
return group
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *application) repoListCommand() *cobra.Command {
|
||||||
|
var opts services.RepositoryListOptions
|
||||||
|
list := &cobra.Command{
|
||||||
|
Use: "list [<owner>]",
|
||||||
|
Short: "List repositories",
|
||||||
|
Long: `List the repositories you can access, those of a user or organization, or the repositories you
|
||||||
|
starred or watch.`,
|
||||||
|
Example: `# List the repositories you can access
|
||||||
|
$ tea repo list
|
||||||
|
|
||||||
|
# List the forks of an organization
|
||||||
|
$ tea repo list acme --type fork
|
||||||
|
|
||||||
|
# List your starred repositories as JSON
|
||||||
|
$ tea repo list --starred --json full_name,url`,
|
||||||
|
Args: usageArgs(cobra.MaximumNArgs(1)),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
if err := a.rejectRepoFlag(cmd, func(owner string) string { return "tea repo list " + owner }); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(args) > 0 {
|
||||||
|
if cmd.Flags().Changed("owner") {
|
||||||
|
return clierr.Wrap(clierr.Usage, errors.New("supply the owner as an argument or --owner, not both"))
|
||||||
|
}
|
||||||
|
opts.Owner = args[0]
|
||||||
|
}
|
||||||
|
scopes := 0
|
||||||
|
for _, selected := range []bool{opts.Owner != "", opts.Starred, opts.Watched} {
|
||||||
|
if selected {
|
||||||
|
scopes++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if scopes > 1 {
|
||||||
|
return clierr.Wrap(clierr.Usage, errors.New("an owner, --starred and --watched select different collections; pass one"))
|
||||||
|
}
|
||||||
|
target, err := a.resolve(cmd.Context(), false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
result, err := (services.Repositories{Client: target.client}).ListPage(cmd.Context(), opts)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
collection := repositoryCollection(opts)
|
||||||
|
spec := listSpec[services.Repository]{Columns: repositoryColumns, Row: repositoryRow, Context: collection, Empty: "no " + collection}
|
||||||
|
return writeTable(a, cmd, result.Items, spec, listPage{Number: opts.Page, More: result.More, Total: result.Total, Counted: result.Counted})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
pageFlags(list, &opts.Limit, &opts.Page)
|
||||||
|
list.Flags().StringVar(&opts.Owner, "owner", "", "List the repositories of this user or organization")
|
||||||
|
_ = list.Flags().MarkHidden("owner")
|
||||||
|
enumFlag(list, &opts.Kind, "type", "", "", "Only list repositories of this kind", "fork", "source", "mirror")
|
||||||
|
list.Flags().BoolVar(&opts.Starred, "starred", false, "List your starred repositories")
|
||||||
|
list.Flags().BoolVar(&opts.Watched, "watched", false, "List the repositories you watch")
|
||||||
|
_ = list.RegisterFlagCompletionFunc("owner", a.completeOrgs())
|
||||||
|
list.ValidArgsFunction = a.completeOrgs()
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
// repositoryCollection names the repositories that repo list selects, as in "forks owned by acme".
|
||||||
|
func repositoryCollection(opts services.RepositoryListOptions) string {
|
||||||
|
kinds := map[string]string{"fork": "forks", "mirror": "mirrors", "source": "source repositories"}
|
||||||
|
noun, ok := kinds[opts.Kind]
|
||||||
|
if !ok {
|
||||||
|
noun = "repositories"
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case opts.Owner != "":
|
||||||
|
return noun + " owned by " + opts.Owner
|
||||||
|
case opts.Starred:
|
||||||
|
return "starred " + noun
|
||||||
|
case opts.Watched:
|
||||||
|
return "watched " + noun
|
||||||
|
}
|
||||||
|
return noun + " you can access"
|
||||||
|
}
|
||||||
|
|
||||||
|
// repositoryColumns head the rows of repositoryRow in repo list and search repo.
|
||||||
|
var repositoryColumns = []terminal.Column{{Header: "Name"}, {Header: "Description", Flex: true}, {Header: "Info"}, {Header: "Updated"}, {Header: "URL", PipedOnly: true}}
|
||||||
|
|
||||||
|
func repositoryRow(item services.Repository) []terminal.Cell {
|
||||||
|
return []terminal.Cell{{Text: item.FullName, Link: item.URL}, {Text: item.Description}, {Text: strings.Join(repositoryInfo(item), ", ")}, {Time: terminal.ParseTime(item.UpdatedAt)}, {Text: item.URL}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// repositoryInfo describes the visibility and the kind of a repository: public or private, then
|
||||||
|
// fork, mirror, template, archived or empty.
|
||||||
|
func repositoryInfo(item services.Repository) []string {
|
||||||
|
info := []string{"public"}
|
||||||
|
if item.Private {
|
||||||
|
info[0] = "private"
|
||||||
|
}
|
||||||
|
for _, kind := range []struct {
|
||||||
|
is bool
|
||||||
|
name string
|
||||||
|
}{{item.Fork, "fork"}, {item.Mirror, "mirror"}, {item.Template, "template"}, {item.Archived, "archived"}, {item.Empty, "empty"}} {
|
||||||
|
if kind.is {
|
||||||
|
info = append(info, kind.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *application) repoViewCommand() *cobra.Command {
|
||||||
|
var web bool
|
||||||
|
var branch string
|
||||||
|
view := &cobra.Command{
|
||||||
|
Use: "view [<repository>]",
|
||||||
|
Short: "View a repository",
|
||||||
|
Long: `View a repository: its description, state, counts and clone URLs, followed by its README, which
|
||||||
|
is rendered on a terminal.`,
|
||||||
|
Example: `# View the repository of the current clone
|
||||||
|
$ tea repo view
|
||||||
|
|
||||||
|
# View the README of another branch
|
||||||
|
$ tea repo view alice/project --branch develop
|
||||||
|
|
||||||
|
# Open the repository in the browser
|
||||||
|
$ tea repo view alice/project --web`,
|
||||||
|
Annotations: map[string]string{"tea.arguments": repositoryArguments},
|
||||||
|
Args: usageArgs(cobra.MaximumNArgs(1)),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
if err := a.repositoryArgument(args); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
target, err := a.resolve(cmd.Context(), true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
service := services.Repositories{Client: target.client}
|
||||||
|
item, err := service.View(cmd.Context(), target.Repo)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if web {
|
||||||
|
return a.openBrowser(cmd, item.URL)
|
||||||
|
}
|
||||||
|
readme := ""
|
||||||
|
if a.options.json == "" && !item.Empty {
|
||||||
|
if _, readme, err = service.Readme(cmd.Context(), target.Repo, branch); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return writeView(a, cmd, item, func(item services.Repository) terminal.View { return a.repositoryView(item, readme) })
|
||||||
|
},
|
||||||
|
}
|
||||||
|
view.Flags().BoolVarP(&web, "web", "w", false, "Open the repository in the browser")
|
||||||
|
view.Flags().StringVarP(&branch, "branch", "b", "", "Show the README of this branch (default: the default branch)")
|
||||||
|
_ = view.RegisterFlagCompletionFunc("branch", a.completeBranches())
|
||||||
|
view.ValidArgsFunction = a.completeRepos()
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
// repositoryView shows a repository with its README as the body.
|
||||||
|
func (a *application) repositoryView(item services.Repository, readme string) terminal.View {
|
||||||
|
badges := []terminal.Cell{{Text: "public", Color: "green"}}
|
||||||
|
if item.Private {
|
||||||
|
badges[0] = terminal.Cell{Text: "private", Color: "yellow"}
|
||||||
|
}
|
||||||
|
for _, kind := range repositoryInfo(item)[1:] {
|
||||||
|
badge := terminal.Cell{Text: kind, Muted: true}
|
||||||
|
if kind == "archived" {
|
||||||
|
badge = terminal.Cell{Text: "archived (read-only)", Color: "yellow"}
|
||||||
|
}
|
||||||
|
badges = append(badges, badge)
|
||||||
|
}
|
||||||
|
meta := []string{
|
||||||
|
fmt.Sprintf("★ %d", item.Stars), countNoun(item.Forks, "fork"), countNoun(item.OpenIssues, "open issue"),
|
||||||
|
countNoun(item.OpenPulls, "open pull request"),
|
||||||
|
}
|
||||||
|
if updated := terminal.ParseTime(item.UpdatedAt); !updated.IsZero() {
|
||||||
|
meta = append(meta, "updated "+terminal.RelativeTime(a.deps.Now(), updated))
|
||||||
|
}
|
||||||
|
view := terminal.View{
|
||||||
|
Title: item.FullName, Badges: badges, Meta: strings.Join(meta, " · "), Body: readme, Kind: "repository", URL: item.URL,
|
||||||
|
Fields: []terminal.Field{
|
||||||
|
{Label: "Description", Value: item.Description},
|
||||||
|
{Label: "Forked from", Value: parentName(item)},
|
||||||
|
{Label: "Mirror of", Value: mirrorSourceURL(item)},
|
||||||
|
{Label: "Default branch", Value: item.DefaultBranch},
|
||||||
|
{Label: "Website", Value: item.Website},
|
||||||
|
{Label: "Topics", Value: strings.Join(item.Topics, ", ")},
|
||||||
|
{Label: "Clone", Value: item.CloneURL},
|
||||||
|
{Label: "SSH", Value: item.SSHURL},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if a.deps.Streams.OutTTY && strings.TrimSpace(readme) == "" {
|
||||||
|
view.Body = "_This repository has no README._"
|
||||||
|
if item.Empty {
|
||||||
|
view.Body = "_This repository is empty._"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
func parentName(item services.Repository) string {
|
||||||
|
if item.Parent == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return item.Parent.FullName
|
||||||
|
}
|
||||||
|
|
||||||
|
// countNoun counts n of a noun, as in "1 fork" or "3 forks".
|
||||||
|
func countNoun(n int, noun string) string {
|
||||||
|
if n == 1 {
|
||||||
|
return "1 " + noun
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%d %ss", n, noun)
|
||||||
|
}
|
||||||
+101
-1
@@ -5,14 +5,114 @@ package app
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
// repoTTYDeps returns serverDeps for a 100-column terminal without color or links, as people read tea's output.
|
// repoTTYDeps returns serverDeps for a 100-column terminal without color or links, as people read
|
||||||
|
// tea's output, at a fixed time.
|
||||||
func repoTTYDeps(t *testing.T, serverURL string) (Dependencies, *bytes.Buffer, *bytes.Buffer) {
|
func repoTTYDeps(t *testing.T, serverURL string) (Dependencies, *bytes.Buffer, *bytes.Buffer) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
deps, out, diagnostics := serverDeps(t, serverURL)
|
deps, out, diagnostics := serverDeps(t, serverURL)
|
||||||
deps.Getenv = serverEnv(serverURL, map[string]string{"TERM": "dumb"})
|
deps.Getenv = serverEnv(serverURL, map[string]string{"TERM": "dumb"})
|
||||||
deps.Streams.OutTTY, deps.Streams.Width = true, 100
|
deps.Streams.OutTTY, deps.Streams.Width = true, 100
|
||||||
|
deps.Now = func() time.Time { return time.Date(2026, 10, 11, 20, 0, 0, 0, time.UTC) }
|
||||||
return deps, out, diagnostics
|
return deps, out, diagnostics
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const listedRepositories = `[
|
||||||
|
{"full_name":"alice/old-stuff","description":"Archived repository","archived":true,"html_url":"https://gitea.test/alice/old-stuff","updated_at":"2026-10-11T18:00:00Z"},
|
||||||
|
{"full_name":"acme/service","private":true,"fork":true,"html_url":"https://gitea.test/acme/service","updated_at":"2026-10-09T18:00:00Z"}
|
||||||
|
]`
|
||||||
|
|
||||||
|
func TestRepoListShowsColumns(t *testing.T) {
|
||||||
|
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
require.Equal(t, "/api/v1/user/repos", r.URL.Path)
|
||||||
|
w.Header().Set("X-Total-Count", "2")
|
||||||
|
_, _ = w.Write([]byte(listedRepositories))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
deps, out, _ := repoTTYDeps(t, server.URL)
|
||||||
|
require.NoError(t, Execute(context.Background(), deps, []string{"repo", "list"}))
|
||||||
|
require.Equal(t, `Showing 2 of 2 repositories you can access
|
||||||
|
|
||||||
|
NAME DESCRIPTION INFO UPDATED
|
||||||
|
alice/old-stuff Archived repository public, archived about 2 hours ago
|
||||||
|
acme/service private, fork about 2 days ago
|
||||||
|
`, out.String())
|
||||||
|
deps, out, _ = serverDeps(t, server.URL)
|
||||||
|
require.NoError(t, Execute(context.Background(), deps, []string{"repo", "list"}))
|
||||||
|
require.Equal(t, "alice/old-stuff\tArchived repository\tpublic, archived\t2026-10-11T18:00:00Z\thttps://gitea.test/alice/old-stuff\n"+
|
||||||
|
"acme/service\t\tprivate, fork\t2026-10-09T18:00:00Z\thttps://gitea.test/acme/service\n", out.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRepoListReportsAnEmptyCollection(t *testing.T) {
|
||||||
|
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.URL.Path {
|
||||||
|
case "/api/v1/orgs/acme":
|
||||||
|
_, _ = w.Write([]byte(`{"id":3,"username":"acme"}`))
|
||||||
|
default:
|
||||||
|
_, _ = w.Write([]byte(`[]`))
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
deps, out, diagnostics := repoTTYDeps(t, server.URL)
|
||||||
|
require.NoError(t, Execute(context.Background(), deps, []string{"repo", "list", "acme", "--type", "fork"}))
|
||||||
|
require.Empty(t, out.String())
|
||||||
|
require.Equal(t, "no forks owned by acme\n", diagnostics.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRepoViewShowsStateAndReadme(t *testing.T) {
|
||||||
|
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.URL.Path {
|
||||||
|
case "/api/v1/repos/bob/project":
|
||||||
|
_, _ = w.Write([]byte(`{"full_name":"bob/project","description":"Demo","fork":true,"archived":true,"default_branch":"main",
|
||||||
|
"stars_count":1,"forks_count":0,"open_issues_count":2,"open_pr_counter":1,"updated_at":"2026-10-11T19:00:00Z",
|
||||||
|
"html_url":"https://gitea.test/bob/project","clone_url":"https://gitea.test/bob/project.git",
|
||||||
|
"parent":{"full_name":"alice/project"}}`))
|
||||||
|
case "/api/v1/repos/bob/project/contents/":
|
||||||
|
require.Equal(t, "develop", r.URL.Query().Get("ref"))
|
||||||
|
_, _ = w.Write([]byte(`[{"name":"docs","type":"dir"},{"name":"README","type":"file"},{"name":"readme.md","type":"file"}]`))
|
||||||
|
case "/api/v1/repos/bob/project/raw/readme.md":
|
||||||
|
_, _ = w.Write([]byte("Hello **there**"))
|
||||||
|
default:
|
||||||
|
t.Errorf("unexpected request %s", r.URL)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
deps, out, _ := repoTTYDeps(t, server.URL)
|
||||||
|
deps.Getenv = serverEnv(server.URL, map[string]string{"TERM": "dumb", "TEA_MARKDOWN": "raw"})
|
||||||
|
require.NoError(t, Execute(context.Background(), deps, []string{"repo", "view", "bob/project", "--branch", "develop"}))
|
||||||
|
require.Equal(t, `bob/project
|
||||||
|
public • fork • archived (read-only) • ★ 1 · 0 forks · 2 open issues · 1 open pull request · updated about 1 hour ago
|
||||||
|
Description: Demo
|
||||||
|
Forked from: alice/project
|
||||||
|
Default branch: main
|
||||||
|
Clone: https://gitea.test/bob/project.git
|
||||||
|
|
||||||
|
Hello **there**
|
||||||
|
|
||||||
|
View this repository on Gitea: https://gitea.test/bob/project
|
||||||
|
`, out.String())
|
||||||
|
deps, out, _ = serverDeps(t, server.URL)
|
||||||
|
require.NoError(t, Execute(context.Background(), deps, []string{"repo", "view", "bob/project", "-b", "develop"}))
|
||||||
|
require.Equal(t, "title:\tbob/project\ndescription:\tDemo\nforked from:\talice/project\nmirror of:\t\ndefault branch:\tmain\nwebsite:\t\ntopics:\t\n"+
|
||||||
|
"clone:\thttps://gitea.test/bob/project.git\nssh:\t\nurl:\thttps://gitea.test/bob/project\n--\nHello **there**\n", out.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRepoViewOfAnEmptyRepositorySkipsTheReadme(t *testing.T) {
|
||||||
|
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
require.Equal(t, "/api/v1/repos/alice/empty", r.URL.Path)
|
||||||
|
_, _ = w.Write([]byte(`{"full_name":"alice/empty","private":true,"empty":true,"html_url":"https://gitea.test/alice/empty"}`))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
deps, out, _ := repoTTYDeps(t, server.URL)
|
||||||
|
deps.Getenv = serverEnv(server.URL, map[string]string{"TERM": "dumb", "TEA_MARKDOWN": "raw"})
|
||||||
|
require.NoError(t, Execute(context.Background(), deps, []string{"repo", "view", "alice/empty"}))
|
||||||
|
require.Contains(t, out.String(), "private • empty • ★ 0")
|
||||||
|
require.Contains(t, out.String(), "_This repository is empty._")
|
||||||
|
}
|
||||||
+33
-32
@@ -4,41 +4,37 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gitea.dev/tea/internal/clierr"
|
|
||||||
"gitea.dev/tea/internal/services"
|
"gitea.dev/tea/internal/services"
|
||||||
"gitea.dev/tea/internal/terminal"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
func validateRepositoryKind(kind string) error {
|
|
||||||
switch kind {
|
|
||||||
case "", "fork", "source", "mirror":
|
|
||||||
return nil
|
|
||||||
default:
|
|
||||||
return clierr.Wrap(clierr.Usage, errors.New("--type must be fork, source, or mirror"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *application) searchCommand() *cobra.Command {
|
func (a *application) searchCommand() *cobra.Command {
|
||||||
group := &cobra.Command{Use: "search", Short: "Search within one selected Gitea instance"}
|
group := &cobra.Command{
|
||||||
|
Use: "search",
|
||||||
|
Short: "Search within one selected Gitea instance",
|
||||||
|
Long: `Search the repositories, issues and pull requests of the selected Gitea instance that your
|
||||||
|
account can see.`,
|
||||||
|
}
|
||||||
var opts services.RepositorySearchOptions
|
var opts services.RepositorySearchOptions
|
||||||
var private, archived bool
|
var private, archived bool
|
||||||
command := &cobra.Command{Use: "repo [query]...", Short: "Search repository names, descriptions or topics", RunE: func(cmd *cobra.Command, args []string) error {
|
command := &cobra.Command{
|
||||||
|
Use: "repo [<query>...]",
|
||||||
|
Short: "Search repositories",
|
||||||
|
Long: "Search the names and descriptions of repositories, or their topics with --topic.",
|
||||||
|
Example: `# Search repositories about a CLI
|
||||||
|
$ tea search repo command line
|
||||||
|
|
||||||
|
# Search the archived repositories of an organization
|
||||||
|
$ tea search repo --owner acme --archived
|
||||||
|
|
||||||
|
# Search by topic, most recently updated first
|
||||||
|
$ tea search repo go --topic --sort updated --order desc`,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
if err := a.rejectRepoFlag(cmd, func(owner string) string { return "tea search repo --owner " + owner }); err != nil {
|
if err := a.rejectRepoFlag(cmd, func(owner string) string { return "tea search repo --owner " + owner }); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := validateRepositoryKind(opts.Kind); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := oneOf("sort", opts.Sort, "alpha", "created", "updated", "size", "id"); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := oneOf("order", opts.Order, "asc", "desc"); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if cmd.Flags().Changed("private") {
|
if cmd.Flags().Changed("private") {
|
||||||
opts.Private = &private
|
opts.Private = &private
|
||||||
}
|
}
|
||||||
@@ -50,23 +46,28 @@ func (a *application) searchCommand() *cobra.Command {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
items, err := (services.Repositories{Client: target.client}).Search(cmd.Context(), opts)
|
result, err := (services.Repositories{Client: target.client}).Search(cmd.Context(), opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return writeList(a, cmd, items, func(item services.Repository) []terminal.Cell {
|
what := "repositories"
|
||||||
return terminal.Row(item.FullName, item.Description, item.URL)
|
if opts.Query != "" {
|
||||||
})
|
what += " matching " + opts.Query
|
||||||
}}
|
}
|
||||||
|
spec := listSpec[services.Repository]{Columns: repositoryColumns, Row: repositoryRow, Context: what, Empty: "no " + what}
|
||||||
|
return writeTable(a, cmd, result.Items, spec, listPage{Number: opts.Page, More: result.More, Total: result.Total, Counted: result.Counted})
|
||||||
|
},
|
||||||
|
}
|
||||||
flags := command.Flags()
|
flags := command.Flags()
|
||||||
pageFlags(command, &opts.Limit, &opts.Page)
|
pageFlags(command, &opts.Limit, &opts.Page)
|
||||||
flags.StringVar(&opts.Owner, "owner", "", "Repository owner")
|
flags.StringVar(&opts.Owner, "owner", "", "Only repositories of this user or organization")
|
||||||
flags.StringVar(&opts.Kind, "type", "", "Repository kind: fork, source, mirror")
|
enumFlag(command, &opts.Kind, "type", "", "", "Only repositories of this kind", "fork", "source", "mirror")
|
||||||
flags.StringVar(&opts.Sort, "sort", "alpha", "Sort by alpha, created, updated, size, id")
|
enumFlag(command, &opts.Sort, "sort", "", "alpha", "Sort by", "alpha", "created", "updated", "size", "id")
|
||||||
flags.StringVar(&opts.Order, "order", "asc", "Sort order: asc, desc")
|
enumFlag(command, &opts.Order, "order", "", "asc", "Sort order", "asc", "desc")
|
||||||
flags.BoolVar(&opts.Topic, "topic", false, "Match repository topics")
|
flags.BoolVar(&opts.Topic, "topic", false, "Match repository topics")
|
||||||
flags.BoolVar(&private, "private", false, "Only private repositories (use =false for public)")
|
flags.BoolVar(&private, "private", false, "Only private repositories (use =false for public)")
|
||||||
flags.BoolVar(&archived, "archived", false, "Only archived repositories (use =false for active)")
|
flags.BoolVar(&archived, "archived", false, "Only archived repositories (use =false for active)")
|
||||||
|
_ = command.RegisterFlagCompletionFunc("owner", a.completeOrgs())
|
||||||
group.AddCommand(command, a.discussionSearchCommand(false), a.discussionSearchCommand(true))
|
group.AddCommand(command, a.discussionSearchCommand(false), a.discussionSearchCommand(true))
|
||||||
return group
|
return group
|
||||||
}
|
}
|
||||||
@@ -166,9 +166,9 @@ func TestUsageErrorsPrecedeRequests(t *testing.T) {
|
|||||||
{[]string{"repo", "edit", "--default-branch="}, "default branch cannot be empty"},
|
{[]string{"repo", "edit", "--default-branch="}, "default branch cannot be empty"},
|
||||||
{[]string{"repo", "archive", "alice/project", "-R", "alice/other", "--yes"}, "supply the repository as an argument or --repo, not both"},
|
{[]string{"repo", "archive", "alice/project", "-R", "alice/other", "--yes"}, "supply the repository as an argument or --repo, not both"},
|
||||||
{[]string{"repo", "view", "alice/project", "--repo", "alice/other"}, "supply the repository as an argument or --repo, not both"},
|
{[]string{"repo", "view", "alice/project", "--repo", "alice/other"}, "supply the repository as an argument or --repo, not both"},
|
||||||
{[]string{"repo", "view", "alice/project", "alice/other"}, "too many arguments\nUsage: tea repo view [repository] [flags]"},
|
{[]string{"repo", "view", "alice/project", "alice/other"}, "too many arguments\nUsage: tea repo view [<repository>] [flags]"},
|
||||||
{[]string{"repo", "list", "--owner", "account", "--starred"}, "--owner, --starred, and --watched select mutually exclusive collections"},
|
{[]string{"repo", "list", "--owner", "account", "--starred"}, "an owner, --starred and --watched select different collections; pass one"},
|
||||||
{[]string{"repo", "list", "--starred", "--watched"}, "--owner, --starred, and --watched select mutually exclusive collections"},
|
{[]string{"repo", "list", "--starred", "--watched"}, "an owner, --starred and --watched select different collections; pass one"},
|
||||||
{[]string{"repo", "list", "--type", "invalid"}, "--type must be fork, source, or mirror"},
|
{[]string{"repo", "list", "--type", "invalid"}, "--type must be fork, source, or mirror"},
|
||||||
{[]string{"repo", "set-default", "--json"}, "set-default configures Git"},
|
{[]string{"repo", "set-default", "--json"}, "set-default configures Git"},
|
||||||
{[]string{"repo", "sync", "--branch="}, "--branch cannot be empty"},
|
{[]string{"repo", "sync", "--branch="}, "--branch cannot be empty"},
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ func repositoryFrom(repo *gitea.Repository) Repository {
|
|||||||
FullName: repo.FullName, Description: repo.Description, URL: repo.HTMLURL, Private: repo.Private,
|
FullName: repo.FullName, Description: repo.Description, URL: repo.HTMLURL, Private: repo.Private,
|
||||||
DefaultBranch: repo.DefaultBranch, CloneURL: repo.CloneURL, SSHURL: repo.SSHURL,
|
DefaultBranch: repo.DefaultBranch, CloneURL: repo.CloneURL, SSHURL: repo.SSHURL,
|
||||||
Archived: repo.Archived, Template: repo.Template, Fork: repo.Fork, Mirror: repo.Mirror, OriginalURL: repo.OriginalURL,
|
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 {
|
if repo.Parent != nil {
|
||||||
parent := repositoryFrom(repo.Parent)
|
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",
|
"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",
|
"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,
|
"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{
|
require.Equal(t, Repository{
|
||||||
ActionsEnabled: true, Website: "https://example.test", ID: 2, Name: "fork", FullName: "bob/fork", Description: "Copy",
|
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",
|
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,
|
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",
|
Parent: &Repository{ID: 1, Name: "project", FullName: "alice/project"}, OriginalURL: "https://source.test/repo",
|
||||||
}, repositoryFrom(repo))
|
}, repositoryFrom(repo))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ package services
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
gitea "gitea.dev/sdk"
|
gitea "gitea.dev/sdk"
|
||||||
"gitea.dev/tea/internal/transport"
|
"gitea.dev/tea/internal/transport"
|
||||||
@@ -30,6 +34,13 @@ type Repository struct {
|
|||||||
Template bool `json:"template"`
|
Template bool `json:"template"`
|
||||||
Fork bool `json:"fork"`
|
Fork bool `json:"fork"`
|
||||||
Mirror bool `json:"mirror"`
|
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"`
|
Parent *Repository `json:"parent,omitempty"`
|
||||||
// OriginalURL stays out of JSON output: migration sources may embed credentials.
|
// OriginalURL stays out of JSON output: migration sources may embed credentials.
|
||||||
OriginalURL string `json:"-"`
|
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.
|
// RepositoryPermissions holds server-side access flags used only for local decisions.
|
||||||
// It is intentionally omitted from the stable Repository JSON schema.
|
// It is intentionally omitted from the stable Repository JSON schema.
|
||||||
type RepositoryPermissions struct {
|
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.
|
// List returns repositories accessible to the account, or its selected collection.
|
||||||
// It reports whether more repositories follow the selected page.
|
// It reports whether more repositories follow the selected page.
|
||||||
func (s Repositories) List(ctx context.Context, opts RepositoryListOptions) ([]Repository, bool, error) {
|
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
|
isOrg := false
|
||||||
var err error
|
var err error
|
||||||
if opts.Owner != "" {
|
if opts.Owner != "" {
|
||||||
if _, isOrg, err = s.ownerID(ctx, opts.Owner); err != nil {
|
if _, isOrg, err = s.ownerID(ctx, opts.Owner); err != nil {
|
||||||
return nil, false, err
|
return Page[Repository]{}, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var items []*gitea.Repository
|
var window Page[*gitea.Repository]
|
||||||
var more bool
|
|
||||||
if opts.Owner == "" && (opts.Starred || opts.Watched) {
|
if opts.Owner == "" && (opts.Starred || opts.Watched) {
|
||||||
// TODO(sdk): GetMyStarredRepos and GetMyWatchedRepos accept no ListOptions.
|
// TODO(sdk): GetMyStarredRepos and GetMyWatchedRepos accept no ListOptions.
|
||||||
endpoint := "user/subscriptions"
|
endpoint := "user/subscriptions"
|
||||||
if opts.Starred {
|
if opts.Starred {
|
||||||
endpoint = "user/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 {
|
} 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) {
|
items, resp, err := call(ctx, s.Client, func(ctx context.Context, c *gitea.Client) ([]*gitea.Repository, *gitea.Response, error) {
|
||||||
switch {
|
switch {
|
||||||
case isOrg:
|
case isOrg:
|
||||||
@@ -86,22 +92,22 @@ func (s Repositories) List(ctx context.Context, opts RepositoryListOptions) ([]R
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, err
|
return Page[Repository]{}, err
|
||||||
}
|
}
|
||||||
result := make([]Repository, 0, len(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 items {
|
for _, item := range window.Items {
|
||||||
if repo := repositoryFrom(item); repositoryKind(repo, opts.Kind) {
|
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.
|
// 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{
|
options := gitea.SearchRepoOptions{
|
||||||
ListOptions: listOptions(opts.Limit, opts.Page), Keyword: opts.Query, KeywordIsTopic: opts.Topic,
|
Keyword: opts.Query, KeywordIsTopic: opts.Topic, KeywordInDescription: true,
|
||||||
KeywordInDescription: true, IsPrivate: opts.Private, IsArchived: opts.Archived, Type: gitea.RepoType(opts.Kind),
|
IsPrivate: opts.Private, IsArchived: opts.Archived, Type: gitea.RepoType(opts.Kind),
|
||||||
}
|
}
|
||||||
if opts.Sort != "" {
|
if opts.Sort != "" {
|
||||||
options.Sort, options.Order = opts.Sort, opts.Order
|
options.Sort, options.Order = opts.Sort, opts.Order
|
||||||
@@ -109,7 +115,7 @@ func (s Repositories) Search(ctx context.Context, opts RepositorySearchOptions)
|
|||||||
if opts.Owner != "" {
|
if opts.Owner != "" {
|
||||||
id, _, err := s.ownerID(ctx, opts.Owner)
|
id, _, err := s.ownerID(ctx, opts.Owner)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return Page[Repository]{}, err
|
||||||
}
|
}
|
||||||
options.OwnerID = id
|
options.OwnerID = id
|
||||||
}
|
}
|
||||||
@@ -117,14 +123,18 @@ func (s Repositories) Search(ctx context.Context, opts RepositorySearchOptions)
|
|||||||
return c.Users.GetMyUserInfo(ctx)
|
return c.Users.GetMyUserInfo(ctx)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return Page[Repository]{}, err
|
||||||
}
|
}
|
||||||
options.PrioritizedByOwnerID = account.ID
|
options.PrioritizedByOwnerID = account.ID
|
||||||
items, _, err := call(ctx, s.Client, func(ctx context.Context, c *gitea.Client) ([]*gitea.Repository, *gitea.Response, error) {
|
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 c.Repositories.SearchRepos(ctx, options)
|
||||||
})
|
})
|
||||||
|
return items, headerOf(resp), err
|
||||||
|
})
|
||||||
if err != nil {
|
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":[]}`))
|
_, _ = w.Write([]byte(`{"data":[]}`))
|
||||||
}))
|
}))
|
||||||
value := false
|
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.NoError(t, err)
|
||||||
require.NotNil(t, items)
|
require.NotNil(t, page.Items)
|
||||||
require.Empty(t, items)
|
require.Empty(t, page.Items)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRepositorySearchScopesOwner(t *testing.T) {
|
func TestRepositorySearchScopesOwner(t *testing.T) {
|
||||||
@@ -136,7 +136,7 @@ func TestRepositorySearchScopesOwner(t *testing.T) {
|
|||||||
_, _ = w.Write([]byte(`{"data":[{"id":4,"full_name":"acme/tool"}]}`))
|
_, _ = 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.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