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:
bircni committed 2026-10-11 23:51:45 +02:00
1 parent eedbe9893d
commit 7cd13ef64e
6 files changed
+327 -46

No files matched your search

+98
View File
@@ -5,6 +5,7 @@ package app
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"testing"
@@ -72,3 +73,100 @@ func TestRepoDeleteAsksToTypeTheRepositoryName(t *testing.T) {
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())
}