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

+39
View File
@@ -7,6 +7,7 @@ import (
"bytes"
"context"
"net/http"
"strings"
"testing"
"time"
@@ -104,6 +105,44 @@ View this repository on Gitea: https://gitea.test/bob/project
"clone:\thttps://gitea.test/bob/project.git\nssh:\t\nurl:\thttps://gitea.test/bob/project\n--\nHello **there**\n", out.String())
}
func TestRepoSetDefaultValidatesTheRepository(t *testing.T) {
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/v1/repos/alice/project" {
w.WriteHeader(http.StatusNotFound)
return
}
_, _ = w.Write([]byte(`{"full_name":"alice/project"}`))
}))
defer server.Close()
deps, _, diagnostics := repoTTYDeps(t, server.URL)
saved := "config --local tea.repository " + server.URL + "/alice/project"
runner := &fakeGit{results: map[string]string{"rev-parse --is-inside-work-tree": "true", saved: ""}}
deps.Git = runner
err := Execute(context.Background(), deps, []string{"repo", "set-default", "nobody/none"})
require.Error(t, err)
require.NotContains(t, strings.Join(runner.calls, "\n"), "config --local tea.repository")
require.NoError(t, Execute(context.Background(), deps, []string{"repo", "set-default", "alice/project"}))
require.Contains(t, runner.calls, saved)
require.Equal(t, "✓ Set alice/project as the default repository for this clone\n", diagnostics.String())
}
func TestRepoSetDefaultViewAndUnset(t *testing.T) {
deps, out, diagnostics := repoTTYDeps(t, "http://gitea.test")
runner := &fakeGit{results: map[string]string{
"rev-parse --is-inside-work-tree": "true",
"config --local --get tea.repository": "http://gitea.test/alice/project",
"config --local --unset tea.repository": "",
}}
deps.Git = runner
require.NoError(t, Execute(context.Background(), deps, []string{"repo", "set-default", "--view"}))
require.Equal(t, "http://gitea.test/alice/project\n", out.String())
require.NoError(t, Execute(context.Background(), deps, []string{"repo", "set-default", "-u"}))
require.Equal(t, "✓ Removed the default repository http://gitea.test/alice/project from this clone\n", diagnostics.String())
deps.Git = &fakeGit{}
err := Execute(context.Background(), deps, []string{"repo", "set-default", "-v"})
require.ErrorContains(t, err, "set-default must run inside a Git clone")
}
func TestRepoViewOfAnEmptyRepositorySkipsTheReadme(t *testing.T) {
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "/api/v1/repos/alice/empty", r.URL.Path)