Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
[gh repo clone] Add --no-upstream flag
  • Loading branch information
iamazeem committed Mar 15, 2025
commit 1bb599c1ef0c9760eac6b8874ed250146c9684af
27 changes: 24 additions & 3 deletions pkg/cmd/repo/clone/clone.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -40,7 +41,7 @@ func NewCmdClone(f *cmdutil.Factory, runF func(*CloneOptions) error) *cobra.Comm
cmd := &cobra.Command{
DisableFlagsInUseLine: true,

Use: "clone <repository> [<directory>] [-- <gitflags>...]",
Use: "clone <repository> [<directory>] [flags] [-- <gitflags>...]",
Args: cmdutil.MinimumArgs(1, "cannot clone: repository argument required"),
Short: "Clone a repository locally",
Long: heredoc.Docf(`
Expand All @@ -60,6 +61,9 @@ 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 disable the addition of the %[1]supstream%[1]s remote for a forked repository,
use the %[1]s--no-upstream%[1]s flag. For a non-forked repository, this flag has no effect.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggest: I'd like to attempt rephrasing a few of these sections to be smaller and easier to read.

			If the repository is a fork, its parent repository will be added as an additional git remote named
			%[1]supstream%[1]s and set as the default remote unless %[1]s--no-upstream%[1]s flag is specified.

			The remote name can be configured using the %[1]s--upstream-remote-name%[1]s flag, which supports
			the %[1]s@owner%[1]s syntax to name it after the owner of the parent repository.

as opposed to all of this:

If the repository is a fork, its parent repository will be added as an additional
git remote called %[1]supstream%[1]s. The remote name can be configured using %[1]s--upstream-remote-name%[1]s.
The %[1]s--upstream-remote-name%[1]s option supports an %[1]s@owner%[1]s value which will name
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 disable the addition of the %[1]supstream%[1]s remote for a forked repository,
use the %[1]s--no-upstream%[1]s flag. For a non-forked repository, this flag has no effect.

`, "`"),
Example: heredoc.Doc(`
# Clone a repository from a specific org
Expand All @@ -77,8 +81,19 @@ 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 forked repository without the upstream remote
$ gh repo clone myuser/cli --no-upstream
`),
RunE: func(cmd *cobra.Command, args []string) error {
if err := cmdutil.MutuallyExclusive(
"specify only one of `--upstream-remote-name` or `--no-upstream`",
cmd.Flags().Changed("upstream-remote-name") && opts.UpstreamName != "",
opts.NoUpstream,
); err != nil {
return err
}

opts.Repository = args[0]
opts.GitArgs = args[1:]

Expand All @@ -91,6 +106,7 @@ 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 set/fetch upstream remote when cloning a fork")
cmd.SetFlagErrorFunc(func(cmd *cobra.Command, err error) error {
if err == pflag.ErrHelp {
return err
Expand Down Expand Up @@ -186,7 +202,7 @@ 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 {
if !opts.NoUpstream && canonicalRepo.Parent != nil {
Copy link
Member

@andyfeller andyfeller Apr 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: I believe these changes create a situation where forked repositories are not resolved properly via remotes as gh repo view fails. 🤔

I want to raise this up with the other maintainers as I'm unsure if we still need to set remote resolution even though there is only 1 remote.

Demo

Repository: https://github.com/andyfeller/cli

$ make clean && make
rm -rf bin share
go build -trimpath -ldflags "-X github.com/cli/cli/v2/internal/build.Date=2025-04-14 -X github.com/cli/cli/v2/internal/build.Version=v2.68.1-23-g1bb599c1e " -o bin/gh ./cmd/gh

$ alias gh=$(realpath bin/gh)               
$ cd ~/Documents/workspace/andyfeller

$ gh repo clone andyfeller/cli --no-upstream
Cloning into 'cli'...
remote: Enumerating objects: 56037, done.
remote: Total 56037 (delta 0), reused 0 (delta 0), pack-reused 56037 (from 1)
Receiving objects: 100% (56037/56037), 29.54 MiB | 22.13 MiB/s, done.
Resolving deltas: 100% (38350/38350), done.

$ cd cli
$ cat .git/config 
[core]
	repositoryformatversion = 0
	filemode = true
	bare = false
	logallrefupdates = true
	ignorecase = true
	precomposeunicode = true
[remote "origin"]
	url = https://github.com/andyfeller/cli.git
	fetch = +refs/heads/*:refs/remotes/origin/*
[branch "trunk"]
	remote = origin
	merge = refs/heads/trunk

$ gh repo view
X No default remote repository has been set. To learn more about the default repository, run: gh repo set-default --help

please run `gh repo set-default` to select a default remote repository.

Looking at GH_DEBUG=api output below, the RepositoryNetwork GraphQL query points to logic that resolves the remotes:

Expand for full GH_DEBUG=api gh repo view output

$ GH_DEBUG=api gh repo view
[git remote -v]
[git config --get-regexp ^remote\..*\.gh-resolved$]
* Request at 2025-04-14 11:44:42.424889 -0400 EDT m=+0.084322709
* Request to https://api.github.com/graphql
> POST /graphql HTTP/1.1
> Host: api.github.com
> Accept: application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview
> Authorization: token ████████████████████
> Content-Length: 388
> Content-Type: application/json; charset=utf-8
> Graphql-Features: merge_queue
> Time-Zone: America/New_York
> User-Agent: GitHub CLI v2.68.1-23-g1bb599c1e

GraphQL query:
fragment repo on Repository {
    id
    name
    owner { login }
    viewerPermission
    defaultBranchRef {
      name
    }
    isPrivate
  }
  query RepositoryNetwork {
    viewer { login }
    
    repo_000: repository(owner: "andyfeller", name: "cli") {
      ...repo
      parent {
        ...repo
      }
    }
    
  }
GraphQL variables: null

< HTTP/2.0 200 OK
< Access-Control-Allow-Origin: *
< Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
< Content-Security-Policy: default-src 'none'
< Content-Type: application/json; charset=utf-8
< Date: Mon, 14 Apr 2025 15:44:42 GMT
< Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
< Server: github.com
< Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
< Vary: Accept-Encoding, Accept, X-Requested-With
< X-Accepted-Oauth-Scopes: repo
< X-Content-Type-Options: nosniff
< X-Frame-Options: deny
< X-Github-Media-Type: github.v4; param=merge-info-preview.nebula-preview; format=json
< X-Github-Request-Id: C7FB:2D65EB:521533D:A3DF8F7:67FD2D6A
< X-Oauth-Client-Id: 178c6fc778ccc68e1d6a
< X-Oauth-Scopes: admin:org, gist, repo, workflow
< X-Ratelimit-Limit: 5000
< X-Ratelimit-Remaining: 5000
< X-Ratelimit-Reset: 1744645618
< X-Ratelimit-Resource: graphql
< X-Ratelimit-Used: 476
< X-Xss-Protection: 0

{
  "data": {
    "viewer": {
      "login": "andyfeller"
    },
    "repo_000": {
      "id": "R_kgDOMUqY2A",
      "name": "cli",
      "owner": {
        "login": "andyfeller"
      },
      "viewerPermission": "ADMIN",
      "defaultBranchRef": {
        "name": "trunk"
      },
      "isPrivate": false,
      "parent": {
        "id": "MDEwOlJlcG9zaXRvcnkyMTI2MTMwNDk=",
        "name": "cli",
        "owner": {
          "login": "cli"
        },
        "viewerPermission": "ADMIN",
        "defaultBranchRef": {
          "name": "trunk"
        },
        "isPrivate": false
      }
    }
  }
}

* Request took 478.958417ms
X No default remote repository has been set. To learn more about the default repository, run: gh repo set-default --help

please run `gh repo set-default` to select a default remote repository.

cli/context/context.go

Lines 61 to 109 in 408e21e

func (r *ResolvedRemotes) BaseRepo(io *iostreams.IOStreams) (ghrepo.Interface, error) {
if r.baseOverride != nil {
return r.baseOverride, nil
}
if len(r.remotes) == 0 {
return nil, errors.New("no git remotes")
}
// if any of the remotes already has a resolution, respect that
for _, r := range r.remotes {
if r.Resolved == "base" {
return r, nil
} else if r.Resolved != "" {
repo, err := ghrepo.FromFullName(r.Resolved)
if err != nil {
return nil, err
}
return ghrepo.NewWithHost(repo.RepoOwner(), repo.RepoName(), r.RepoHost()), nil
}
}
if !io.CanPrompt() {
// we cannot prompt, so just resort to the 1st remote
return r.remotes[0], nil
}
repos, err := r.NetworkRepos(defaultRemotesForLookup)
if err != nil {
return nil, err
}
if len(repos) == 0 {
return r.remotes[0], nil
} else if len(repos) == 1 {
return repos[0], nil
}
cs := io.ColorScheme()
fmt.Fprintf(io.ErrOut,
"%s No default remote repository has been set. To learn more about the default repository, run: gh repo set-default --help\n",
cs.FailureIcon())
fmt.Fprintln(io.Out)
return nil, errors.New(
"please run `gh repo set-default` to select a default remote repository.")
}

cli/context/context.go

Lines 130 to 162 in 408e21e

// NetworkRepos fetches info about remotes for the network of repos.
// Pass a value of 0 to fetch info on all remotes.
func (r *ResolvedRemotes) NetworkRepos(remotesForLookup int) ([]*api.Repository, error) {
if r.network == nil {
err := resolveNetwork(r, remotesForLookup)
if err != nil {
return nil, err
}
}
var repos []*api.Repository
repoMap := map[string]bool{}
add := func(r *api.Repository) {
fn := ghrepo.FullName(r)
if _, ok := repoMap[fn]; !ok {
repoMap[fn] = true
repos = append(repos, r)
}
}
for _, repo := range r.network.Repositories {
if repo == nil {
continue
}
if repo.Parent != nil {
add(repo.Parent)
}
add(repo)
}
return repos, nil
}

protocol := cfg.GitProtocol(canonicalRepo.Parent.RepoHost()).Value
upstreamURL := ghrepo.FormatRemoteURL(canonicalRepo.Parent, protocol)

Expand All @@ -202,6 +218,12 @@ func cloneRun(opts *CloneOptions) error {
return err
}

connectedToTerminal := opts.IO.IsStdoutTTY()
if connectedToTerminal {
cs := opts.IO.ColorScheme()
fmt.Fprintf(opts.IO.ErrOut, "%s Fetching %s of %s\n", cs.WarningIcon(), cs.Bold(upstreamName), cs.Bold(ghrepo.FullName(canonicalRepo.Parent)))
}

if err := gc.Fetch(ctx, upstreamName, ""); err != nil {
return err
}
Expand All @@ -214,7 +236,6 @@ func cloneRun(opts *CloneOptions) error {
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)))
Expand Down
79 changes: 73 additions & 6 deletions pkg/cmd/repo/clone/clone_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,26 +34,57 @@ func TestNewCmdClone(t *testing.T) {
name: "repo argument",
args: "OWNER/REPO",
wantOpts: CloneOptions{
Repository: "OWNER/REPO",
GitArgs: []string{},
Repository: "OWNER/REPO",
GitArgs: []string{},
UpstreamName: "upstream",
NoUpstream: false,
},
},
{
name: "directory argument",
args: "OWNER/REPO mydir",
wantOpts: CloneOptions{
Repository: "OWNER/REPO",
GitArgs: []string{"mydir"},
Repository: "OWNER/REPO",
GitArgs: []string{"mydir"},
UpstreamName: "upstream",
NoUpstream: false,
},
},
{
name: "git clone arguments",
args: "OWNER/REPO -- --depth 1 --recurse-submodules",
wantOpts: CloneOptions{
Repository: "OWNER/REPO",
GitArgs: []string{"--depth", "1", "--recurse-submodules"},
Repository: "OWNER/REPO",
GitArgs: []string{"--depth", "1", "--recurse-submodules"},
UpstreamName: "upstream",
NoUpstream: false,
},
},
{
name: "upstream-remote-name argument",
args: "OWNER/REPO --upstream-remote-name test",
wantOpts: CloneOptions{
Repository: "OWNER/REPO",
GitArgs: []string{},
UpstreamName: "test",
NoUpstream: false,
},
},
{
name: "no-upstream argument",
args: "OWNER/REPO --no-upstream",
wantOpts: CloneOptions{
Repository: "OWNER/REPO",
GitArgs: []string{},
UpstreamName: "upstream",
NoUpstream: true,
},
},
{
name: "upstream-remote-name and no-upstream arguments",
args: "OWNER/REPO --upstream-remote-name test --no-upstream",
wantErr: "specify only one of `--upstream-remote-name` or `--no-upstream`",
},
{
name: "unknown argument",
args: "OWNER/REPO --depth 1",
Expand Down Expand Up @@ -269,6 +300,42 @@ func Test_RepoClone_hasParent(t *testing.T) {
}
}

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, "")

_, err := runCloneCommand(httpClient, "OWNER/REPO --no-upstream")
if err != nil {
t.Fatalf("error running command `repo clone` with `--no-upstream`: %v", err)
}
}

func Test_RepoClone_hasParent_upstreamRemoteName(t *testing.T) {
reg := &httpmock.Registry{}
defer reg.Verify(t)
Expand Down
Loading