mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 11:01:25 +01:00
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:
16 files changed
+211
-37
No files matched your search
@@ -217,7 +217,7 @@ Subcommands:
|
|||||||
| `times add` | `time create` |
|
| `times add` | `time create` |
|
||||||
| `times list [user\|#issue]` | `time list [--username USER\|--issue N]` |
|
| `times list [user\|#issue]` | `time list [--username USER\|--issue N]` |
|
||||||
| `repos search` | `search repo` |
|
| `repos search` | `search repo` |
|
||||||
| `repos create-from-template` | `repo create --template OWNER/REPO` |
|
| `repos create-from-template` | `repo create --template OWNER/REPO`; `--content`, `--githooks`, `--avatar`, `--labels`, `--topics`, `--webhooks` are now `--copy-content`, `--copy-git-hooks`, `--copy-avatar`, `--copy-labels`, `--copy-topics`, `--copy-webhooks`, and `--owner` is `--org` |
|
||||||
| `actions secrets create` | `secret set` (value: see below) |
|
| `actions secrets create` | `secret set` (value: see below) |
|
||||||
| `actions variables set` | `variable set` (value: see below) |
|
| `actions variables set` | `variable set` (value: see below) |
|
||||||
| `actions variables list --name NAME` | `variable view NAME` |
|
| `actions variables list --name NAME` | `variable view NAME` |
|
||||||
@@ -248,6 +248,7 @@ Renamed flags that appear on many commands:
|
|||||||
| `--page` / `-p`, `--limit` / `--lm` | `--page`, `--limit` / `-L` |
|
| `--page` / `-p`, `--limit` / `--lm` | `--page`, `--limit` / `-L` |
|
||||||
| `--style` / `-s` (pr merge) | `--merge`, `--squash` or `--rebase`, or `--method` / `-s`; without one, the repository's default merge style |
|
| `--style` / `-s` (pr merge) | `--merge`, `--squash` or `--rebase`, or `--method` / `-s`; without one, the repository's default merge style |
|
||||||
| `--owner` / `-O`, `--init`, `--labels`, `--trustmodel`, `--branch`, `--template` (repo create) | `--org`, `--auto-init`, `--label-template`, `--trust-model`, `--default-branch`, `--as-template` |
|
| `--owner` / `-O`, `--init`, `--labels`, `--trustmodel`, `--branch`, `--template` (repo create) | `--org`, `--auto-init`, `--label-template`, `--trust-model`, `--default-branch`, `--as-template` |
|
||||||
|
| `--trustmodel collaborator+committer` (repos create) | `--trust-model collaboratorcommitter` |
|
||||||
| `--desc` (repo edit) | `--description` / `-d` |
|
| `--desc` (repo edit) | `--description` / `-d` |
|
||||||
| `--template true\|false`, `--private true\|false`, `--archived true\|false` (repo edit) | `--as-template`, `--private`, `tea repo archive` / `unarchive`; these are on/off switches, so write `--private=false` (`--private false` names a repository `false`) |
|
| `--template true\|false`, `--private true\|false`, `--archived true\|false` (repo edit) | `--as-template`, `--private`, `tea repo archive` / `unarchive`; these are on/off switches, so write `--private=false` (`--private false` names a repository `false`) |
|
||||||
| `--owner` / `-O` (repo fork) | `--org` |
|
| `--owner` / `-O` (repo fork) | `--org` |
|
||||||
|
|||||||
@@ -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) {
|
func TestCompletionInstallHelp(t *testing.T) {
|
||||||
deps, out, _ := testDeps(t)
|
deps, out, _ := testDeps(t)
|
||||||
require.NoError(t, Execute(context.Background(), deps, []string{"completion", "bash", "--help"}))
|
require.NoError(t, Execute(context.Background(), deps, []string{"completion", "bash", "--help"}))
|
||||||
|
|||||||
@@ -413,15 +413,29 @@ func (a *application) completeUsers() cobra.CompletionFunc {
|
|||||||
|
|
||||||
// completeBranches completes the local branches, then the repository's branches.
|
// completeBranches completes the local branches, then the repository's branches.
|
||||||
func (a *application) completeBranches() cobra.CompletionFunc {
|
func (a *application) completeBranches() cobra.CompletionFunc {
|
||||||
return mergeCompletions(a.completeLocalBranches, a.apiCompletion(true, func(ctx context.Context, t target, _ string) ([]cobra.Completion, error) {
|
return mergeCompletions(a.completeLocalBranches, a.apiCompletion(true, listBranches))
|
||||||
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 {
|
// completeArgumentBranches is completeBranches for a command with a positional [<repository>]:
|
||||||
return item.Name, "protected"
|
// once that argument is given, it completes that repository's branches, not those of the clone here.
|
||||||
}
|
func (a *application) completeArgumentBranches() cobra.CompletionFunc {
|
||||||
return item.Name, ""
|
inClone := a.completeBranches()
|
||||||
}), err
|
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.
|
// completeTags completes the repository's tags.
|
||||||
|
|||||||
@@ -281,6 +281,7 @@ func TestWriteTable(t *testing.T) {
|
|||||||
stderr: "more results available; use --page 3\n",
|
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: "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: "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", 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"},
|
{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"},
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"gitea.dev/tea/internal/clierr"
|
||||||
"gitea.dev/tea/internal/terminal"
|
"gitea.dev/tea/internal/terminal"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
@@ -52,5 +55,9 @@ func (a *application) requireInput(cmd *cobra.Command, value *string, flag, labe
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
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)
|
return terminal.RequireInput(cmd.Context(), value, flag, label, validate, a.deps.Streams, a.policy(s), a.deps.Prompter)
|
||||||
}
|
}
|
||||||
@@ -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().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.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()
|
view.ValidArgsFunction = a.completeRepos()
|
||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -147,7 +147,14 @@ $ tea repo create project --source . --push`,
|
|||||||
target.Repo = item.FullName
|
target.Repo = item.FullName
|
||||||
switch {
|
switch {
|
||||||
case clone:
|
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 != "":
|
case localSource != "":
|
||||||
err = a.publishRepository(cmd, target, item, localSource, remote, push)
|
err = a.publishRepository(cmd, target, item, localSource, remote, push)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,6 +63,18 @@ func TestRepositoryCreateClonesNewRepository(t *testing.T) {
|
|||||||
require.JSONEq(t, `{"id":7,"full_name":"alice/project"}`, out.String())
|
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) {
|
func TestRepositoryCreateValidatesTemplatesBeforeCreating(t *testing.T) {
|
||||||
var requests []string
|
var requests []string
|
||||||
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
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")
|
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) {
|
func TestRepositoryMigrateNamesTheRepositoryAfterItsSource(t *testing.T) {
|
||||||
for _, test := range []struct{ source, want string }{
|
for _, test := range []struct{ source, want string }{
|
||||||
{"https://github.com/go-gitea/tea.git", "tea"},
|
{"https://github.com/go-gitea/tea.git", "tea"},
|
||||||
|
|||||||
@@ -133,7 +133,10 @@ $ tea repo migrate https://github.com/acme/tool --service github --issues --pull
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
detail := ""
|
detail := ""
|
||||||
|
interval, _ := time.ParseDuration(opts.MirrorInterval)
|
||||||
switch {
|
switch {
|
||||||
|
case item.Mirror && opts.MirrorInterval != "" && interval == 0:
|
||||||
|
detail = " (pull mirror, synced manually)"
|
||||||
case item.Mirror && opts.MirrorInterval != "":
|
case item.Mirror && opts.MirrorInterval != "":
|
||||||
detail = " (pull mirror, every " + opts.MirrorInterval + ")"
|
detail = " (pull mirror, every " + opts.MirrorInterval + ")"
|
||||||
case item.Mirror:
|
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.PullRequests, "pull-requests", false, "Copy pull requests")
|
||||||
flags.BoolVar(&opts.Releases, "releases", false, "Copy releases")
|
flags.BoolVar(&opts.Releases, "releases", false, "Copy releases")
|
||||||
flags.BoolVar(&opts.Milestones, "milestones", false, "Copy milestones")
|
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.BoolVar(&opts.LFS, "lfs", false, "Copy LFS objects")
|
||||||
flags.StringVar(&opts.LFSEndpoint, "lfs-endpoint", "", "Source LFS endpoint")
|
flags.StringVar(&opts.LFSEndpoint, "lfs-endpoint", "", "Source LFS endpoint")
|
||||||
flags.StringVar(&opts.AuthUser, "auth-user", "", "Source authentication username")
|
flags.StringVar(&opts.AuthUser, "auth-user", "", "Source authentication username")
|
||||||
@@ -201,8 +204,8 @@ func validateMigrationFlags(cmd *cobra.Command, opts services.RepositoryMigratio
|
|||||||
}
|
}
|
||||||
if opts.MirrorInterval != "" {
|
if opts.MirrorInterval != "" {
|
||||||
interval, err := time.ParseDuration(opts.MirrorInterval)
|
interval, err := time.ParseDuration(opts.MirrorInterval)
|
||||||
if err != nil || interval <= 0 || !opts.Mirror {
|
if err != nil || interval < 0 || !opts.Mirror {
|
||||||
return clierr.Wrap(clierr.Usage, errors.New("--mirror-interval requires --mirror and a positive duration"))
|
return clierr.Wrap(clierr.Usage, errors.New("--mirror-interval requires --mirror and a duration of 0 or more"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -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.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()
|
command.ValidArgsFunction = a.completeRepos()
|
||||||
return command
|
return command
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ $ tea search repo go --topic --sort updated --order desc`,
|
|||||||
pageFlags(command, &opts.Limit, &opts.Page)
|
pageFlags(command, &opts.Limit, &opts.Page)
|
||||||
flags.StringVar(&opts.Owner, "owner", "", "Only repositories of this user or organization")
|
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.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")
|
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)")
|
||||||
|
|||||||
@@ -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", "--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", "-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{"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", "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", "--name", "other"}, "supply name as an argument or --name, not both"},
|
||||||
{[]string{"repo", "create", "name", "--default-branch", "bad..branch", "--auto-init"}, "invalid branch name"},
|
{[]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", "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"},
|
||||||
|
{[]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://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://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", "--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", "--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", "--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", "--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"},
|
{[]string{"repo", "migrate", "https://source.test/repo", "--name", "new", "--auth-password", "secret"}, "--auth-password requires --auth-user"},
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
gitea "gitea.dev/sdk"
|
gitea "gitea.dev/sdk"
|
||||||
@@ -99,35 +100,75 @@ func (s Repositories) get(ctx context.Context, repo string) (*gitea.Repository,
|
|||||||
return item, Classify(err, "repository", repo, "")
|
return item, Classify(err, "repository", repo, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
// readmeNames are the README file names that Gitea shows, in order of preference.
|
// readmeDirs are the directories Gitea searches for a README, in order; "" is the repository root.
|
||||||
var readmeNames = []string{"readme.md", "readme.markdown", "readme", "readme.txt"}
|
var readmeDirs = []string{".gitea", ".github", "", "docs"}
|
||||||
|
|
||||||
// Readme returns the README in the top directory of the repository at ref: its file name and its
|
// readmeRank orders README file names as Gitea does: Markdown first, then text, then any other
|
||||||
// content, or "" when there is none.
|
// README.* such as README.rst. It returns -1 for names that are not a README.
|
||||||
func (s Repositories) Readme(ctx context.Context, repo, ref string) (name, content string, err error) {
|
func readmeRank(name string) int {
|
||||||
entries, err := repoCall(ctx, s.Client, repo, func(ctx context.Context, c *gitea.Client, owner, name string) ([]*gitea.ContentsResponse, *gitea.Response, error) {
|
switch lower := strings.ToLower(name); {
|
||||||
return c.Repositories.ListContents(ctx, owner, name, ref, "")
|
case lower == "readme.md":
|
||||||
})
|
return 0
|
||||||
|
case lower == "readme.markdown":
|
||||||
|
return 1
|
||||||
|
case lower == "readme.txt":
|
||||||
|
return 2
|
||||||
|
case lower == "readme":
|
||||||
|
return 3
|
||||||
|
case strings.HasPrefix(lower, "readme."):
|
||||||
|
return 4
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Readme returns the README of the repository at ref as Gitea's web view finds it, searching .gitea,
|
||||||
|
// .github, the root and docs: its path and its content, or "" when there is none. An explicit ref
|
||||||
|
// that does not exist is an error.
|
||||||
|
func (s Repositories) Readme(ctx context.Context, repo, ref string) (path, content string, err error) {
|
||||||
|
list := func(dir string) ([]*gitea.ContentsResponse, error) {
|
||||||
|
return 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, dir)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
root, err := list("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if IsNotFound(err) && ref != "" {
|
||||||
|
return "", "", NotFound("branch", strconv.Quote(ref), repo)
|
||||||
|
}
|
||||||
// A reader without code access sees the repository but not its files.
|
// A reader without code access sees the repository but not its files.
|
||||||
if IsNotFound(err) || IsForbidden(err) {
|
if IsNotFound(err) || IsForbidden(err) {
|
||||||
return "", "", nil
|
return "", "", nil
|
||||||
}
|
}
|
||||||
return "", "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
rank := len(readmeNames)
|
for _, dir := range readmeDirs {
|
||||||
for _, entry := range entries {
|
entries := root
|
||||||
if i := slices.Index(readmeNames, strings.ToLower(entry.Name)); entry.Type == "file" && i >= 0 && i < rank {
|
if dir != "" {
|
||||||
name, rank = entry.Name, i
|
i := slices.IndexFunc(root, func(e *gitea.ContentsResponse) bool { return e.Type == "dir" && e.Name == dir })
|
||||||
|
if i < 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if entries, err = list(dir); err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rank := -1
|
||||||
|
for _, entry := range entries {
|
||||||
|
if r := readmeRank(entry.Name); entry.Type == "file" && r >= 0 && (rank < 0 || r < rank) {
|
||||||
|
path, rank = strings.TrimPrefix(dir+"/"+entry.Name, "/"), r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if path != "" {
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if name == "" {
|
if path == "" {
|
||||||
return "", "", nil
|
return "", "", nil
|
||||||
}
|
}
|
||||||
data, err := repoCall(ctx, s.Client, repo, func(ctx context.Context, c *gitea.Client, owner, repo string) ([]byte, *gitea.Response, error) {
|
data, err := repoCall(ctx, s.Client, repo, func(ctx context.Context, c *gitea.Client, owner, name string) ([]byte, *gitea.Response, error) {
|
||||||
return c.Repositories.GetFile(ctx, owner, repo, ref, name)
|
return c.Repositories.GetFile(ctx, owner, name, ref, path)
|
||||||
})
|
})
|
||||||
return name, string(data), err
|
return path, 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.
|
||||||
@@ -204,12 +245,16 @@ func (s Repositories) Create(ctx context.Context, org string, opts RepositoryOpt
|
|||||||
return c.Repositories.CreateRepo(ctx, options)
|
return c.Repositories.CreateRepo(ctx, options)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if org != "" && IsNotFound(err) {
|
||||||
|
// The repository cannot be missing before it exists: the organization is.
|
||||||
|
return Repository{}, &NotFoundError{Kind: "organization", Key: strconv.Quote(org), Err: err}
|
||||||
|
}
|
||||||
return Repository{}, Classify(err, "repository", "", "")
|
return Repository{}, Classify(err, "repository", "", "")
|
||||||
}
|
}
|
||||||
return repositoryFrom(item), nil
|
return repositoryFrom(item), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Edit updates only explicitly supplied resource fields.
|
// Editupdates only explicitly supplied resource fields.
|
||||||
func (s Repositories) Edit(ctx context.Context, repo string, opts RepositoryEdit) (Repository, error) {
|
func (s Repositories) Edit(ctx context.Context, repo string, opts RepositoryEdit) (Repository, error) {
|
||||||
var mergeStyle *gitea.MergeStyle
|
var mergeStyle *gitea.MergeStyle
|
||||||
if opts.MergeStyle != nil {
|
if opts.MergeStyle != nil {
|
||||||
|
|||||||
@@ -79,3 +79,42 @@ func TestRepositoryAssigneesTagsAndMissingPermissions(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, RepositoryPermissions{}, permissions)
|
require.Equal(t, RepositoryPermissions{}, permissions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRepositoryCreateReportsMissingOrganization(t *testing.T) {
|
||||||
|
client := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
_, _ = w.Write([]byte(`{"message":"GetOrgByName"}`))
|
||||||
|
}))
|
||||||
|
_, err := (Repositories{Client: client}).Create(context.Background(), "acme-typo", RepositoryOptions{Name: "tool"})
|
||||||
|
require.EqualError(t, err, `organization "acme-typo" not found`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRepositoryReadme(t *testing.T) {
|
||||||
|
dirEntry := func(name string) string { return `{"name":"` + name + `","path":"` + name + `","type":"dir"}` }
|
||||||
|
fileEntry := func(dir, name string) string {
|
||||||
|
return `{"name":"` + name + `","path":"` + dir + name + `","type":"file"}`
|
||||||
|
}
|
||||||
|
listings := map[string]string{
|
||||||
|
"/api/v1/repos/alice/project/contents/": "[" + fileEntry("", "README.rst") + "," + fileEntry("", "readme.txt") + "," + dirEntry(".github") + "]",
|
||||||
|
"/api/v1/repos/alice/project/contents/.github": "[" + fileEntry(".github/", "README.md") + "]",
|
||||||
|
}
|
||||||
|
client := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Query().Get("ref") == "develp" {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if body, ok := listings[r.URL.Path]; ok {
|
||||||
|
_, _ = w.Write([]byte(body))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert.Equal(t, "/api/v1/repos/alice/project/raw/.github/README.md", r.URL.Path)
|
||||||
|
_, _ = w.Write([]byte("hello"))
|
||||||
|
}))
|
||||||
|
path, content, err := (Repositories{Client: client}).Readme(context.Background(), "alice/project", "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, ".github/README.md", path)
|
||||||
|
assert.Equal(t, "hello", content)
|
||||||
|
|
||||||
|
_, _, err = (Repositories{Client: client}).Readme(context.Background(), "alice/project", "develp")
|
||||||
|
require.EqualError(t, err, `branch "develp" not found in alice/project`)
|
||||||
|
}
|
||||||
@@ -121,7 +121,10 @@ func (s Repositories) Search(ctx context.Context, opts RepositorySearchOptions)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return Page[Repository]{}, err
|
return Page[Repository]{}, err
|
||||||
}
|
}
|
||||||
options.PrioritizedByOwnerID = account.ID
|
// Gitea groups results by owner ahead of the requested sort, so an explicit sort cannot keep it.
|
||||||
|
if opts.Sort == "" {
|
||||||
|
options.PrioritizedByOwnerID = account.ID
|
||||||
|
}
|
||||||
window, err := callPage(ctx, s.Client, opts.Limit, opts.Page, func(ctx context.Context, c *gitea.Client, list gitea.ListOptions) ([]*gitea.Repository, *gitea.Response, error) {
|
window, err := callPage(ctx, s.Client, opts.Limit, opts.Page, func(ctx context.Context, c *gitea.Client, list gitea.ListOptions) ([]*gitea.Repository, *gitea.Response, error) {
|
||||||
options.ListOptions = list
|
options.ListOptions = list
|
||||||
return c.Repositories.SearchRepos(ctx, options)
|
return c.Repositories.SearchRepos(ctx, options)
|
||||||
|
|||||||
@@ -118,6 +118,22 @@ func TestRepositorySearchSendsExplicitFalseFilters(t *testing.T) {
|
|||||||
require.Empty(t, page.Items)
|
require.Empty(t, page.Items)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRepositorySearchSortDropsOwnerPriority(t *testing.T) {
|
||||||
|
client := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path == "/api/v1/user" {
|
||||||
|
_, _ = w.Write([]byte(`{"id":7}`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
query := r.URL.Query()
|
||||||
|
assert.False(t, query.Has("priority_owner_id"))
|
||||||
|
assert.Equal(t, "updated", query.Get("sort"))
|
||||||
|
assert.Equal(t, "desc", query.Get("order"))
|
||||||
|
_, _ = w.Write([]byte(`{"data":[]}`))
|
||||||
|
}))
|
||||||
|
_, err := (Repositories{Client: client}).Search(context.Background(), RepositorySearchOptions{Query: "go", Sort: "updated", Order: "desc", Limit: 30, Page: 1})
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
func TestRepositorySearchScopesOwner(t *testing.T) {
|
func TestRepositorySearchScopesOwner(t *testing.T) {
|
||||||
client := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
client := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
switch r.URL.Path {
|
switch r.URL.Path {
|
||||||
|
|||||||
Reference in new issue
Block a user