diff --git a/api/client.go b/api/client.go index bf6827d34..3c95f6250 100644 --- a/api/client.go +++ b/api/client.go @@ -326,6 +326,7 @@ func (c Client) REST(hostname string, method string, p string, body io.Reader, d if err != nil { return err } + err = json.Unmarshal(b, &data) if err != nil { return err diff --git a/api/queries_repo.go b/api/queries_repo.go index 1c8a210fb..65f1027a8 100644 --- a/api/queries_repo.go +++ b/api/queries_repo.go @@ -5,6 +5,7 @@ import ( "context" "encoding/json" "fmt" + "github.com/cli/cli/v2/internal/ghinstance" "io" "net/http" "sort" @@ -524,6 +525,37 @@ func ForkRepo(client *Client, repo ghrepo.Interface, org string) (*Repository, e }, nil } +// RenameRepo renames the repository on GitHub and returns the renamed repository +func RenameRepo(client *Client, repo ghrepo.Interface, newRepoName string) (*Repository, error) { + input := map[string]string{"name": newRepoName} + body := &bytes.Buffer{} + enc := json.NewEncoder(body) + if err := enc.Encode(input); err != nil { + return nil, err + } + + path := fmt.Sprintf("%srepos/%s", + ghinstance.RESTPrefix(repo.RepoHost()), + ghrepo.FullName(repo)) + + result := repositoryV3{} + err := client.REST(repo.RepoHost(), "PATCH", path, body, &result) + if err != nil { + return nil, err + } + + return &Repository{ + ID: result.NodeID, + Name: result.Name, + CreatedAt: result.CreatedAt, + Owner: RepositoryOwner{ + Login: result.Owner.Login, + }, + ViewerPermission: "WRITE", + hostname: repo.RepoHost(), + }, nil +} + func LastCommit(client *Client, repo ghrepo.Interface) (*Commit, error) { var responseData struct { Repository struct { diff --git a/pkg/cmd/repo/fork/fork.go b/pkg/cmd/repo/fork/fork.go index 98fc79e2f..952071fb5 100644 --- a/pkg/cmd/repo/fork/fork.go +++ b/pkg/cmd/repo/fork/fork.go @@ -39,6 +39,7 @@ type ForkOptions struct { PromptRemote bool RemoteName string Organization string + ForkName string Rename bool } @@ -115,6 +116,7 @@ Additional 'git clone' flags can be passed in by listing them after '--'.`, cmd.Flags().BoolVar(&opts.Remote, "remote", false, "Add remote for fork {true|false}") cmd.Flags().StringVar(&opts.RemoteName, "remote-name", defaultRemoteName, "Specify a name for a fork's new remote.") cmd.Flags().StringVar(&opts.Organization, "org", "", "Create the fork in an organization") + cmd.Flags().StringVar(&opts.ForkName, "fork-name", "", "Specify a name for the forked repo") return cmd } @@ -201,6 +203,17 @@ func forkRun(opts *ForkOptions) error { } } + // Rename the forked repo if ForkName is specified in opts. + if opts.ForkName != "" { + forkedRepo, err = api.RenameRepo(apiClient, forkedRepo, opts.ForkName) + if err != nil { + return fmt.Errorf("could not rename fork: %w", err) + } + if connectedToTerminal { + fmt.Fprintf(stderr, "%s Renamed fork to %s\n", cs.SuccessIconWithColor(cs.Green), cs.Bold(ghrepo.FullName(forkedRepo))) + } + } + if (inParent && (!opts.Remote && !opts.PromptRemote)) || (!inParent && (!opts.Clone && !opts.PromptClone)) { return nil } diff --git a/pkg/cmd/repo/fork/fork_test.go b/pkg/cmd/repo/fork/fork_test.go index 5fb458b93..ac248669d 100644 --- a/pkg/cmd/repo/fork/fork_test.go +++ b/pkg/cmd/repo/fork/fork_test.go @@ -132,6 +132,16 @@ func TestNewCmdFork(t *testing.T) { wantErr: true, errMsg: "unknown flag: --depth\nSeparate git clone flags with '--'.", }, + { + name: "with fork name", + cli: "--fork-name new-fork", + wants: ForkOptions{ + Remote: false, + RemoteName: "origin", + ForkName: "new-fork", + Rename: false, + }, + }, } for _, tt := range tests { @@ -534,6 +544,78 @@ func TestRepoFork(t *testing.T) { cs.Register(`git -C REPO remote add -f upstream https://github\.com/OWNER/REPO\.git`, 0, "") }, }, + { + name: "non tty repo arg with fork-name", + opts: &ForkOptions{ + Repository: "someone/REPO", + Clone: false, + ForkName: "NEW_REPO", + }, + tty: false, + httpStubs: func(reg *httpmock.Registry) { + forkResult := `{ + "node_id": "123", + "name": "REPO", + "clone_url": "https://github.com/OWNER/REPO.git", + "created_at": "2011-01-26T19:01:12Z", + "owner": { + "login": "OWNER" + } + }` + renameResult := `{ + "node_id": "1234", + "name": "NEW_REPO", + "clone_url": "https://github.com/OWNER/NEW_REPO.git", + "created_at": "2012-01-26T19:01:12Z", + "owner": { + "login": "OWNER" + } + }` + reg.Register( + httpmock.REST("POST", "repos/someone/REPO/forks"), + httpmock.StringResponse(forkResult)) + reg.Register( + httpmock.REST("PATCH", "repos/OWNER/REPO"), + httpmock.StringResponse(renameResult)) + }, + wantErrOut: "", + }, + { + name: "tty repo arg with fork-name", + opts: &ForkOptions{ + Repository: "someone/REPO", + Clone: false, + ForkName: "NEW_REPO", + }, + tty: true, + httpStubs: func(reg *httpmock.Registry) { + forkResult := `{ + "node_id": "123", + "name": "REPO", + "clone_url": "https://github.com/OWNER/REPO.git", + "created_at": "2011-01-26T19:01:12Z", + "owner": { + "login": "OWNER" + } + }` + renameResult := `{ + "node_id": "1234", + "name": "NEW_REPO", + "clone_url": "https://github.com/OWNER/NEW_REPO.git", + "created_at": "2012-01-26T19:01:12Z", + "owner": { + "login": "OWNER" + } + }` + reg.Register( + httpmock.REST("POST", "repos/someone/REPO/forks"), + httpmock.StringResponse(forkResult)) + reg.Register( + httpmock.REST("PATCH", "repos/OWNER/REPO"), + httpmock.StringResponse(renameResult)) + }, + wantErrOut: "āœ“ Created fork OWNER/REPO\nāœ“ Renamed fork to OWNER/NEW_REPO\n", + }, } for _, tt := range tests { diff --git a/pkg/cmd/repo/rename/http.go b/pkg/cmd/repo/rename/http.go deleted file mode 100644 index 1e5d73477..000000000 --- a/pkg/cmd/repo/rename/http.go +++ /dev/null @@ -1,61 +0,0 @@ -package rename - -import ( - "bytes" - "encoding/json" - "fmt" - "io/ioutil" - "net/http" - - "github.com/cli/cli/v2/api" - "github.com/cli/cli/v2/internal/ghinstance" - "github.com/cli/cli/v2/internal/ghrepo" -) - -func apiRename(client *http.Client, repo ghrepo.Interface, newRepoName string) (ghrepo.Interface, error) { - input := map[string]string{"name": newRepoName} - body, err := json.Marshal(input) - if err != nil { - return nil, err - } - - path := fmt.Sprintf("%srepos/%s", - ghinstance.RESTPrefix(repo.RepoHost()), - ghrepo.FullName(repo)) - - request, err := http.NewRequest("PATCH", path, bytes.NewBuffer(body)) - if err != nil { - return nil, err - } - - request.Header.Set("Content-Type", "application/json; charset=utf-8") - - resp, err := client.Do(request) - if err != nil { - return nil, err - } - - if resp.StatusCode > 299 { - return nil, api.HandleHTTPError(resp) - } - - defer resp.Body.Close() - b, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, err - } - - result := struct { - Name string - Owner struct { - Login string - } - }{} - if err := json.Unmarshal(b, &result); err != nil { - return nil, fmt.Errorf("error unmarshaling response: %w", err) - } - - newRepo := ghrepo.NewWithHost(result.Owner.Login, result.Name, repo.RepoHost()) - - return newRepo, nil -} diff --git a/pkg/cmd/repo/rename/rename.go b/pkg/cmd/repo/rename/rename.go index 8bbcc4f9a..1921aca73 100644 --- a/pkg/cmd/repo/rename/rename.go +++ b/pkg/cmd/repo/rename/rename.go @@ -2,6 +2,7 @@ package rename import ( "fmt" + "github.com/cli/cli/v2/api" "net/http" "github.com/AlecAivazis/survey/v2" @@ -114,11 +115,15 @@ func renameRun(opts *RenameOptions) error { } } - newRepo, err := apiRename(httpClient, currRepo, newRepoName) + apiClient := api.NewClientFromHTTP(httpClient) + + newRepo, err := api.RenameRepo(apiClient, currRepo, newRepoName) if err != nil { return err } + renamedRepo := ghrepo.New(newRepo.Owner.Login, newRepo.Name) + cs := opts.IO.ColorScheme() if opts.IO.IsStdoutTTY() { fmt.Fprintf(opts.IO.Out, "%s Renamed repository %s\n", cs.SuccessIcon(), ghrepo.FullName(newRepo)) @@ -128,7 +133,7 @@ func renameRun(opts *RenameOptions) error { return nil } - remote, err := updateRemote(currRepo, newRepo, opts) + remote, err := updateRemote(currRepo, renamedRepo, opts) if err != nil { fmt.Fprintf(opts.IO.ErrOut, "%s Warning: unable to update remote %q: %v\n", cs.WarningIcon(), remote.Name, err) } else if opts.IO.IsStdoutTTY() { diff --git a/pkg/cmd/repo/rename/rename_test.go b/pkg/cmd/repo/rename/rename_test.go index a886befaa..42d20eee4 100644 --- a/pkg/cmd/repo/rename/rename_test.go +++ b/pkg/cmd/repo/rename/rename_test.go @@ -123,7 +123,7 @@ func TestRenameRun(t *testing.T) { httpStubs: func(reg *httpmock.Registry) { reg.Register( httpmock.REST("PATCH", "repos/OWNER/REPO"), - httpmock.StatusStringResponse(204, `{"name":"NEW_REPO","owner":{"login":"OWNER"}}`)) + httpmock.StatusStringResponse(200, `{"name":"NEW_REPO","owner":{"login":"OWNER"}}`)) }, execStubs: func(cs *run.CommandStubber) { cs.Register(`git remote set-url origin https://github.com/OWNER/NEW_REPO.git`, 0, "") @@ -143,7 +143,7 @@ func TestRenameRun(t *testing.T) { httpStubs: func(reg *httpmock.Registry) { reg.Register( httpmock.REST("PATCH", "repos/OWNER/REPO"), - httpmock.StatusStringResponse(204, `{"name":"NEW_REPO","owner":{"login":"OWNER"}}`)) + httpmock.StatusStringResponse(200, `{"name":"NEW_REPO","owner":{"login":"OWNER"}}`)) }, tty: true, }, @@ -156,7 +156,7 @@ func TestRenameRun(t *testing.T) { httpStubs: func(reg *httpmock.Registry) { reg.Register( httpmock.REST("PATCH", "repos/OWNER/REPO"), - httpmock.StatusStringResponse(204, `{"name":"NEW_REPO","owner":{"login":"OWNER"}}`)) + httpmock.StatusStringResponse(200, `{"name":"NEW_REPO","owner":{"login":"OWNER"}}`)) }, execStubs: func(cs *run.CommandStubber) { cs.Register(`git remote set-url origin https://github.com/OWNER/NEW_REPO.git`, 0, "") @@ -171,7 +171,7 @@ func TestRenameRun(t *testing.T) { httpStubs: func(reg *httpmock.Registry) { reg.Register( httpmock.REST("PATCH", "repos/OWNER/REPO"), - httpmock.StatusStringResponse(204, `{"name":"NEW_REPO","owner":{"login":"OWNER"}}`)) + httpmock.StatusStringResponse(200, `{"name":"NEW_REPO","owner":{"login":"OWNER"}}`)) }, execStubs: func(cs *run.CommandStubber) { cs.Register(`git remote set-url origin https://github.com/OWNER/NEW_REPO.git`, 0, "") @@ -192,7 +192,7 @@ func TestRenameRun(t *testing.T) { httpStubs: func(reg *httpmock.Registry) { reg.Register( httpmock.REST("PATCH", "repos/OWNER/REPO"), - httpmock.StatusStringResponse(204, `{"name":"NEW_REPO","owner":{"login":"OWNER"}}`)) + httpmock.StatusStringResponse(200, `{"name":"NEW_REPO","owner":{"login":"OWNER"}}`)) }, execStubs: func(cs *run.CommandStubber) { cs.Register(`git remote set-url origin https://github.com/OWNER/NEW_REPO.git`, 0, "")