Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automatically add the upstream remote when cloning a fork #1977

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
153 changes: 87 additions & 66 deletions commands/clone.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import (
"regexp"
"strings"

"github.com/github/hub/cmd"
"github.com/github/hub/github"
"github.com/github/hub/ui"
"github.com/github/hub/utils"
)

Expand Down Expand Up @@ -72,17 +74,96 @@ func transformCloneArgs(args *Args) {
p.RegisterValue("--shallow-since")
p.RegisterValue("--template")
p.RegisterValue("--upload-pack", "-u")
p.RegisterBool("--quiet", "-q")
}
p.Parse(args.Params)

upstreamName := "upstream"
originName := p.Value("--origin")
quiet := p.Bool("--quiet")
targetDir := ""

nameWithOwnerRegexp := regexp.MustCompile(NameWithOwnerRe)
for _, i := range p.PositionalIndices {
a := args.Params[i]
if nameWithOwnerRegexp.MatchString(a) && !isCloneable(a) {
url := getCloneUrl(a, isSSH, args.Command != "submodule")
args.ReplaceParam(i, url)
for n, i := range p.PositionalIndices {
switch n {
case 0:
repo := args.Params[i]
if nameWithOwnerRegexp.MatchString(repo) && !isCloneable(repo) {
name := repo
owner := ""
if strings.Contains(name, "/") {
split := strings.SplitN(name, "/", 2)
owner = split[0]
name = split[1]
}

config := github.CurrentConfig()
host, err := config.DefaultHost()
if err != nil {
utils.Check(github.FormatError("cloning repository", err))
}
if owner == "" {
owner = host.User
}

expectWiki := strings.HasSuffix(name, ".wiki")
if expectWiki {
name = strings.TrimSuffix(name, ".wiki")
}

project := github.NewProject(owner, name, host.Host)
gh := github.NewClient(project.Host)
repo, err := gh.Repository(project)
if err != nil {
if strings.Contains(err.Error(), "HTTP 404") {
err = fmt.Errorf("Error: repository %s/%s doesn't exist", project.Owner, project.Name)
}
utils.Check(err)
}

owner = repo.Owner.Login
name = repo.Name
if expectWiki {
if !repo.HasWiki {
utils.Check(fmt.Errorf("Error: %s/%s doesn't have a wiki", owner, name))
} else {
name = name + ".wiki"
}
}

if !isSSH &&
args.Command != "submodule" &&
!github.IsHttpsProtocol() {
isSSH = repo.Private || repo.Permissions.Push
}

targetDir = name
url := project.GitURL(name, owner, isSSH)
args.ReplaceParam(i, url)

if repo.Parent != nil && args.Command == "clone" && originName != upstreamName {
args.AfterFn(func() error {
upstreamUrl := project.GitURL(repo.Parent.Name, repo.Parent.Owner.Login, repo.Parent.Private)
addRemote := cmd.New("git")
addRemote.WithArgs("-C", targetDir)
addRemote.WithArgs("remote", "add", "-f", upstreamName, upstreamUrl)
if !quiet {
ui.Errorf("Adding remote '%s' for the '%s/%s' repo\n",
upstreamName, repo.Parent.Owner.Login, repo.Parent.Name)
}
output, err := addRemote.CombinedOutput()
if err != nil {
ui.Errorln(output)
}
return err
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe it should do a git fetch for the added remote at this point? The normal case for doing a git clone fetches the full history for the cloned repo; since we're basically cloning two repos at once here, maybe we should do fetch so we have both repos' history.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Notice that we're doing git remote add -f. The extra flag automatically initiates a fetch.

Copy link
Contributor

Choose a reason for hiding this comment

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

Gotcha. Missed that.

})
} else {
break
}
}
case 1:
targetDir = args.Params[i]
}
break
}
}

Expand All @@ -94,63 +175,3 @@ func parseClonePrivateFlag(args *Args) bool {

return false
}

func getCloneUrl(nameWithOwner string, isSSH, allowSSH bool) string {
name := nameWithOwner
owner := ""
if strings.Contains(name, "/") {
split := strings.SplitN(name, "/", 2)
owner = split[0]
name = split[1]
}

var host *github.Host
if owner == "" {
config := github.CurrentConfig()
h, err := config.DefaultHost()
if err != nil {
utils.Check(github.FormatError("cloning repository", err))
}

host = h
owner = host.User
}

var hostStr string
if host != nil {
hostStr = host.Host
}

expectWiki := strings.HasSuffix(name, ".wiki")
if expectWiki {
name = strings.TrimSuffix(name, ".wiki")
}

project := github.NewProject(owner, name, hostStr)
gh := github.NewClient(project.Host)
repo, err := gh.Repository(project)
if err != nil {
if strings.Contains(err.Error(), "HTTP 404") {
err = fmt.Errorf("Error: repository %s/%s doesn't exist", project.Owner, project.Name)
}
utils.Check(err)
}

owner = repo.Owner.Login
name = repo.Name
if expectWiki {
if !repo.HasWiki {
utils.Check(fmt.Errorf("Error: %s/%s doesn't have a wiki", owner, name))
} else {
name = name + ".wiki"
}
}

if !isSSH &&
allowSSH &&
!github.IsHttpsProtocol() {
isSSH = repo.Private || repo.Permissions.Push
}

return project.GitURL(name, owner, isSSH)
}
77 changes: 55 additions & 22 deletions features/clone.feature
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,61 @@ Feature: hub clone
"""
When I successfully run `hub clone rtomayko/ronn`
Then it should clone "git://github.com/rtomayko/ronn.git"
And there should be no output

Scenario: Clone a fork
Given the GitHub API server:
"""
get('/repos/defunkt/ronn1') {
json :private => false,
:name => 'ronn1', :owner => { :login => 'defunkt' },
:parent => {
:private => true,
:name => 'ronn', :owner => { :login => 'rtomayko' }
},
:permissions => { :push => false }
}
"""
When I successfully run `hub clone defunkt/ronn1`
Then the stderr should contain "Adding remote 'upstream' for the 'rtomayko/ronn' repo"
When I cd to "ronn1"
Then the url for "origin" should be "git://github.com/defunkt/ronn1.git"
Then the url for "upstream" should be "git@github.com:rtomayko/ronn.git"

Scenario: Clone a fork into a custom directory
Given the GitHub API server:
"""
get('/repos/defunkt/ronn1') {
json :private => false,
:name => 'ronn1', :owner => { :login => 'defunkt' },
:parent => {
:private => true,
:name => 'ronn', :owner => { :login => 'rtomayko' }
},
:permissions => { :push => false }
}
"""
When I successfully run `hub clone defunkt/ronn1 my-fork`
And I cd to "my-fork"
Then the url for "origin" should be "git://github.com/defunkt/ronn1.git"
Then the url for "upstream" should be "git@github.com:rtomayko/ronn.git"

Scenario: Clone a fork as "upstream"
Given the GitHub API server:
"""
get('/repos/defunkt/ronn1') {
json :private => false,
:name => 'ronn1', :owner => { :login => 'defunkt' },
:parent => {
:private => true,
:name => 'ronn', :owner => { :login => 'rtomayko' }
},
:permissions => { :push => false }
}
"""
When I successfully run `hub clone -o upstream defunkt/ronn1`
And I cd to "ronn1"
Then the url for "upstream" should be "git://github.com/defunkt/ronn1.git"
And there should be no "origin" remote

Scenario: Clone a public repo with period in name
Given the GitHub API server:
Expand All @@ -26,7 +80,6 @@ Feature: hub clone
"""
When I successfully run `hub clone hookio/hook.js`
Then it should clone "git://github.com/hookio/hook.js.git"
And there should be no output

Scenario: Clone a public repo that starts with a period
Given the GitHub API server:
Expand All @@ -39,7 +92,6 @@ Feature: hub clone
"""
When I successfully run `hub clone zhuangya/.vim`
Then it should clone "git://github.com/zhuangya/.vim.git"
And there should be no output

Scenario: Clone a repo even if same-named directory exists
Given the GitHub API server:
Expand All @@ -53,7 +105,6 @@ Feature: hub clone
And a directory named "rtomayko/ronn"
When I successfully run `hub clone rtomayko/ronn`
Then it should clone "git://github.com/rtomayko/ronn.git"
And there should be no output

Scenario: Clone a public repo with HTTPS
Given HTTPS is preferred
Expand All @@ -67,7 +118,6 @@ Feature: hub clone
"""
When I successfully run `hub clone rtomayko/ronn`
Then it should clone "https://github.com/rtomayko/ronn.git"
And there should be no output

Scenario: Clone command aliased
Given the GitHub API server:
Expand All @@ -81,7 +131,6 @@ Feature: hub clone
When I successfully run `git config --global alias.c "clone --bare"`
And I successfully run `hub c rtomayko/ronn`
Then "git clone --bare git://github.com/rtomayko/ronn.git" should be run
And there should be no output

Scenario: Unchanged public clone
When I successfully run `hub clone git://github.com/rtomayko/ronn.git`
Expand All @@ -90,39 +139,32 @@ Feature: hub clone
Scenario: Unchanged public clone with path
When I successfully run `hub clone git://github.com/rtomayko/ronn.git ronnie`
Then the git command should be unchanged
And there should be no output

Scenario: Unchanged private clone
When I successfully run `hub clone git@github.com:rtomayko/ronn.git`
Then the git command should be unchanged
And there should be no output

Scenario: Unchanged clone with complex arguments
When I successfully run `hub clone --template=one/two git://github.com/defunkt/resque.git --origin master resquetastic`
Then the git command should be unchanged
And there should be no output

Scenario: Unchanged local clone
When I successfully run `hub clone ./dotfiles`
Then the git command should be unchanged
And there should be no output

Scenario: Unchanged local clone with destination
Given a directory named ".git"
When I successfully run `hub clone -l . ../copy`
Then the git command should be unchanged
And there should be no output

Scenario: Unchanged local clone from bare repo
Given a bare git repo in "rtomayko/ronn"
When I successfully run `hub clone rtomayko/ronn`
Then the git command should be unchanged
And there should be no output

Scenario: Unchanged clone with host alias
When I successfully run `hub clone shortcut:git/repo.git`
Then the git command should be unchanged
And there should be no output

Scenario: Preview cloning a private repo
Given the GitHub API server:
Expand All @@ -148,7 +190,6 @@ Feature: hub clone
"""
When I successfully run `hub clone -p rtomayko/ronn`
Then it should clone "git@github.com:rtomayko/ronn.git"
And there should be no output

Scenario: Clone my repo
Given the GitHub API server:
Expand All @@ -161,7 +202,6 @@ Feature: hub clone
"""
When I successfully run `hub clone dotfiles`
Then it should clone "git@github.com:mislav/dotfiles.git"
And there should be no output

Scenario: Clone my repo that doesn't exist
Given the GitHub API server:
Expand All @@ -185,7 +225,6 @@ Feature: hub clone
"""
When I successfully run `hub clone --bare -o master dotfiles`
Then "git clone --bare -o master git@github.com:mislav/dotfiles.git" should be run
And there should be no output

Scenario: Clone repo to which I have push access to
Given the GitHub API server:
Expand All @@ -198,7 +237,6 @@ Feature: hub clone
"""
When I successfully run `hub clone sstephenson/rbenv`
Then "git clone git@github.com:sstephenson/rbenv.git" should be run
And there should be no output

Scenario: Preview cloning a repo I have push access to
Given the GitHub API server:
Expand Down Expand Up @@ -226,19 +264,16 @@ Feature: hub clone
"""
When I successfully run `hub clone myorg/myrepo`
Then it should clone "git@git.my.org:myorg/myrepo.git"
And there should be no output

Scenario: Clone from existing directory is a local clone
Given a directory named "dotfiles/.git"
When I successfully run `hub clone dotfiles`
Then the git command should be unchanged
And there should be no output

Scenario: Clone from git bundle is a local clone
Given a git bundle named "my-bundle"
When I successfully run `hub clone my-bundle`
Then the git command should be unchanged
And there should be no output

Scenario: Clone a wiki
Given the GitHub API server:
Expand All @@ -252,7 +287,6 @@ Feature: hub clone
"""
When I successfully run `hub clone rtomayko/ronn.wiki`
Then it should clone "git://github.com/RTomayko/ronin.wiki.git"
And there should be no output

Scenario: Clone a nonexisting wiki
Given the GitHub API server:
Expand Down Expand Up @@ -285,4 +319,3 @@ Feature: hub clone
"""
When I successfully run `hub clone rtomayko/ronn`
Then it should clone "git://github.com/RTomayko/ronin.git"
And there should be no output
Loading