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
This commit is contained in:
parent
1af2823fc3
commit
fa95f3a21b
2 changed files with 117 additions and 24 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue