Merge pull request #12686 from 4RH1T3CT0R7/add-no-upstream-flag

Add --no-upstream flag to repo clone
This commit is contained in:
Kynan Ware 2026-03-03 16:01:05 -07:00 committed by GitHub
commit 6e979c6b32
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 116 additions and 24 deletions

View file

@ -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

View file

@ -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