Skip to content

Commit 8460609

Browse files
committed
repo clone: automatically set up "upstream" remote for forks
1 parent 7555a4c commit 8460609

File tree

5 files changed

+120
-12
lines changed

5 files changed

+120
-12
lines changed

api/queries_repo.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package api
22

33
import (
44
"bytes"
5+
"context"
56
"encoding/base64"
67
"encoding/json"
78
"errors"
@@ -12,6 +13,7 @@ import (
1213

1314
"github.com/cli/cli/internal/ghrepo"
1415
"github.com/cli/cli/utils"
16+
"github.com/shurcooL/githubv4"
1517
)
1618

1719
// Repository contains information about a GitHub repo
@@ -93,6 +95,37 @@ func GitHubRepo(client *Client, repo ghrepo.Interface) (*Repository, error) {
9395
return &result.Repository, nil
9496
}
9597

98+
// RepoParent finds out the parent repository of a fork
99+
func RepoParent(client *Client, repo ghrepo.Interface) (ghrepo.Interface, error) {
100+
var query struct {
101+
Repository struct {
102+
Parent *struct {
103+
Name string
104+
Owner struct {
105+
Login string
106+
}
107+
}
108+
} `graphql:"repository(owner: $owner, name: $name)"`
109+
}
110+
111+
variables := map[string]interface{}{
112+
"owner": githubv4.String(repo.RepoOwner()),
113+
"name": githubv4.String(repo.RepoName()),
114+
}
115+
116+
v4 := githubv4.NewClient(client.http)
117+
err := v4.Query(context.Background(), &query, variables)
118+
if err != nil {
119+
return nil, err
120+
}
121+
if query.Repository.Parent == nil {
122+
return nil, nil
123+
}
124+
125+
parent := ghrepo.New(query.Repository.Parent.Owner.Login, query.Repository.Parent.Name)
126+
return parent, nil
127+
}
128+
96129
// RepoNetworkResult describes the relationship between related repositories
97130
type RepoNetworkResult struct {
98131
ViewerLogin string

command/repo.go

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,28 @@ func repoClone(cmd *cobra.Command, args []string) error {
9393
cloneURL = fmt.Sprintf("https://github.com/%s.git", cloneURL)
9494
}
9595

96+
var repo ghrepo.Interface
97+
var parentRepo ghrepo.Interface
98+
99+
// TODO: consider caching and reusing `git.ParseSSHConfig().Translator()`
100+
// here to handle hostname aliases in SSH remotes
101+
if u, err := git.ParseURL(cloneURL); err == nil {
102+
repo, _ = ghrepo.FromURL(u)
103+
}
104+
105+
if repo != nil {
106+
ctx := contextForCommand(cmd)
107+
apiClient, err := apiClientForContext(ctx)
108+
if err != nil {
109+
return err
110+
}
111+
112+
parentRepo, err = api.RepoParent(apiClient, repo)
113+
if err != nil {
114+
return err
115+
}
116+
}
117+
96118
cloneArgs := []string{"clone"}
97119
cloneArgs = append(cloneArgs, args[1:]...)
98120
cloneArgs = append(cloneArgs, cloneURL)
@@ -101,7 +123,26 @@ func repoClone(cmd *cobra.Command, args []string) error {
101123
cloneCmd.Stdin = os.Stdin
102124
cloneCmd.Stdout = os.Stdout
103125
cloneCmd.Stderr = os.Stderr
104-
return run.PrepareCmd(cloneCmd).Run()
126+
err := run.PrepareCmd(cloneCmd).Run()
127+
if err != nil {
128+
return err
129+
}
130+
131+
if parentRepo != nil {
132+
// TODO: support SSH remote URLs
133+
upstreamURL := fmt.Sprintf("https://github.com/%s.git", ghrepo.FullName(parentRepo))
134+
cloneDir := path.Base(strings.TrimSuffix(cloneURL, ".git"))
135+
136+
cloneCmd := git.GitCommand("-C", cloneDir, "remote", "add", "upstream", upstreamURL)
137+
cloneCmd.Stdout = os.Stdout
138+
cloneCmd.Stderr = os.Stderr
139+
err := run.PrepareCmd(cloneCmd).Run()
140+
if err != nil {
141+
return err
142+
}
143+
}
144+
145+
return nil
105146
}
106147

107148
func repoCreate(cmd *cobra.Command, args []string) error {

command/repo_test.go

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -363,12 +363,17 @@ func TestRepoClone(t *testing.T) {
363363
}
364364
for _, tt := range tests {
365365
t.Run(tt.name, func(t *testing.T) {
366-
var seenCmd *exec.Cmd
367-
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
368-
seenCmd = cmd
369-
return &test.OutputStub{}
370-
})
371-
defer restoreCmd()
366+
http := initFakeHTTP()
367+
http.StubResponse(200, bytes.NewBufferString(`
368+
{ "data": { "repository": {
369+
"parent": null
370+
} } }
371+
`))
372+
373+
cs, restore := test.InitCmdStubber()
374+
defer restore()
375+
376+
cs.Stub("") // git clone
372377

373378
output, err := RunCommand(repoCloneCmd, tt.args)
374379
if err != nil {
@@ -377,15 +382,38 @@ func TestRepoClone(t *testing.T) {
377382

378383
eq(t, output.String(), "")
379384
eq(t, output.Stderr(), "")
380-
381-
if seenCmd == nil {
382-
t.Fatal("expected a command to run")
383-
}
384-
eq(t, strings.Join(seenCmd.Args, " "), tt.want)
385+
eq(t, cs.Count, 1)
386+
eq(t, strings.Join(cs.Calls[0].Args, " "), tt.want)
385387
})
386388
}
387389
}
388390

391+
func TestRepoClone_hasParent(t *testing.T) {
392+
http := initFakeHTTP()
393+
http.StubResponse(200, bytes.NewBufferString(`
394+
{ "data": { "repository": {
395+
"parent": {
396+
"owner": {"login": "hubot"},
397+
"name": "ORIG"
398+
}
399+
} } }
400+
`))
401+
402+
cs, restore := test.InitCmdStubber()
403+
defer restore()
404+
405+
cs.Stub("") // git clone
406+
cs.Stub("") // git remote add
407+
408+
_, err := RunCommand(repoCloneCmd, "repo clone OWNER/REPO")
409+
if err != nil {
410+
t.Fatalf("error running command `repo clone`: %v", err)
411+
}
412+
413+
eq(t, cs.Count, 2)
414+
eq(t, strings.Join(cs.Calls[1].Args, " "), "git -C REPO remote add upstream https://github.com/hubot/ORIG.git")
415+
}
416+
389417
func TestRepoCreate(t *testing.T) {
390418
ctx := context.NewBlank()
391419
ctx.SetBranch("master")

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ require (
1616
github.com/mattn/go-runewidth v0.0.8 // indirect
1717
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b
1818
github.com/mitchellh/go-homedir v1.1.0
19+
github.com/shurcooL/githubv4 v0.0.0-20191127044304-8f68eb5628d0
20+
github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f // indirect
1921
github.com/spf13/cobra v0.0.6
2022
github.com/spf13/pflag v1.0.5
2123
github.com/stretchr/testify v1.4.0 // indirect

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,10 @@ github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0
145145
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
146146
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
147147
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
148+
github.com/shurcooL/githubv4 v0.0.0-20191127044304-8f68eb5628d0 h1:T9uus1QvcPgeLShS30YOnnzk3r9Vvygp45muhlrufgY=
149+
github.com/shurcooL/githubv4 v0.0.0-20191127044304-8f68eb5628d0/go.mod h1:hAF0iLZy4td2EX+/8Tw+4nodhlMrwN3HupfaXj3zkGo=
150+
github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f h1:tygelZueB1EtXkPI6mQ4o9DQ0+FKW41hTbunoXZCTqk=
151+
github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f/go.mod h1:AuYgA5Kyo4c7HfUmvRGs/6rGlMMV/6B1bVnB9JxJEEg=
148152
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
149153
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
150154
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=

0 commit comments

Comments
 (0)