feat(repo): sync forks on the server, wire forks into clones, validate set-default

repo sync OWNER/REPO, run where no Git remote points to the repository,
now updates it on the server: a fork's branch fast-forwards to its
parent's through merge-upstream, and a pull mirror fetches its source.

repo fork reports an existing fork ("! bob/x already exists") instead of
a raw HTTP 409, refuses to fork a repository into its own owner, and
gains --fork-name, --clone (Git flags after --) and --remote, which adds
the fork as origin and renames the old origin to upstream.

repo clone passes Git flags after --, and takes --upstream-remote-name
and --no-upstream for forks. Clones report what they set up.

repo set-default checks that the repository exists before saving it,
prints the saved repository with --view, removes it with --unset, and
reports the change.

Signed-off-by: bircni <bircni@icloud.com>
This commit is contained in:
bircni committed 2026-10-11 23:51:47 +02:00
1 parent 7cd13ef64e
commit 368de47c40
13 files changed
+599 -98

No files matched your search

+61
View File
@@ -159,6 +159,67 @@ func TestRenamedURL(t *testing.T) {
}
}
func TestRepoForkReportsAnExistingFork(t *testing.T) {
var requests []string
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requests = append(requests, r.Method+" "+r.URL.Path)
switch r.Method + " " + r.URL.Path {
case "POST /api/v1/repos/bob/project/forks":
w.WriteHeader(http.StatusConflict)
_, _ = w.Write([]byte(`{"message":"repository is already forked by user [uname: alice, repo path: bob/project, fork path: alice/project-fork]"}`))
case "GET /api/v1/repos/bob/project/forks":
if r.URL.Query().Get("page") != "1" {
_, _ = w.Write([]byte(`[]`))
return
}
_, _ = w.Write([]byte(`[{"full_name":"carol/project","owner":{"login":"carol"}},{"full_name":"alice/project-fork","owner":{"login":"alice"}}]`))
case "GET /api/v1/repos/alice/project-fork":
_, _ = w.Write([]byte(`{"full_name":"alice/project-fork","fork":true,"html_url":"https://gitea.test/alice/project-fork","parent":{"full_name":"bob/project"}}`))
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
deps, out, diagnostics := repoTTYDeps(t, server.URL)
require.NoError(t, Execute(context.Background(), deps, []string{"repo", "fork", "bob/project"}))
require.Equal(t, "! alice/project-fork already exists\n", diagnostics.String())
require.Equal(t, "https://gitea.test/alice/project-fork\n", out.String())
require.Equal(t, "GET /api/v1/repos/alice/project-fork", requests[len(requests)-1])
}
func TestRepoForkRefusesTheOwnersRepository(t *testing.T) {
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Errorf("unexpected request %s %s", r.Method, r.URL.Path)
}))
defer server.Close()
deps, _, _ := serverDeps(t, server.URL)
err := Execute(context.Background(), deps, []string{"repo", "fork", "alice/project"})
require.Equal(t, clierr.Usage, clierr.ExitCode(err), "%v", err)
require.ErrorContains(t, err, "cannot fork alice/project into alice: it already owns it; pass --org")
}
func TestRepoForkAddsTheForkAsOrigin(t *testing.T) {
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "POST /api/v1/repos/bob/project/forks", r.Method+" "+r.URL.Path)
requirePayload(t, `{"name":"mine"}`, r.Body)
_, _ = fmt.Fprintf(w, `{"full_name":"alice/mine","html_url":"https://gitea.test/alice/mine","clone_url":"http://%s/alice/mine.git"}`, r.Host)
}))
defer server.Close()
deps, out, diagnostics := repoTTYDeps(t, server.URL)
fork := server.URL + "/alice/mine.git"
runner := &fakeGit{results: map[string]string{
"rev-parse --is-inside-work-tree": "true",
"remote get-url origin": server.URL + "/bob/project.git",
"remote rename origin upstream": "",
"remote add -- origin " + fork: "",
}}
deps.Git = runner
require.NoError(t, Execute(context.Background(), deps, []string{"repo", "fork", "bob/project", "--fork-name", "mine", "--remote"}))
require.Equal(t, []string{"rev-parse --is-inside-work-tree", "remote get-url origin", "remote rename origin upstream", "remote add -- origin " + fork}, runner.calls)
require.Equal(t, "✓ Forked bob/project to alice/mine\n✓ Renamed Git remote origin to upstream\n✓ Added Git remote origin for alice/mine\n", diagnostics.String())
require.Equal(t, "https://gitea.test/alice/mine\n", out.String())
}
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)