Skip to content

Commit 67d1a90

Browse files
authored
Merge pull request cli#4450 from cli/repo-rename
gh repo rename
2 parents dd0edc8 + d410580 commit 67d1a90

File tree

5 files changed

+500
-0
lines changed

5 files changed

+500
-0
lines changed

git/remote.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,14 @@ func AddRemote(name, u string) (*Remote, error) {
140140
}, nil
141141
}
142142

143+
func UpdateRemoteURL(name, u string) error {
144+
addCmd, err := GitCommand("remote", "set-url", name, u)
145+
if err != nil {
146+
return err
147+
}
148+
return run.PrepareCmd(addCmd).Run()
149+
}
150+
143151
func SetRemoteResolution(name, resolution string) error {
144152
addCmd, err := GitCommand("config", "--add", fmt.Sprintf("remote.%s.gh-resolved", name), resolution)
145153
if err != nil {

pkg/cmd/repo/rename/http.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package rename
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"fmt"
7+
"io/ioutil"
8+
"net/http"
9+
10+
"github.com/cli/cli/v2/api"
11+
"github.com/cli/cli/v2/internal/ghinstance"
12+
"github.com/cli/cli/v2/internal/ghrepo"
13+
)
14+
15+
func apiRename(client *http.Client, repo ghrepo.Interface, newRepoName string) (ghrepo.Interface, error) {
16+
input := map[string]string{"name": newRepoName}
17+
body, err := json.Marshal(input)
18+
if err != nil {
19+
return nil, err
20+
}
21+
22+
path := fmt.Sprintf("%srepos/%s",
23+
ghinstance.RESTPrefix(repo.RepoHost()),
24+
ghrepo.FullName(repo))
25+
26+
request, err := http.NewRequest("PATCH", path, bytes.NewBuffer(body))
27+
if err != nil {
28+
return nil, err
29+
}
30+
31+
request.Header.Set("Content-Type", "application/json; charset=utf-8")
32+
33+
resp, err := client.Do(request)
34+
if err != nil {
35+
return nil, err
36+
}
37+
38+
if resp.StatusCode > 299 {
39+
return nil, api.HandleHTTPError(resp)
40+
}
41+
42+
defer resp.Body.Close()
43+
b, err := ioutil.ReadAll(resp.Body)
44+
if err != nil {
45+
return nil, err
46+
}
47+
48+
result := struct {
49+
Name string
50+
Owner struct {
51+
Login string
52+
}
53+
}{}
54+
if err := json.Unmarshal(b, &result); err != nil {
55+
return nil, fmt.Errorf("error unmarshaling response: %w", err)
56+
}
57+
58+
newRepo := ghrepo.NewWithHost(result.Owner.Login, result.Name, repo.RepoHost())
59+
60+
return newRepo, nil
61+
}

pkg/cmd/repo/rename/rename.go

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
package rename
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
7+
"github.com/AlecAivazis/survey/v2"
8+
"github.com/MakeNowJust/heredoc"
9+
"github.com/cli/cli/v2/context"
10+
"github.com/cli/cli/v2/git"
11+
"github.com/cli/cli/v2/internal/config"
12+
"github.com/cli/cli/v2/internal/ghrepo"
13+
"github.com/cli/cli/v2/pkg/cmdutil"
14+
"github.com/cli/cli/v2/pkg/iostreams"
15+
"github.com/cli/cli/v2/pkg/prompt"
16+
"github.com/spf13/cobra"
17+
)
18+
19+
type RenameOptions struct {
20+
HttpClient func() (*http.Client, error)
21+
IO *iostreams.IOStreams
22+
Config func() (config.Config, error)
23+
BaseRepo func() (ghrepo.Interface, error)
24+
Remotes func() (context.Remotes, error)
25+
DoConfirm bool
26+
HasRepoOverride bool
27+
newRepoSelector string
28+
}
29+
30+
func NewCmdRename(f *cmdutil.Factory, runf func(*RenameOptions) error) *cobra.Command {
31+
opts := &RenameOptions{
32+
IO: f.IOStreams,
33+
HttpClient: f.HttpClient,
34+
Remotes: f.Remotes,
35+
Config: f.Config,
36+
}
37+
38+
var confirm bool
39+
40+
cmd := &cobra.Command{
41+
Use: "rename [<new-name>]",
42+
Short: "Rename a repository",
43+
Long: heredoc.Doc(`Rename a GitHub repository
44+
45+
By default, this renames the current repository; otherwise renames the specified repository.`),
46+
Args: cobra.MaximumNArgs(1),
47+
RunE: func(cmd *cobra.Command, args []string) error {
48+
opts.BaseRepo = f.BaseRepo
49+
opts.HasRepoOverride = cmd.Flags().Changed("repo")
50+
51+
if len(args) > 0 {
52+
opts.newRepoSelector = args[0]
53+
} else if !opts.IO.CanPrompt() {
54+
return cmdutil.FlagErrorf("new name argument required when not running interactively")
55+
}
56+
57+
if len(args) == 1 && !confirm && !opts.HasRepoOverride {
58+
if !opts.IO.CanPrompt() {
59+
return cmdutil.FlagErrorf("--confirm required when passing a single argument")
60+
}
61+
opts.DoConfirm = true
62+
}
63+
64+
if runf != nil {
65+
return runf(opts)
66+
}
67+
return renameRun(opts)
68+
},
69+
}
70+
71+
cmdutil.EnableRepoOverride(cmd, f)
72+
cmd.Flags().BoolVarP(&confirm, "confirm", "y", false, "skip confirmation prompt")
73+
74+
return cmd
75+
}
76+
77+
func renameRun(opts *RenameOptions) error {
78+
httpClient, err := opts.HttpClient()
79+
if err != nil {
80+
return err
81+
}
82+
83+
newRepoName := opts.newRepoSelector
84+
85+
currRepo, err := opts.BaseRepo()
86+
if err != nil {
87+
return err
88+
}
89+
90+
if newRepoName == "" {
91+
err = prompt.SurveyAskOne(
92+
&survey.Input{
93+
Message: fmt.Sprintf("Rename %s to: ", ghrepo.FullName(currRepo)),
94+
},
95+
&newRepoName,
96+
)
97+
if err != nil {
98+
return err
99+
}
100+
}
101+
102+
if opts.DoConfirm {
103+
var confirmed bool
104+
p := &survey.Confirm{
105+
Message: fmt.Sprintf("Rename %s to %s?", ghrepo.FullName(currRepo), newRepoName),
106+
Default: false,
107+
}
108+
err = prompt.SurveyAskOne(p, &confirmed)
109+
if err != nil {
110+
return fmt.Errorf("failed to prompt: %w", err)
111+
}
112+
if !confirmed {
113+
return nil
114+
}
115+
}
116+
117+
newRepo, err := apiRename(httpClient, currRepo, newRepoName)
118+
if err != nil {
119+
return err
120+
}
121+
122+
cs := opts.IO.ColorScheme()
123+
if opts.IO.IsStdoutTTY() {
124+
fmt.Fprintf(opts.IO.Out, "%s Renamed repository %s\n", cs.SuccessIcon(), ghrepo.FullName(newRepo))
125+
}
126+
127+
if opts.HasRepoOverride {
128+
return nil
129+
}
130+
131+
remote, err := updateRemote(currRepo, newRepo, opts)
132+
if err != nil {
133+
fmt.Fprintf(opts.IO.ErrOut, "%s Warning: unable to update remote %q: %v\n", cs.WarningIcon(), remote.Name, err)
134+
} else if opts.IO.IsStdoutTTY() {
135+
fmt.Fprintf(opts.IO.Out, "%s Updated the %q remote\n", cs.SuccessIcon(), remote.Name)
136+
}
137+
138+
return nil
139+
}
140+
141+
func updateRemote(repo ghrepo.Interface, renamed ghrepo.Interface, opts *RenameOptions) (*context.Remote, error) {
142+
cfg, err := opts.Config()
143+
if err != nil {
144+
return nil, err
145+
}
146+
147+
protocol, err := cfg.Get(repo.RepoHost(), "git_protocol")
148+
if err != nil {
149+
return nil, err
150+
}
151+
152+
remotes, err := opts.Remotes()
153+
if err != nil {
154+
return nil, err
155+
}
156+
157+
remote, err := remotes.FindByRepo(repo.RepoOwner(), repo.RepoName())
158+
if err != nil {
159+
return nil, err
160+
}
161+
162+
remoteURL := ghrepo.FormatRemoteURL(renamed, protocol)
163+
err = git.UpdateRemoteURL(remote.Name, remoteURL)
164+
return remote, err
165+
}

0 commit comments

Comments
 (0)