mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 11:01:25 +01:00
feat(repo): add repo unarchive and repo rename, and report archive and delete
repo unarchive restores an archived repository, and repo rename renames one, moving the Git remote or the saved default repository that selected it to the new name. The archived-repository hint now points to tea repo unarchive. repo archive asks "Archive alice/x? It becomes read-only." and warns "! alice/x is already archived" without a request when there is nothing to do. Archive, unarchive, rename and delete confirm with a ✓ line on a terminal, and --json returns the repository. Signed-off-by: bircni <bircni@icloud.com>
This commit is contained in:
6 files changed
+327
-46
No files matched your search
@@ -505,10 +505,8 @@ const (
|
|||||||
release list
|
release list
|
||||||
release publish
|
release publish
|
||||||
release view
|
release view
|
||||||
repo archive
|
|
||||||
repo clone
|
repo clone
|
||||||
repo create
|
repo create
|
||||||
repo delete
|
|
||||||
repo edit
|
repo edit
|
||||||
repo fork
|
repo fork
|
||||||
repo migrate
|
repo migrate
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ func conflictHint(ctx context.Context, apiErr *transport.APIError, conflict *ser
|
|||||||
switch conflict.Reason {
|
switch conflict.Reason {
|
||||||
case services.ReasonArchived:
|
case services.ReasonArchived:
|
||||||
if conflict.Scope != "" {
|
if conflict.Scope != "" {
|
||||||
return "unarchive it with: tea repo edit " + conflict.Scope + " --archived=false"
|
return "unarchive it with: tea repo unarchive " + conflict.Scope
|
||||||
}
|
}
|
||||||
case services.ReasonExists:
|
case services.ReasonExists:
|
||||||
return existsHint(ctx, apiErr, conflict)
|
return existsHint(ctx, apiErr, conflict)
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ func TestExplainNamesResourcesAndTellsWhyRepositoriesFail(t *testing.T) {
|
|||||||
{"issue as pull request", []string{"pr", "view", "1", "-R", "alice/project"}, "#1 in alice/project is an issue, not a pull request; view it with: tea issue view 1"},
|
{"issue as pull request", []string{"pr", "view", "1", "-R", "alice/project"}, "#1 in alice/project is an issue, not a pull request; view it with: tea issue view 1"},
|
||||||
{"missing pull request", []string{"pr", "view", "999", "-R", "alice/project"}, "pull request #999 not found in alice/project"},
|
{"missing pull request", []string{"pr", "view", "999", "-R", "alice/project"}, "pull request #999 not found in alice/project"},
|
||||||
{"empty repository", []string{"pr", "list", "-R", "alice/empty"}, "alice/empty is empty; it has no pull requests"},
|
{"empty repository", []string{"pr", "list", "-R", "alice/empty"}, "alice/empty is empty; it has no pull requests"},
|
||||||
{"archived repository", []string{"issue", "create", "-R", "alice/old", "--title", "x", "--body", "y"}, "alice/old is archived (read-only); unarchive it with: tea repo edit alice/old --archived=false"},
|
{"archived repository", []string{"issue", "create", "-R", "alice/old", "--title", "x", "--body", "y"}, "alice/old is archived (read-only); unarchive it with: tea repo unarchive alice/old"},
|
||||||
{"existing pull request", []string{"pr", "create", "-R", "alice/project", "--head", "feat", "--base", "main", "--title", "x", "--body", "y"}, "pull request for feat into main already exists in alice/project; see https://example.org/alice/project/pulls/3"},
|
{"existing pull request", []string{"pr", "create", "-R", "alice/project", "--head", "feat", "--base", "main", "--title", "x", "--body", "y"}, "pull request for feat into main already exists in alice/project; see https://example.org/alice/project/pulls/3"},
|
||||||
{"context added by the command", []string{"pr", "merge", "3", "-R", "alice/project", "--yes"}, "pull request #3 is a draft; mark it ready for review first: Gitea API returned HTTP 405 for POST /api/v1/repos/alice/project/pulls/3/merge: Work in progress PRs cannot be merged"},
|
{"context added by the command", []string{"pr", "merge", "3", "-R", "alice/project", "--yes"}, "pull request #3 is a draft; mark it ready for review first: Gitea API returned HTTP 405 for POST /api/v1/repos/alice/project/pulls/3/merge: Work in progress PRs cannot be merged"},
|
||||||
{"raw api response", []string{"api", "users/nobody"}, "not found (HTTP 404 GET /api/v1/users/nobody)"},
|
{"raw api response", []string{"api", "users/nobody"}, "not found (HTTP 404 GET /api/v1/users/nobody)"},
|
||||||
|
|||||||
+223
-40
@@ -154,46 +154,7 @@ func (a *application) addRepositoryMutations(group *cobra.Command) {
|
|||||||
edit.Flags().BoolVar(&private, "private", false, "Make repository private (use --private=false to make public)")
|
edit.Flags().BoolVar(&private, "private", false, "Make repository private (use --private=false to make public)")
|
||||||
edit.Flags().BoolVar(&template, "as-template", false, "Make repository a template (use =false to disable)")
|
edit.Flags().BoolVar(&template, "as-template", false, "Make repository a template (use =false to disable)")
|
||||||
edit.Flags().BoolVar(&actionsEnabled, "actions", false, "Enable Actions (use --actions=false to disable)")
|
edit.Flags().BoolVar(&actionsEnabled, "actions", false, "Enable Actions (use --actions=false to disable)")
|
||||||
group.AddCommand(edit)
|
group.AddCommand(edit, a.repoArchiveCommand(true), a.repoArchiveCommand(false), a.repoRenameCommand(), a.repoDeleteCommand())
|
||||||
for _, verb := range []string{"archive", "delete"} {
|
|
||||||
command := &cobra.Command{Use: verb + " [repository]", Short: titleCase(verb) + " 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
|
|
||||||
}
|
|
||||||
t, err := a.resolve(cmd.Context(), true)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
inferred := ""
|
|
||||||
if t.RepoSource != "--repo" {
|
|
||||||
source := strings.Replace(t.RepoSource, "remote ", "git remote ", 1)
|
|
||||||
if a.options.yes {
|
|
||||||
return clierr.Wrap(clierr.Usage, fmt.Errorf("--yes requires an explicit repository, not one inferred from %s: tea repo %s %s --yes", source, verb, t.Repo))
|
|
||||||
}
|
|
||||||
inferred = " (from " + source + ")"
|
|
||||||
}
|
|
||||||
service := services.Repositories{Client: t.client}
|
|
||||||
if verb == "delete" {
|
|
||||||
if err := a.confirmTyped(cmd, "Delete "+t.Repo+inferred+"? This cannot be undone.", t.Repo); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return service.Delete(cmd.Context(), t.Repo)
|
|
||||||
}
|
|
||||||
if err := a.confirm(cmd, "Archive "+t.Repo+inferred+"?"); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
archived := true
|
|
||||||
item, err := service.Edit(cmd.Context(), t.Repo, services.RepositoryEdit{Archived: &archived})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return writeRecord(a, cmd, item, repositoryRecord)
|
|
||||||
}}
|
|
||||||
if verb == "delete" {
|
|
||||||
command.Annotations = textOutput("repository deletion does not return a resource")
|
|
||||||
}
|
|
||||||
group.AddCommand(command)
|
|
||||||
}
|
|
||||||
var depth int
|
var depth int
|
||||||
clone := &cobra.Command{Use: "clone <repository> [directory]", Short: "Clone a repository using the configured Git protocol", Args: usageArgs(cobra.RangeArgs(1, 2)), RunE: func(cmd *cobra.Command, args []string) error {
|
clone := &cobra.Command{Use: "clone <repository> [directory]", Short: "Clone a repository using the configured Git protocol", Args: usageArgs(cobra.RangeArgs(1, 2)), RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
if depth < 0 {
|
if depth < 0 {
|
||||||
@@ -295,3 +256,225 @@ func (a *application) cloneRepository(cmd *cobra.Command, t target, item service
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// inferredRepository words where a repository that no argument or --repo named came from, as in
|
||||||
|
// " (from git remote upstream)", for confirmations. Irreversible commands refuse --yes for it.
|
||||||
|
func (a *application) inferredRepository(t target, verb string, refuseYes bool) (string, error) {
|
||||||
|
if t.RepoSource == "--repo" {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
source := describeRepoSource(t.RepoSource)
|
||||||
|
if refuseYes && a.options.yes {
|
||||||
|
return "", clierr.Wrap(clierr.Usage, fmt.Errorf("--yes requires an explicit repository, not one inferred from %s: tea repo %s %s --yes", source, verb, t.Repo))
|
||||||
|
}
|
||||||
|
return " (from " + source + ")", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// describeRepoSource words a repository source for people: git remote upstream, TEA_REPO, ...
|
||||||
|
func describeRepoSource(source string) string {
|
||||||
|
return strings.Replace(source, "remote ", "git remote ", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// repoArchiveCommand builds repo archive, or repo unarchive.
|
||||||
|
func (a *application) repoArchiveCommand(archive bool) *cobra.Command {
|
||||||
|
command := &cobra.Command{
|
||||||
|
Use: "archive [<repository>]",
|
||||||
|
Short: "Archive a repository",
|
||||||
|
Long: `Archive a repository. An archived repository is read-only: nobody can push to it or change its
|
||||||
|
issues, pull requests, releases or wiki until it is unarchived with tea repo unarchive.`,
|
||||||
|
Example: `# Archive a repository
|
||||||
|
$ tea repo archive alice/old-project
|
||||||
|
|
||||||
|
# Archive without a prompt; scripts must name the repository
|
||||||
|
$ tea repo archive alice/old-project --yes`,
|
||||||
|
Annotations: map[string]string{"tea.arguments": repositoryArguments, "tea.confirm": "true"},
|
||||||
|
Args: usageArgs(cobra.MaximumNArgs(1)),
|
||||||
|
}
|
||||||
|
if !archive {
|
||||||
|
command.Use, command.Short = "unarchive [<repository>]", "Unarchive a repository"
|
||||||
|
command.Long = "Unarchive an archived repository, so that it accepts pushes and changes again."
|
||||||
|
command.Example = "$ tea repo unarchive alice/old-project"
|
||||||
|
delete(command.Annotations, "tea.confirm")
|
||||||
|
}
|
||||||
|
command.RunE = func(cmd *cobra.Command, args []string) error {
|
||||||
|
if err := a.repositoryArgument(args); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t, err := a.resolve(cmd.Context(), true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
inferred, err := a.inferredRepository(t, "archive", archive)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
service := services.Repositories{Client: t.client}
|
||||||
|
item, err := service.View(cmd.Context(), t.Repo)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if item.Archived == archive {
|
||||||
|
if archive {
|
||||||
|
a.warn(cmd, "%s is already archived", item.FullName)
|
||||||
|
} else {
|
||||||
|
a.warn(cmd, "%s is not archived", item.FullName)
|
||||||
|
}
|
||||||
|
return a.writeMutationJSON(cmd, item)
|
||||||
|
}
|
||||||
|
if archive {
|
||||||
|
if err := a.confirmAction(cmd, terminal.Confirmation{Prompt: "Archive " + t.Repo + inferred + "? It becomes read-only.", NonTTY: "archive " + t.Repo}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
item, err = service.Edit(cmd.Context(), t.Repo, services.RepositoryEdit{Archived: &archive})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if archive {
|
||||||
|
a.done(cmd, "Archived repository %s", item.FullName)
|
||||||
|
} else {
|
||||||
|
a.done(cmd, "Unarchived repository %s", item.FullName)
|
||||||
|
}
|
||||||
|
return a.writeMutationJSON(cmd, item)
|
||||||
|
}
|
||||||
|
command.ValidArgsFunction = a.completeRepos()
|
||||||
|
return command
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeMutationJSON writes the result of a mutation with --json; people get done lines instead.
|
||||||
|
func (a *application) writeMutationJSON(cmd *cobra.Command, item any) error {
|
||||||
|
if a.options.json == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return a.writeJSON(cmd, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *application) repoRenameCommand() *cobra.Command {
|
||||||
|
command := &cobra.Command{
|
||||||
|
Use: "rename <new-name>",
|
||||||
|
Short: "Rename a repository",
|
||||||
|
Long: `Rename a repository; its owner stays the same. Gitea redirects the old name to the new one. When
|
||||||
|
the repository was found through a Git remote of the current clone, or through tea repo set-default,
|
||||||
|
that remote or setting moves to the new name.`,
|
||||||
|
Example: `# Rename the repository of the current clone
|
||||||
|
$ tea repo rename new-name
|
||||||
|
|
||||||
|
# Rename another repository
|
||||||
|
$ tea repo rename new-name -R alice/old-name`,
|
||||||
|
Annotations: map[string]string{"tea.confirm": "true"},
|
||||||
|
Args: argsExact(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
name := strings.TrimSpace(args[0])
|
||||||
|
if err := validateRepositoryName(name); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t, err := a.resolve(cmd.Context(), true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
owner, current, _ := strings.Cut(t.Repo, "/")
|
||||||
|
if current == name {
|
||||||
|
return clierr.Wrap(clierr.Usage, fmt.Errorf("%s is already named %s", t.Repo, name))
|
||||||
|
}
|
||||||
|
inferred, err := a.inferredRepository(t, "rename", false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if inferred != "" {
|
||||||
|
if err := a.confirmAction(cmd, terminal.Confirmation{Prompt: "Rename " + t.Repo + inferred + " to " + owner + "/" + name + "?", DefaultYes: true, NonTTY: "rename " + t.Repo}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
item, err := (services.Repositories{Client: t.client}).Edit(cmd.Context(), t.Repo, services.RepositoryEdit{Name: &name})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.done(cmd, "Renamed repository %s to %s", t.Repo, item.FullName)
|
||||||
|
if err := a.followRename(cmd, t, current, item); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return a.writeMutationJSON(cmd, item)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return command
|
||||||
|
}
|
||||||
|
|
||||||
|
// followRename points the Git remote or the saved default repository that selected a renamed
|
||||||
|
// repository, previously named old, at its new name.
|
||||||
|
func (a *application) followRename(cmd *cobra.Command, t target, old string, item services.Repository) error {
|
||||||
|
if remote, ok := strings.CutPrefix(t.RepoSource, "remote "); ok {
|
||||||
|
raw, err := a.git(cmd.Context(), "remote", "get-url", remote)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("renamed %s, but could not read Git remote %s: %w", item.FullName, remote, err)
|
||||||
|
}
|
||||||
|
renamed, ok := renamedURL(raw, old, item.Name)
|
||||||
|
if !ok {
|
||||||
|
a.warn(cmd, "Git remote %s still points to %s; update it with git remote set-url", remote, t.Repo)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if _, err := a.git(cmd.Context(), "remote", "set-url", remote, renamed); err != nil {
|
||||||
|
return fmt.Errorf("renamed %s, but could not update Git remote %s: %w", item.FullName, remote, err)
|
||||||
|
}
|
||||||
|
a.done(cmd, "Updated Git remote %s to %s", remote, item.FullName)
|
||||||
|
}
|
||||||
|
if t.RepoSource == "git config tea.repository" {
|
||||||
|
if _, err := a.git(cmd.Context(), "config", "--local", "tea.repository", t.Host+"/"+item.FullName); err != nil {
|
||||||
|
return fmt.Errorf("renamed %s, but could not update the default repository: %w", item.FullName, err)
|
||||||
|
}
|
||||||
|
a.done(cmd, "Set %s as the default repository for this clone", item.FullName)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// renamedURL replaces the repository name old at the end of a remote URL with name, keeping the
|
||||||
|
// rest of the URL, such as an SSH host alias, and its .git suffix.
|
||||||
|
func renamedURL(raw, old, name string) (string, bool) {
|
||||||
|
base, suffix := strings.TrimSuffix(raw, "/"), ""
|
||||||
|
if trimmed, ok := strings.CutSuffix(base, ".git"); ok {
|
||||||
|
base, suffix = trimmed, ".git"
|
||||||
|
}
|
||||||
|
if len(base) <= len(old) || !strings.EqualFold(base[len(base)-len(old):], old) || !strings.ContainsRune("/:", rune(base[len(base)-len(old)-1])) {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return base[:len(base)-len(old)] + name + suffix, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *application) repoDeleteCommand() *cobra.Command {
|
||||||
|
command := &cobra.Command{
|
||||||
|
Use: "delete [<repository>]",
|
||||||
|
Short: "Delete a repository",
|
||||||
|
Long: `Delete a repository with its issues, pull requests, releases and wiki. This cannot be undone.
|
||||||
|
|
||||||
|
Type the repository's name to confirm. With --yes, name the repository explicitly.`,
|
||||||
|
Example: `# Delete a repository after typing its name
|
||||||
|
$ tea repo delete alice/old-project
|
||||||
|
|
||||||
|
# Delete without a prompt
|
||||||
|
$ tea repo delete alice/old-project --yes`,
|
||||||
|
Annotations: map[string]string{"tea.arguments": repositoryArguments, "tea.confirm": "true", "tea.text": "repository deletion does not return a resource"},
|
||||||
|
Args: usageArgs(cobra.MaximumNArgs(1)),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
if err := a.repositoryArgument(args); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t, err := a.resolve(cmd.Context(), true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
inferred, err := a.inferredRepository(t, "delete", true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := a.confirmAction(cmd, terminal.Confirmation{Prompt: "Delete " + t.Repo + inferred + "? This cannot be undone.", Typed: t.Repo, NonTTY: "delete " + t.Repo}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := (services.Repositories{Client: t.client}).Delete(cmd.Context(), t.Repo); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.done(cmd, "Deleted repository %s", t.Repo)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
command.ValidArgsFunction = a.completeRepos()
|
||||||
|
return command
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ package app
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -72,3 +73,100 @@ func TestRepoDeleteAsksToTypeTheRepositoryName(t *testing.T) {
|
|||||||
require.Equal(t, []string{"DELETE /api/v1/repos/alice/project"}, requests)
|
require.Equal(t, []string{"DELETE /api/v1/repos/alice/project"}, requests)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRepoArchiveStates(t *testing.T) {
|
||||||
|
for _, test := range []struct {
|
||||||
|
name string
|
||||||
|
args []string
|
||||||
|
archived bool
|
||||||
|
requests []string
|
||||||
|
feedback string
|
||||||
|
}{
|
||||||
|
{"archive", []string{"repo", "archive", "alice/x", "--yes"}, false, []string{"GET", "PATCH"}, "✓ Archived repository alice/x\n"},
|
||||||
|
{"already archived", []string{"repo", "archive", "alice/x", "--yes"}, true, []string{"GET"}, "! alice/x is already archived\n"},
|
||||||
|
{"unarchive", []string{"repo", "unarchive", "alice/x"}, true, []string{"GET", "PATCH"}, "✓ Unarchived repository alice/x\n"},
|
||||||
|
{"not archived", []string{"repo", "unarchive", "alice/x"}, false, []string{"GET"}, "! alice/x is not archived\n"},
|
||||||
|
} {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
var requests []string
|
||||||
|
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
require.Equal(t, "/api/v1/repos/alice/x", r.URL.Path)
|
||||||
|
requests = append(requests, r.Method)
|
||||||
|
archived := test.archived
|
||||||
|
if r.Method == http.MethodPatch {
|
||||||
|
requirePayload(t, fmt.Sprintf(`{"archived":%t}`, !archived), r.Body)
|
||||||
|
archived = !archived
|
||||||
|
}
|
||||||
|
_, _ = fmt.Fprintf(w, `{"full_name":"alice/x","archived":%t}`, archived)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
deps, out, diagnostics := repoTTYDeps(t, server.URL)
|
||||||
|
require.NoError(t, Execute(context.Background(), deps, test.args))
|
||||||
|
require.Equal(t, test.requests, requests)
|
||||||
|
require.Empty(t, out.String())
|
||||||
|
require.Equal(t, test.feedback, diagnostics.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRepoArchiveAsksWithTheConsequence(t *testing.T) {
|
||||||
|
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
require.Equal(t, http.MethodGet, r.Method)
|
||||||
|
_, _ = w.Write([]byte(`{"full_name":"alice/project"}`))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
deps, _, _ := serverDeps(t, server.URL)
|
||||||
|
deps.Git = upstreamCheckout(server)
|
||||||
|
prompt := &scriptedPrompter{}
|
||||||
|
deps.Prompter, deps.Streams.InTTY, deps.Streams.ErrTTY = prompt, true, true
|
||||||
|
err := Execute(context.Background(), deps, []string{"repo", "archive"})
|
||||||
|
require.Equal(t, clierr.Canceled, clierr.ExitCode(err), "%v", err)
|
||||||
|
require.Equal(t, []string{"Archive alice/project (from git remote upstream)? It becomes read-only."}, prompt.confirmations)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRepoRenameFollowsTheRemote(t *testing.T) {
|
||||||
|
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
require.Equal(t, http.MethodPatch+" /api/v1/repos/alice/project", r.Method+" "+r.URL.Path)
|
||||||
|
requirePayload(t, `{"name":"renamed"}`, r.Body)
|
||||||
|
_, _ = w.Write([]byte(`{"name":"renamed","full_name":"alice/renamed"}`))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
deps, out, diagnostics := repoTTYDeps(t, server.URL)
|
||||||
|
runner := &fakeGit{results: map[string]string{
|
||||||
|
"symbolic-ref --short HEAD": "main",
|
||||||
|
"config --get branch.main.remote": "origin",
|
||||||
|
"remote get-url origin": server.URL + "/alice/project.git",
|
||||||
|
"remote set-url origin " + server.URL + "/alice/renamed.git": "",
|
||||||
|
}}
|
||||||
|
deps.Git = runner
|
||||||
|
require.NoError(t, Execute(context.Background(), deps, []string{"repo", "rename", "renamed", "--yes"}))
|
||||||
|
require.Contains(t, runner.calls, "remote set-url origin "+server.URL+"/alice/renamed.git")
|
||||||
|
require.Empty(t, out.String())
|
||||||
|
require.Equal(t, "✓ Renamed repository alice/project to alice/renamed\n✓ Updated Git remote origin to alice/renamed\n", diagnostics.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRenamedURL(t *testing.T) {
|
||||||
|
for _, test := range []struct{ raw, want string }{
|
||||||
|
{"https://gitea.test/alice/project.git", "https://gitea.test/alice/renamed.git"},
|
||||||
|
{"https://gitea.test/alice/project/", "https://gitea.test/alice/renamed"},
|
||||||
|
{"git@gitea-alias:alice/Project.git", "git@gitea-alias:alice/renamed.git"},
|
||||||
|
{"ssh://git@gitea.test:2222/alice/project", "ssh://git@gitea.test:2222/alice/renamed"},
|
||||||
|
{"https://gitea.test/alice/subproject.git", ""},
|
||||||
|
} {
|
||||||
|
renamed, ok := renamedURL(test.raw, "project", "renamed")
|
||||||
|
require.Equal(t, test.want != "", ok, test.raw)
|
||||||
|
require.Equal(t, test.want, renamed, test.raw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRepoDeleteReportsTheDeletion(t *testing.T) {
|
||||||
|
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
require.Equal(t, http.MethodDelete+" /api/v1/repos/alice/x", r.Method+" "+r.URL.Path)
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
deps, out, diagnostics := repoTTYDeps(t, server.URL)
|
||||||
|
require.NoError(t, Execute(context.Background(), deps, []string{"repo", "rm", "alice/x", "--yes"}))
|
||||||
|
require.Empty(t, out.String())
|
||||||
|
require.Equal(t, "✓ Deleted repository alice/x\n", diagnostics.String())
|
||||||
|
}
|
||||||
@@ -18,8 +18,10 @@ func TestRepositoryCommandsAcceptPositionalRepository(t *testing.T) {
|
|||||||
requests = append(requests, r.Method+" "+r.URL.Path)
|
requests = append(requests, r.Method+" "+r.URL.Path)
|
||||||
if r.Method == http.MethodPatch {
|
if r.Method == http.MethodPatch {
|
||||||
requirePayload(t, `{"archived":true}`, r.Body)
|
requirePayload(t, `{"archived":true}`, r.Body)
|
||||||
|
_, _ = w.Write([]byte(`{"full_name":"bob/tool","archived":true}`))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
_, _ = w.Write([]byte(`{"full_name":"bob/tool","archived":true}`))
|
_, _ = w.Write([]byte(`{"full_name":"bob/tool"}`))
|
||||||
}))
|
}))
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
for _, test := range []struct {
|
for _, test := range []struct {
|
||||||
@@ -36,7 +38,7 @@ func TestRepositoryCommandsAcceptPositionalRepository(t *testing.T) {
|
|||||||
deps, out, _ := testDeps(t)
|
deps, out, _ := testDeps(t)
|
||||||
deps.Getenv = serverEnv(server.URL, map[string]string{"TEA_REPO": "alice/project"})
|
deps.Getenv = serverEnv(server.URL, map[string]string{"TEA_REPO": "alice/project"})
|
||||||
require.NoError(t, Execute(context.Background(), deps, append(test.args, "--no-input", "--json=full_name")))
|
require.NoError(t, Execute(context.Background(), deps, append(test.args, "--no-input", "--json=full_name")))
|
||||||
require.Equal(t, []string{test.want}, requests)
|
require.Equal(t, test.want, requests[len(requests)-1])
|
||||||
require.JSONEq(t, `{"full_name":"bob/tool"}`, out.String())
|
require.JSONEq(t, `{"full_name":"bob/tool"}`, out.String())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user