diff --git a/pkg/cmd/repo/clone/clone.go b/pkg/cmd/repo/clone/clone.go index 1466cd96a..b29b25038 100644 --- a/pkg/cmd/repo/clone/clone.go +++ b/pkg/cmd/repo/clone/clone.go @@ -27,6 +27,7 @@ type CloneOptions struct { GitArgs []string Repository string UpstreamName string + NoUpstream bool } func NewCmdClone(f *cmdutil.Factory, runF func(*CloneOptions) error) *cobra.Command { @@ -60,6 +61,7 @@ func NewCmdClone(f *cmdutil.Factory, runF func(*CloneOptions) error) *cobra.Comm the remote after the owner of the parent repository. If the repository is a fork, its parent repository will be set as the default remote repository. + To skip this behavior, use %[1]s--no-upstream%[1]s. `, "`"), Example: heredoc.Doc(` # Clone a repository from a specific org @@ -77,6 +79,9 @@ func NewCmdClone(f *cmdutil.Factory, runF func(*CloneOptions) error) *cobra.Comm # Clone a repository with additional git clone flags $ gh repo clone cli/cli -- --depth=1 + + # Clone a fork without adding an upstream remote + $ gh repo clone myfork --no-upstream `), RunE: func(cmd *cobra.Command, args []string) error { opts.Repository = args[0] @@ -91,6 +96,8 @@ func NewCmdClone(f *cmdutil.Factory, runF func(*CloneOptions) error) *cobra.Comm } cmd.Flags().StringVarP(&opts.UpstreamName, "upstream-remote-name", "u", "upstream", "Upstream remote name when cloning a fork") + cmd.Flags().BoolVar(&opts.NoUpstream, "no-upstream", false, "Do not add an upstream remote when cloning a fork") + cmd.MarkFlagsMutuallyExclusive("upstream-remote-name", "no-upstream") cmd.SetFlagErrorFunc(func(cmd *cobra.Command, err error) error { if err == pflag.ErrHelp { return err @@ -187,37 +194,43 @@ func cloneRun(opts *CloneOptions) error { // If the repo is a fork, add the parent as an upstream remote and set the parent as the default repo. if canonicalRepo.Parent != nil { - protocol := cfg.GitProtocol(canonicalRepo.Parent.RepoHost()).Value - upstreamURL := ghrepo.FormatRemoteURL(canonicalRepo.Parent, protocol) - - upstreamName := opts.UpstreamName - if opts.UpstreamName == "@owner" { - upstreamName = canonicalRepo.Parent.RepoOwner() - } - gc := gitClient.Copy() gc.RepoDir = cloneDir - if _, err := gc.AddRemote(ctx, upstreamName, upstreamURL, []string{canonicalRepo.Parent.DefaultBranchRef.Name}); err != nil { - return err - } + if opts.NoUpstream { + if err := gc.SetRemoteResolution(ctx, "origin", "base"); err != nil { + return err + } + } else { + protocol := cfg.GitProtocol(canonicalRepo.Parent.RepoHost()).Value + upstreamURL := ghrepo.FormatRemoteURL(canonicalRepo.Parent, protocol) - if err := gc.Fetch(ctx, upstreamName, ""); err != nil { - return err - } + upstreamName := opts.UpstreamName + if opts.UpstreamName == "@owner" { + upstreamName = canonicalRepo.Parent.RepoOwner() + } - if err := gc.SetRemoteBranches(ctx, upstreamName, `*`); err != nil { - return err - } + if _, err := gc.AddRemote(ctx, upstreamName, upstreamURL, []string{canonicalRepo.Parent.DefaultBranchRef.Name}); err != nil { + return err + } - if err = gc.SetRemoteResolution(ctx, upstreamName, "base"); err != nil { - return err - } + if err := gc.Fetch(ctx, upstreamName, ""); err != nil { + return err + } - connectedToTerminal := opts.IO.IsStdoutTTY() - if connectedToTerminal { - cs := opts.IO.ColorScheme() - fmt.Fprintf(opts.IO.ErrOut, "%s Repository %s set as the default repository. To learn more about the default repository, run: gh repo set-default --help\n", cs.WarningIcon(), cs.Bold(ghrepo.FullName(canonicalRepo.Parent))) + if err := gc.SetRemoteBranches(ctx, upstreamName, `*`); err != nil { + return err + } + + if err := gc.SetRemoteResolution(ctx, upstreamName, "base"); err != nil { + return err + } + + connectedToTerminal := opts.IO.IsStdoutTTY() + if connectedToTerminal { + cs := opts.IO.ColorScheme() + fmt.Fprintf(opts.IO.ErrOut, "%s Repository %s set as the default repository. To learn more about the default repository, run: gh repo set-default --help\n", cs.WarningIcon(), cs.Bold(ghrepo.FullName(canonicalRepo.Parent))) + } } } return nil diff --git a/pkg/cmd/repo/clone/clone_test.go b/pkg/cmd/repo/clone/clone_test.go index 471ed05dd..bab2bd670 100644 --- a/pkg/cmd/repo/clone/clone_test.go +++ b/pkg/cmd/repo/clone/clone_test.go @@ -54,6 +54,20 @@ func TestNewCmdClone(t *testing.T) { GitArgs: []string{"--depth", "1", "--recurse-submodules"}, }, }, + { + name: "no-upstream flag", + args: "OWNER/REPO --no-upstream", + wantOpts: CloneOptions{ + Repository: "OWNER/REPO", + GitArgs: []string{}, + NoUpstream: true, + }, + }, + { + name: "no-upstream with upstream-remote-name", + args: "OWNER/REPO --no-upstream --upstream-remote-name test", + wantErr: "if any flags in the group [upstream-remote-name no-upstream] are set none of the others can be; [no-upstream upstream-remote-name] were all set", + }, { name: "unknown argument", args: "OWNER/REPO --depth 1", @@ -92,6 +106,7 @@ func TestNewCmdClone(t *testing.T) { assert.Equal(t, tt.wantOpts.Repository, opts.Repository) assert.Equal(t, tt.wantOpts.GitArgs, opts.GitArgs) + assert.Equal(t, tt.wantOpts.NoUpstream, opts.NoUpstream) }) } } @@ -344,6 +359,70 @@ func Test_RepoClone_withoutUsername(t *testing.T) { assert.Equal(t, "", output.Stderr()) } +func Test_RepoClone_hasParent_noUpstream(t *testing.T) { + reg := &httpmock.Registry{} + defer reg.Verify(t) + reg.Register( + httpmock.GraphQL(`query RepositoryInfo\b`), + httpmock.StringResponse(` + { "data": { "repository": { + "name": "REPO", + "owner": { + "login": "OWNER" + }, + "parent": { + "name": "ORIG", + "owner": { + "login": "hubot" + }, + "defaultBranchRef": { + "name": "trunk" + } + } + } } } + `)) + + httpClient := &http.Client{Transport: reg} + + cs, cmdTeardown := run.Stub() + defer cmdTeardown(t) + + cs.Register(`git clone https://github.com/OWNER/REPO.git`, 0, "") + cs.Register(`git -C REPO config --add remote.origin.gh-resolved base`, 0, "") + + _, err := runCloneCommand(httpClient, "OWNER/REPO --no-upstream") + if err != nil { + t.Fatalf("error running command `repo clone`: %v", err) + } +} + +func Test_RepoClone_noParent_noUpstream(t *testing.T) { + reg := &httpmock.Registry{} + defer reg.Verify(t) + reg.Register( + httpmock.GraphQL(`query RepositoryInfo\b`), + httpmock.StringResponse(` + { "data": { "repository": { + "name": "REPO", + "owner": { + "login": "OWNER" + } + } } } + `)) + + httpClient := &http.Client{Transport: reg} + + cs, cmdTeardown := run.Stub() + defer cmdTeardown(t) + + cs.Register(`git clone https://github.com/OWNER/REPO.git`, 0, "") + + _, err := runCloneCommand(httpClient, "OWNER/REPO --no-upstream") + if err != nil { + t.Fatalf("error running command `repo clone`: %v", err) + } +} + func TestSimplifyURL(t *testing.T) { tests := []struct { name string