From fa95f3a21b2bd525e834786594054e513fdb9eeb Mon Sep 17 00:00:00 2001 From: 4RH1T3CT0R7 Date: Sat, 14 Feb 2026 20:31:02 +0300 Subject: [PATCH 1/2] Add --no-upstream flag to gh repo clone When cloning a forked repository, `gh repo clone` automatically adds the parent repo as an `upstream` remote and sets it as the default repository. This can be problematic when the user lacks access to the parent repo, the upstream fetch is expensive for large repos, or the user simply doesn't want the upstream remote. Add a `--no-upstream` flag that skips adding the upstream remote when cloning a fork. When used, origin (the fork) is set as the default repository instead. The flag is mutually exclusive with `--upstream-remote-name`. For non-fork repos the flag is a harmless no-op. Closes #8274 --- pkg/cmd/repo/clone/clone.go | 62 +++++++++++++++---------- pkg/cmd/repo/clone/clone_test.go | 79 ++++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 24 deletions(-) diff --git a/pkg/cmd/repo/clone/clone.go b/pkg/cmd/repo/clone/clone.go index 1466cd96a..7c5382144 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,8 @@ 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 adding the upstream remote entirely, use %[1]s--no-upstream%[1]s. `, "`"), Example: heredoc.Doc(` # Clone a repository from a specific org @@ -77,6 +80,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 +97,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 +195,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 From 6045a593a35b9eb9913e89269fc326851145ec9a Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Tue, 3 Mar 2026 15:51:27 -0700 Subject: [PATCH 2/2] Reword `--no-upstream` help doc --- pkg/cmd/repo/clone/clone.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/cmd/repo/clone/clone.go b/pkg/cmd/repo/clone/clone.go index 7c5382144..b29b25038 100644 --- a/pkg/cmd/repo/clone/clone.go +++ b/pkg/cmd/repo/clone/clone.go @@ -61,8 +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 adding the upstream remote entirely, use %[1]s--no-upstream%[1]s. + To skip this behavior, use %[1]s--no-upstream%[1]s. `, "`"), Example: heredoc.Doc(` # Clone a repository from a specific org