diff --git a/internal/app/clone_test.go b/internal/app/clone_test.go index 676c2364..f0fc342b 100644 --- a/internal/app/clone_test.go +++ b/internal/app/clone_test.go @@ -150,6 +150,35 @@ func TestSSHCloneTrustsAPIEndpointUnlessSSHHostIsConfigured(t *testing.T) { require.Empty(t, runner.calls) } +func TestCloneAddsSourceRemoteForMirrors(t *testing.T) { + original := "" + server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = fmt.Fprintf(w, `{"id":9,"name":"project","full_name":"alice/project","mirror":true,"clone_url":"http://%s/alice/project.git","original_url":%q}`, r.Host, original) + })) + defer server.Close() + deps, out, diagnostics := repoTTYDeps(t, server.URL) + clone := credentialCall(t, deps, server.URL, "alice", "clone", "--", server.URL+"/alice/project.git", "destination") + original = "https://github.com/upstream/project.git" + runner := &fakeGit{results: map[string]string{clone: "", "remote add -- source " + original: ""}} + deps.Git = runner + args := []string{"repo", "clone", "alice/project", "destination"} + require.NoError(t, Execute(context.Background(), deps, args)) + require.Equal(t, []string{clone, "remote add -- source " + original}, runner.calls) + require.Equal(t, "āœ“ Cloned alice/project into destination\nāœ“ Added remote source for "+original+"\n", diagnostics.String()) + runner.calls = nil + require.NoError(t, Execute(context.Background(), deps, append(args, "--json"))) + require.Equal(t, []string{clone, "remote add -- source " + original}, runner.calls) + require.NotContains(t, out.String(), "github.com") + runner.calls = nil + require.NoError(t, Execute(context.Background(), deps, append(args, "--no-upstream"))) + require.Equal(t, []string{clone}, runner.calls) + for _, unsafe := range []string{"https://user:secret@github.com/upstream/project.git", "file:///srv/project.git", "ext::sh -c touch% /tmp/pwned"} { + original, runner.calls = unsafe, nil + require.NoError(t, Execute(context.Background(), deps, args), unsafe) + require.Equal(t, []string{clone}, runner.calls, unsafe) + } +} + func TestClonePassesGitFlagsAndNamesTheUpstreamRemote(t *testing.T) { server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = fmt.Fprintf(w, `{"name":"project","full_name":"bob/project","fork":true,"clone_url":"http://%[1]s/bob/project.git","parent":{"full_name":"alice/project","default_branch":"main","clone_url":"http://%[1]s/alice/project.git"}}`, r.Host) diff --git a/internal/app/repo_mutations.go b/internal/app/repo_mutations.go index 1de1b5fa..d96465f0 100644 --- a/internal/app/repo_mutations.go +++ b/internal/app/repo_mutations.go @@ -241,7 +241,8 @@ func (a *application) repoCloneCommand() *cobra.Command { Short: "Clone a repository locally", Long: `Clone a repository with the configured Git protocol and tea's credentials. Git flags can follow --. A fork's parent is added as the "upstream" remote, and tea commands in the clone then -target the parent.`, +target the parent. A pull mirror's source is added as the "source" remote; tea commands keep +targeting the mirror.`, Example: `# Clone a repository into ./project $ tea repo clone alice/project @@ -287,7 +288,7 @@ $ tea repo clone alice/project -- --branch develop --recurse-submodules`, } command.Flags().IntVarP(&opts.depth, "depth", "d", 0, "Number of commits to fetch (0: full history)") command.Flags().StringVarP(&opts.upstream, "upstream-remote-name", "u", "upstream", "Remote name for the parent of a fork") - command.Flags().BoolVar(&noUpstream, "no-upstream", false, "Do not add a remote for the parent of a fork") + command.Flags().BoolVar(&noUpstream, "no-upstream", false, "Do not add a remote for the parent of a fork or the source of a mirror") command.ValidArgsFunction = pathArgs(1, false, completeDirectories, a.completeRepos()) return command } @@ -419,7 +420,7 @@ type cloneOptions struct { directory string depth int gitFlags []string - // upstream names the remote of a fork's parent; empty adds none. + // upstream names the remote of a fork's parent; empty adds neither it nor a mirror's source. upstream string } @@ -487,6 +488,13 @@ func (a *application) cloneRepository(cmd *cobra.Command, t target, item service } a.done(cmd, "Added remote %s for %s; tea commands in the clone target it (change with tea repo set-default)", opts.upstream, item.Parent.FullName) } + // The remote is named source because repository discovery prefers a remote named upstream. + if source := mirrorSourceURL(item); source != "" && opts.upstream != "" { + if _, err := a.gitIn(cmd.Context(), clone, "remote", "add", "--", "source", source); err != nil { + return clone, fmt.Errorf("cloned the mirror, but could not add the source remote: %w", err) + } + a.done(cmd, "Added remote source for %s", source) + } return clone, nil }