fix(repo): README lookup like Gitea, clearer branch/org errors, clone-after-create report, mirror interval 0 and search sort (follow gh)

Signed-off-by: bircni <bircni@icloud.com>
This commit is contained in:
bircni committed 2026-10-18 20:03:55 +02:00
1 parent 8305a89531
commit aa181a2b5c
16 files changed
+211 -37

No files matched your search

+14
View File
@@ -385,6 +385,20 @@ func TestInstanceCompletions(t *testing.T) {
}
}
func TestCompleteArgumentBranchesUsesTheNamedRepository(t *testing.T) {
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "/api/v1/repos/bob/tool/branches", r.URL.Path)
_, _ = w.Write([]byte(`[{"name":"develop"}]`))
}))
defer server.Close()
deps, _, _ := serverDeps(t, server.URL)
a := &application{deps: deps}
cmd := &cobra.Command{}
cmd.SetContext(context.Background())
values, _ := a.completeArgumentBranches()(cmd, []string{"bob/tool"}, "")
require.Equal(t, []string{"develop"}, values)
}
func TestCompletionInstallHelp(t *testing.T) {
deps, out, _ := testDeps(t)
require.NoError(t, Execute(context.Background(), deps, []string{"completion", "bash", "--help"}))
+23 -9
View File
@@ -413,15 +413,29 @@ func (a *application) completeUsers() cobra.CompletionFunc {
// completeBranches completes the local branches, then the repository's branches.
func (a *application) completeBranches() cobra.CompletionFunc {
return mergeCompletions(a.completeLocalBranches, a.apiCompletion(true, func(ctx context.Context, t target, _ string) ([]cobra.Completion, error) {
items, err := services.Branches{Client: t.client}.ListPage(ctx, t.Repo, completionLimit, 1)
return describe(items.Items, func(item services.Branch) (string, string) {
if item.Protected {
return item.Name, "protected"
}
return item.Name, ""
}), err
}))
return mergeCompletions(a.completeLocalBranches, a.apiCompletion(true, listBranches))
}
// completeArgumentBranches is completeBranches for a command with a positional [<repository>]:
// once that argument is given, it completes that repository's branches, not those of the clone here.
func (a *application) completeArgumentBranches() cobra.CompletionFunc {
inClone := a.completeBranches()
return func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
if len(args) == 0 || a.options.repo != "" {
return inClone(cmd, args, toComplete)
}
return a.apiCompletionIn(args[0], true, listBranches)(cmd, args, toComplete)
}
}
func listBranches(ctx context.Context, t target, _ string) ([]cobra.Completion, error) {
items, err := services.Branches{Client: t.client}.ListPage(ctx, t.Repo, completionLimit, 1)
return describe(items.Items, func(item services.Branch) (string, string) {
if item.Protected {
return item.Name, "protected"
}
return item.Name, ""
}), err
}
// completeTags completes the repository's tags.
+1
View File
@@ -281,6 +281,7 @@ func TestWriteTable(t *testing.T) {
stderr: "more results available; use --page 3\n",
},
{name: "empty message on a terminal", tty: true, page: listPage{Number: 1}, stderr: "no open issues in alice/x\n"},
{name: "next-page hint instead of the empty message on a filtered page", tty: true, page: listPage{Number: 1, More: true}, stderr: "more results available; use --page 2\n"},
{name: "nothing when empty and piped", page: listPage{Number: 1}},
{name: "next-page hint when an empty page has more", tty: true, page: listPage{Number: 1, More: true}, stderr: "more results available; use --page 2\n"},
{name: "next-page hint when an empty page has more and piped", page: listPage{Number: 1, More: true}, stderr: "more results available; use --page 2\n"},
+7
View File
@@ -4,6 +4,9 @@
package app
import (
"fmt"
"gitea.dev/tea/internal/clierr"
"gitea.dev/tea/internal/terminal"
"github.com/spf13/cobra"
)
@@ -52,5 +55,9 @@ func (a *application) requireInput(cmd *cobra.Command, value *string, flag, labe
if err != nil {
return false, err
}
if hidden := cmd.Flags().Lookup(flag); *value == "" && hidden != nil && hidden.Hidden && !a.policy(s).CanPrompt(a.deps.Streams) {
// --help does not list the flag, so point at the argument it stands in for.
return false, clierr.Wrap(clierr.Usage, fmt.Errorf("missing required input: %s (pass it as an argument)", label))
}
return terminal.RequireInput(cmd.Context(), value, flag, label, validate, a.deps.Streams, a.policy(s), a.deps.Prompter)
}
+1 -1
View File
@@ -273,7 +273,7 @@ $ tea repo view alice/project --web`,
}
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.RegisterFlagCompletionFunc("branch", a.completeArgumentBranches())
view.ValidArgsFunction = a.completeRepos()
return view
}
+8 -1
View File
@@ -147,7 +147,14 @@ $ tea repo create project --source . --push`,
target.Repo = item.FullName
switch {
case clone:
_, err = a.cloneRepository(cmd, target, item, cloneOptions{upstream: "upstream"})
// Record the new repository first so a failed clone does not hide that it exists.
if err := a.created(cmd, item, item.URL); err != nil {
return err
}
if _, err := a.cloneRepository(cmd, target, item, cloneOptions{upstream: "upstream"}); err != nil {
return fmt.Errorf("created %s, but could not clone it: %w", item.FullName, err)
}
return nil
case localSource != "":
err = a.publishRepository(cmd, target, item, localSource, remote, push)
}
+23
View File
@@ -63,6 +63,18 @@ func TestRepositoryCreateClonesNewRepository(t *testing.T) {
require.JSONEq(t, `{"id":7,"full_name":"alice/project"}`, out.String())
}
func TestRepositoryCreateReportsFailedCloneAfterCreating(t *testing.T) {
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = fmt.Fprintf(w, `{"id":7,"name":"project","full_name":"alice/project","clone_url":%q}`, "http://"+r.Host+"/alice/project.git")
}))
defer server.Close()
deps, out, _ := serverDeps(t, server.URL)
deps.Git = &fakeGit{}
err := Execute(context.Background(), deps, []string{"repo", "create", "project", "--clone", "--no-input", "--json=full_name"})
require.ErrorContains(t, err, "created alice/project, but could not clone it")
require.JSONEq(t, `{"full_name":"alice/project"}`, out.String())
}
func TestRepositoryCreateValidatesTemplatesBeforeCreating(t *testing.T) {
var requests []string
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -108,6 +120,17 @@ func TestRepositoryCreatePublishesALocalRepository(t *testing.T) {
require.ErrorContains(t, err, "--source publishes a local repository")
}
func TestRepositoryMigrateAcceptsZeroMirrorInterval(t *testing.T) {
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requirePayload(t, `{"repo_name":"tea","clone_addr":"https://github.com/go-gitea/tea.git","service":"git","mirror":true,"mirror_interval":"0"}`, r.Body)
_, _ = w.Write([]byte(`{"full_name":"alice/tea","mirror":true,"html_url":"https://gitea.test/alice/tea"}`))
}))
defer server.Close()
deps, _, diagnostics := repoTTYDeps(t, server.URL)
require.NoError(t, Execute(context.Background(), deps, []string{"repo", "migrate", "https://github.com/go-gitea/tea.git", "--mirror", "--mirror-interval", "0"}))
require.Equal(t, "✓ Imported https://github.com/go-gitea/tea.git into alice/tea (pull mirror, synced manually)\n", diagnostics.String())
}
func TestRepositoryMigrateNamesTheRepositoryAfterItsSource(t *testing.T) {
for _, test := range []struct{ source, want string }{
{"https://github.com/go-gitea/tea.git", "tea"},
+6 -3
View File
@@ -133,7 +133,10 @@ $ tea repo migrate https://github.com/acme/tool --service github --issues --pull
return err
}
detail := ""
interval, _ := time.ParseDuration(opts.MirrorInterval)
switch {
case item.Mirror && opts.MirrorInterval != "" && interval == 0:
detail = " (pull mirror, synced manually)"
case item.Mirror && opts.MirrorInterval != "":
detail = " (pull mirror, every " + opts.MirrorInterval + ")"
case item.Mirror:
@@ -161,7 +164,7 @@ $ tea repo migrate https://github.com/acme/tool --service github --issues --pull
flags.BoolVar(&opts.PullRequests, "pull-requests", false, "Copy pull requests")
flags.BoolVar(&opts.Releases, "releases", false, "Copy releases")
flags.BoolVar(&opts.Milestones, "milestones", false, "Copy milestones")
flags.StringVar(&opts.MirrorInterval, "mirror-interval", "", "Mirror interval, for example 8h")
flags.StringVar(&opts.MirrorInterval, "mirror-interval", "", "Mirror interval, for example 8h (0 turns off periodic sync)")
flags.BoolVar(&opts.LFS, "lfs", false, "Copy LFS objects")
flags.StringVar(&opts.LFSEndpoint, "lfs-endpoint", "", "Source LFS endpoint")
flags.StringVar(&opts.AuthUser, "auth-user", "", "Source authentication username")
@@ -201,8 +204,8 @@ func validateMigrationFlags(cmd *cobra.Command, opts services.RepositoryMigratio
}
if opts.MirrorInterval != "" {
interval, err := time.ParseDuration(opts.MirrorInterval)
if err != nil || interval <= 0 || !opts.Mirror {
return clierr.Wrap(clierr.Usage, errors.New("--mirror-interval requires --mirror and a positive duration"))
if err != nil || interval < 0 || !opts.Mirror {
return clierr.Wrap(clierr.Usage, errors.New("--mirror-interval requires --mirror and a duration of 0 or more"))
}
}
return nil
+1 -1
View File
@@ -100,7 +100,7 @@ $ tea repo sync bob/project`,
},
}
command.Flags().StringVarP(&branch, "branch", "b", "", "Branch to sync (default: the repository's default branch)")
_ = command.RegisterFlagCompletionFunc("branch", a.completeBranches())
_ = command.RegisterFlagCompletionFunc("branch", a.completeArgumentBranches())
command.ValidArgsFunction = a.completeRepos()
return command
}
+1 -1
View File
@@ -62,7 +62,7 @@ $ tea search repo go --topic --sort updated --order desc`,
pageFlags(command, &opts.Limit, &opts.Page)
flags.StringVar(&opts.Owner, "owner", "", "Only repositories of this user or organization")
enumFlag(command, &opts.Kind, "type", "", "", "Only repositories of this kind", "fork", "source", "mirror")
enumFlag(command, &opts.Sort, "sort", "", "alpha", "Sort by", "alpha", "created", "updated", "size", "id")
enumFlag(command, &opts.Sort, "sort", "", "", "Sort by (default: alphabetical, your own repositories first)", "alpha", "created", "updated", "size", "id")
enumFlag(command, &opts.Order, "order", "", "asc", "Sort order", "asc", "desc")
flags.BoolVar(&opts.Topic, "topic", false, "Match repository topics")
flags.BoolVar(&private, "private", false, "Only private repositories (use =false for public)")
+3 -2
View File
@@ -152,7 +152,7 @@ func TestUsageErrorsPrecedeRequests(t *testing.T) {
{[]string{"release", "edit", "v1", "--draft", "false"}, "--draft takes no value; use --draft or --draft=false"},
{[]string{"release", "edit", "v1", "-d", "false", "-p", "False"}, "--draft takes no value; use --draft or --draft=false"},
{[]string{"release", "edit", "v1", "v2", "--prerelease", "TRUE"}, "--prerelease takes no value; use --prerelease or --prerelease=false"},
{[]string{"repo", "create"}, "missing required input: supply --name"},
{[]string{"repo", "create"}, "missing required input: Repository name (pass it as an argument)"},
{[]string{"repo", "create", "acme/bad/name"}, "repository name must be nonempty, at most 100 bytes, and contain no path separators or whitespace"},
{[]string{"repo", "create", "name", "--name", "other"}, "supply name as an argument or --name, not both"},
{[]string{"repo", "create", "name", "--default-branch", "bad..branch", "--auto-init"}, "invalid branch name"},
@@ -175,11 +175,12 @@ func TestUsageErrorsPrecedeRequests(t *testing.T) {
{[]string{"repo", "list", "--type", "invalid"}, "--type must be fork, source, or mirror"},
{[]string{"repo", "set-default", "--json"}, "set-default configures Git"},
{[]string{"repo", "sync", "--branch="}, "--branch cannot be empty"},
{[]string{"repo", "migrate", "--name", "new"}, "missing required input: Clone URL (pass it as an argument)"},
{[]string{"repo", "migrate", "https://source.test/repo", "--name", "new", "--clone-url", "https://other.test/repo"}, "supply clone URL as an argument or --clone-url, not both"},
{[]string{"repo", "migrate", "https://user:secret@source.test/repo", "--name", "new"}, "clone URL must be an HTTP(S) or git URL without embedded credentials"},
{[]string{"repo", "migrate", "https://source.test/repo", "--name", "new", "--service", "unknown"}, "--service must be git, gitea, gitlab, gogs, or github"},
{[]string{"repo", "migrate", "https://source.test/repo", "--name", "new", "--issues"}, "plain Git migration cannot copy forge metadata"},
{[]string{"repo", "migrate", "https://source.test/repo", "--name", "new", "--mirror-interval", "8h"}, "--mirror-interval requires --mirror and a positive duration"},
{[]string{"repo", "migrate", "https://source.test/repo", "--name", "new", "--mirror-interval", "8h"}, "--mirror-interval requires --mirror and a duration of 0 or more"},
{[]string{"repo", "migrate", "https://source.test/repo", "--name", "new", "--mirror", "--issues"}, "mirrors cannot copy issues, milestones, labels, pull requests, or releases"},
{[]string{"repo", "migrate", "https://source.test/repo", "--name", "new", "--lfs-endpoint", "https://lfs.test"}, "--lfs-endpoint requires --lfs"},
{[]string{"repo", "migrate", "https://source.test/repo", "--name", "new", "--auth-password", "secret"}, "--auth-password requires --auth-user"},