repo fork: directly fork under the desired name

A new GitHub feature landed where the API client can specify the desired
name of the new fork. This avoids the necessity of subsequently having
to rename the forked repo after the fork operation has created one.

For backwards compatibility, the renaming logic is still here, but
activates only if the resulting repo name is not the desired name.
This commit is contained in:
Mislav Marohnić 2022-07-11 13:54:58 +02:00
parent dd8ad5c5fa
commit 5656296ade
7 changed files with 74 additions and 54 deletions

View file

@ -500,13 +500,16 @@ type repositoryV3 struct {
}
// ForkRepo forks the repository on GitHub and returns the new repository
func ForkRepo(client *Client, repo ghrepo.Interface, org string) (*Repository, error) {
func ForkRepo(client *Client, repo ghrepo.Interface, org, newName string) (*Repository, error) {
path := fmt.Sprintf("repos/%s/forks", ghrepo.FullName(repo))
params := map[string]interface{}{}
if org != "" {
params["organization"] = org
}
if newName != "" {
params["name"] = newName
}
body := &bytes.Buffer{}
enc := json.NewEncoder(body)

View file

@ -680,7 +680,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

@ -6,7 +6,6 @@ import (
"net/http"
"os/exec"
"path/filepath"
"regexp"
"strings"
"github.com/AlecAivazis/survey/v2"
@ -16,6 +15,7 @@ import (
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/internal/run"
"github.com/cli/cli/v2/pkg/cmd/repo/shared"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/pkg/prompt"
@ -816,9 +816,9 @@ func interactiveSource() (string, error) {
}
func confirmSubmission(repoWithOwner, visibility string) error {
targetRepo := normalizeRepoName(repoWithOwner)
targetRepo := shared.NormalizeRepoName(repoWithOwner)
if idx := strings.IndexRune(repoWithOwner, '/'); idx > 0 {
targetRepo = repoWithOwner[0:idx+1] + normalizeRepoName(repoWithOwner[idx+1:])
targetRepo = repoWithOwner[0:idx+1] + shared.NormalizeRepoName(repoWithOwner[idx+1:])
}
var answer struct {
ConfirmSubmit bool
@ -838,8 +838,3 @@ func confirmSubmission(repoWithOwner, visibility string) error {
}
return nil
}
// normalizeRepoName takes in the repo name the user inputted and normalizes it using the same logic as GitHub (GitHub.com/new)
func normalizeRepoName(repoName string) string {
return strings.TrimSuffix(regexp.MustCompile(`[^\w._-]+`).ReplaceAllString(repoName, "-"), ".git")
}

View file

@ -495,44 +495,3 @@ func Test_createRun(t *testing.T) {
})
}
}
func Test_getModifiedNormalizedName(t *testing.T) {
// confirmed using GitHub.com/new
tests := []struct {
LocalName string
NormalizedName string
}{
{
LocalName: "cli",
NormalizedName: "cli",
},
{
LocalName: "cli.git",
NormalizedName: "cli",
},
{
LocalName: "@-#$^",
NormalizedName: "---",
},
{
LocalName: "[cli]",
NormalizedName: "-cli-",
},
{
LocalName: "Hello World, I'm a new repo!",
NormalizedName: "Hello-World-I-m-a-new-repo-",
},
{
LocalName: " @E3H*(#$#_$-ZVp,n.7lGq*_eMa-(-zAZSJYg!",
NormalizedName: "-E3H-_--ZVp-n.7lGq-_eMa---zAZSJYg-",
},
{
LocalName: "I'm a crazy .git repo name .git.git .git",
NormalizedName: "I-m-a-crazy-.git-repo-name-.git.git-",
},
}
for _, tt := range tests {
output := normalizeRepoName(tt.LocalName)
assert.Equal(t, tt.NormalizedName, output)
}
}

View file

@ -14,6 +14,7 @@ import (
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/internal/run"
"github.com/cli/cli/v2/pkg/cmd/repo/shared"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/pkg/prompt"
@ -179,7 +180,7 @@ func forkRun(opts *ForkOptions) error {
apiClient := api.NewClientFromHTTP(httpClient)
opts.IO.StartProgressIndicator()
forkedRepo, err := api.ForkRepo(apiClient, repoToFork, opts.Organization)
forkedRepo, err := api.ForkRepo(apiClient, repoToFork, opts.Organization, opts.ForkName)
opts.IO.StopProgressIndicator()
if err != nil {
return fmt.Errorf("failed to fork: %w", err)
@ -206,8 +207,8 @@ func forkRun(opts *ForkOptions) error {
}
}
// Rename the forked repo if ForkName is specified in opts.
if opts.ForkName != "" {
// Rename the new repo if necessary
if opts.ForkName != "" && !strings.EqualFold(forkedRepo.RepoName(), shared.NormalizeRepoName(opts.ForkName)) {
forkedRepo, err = api.RenameRepo(apiClient, forkedRepo, opts.ForkName)
if err != nil {
return fmt.Errorf("could not rename fork: %w", err)

View file

@ -0,0 +1,14 @@
package shared
import (
"regexp"
"strings"
)
var invalidCharactersRE = regexp.MustCompile(`[^\w._-]+`)
// NormalizeRepoName takes in the repo name the user inputted and normalizes it using the same logic as GitHub (GitHub.com/new)
func NormalizeRepoName(repoName string) string {
newName := invalidCharactersRE.ReplaceAllString(repoName, "-")
return strings.TrimSuffix(newName, ".git")
}

View file

@ -0,0 +1,48 @@
package shared
import (
"testing"
)
func TestNormalizeRepoName(t *testing.T) {
// confirmed using GitHub.com/new
tests := []struct {
LocalName string
NormalizedName string
}{
{
LocalName: "cli",
NormalizedName: "cli",
},
{
LocalName: "cli.git",
NormalizedName: "cli",
},
{
LocalName: "@-#$^",
NormalizedName: "---",
},
{
LocalName: "[cli]",
NormalizedName: "-cli-",
},
{
LocalName: "Hello World, I'm a new repo!",
NormalizedName: "Hello-World-I-m-a-new-repo-",
},
{
LocalName: " @E3H*(#$#_$-ZVp,n.7lGq*_eMa-(-zAZSJYg!",
NormalizedName: "-E3H-_--ZVp-n.7lGq-_eMa---zAZSJYg-",
},
{
LocalName: "I'm a crazy .git repo name .git.git .git",
NormalizedName: "I-m-a-crazy-.git-repo-name-.git.git-",
},
}
for _, tt := range tests {
output := NormalizeRepoName(tt.LocalName)
if output != tt.NormalizedName {
t.Errorf("Expected %q, got %q", tt.NormalizedName, output)
}
}
}