From 3cbd5b49346378136f5e85601c42882cf0ef4d06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Mon, 10 May 2021 17:09:03 +0200 Subject: [PATCH] Add `repo fork --org` functionality (#3611) Co-authored-by: Gowtham Munukutla --- api/queries_repo.go | 15 +++++- pkg/cmd/pr/create/create.go | 2 +- pkg/cmd/repo/fork/fork.go | 4 +- pkg/cmd/repo/fork/fork_test.go | 84 ++++++++++++++++++++++++++-------- 4 files changed, 81 insertions(+), 24 deletions(-) diff --git a/api/queries_repo.go b/api/queries_repo.go index 90d3949a5..2918ee1d6 100644 --- a/api/queries_repo.go +++ b/api/queries_repo.go @@ -312,9 +312,20 @@ type repositoryV3 struct { } // ForkRepo forks the repository on GitHub and returns the new repository -func ForkRepo(client *Client, repo ghrepo.Interface) (*Repository, error) { +func ForkRepo(client *Client, repo ghrepo.Interface, org string) (*Repository, error) { path := fmt.Sprintf("repos/%s/forks", ghrepo.FullName(repo)) - body := bytes.NewBufferString(`{}`) + + params := map[string]interface{}{} + if org != "" { + params["organization"] = org + } + + body := &bytes.Buffer{} + enc := json.NewEncoder(body) + if err := enc.Encode(params); err != nil { + return nil, err + } + result := repositoryV3{} err := client.REST(repo.RepoHost(), "POST", path, body, &result) if err != nil { diff --git a/pkg/cmd/pr/create/create.go b/pkg/cmd/pr/create/create.go index fc2e70e26..5093d53d1 100644 --- a/pkg/cmd/pr/create/create.go +++ b/pkg/cmd/pr/create/create.go @@ -674,7 +674,7 @@ func handlePush(opts CreateOptions, ctx CreateContext) error { // one by forking the base repository if headRepo == nil && ctx.IsPushEnabled { opts.IO.StartProgressIndicator() - headRepo, err = api.ForkRepo(client, ctx.BaseRepo) + headRepo, err = api.ForkRepo(client, ctx.BaseRepo, "") opts.IO.StopProgressIndicator() if err != nil { return fmt.Errorf("error forking repo: %w", err) diff --git a/pkg/cmd/repo/fork/fork.go b/pkg/cmd/repo/fork/fork.go index 30df5ae67..1bebcd7df 100644 --- a/pkg/cmd/repo/fork/fork.go +++ b/pkg/cmd/repo/fork/fork.go @@ -36,6 +36,7 @@ type ForkOptions struct { PromptClone bool PromptRemote bool RemoteName string + Organization string Rename bool } @@ -110,6 +111,7 @@ Additional 'git clone' flags can be passed in by listing them after '--'.`, cmd.Flags().BoolVar(&opts.Clone, "clone", false, "Clone the fork {true|false}") cmd.Flags().BoolVar(&opts.Remote, "remote", false, "Add remote for fork {true|false}") cmd.Flags().StringVar(&opts.RemoteName, "remote-name", "origin", "Specify a name for a fork's new remote.") + cmd.Flags().StringVar(&opts.Organization, "org", "", "Create the fork in an organization") return cmd } @@ -169,7 +171,7 @@ func forkRun(opts *ForkOptions) error { apiClient := api.NewClientFromHTTP(httpClient) opts.IO.StartProgressIndicator() - forkedRepo, err := api.ForkRepo(apiClient, repoToFork) + forkedRepo, err := api.ForkRepo(apiClient, repoToFork, opts.Organization) opts.IO.StopProgressIndicator() if err != nil { return fmt.Errorf("failed to fork: %w", err) diff --git a/pkg/cmd/repo/fork/fork_test.go b/pkg/cmd/repo/fork/fork_test.go index d1210048b..26f69aa97 100644 --- a/pkg/cmd/repo/fork/fork_test.go +++ b/pkg/cmd/repo/fork/fork_test.go @@ -2,12 +2,14 @@ package fork import ( "bytes" + "io/ioutil" "net/http" "net/url" "regexp" "testing" "time" + "github.com/MakeNowJust/heredoc" "github.com/cli/cli/context" "github.com/cli/cli/git" "github.com/cli/cli/internal/config" @@ -72,8 +74,9 @@ func TestNewCmdFork(t *testing.T) { name: "blank nontty", cli: "", wants: ForkOptions{ - RemoteName: "origin", - Rename: true, + RemoteName: "origin", + Rename: true, + Organization: "", }, }, { @@ -85,6 +88,7 @@ func TestNewCmdFork(t *testing.T) { PromptClone: true, PromptRemote: true, Rename: true, + Organization: "", }, }, { @@ -104,6 +108,16 @@ func TestNewCmdFork(t *testing.T) { Rename: true, }, }, + { + name: "to org", + cli: "--org batmanshome", + wants: ForkOptions{ + RemoteName: "origin", + Remote: false, + Rename: false, + Organization: "batmanshome", + }, + }, } for _, tt := range tests { @@ -141,6 +155,7 @@ func TestNewCmdFork(t *testing.T) { assert.Equal(t, tt.wants.Remote, gotOpts.Remote) assert.Equal(t, tt.wants.PromptRemote, gotOpts.PromptRemote) assert.Equal(t, tt.wants.PromptClone, gotOpts.PromptClone) + assert.Equal(t, tt.wants.Organization, gotOpts.Organization) }) } } @@ -289,6 +304,7 @@ func TestRepoFork_in_parent_tty(t *testing.T) { assert.Equal(t, "✓ Created fork someone/REPO\n✓ Added remote origin\n", output.Stderr()) reg.Verify(t) } + func TestRepoFork_in_parent_nontty(t *testing.T) { defer stubSince(2 * time.Second)() reg := &httpmock.Registry{} @@ -409,37 +425,65 @@ func TestRepoFork_in_parent(t *testing.T) { func TestRepoFork_outside(t *testing.T) { tests := []struct { - name string - args string + name string + args string + postBody string + responseBody string + wantStderr string }{ { - name: "url arg", - args: "--clone=false http://github.com/OWNER/REPO.git", + name: "url arg", + args: "--clone=false http://github.com/OWNER/REPO.git", + postBody: "{}\n", + responseBody: `{"name":"REPO", "owner":{"login":"monalisa"}}`, + wantStderr: heredoc.Doc(` + ✓ Created fork monalisa/REPO + `), }, { - name: "full name arg", - args: "--clone=false OWNER/REPO", + name: "full name arg", + args: "--clone=false OWNER/REPO", + postBody: "{}\n", + responseBody: `{"name":"REPO", "owner":{"login":"monalisa"}}`, + wantStderr: heredoc.Doc(` + ✓ Created fork monalisa/REPO + `), + }, + { + name: "fork to org without clone", + args: "--clone=false OWNER/REPO --org batmanshome", + postBody: "{\"organization\":\"batmanshome\"}\n", + responseBody: `{"name":"REPO", "owner":{"login":"BatmansHome"}}`, + wantStderr: heredoc.Doc(` + ✓ Created fork BatmansHome/REPO + `), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { defer stubSince(2 * time.Second)() + reg := &httpmock.Registry{} - defer reg.StubWithFixturePath(200, "./forkResult.json")() + reg.Register( + httpmock.REST("POST", "repos/OWNER/REPO/forks"), + func(req *http.Request) (*http.Response, error) { + bb, err := ioutil.ReadAll(req.Body) + if err != nil { + return nil, err + } + assert.Equal(t, tt.postBody, string(bb)) + return &http.Response{ + Request: req, + StatusCode: 200, + Body: ioutil.NopCloser(bytes.NewBufferString(tt.responseBody)), + }, nil + }) + httpClient := &http.Client{Transport: reg} - output, err := runCommand(httpClient, nil, true, tt.args) - if err != nil { - t.Errorf("error running command `repo fork`: %v", err) - } - + assert.NoError(t, err) assert.Equal(t, "", output.String()) - - r := regexp.MustCompile(`Created fork.*someone/REPO`) - if !r.MatchString(output.Stderr()) { - t.Errorf("output did not match regexp /%s/\n> output\n%s\n", r, output) - return - } + assert.Equal(t, tt.wantStderr, output.Stderr()) reg.Verify(t) }) }