Add repo fork --org functionality (#3611)

Co-authored-by: Gowtham Munukutla <gowtham.m81197@gmail.com>
This commit is contained in:
Mislav Marohnić 2021-05-10 17:09:03 +02:00 committed by GitHub
parent 026b07d1cf
commit 3cbd5b4934
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 81 additions and 24 deletions

View file

@ -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 {

View file

@ -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)

View file

@ -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)

View file

@ -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)
})
}