diff --git a/commands/clone.go b/commands/clone.go index 9fc11e7c9..696520a7c 100644 --- a/commands/clone.go +++ b/commands/clone.go @@ -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" ) @@ -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 + }) + } else { + break + } + } + case 1: + targetDir = args.Params[i] } - break } } @@ -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) -} diff --git a/features/clone.feature b/features/clone.feature index b9eeb8c9e..cb6de6671 100644 --- a/features/clone.feature +++ b/features/clone.feature @@ -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: @@ -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: @@ -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: @@ -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 @@ -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: @@ -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` @@ -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: @@ -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: @@ -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: @@ -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: @@ -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: @@ -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: @@ -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: @@ -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 diff --git a/features/support/fakebin/git b/features/support/fakebin/git index 2be1629a2..59ee2ccb3 100755 --- a/features/support/fakebin/git +++ b/features/support/fakebin/git @@ -3,6 +3,23 @@ # executed in testing. It logs commands to "~/.history" so afterwards it can be # asserted that they ran. +global_args=() +while [ $# -gt 0 ]; do + case "$1" in + "--git-dir="* ) + global_args+=("$1") + shift 1 + ;; + -c | -C ) + global_args+=("$1" "$2") + shift 2 + ;; + * ) + break + ;; + esac +done + command="$1" [ "$command" = "config" ] || echo git "$@" >> "$HOME"/.history @@ -12,7 +29,22 @@ case "$command" in echo branch echo commit ;; - "clone" | "fetch" | "pull" | "push" ) + clone ) + numargs="${#@}" + url="${@:$numargs:1}" + dir="$(basename "${url%.git}")" + [ "${url/:/}" != "$url" ] || url="${@:$numargs-1:1}" + origin_name="origin" + [ "$2" != "-o" ] || origin_name="$3" + printf "Cloning into '%s'...\n" "$dir" + mkdir -p "$dir" 2>/dev/null && ( cd "$dir" + "$HUB_SYSTEM_GIT" init --quiet + "$HUB_SYSTEM_GIT" commit --quiet --allow-empty -m 'initial commit' + "$HUB_SYSTEM_GIT" remote add "$origin_name" "$url" + ) + true + ;; + fetch | pull | push ) # don't actually execute these commands exit ;; @@ -21,9 +53,9 @@ case "$command" in if [ "$command $2 $3" = "remote add -f" ]; then subcommand=$2 shift 3 - exec "$HUB_SYSTEM_GIT" $command $subcommand "$@" + exec "$HUB_SYSTEM_GIT" "${global_args[@]}" $command $subcommand "$@" else - exec "$HUB_SYSTEM_GIT" "$@" + exec "$HUB_SYSTEM_GIT" "${global_args[@]}" "$@" fi ;; esac