isolate repo fork command and tweak usage
This commit is another isolation refactor, this time for repo fork. However, I got fed up with the --remote="true|false|prompt" style of flags and took this opportunity to switch to a set of bool flags: --remote and --clone --no-remote and --no-clone the string args were really non standard and confusing; with only two bools it was impossible to tell when to prompt.
This commit is contained in:
parent
9035418ca6
commit
f3eb092d7e
10 changed files with 829 additions and 662 deletions
256
command/repo.go
256
command/repo.go
|
|
@ -1,29 +1,11 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/git"
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/cli/cli/internal/run"
|
||||
"github.com/cli/cli/pkg/prompt"
|
||||
"github.com/cli/cli/utils"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
repoCmd.AddCommand(repoForkCmd)
|
||||
repoForkCmd.Flags().String("clone", "prompt", "Clone fork: {true|false|prompt}")
|
||||
repoForkCmd.Flags().String("remote", "prompt", "Add remote for fork: {true|false|prompt}")
|
||||
repoForkCmd.Flags().Lookup("clone").NoOptDefVal = "true"
|
||||
repoForkCmd.Flags().Lookup("remote").NoOptDefVal = "true"
|
||||
|
||||
repoCmd.AddCommand(repoCreditsCmd)
|
||||
repoCreditsCmd.Flags().BoolP("static", "s", false, "Print a static version of the credits")
|
||||
}
|
||||
|
|
@ -45,243 +27,27 @@ A repository can be supplied as an argument in any of the following formats:
|
|||
- by URL, e.g. "https://github.com/OWNER/REPO"`},
|
||||
}
|
||||
|
||||
var repoForkCmd = &cobra.Command{
|
||||
Use: "fork [<repository>]",
|
||||
Short: "Create a fork of a repository",
|
||||
Long: `Create a fork of a repository.
|
||||
|
||||
With no argument, creates a fork of the current repository. Otherwise, forks the specified repository.`,
|
||||
RunE: repoFork,
|
||||
}
|
||||
|
||||
var repoCreditsCmd = &cobra.Command{
|
||||
Use: "credits [<repository>]",
|
||||
Short: "View credits for a repository",
|
||||
Example: heredoc.Doc(`
|
||||
# view credits for the current repository
|
||||
$ gh repo credits
|
||||
|
||||
# view credits for a specific repository
|
||||
$ gh repo credits cool/repo
|
||||
# view credits for the current repository
|
||||
$ gh repo credits
|
||||
|
||||
# print a non-animated thank you
|
||||
$ gh repo credits -s
|
||||
|
||||
# pipe to just print the contributors, one per line
|
||||
$ gh repo credits | cat
|
||||
`),
|
||||
# view credits for a specific repository
|
||||
$ gh repo credits cool/repo
|
||||
|
||||
# print a non-animated thank you
|
||||
$ gh repo credits -s
|
||||
|
||||
# pipe to just print the contributors, one per line
|
||||
$ gh repo credits | cat
|
||||
`),
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: repoCredits,
|
||||
Hidden: true,
|
||||
}
|
||||
|
||||
var Since = func(t time.Time) time.Duration {
|
||||
return time.Since(t)
|
||||
}
|
||||
|
||||
func repoFork(cmd *cobra.Command, args []string) error {
|
||||
ctx := contextForCommand(cmd)
|
||||
|
||||
clonePref, err := cmd.Flags().GetString("clone")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
remotePref, err := cmd.Flags().GetString("remote")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
apiClient, err := apiClientForContext(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create client: %w", err)
|
||||
}
|
||||
|
||||
var repoToFork ghrepo.Interface
|
||||
inParent := false // whether or not we're forking the repo we're currently "in"
|
||||
if len(args) == 0 {
|
||||
baseRepo, err := determineBaseRepo(apiClient, cmd, ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to determine base repository: %w", err)
|
||||
}
|
||||
inParent = true
|
||||
repoToFork = baseRepo
|
||||
} else {
|
||||
repoArg := args[0]
|
||||
|
||||
if utils.IsURL(repoArg) {
|
||||
parsedURL, err := url.Parse(repoArg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("did not understand argument: %w", err)
|
||||
}
|
||||
|
||||
repoToFork, err = ghrepo.FromURL(parsedURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("did not understand argument: %w", err)
|
||||
}
|
||||
|
||||
} else if strings.HasPrefix(repoArg, "git@") {
|
||||
parsedURL, err := git.ParseURL(repoArg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("did not understand argument: %w", err)
|
||||
}
|
||||
repoToFork, err = ghrepo.FromURL(parsedURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("did not understand argument: %w", err)
|
||||
}
|
||||
} else {
|
||||
repoToFork, err = ghrepo.FromFullName(repoArg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("argument error: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !connectedToTerminal(cmd) {
|
||||
if (inParent && remotePref == "prompt") || (!inParent && clonePref == "prompt") {
|
||||
return errors.New("--remote or --clone must be explicitly set when not attached to tty")
|
||||
}
|
||||
}
|
||||
|
||||
greenCheck := utils.Green("✓")
|
||||
stderr := colorableErr(cmd)
|
||||
s := utils.Spinner(stderr)
|
||||
stopSpinner := func() {}
|
||||
|
||||
if connectedToTerminal(cmd) {
|
||||
loading := utils.Gray("Forking ") + utils.Bold(utils.Gray(ghrepo.FullName(repoToFork))) + utils.Gray("...")
|
||||
s.Suffix = " " + loading
|
||||
s.FinalMSG = utils.Gray(fmt.Sprintf("- %s\n", loading))
|
||||
utils.StartSpinner(s)
|
||||
stopSpinner = func() {
|
||||
utils.StopSpinner(s)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
forkedRepo, err := api.ForkRepo(apiClient, repoToFork)
|
||||
if err != nil {
|
||||
stopSpinner()
|
||||
return fmt.Errorf("failed to fork: %w", err)
|
||||
}
|
||||
|
||||
stopSpinner()
|
||||
|
||||
// This is weird. There is not an efficient way to determine via the GitHub API whether or not a
|
||||
// given user has forked a given repo. We noticed, also, that the create fork API endpoint just
|
||||
// returns the fork repo data even if it already exists -- with no change in status code or
|
||||
// anything. We thus check the created time to see if the repo is brand new or not; if it's not,
|
||||
// we assume the fork already existed and report an error.
|
||||
createdAgo := Since(forkedRepo.CreatedAt)
|
||||
if createdAgo > time.Minute {
|
||||
if connectedToTerminal(cmd) {
|
||||
fmt.Fprintf(stderr, "%s %s %s\n",
|
||||
utils.Yellow("!"),
|
||||
utils.Bold(ghrepo.FullName(forkedRepo)),
|
||||
"already exists")
|
||||
} else {
|
||||
fmt.Fprintf(stderr, "%s already exists", ghrepo.FullName(forkedRepo))
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
if connectedToTerminal(cmd) {
|
||||
fmt.Fprintf(stderr, "%s Created fork %s\n", greenCheck, utils.Bold(ghrepo.FullName(forkedRepo)))
|
||||
}
|
||||
}
|
||||
|
||||
if (inParent && remotePref == "false") || (!inParent && clonePref == "false") {
|
||||
return nil
|
||||
}
|
||||
|
||||
if inParent {
|
||||
remotes, err := ctx.Remotes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if remote, err := remotes.FindByRepo(forkedRepo.RepoOwner(), forkedRepo.RepoName()); err == nil {
|
||||
if connectedToTerminal(cmd) {
|
||||
fmt.Fprintf(stderr, "%s Using existing remote %s\n", greenCheck, utils.Bold(remote.Name))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
remoteDesired := remotePref == "true"
|
||||
if remotePref == "prompt" {
|
||||
err = prompt.Confirm("Would you like to add a remote for the fork?", &remoteDesired)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to prompt: %w", err)
|
||||
}
|
||||
}
|
||||
if remoteDesired {
|
||||
remoteName := "origin"
|
||||
|
||||
remotes, err := ctx.Remotes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := remotes.FindByName(remoteName); err == nil {
|
||||
renameTarget := "upstream"
|
||||
renameCmd := git.GitCommand("remote", "rename", remoteName, renameTarget)
|
||||
err = run.PrepareCmd(renameCmd).Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if connectedToTerminal(cmd) {
|
||||
fmt.Fprintf(stderr, "%s Renamed %s remote to %s\n", greenCheck, utils.Bold(remoteName), utils.Bold(renameTarget))
|
||||
}
|
||||
}
|
||||
|
||||
forkedRepoCloneURL := formatRemoteURL(cmd, forkedRepo)
|
||||
|
||||
_, err = git.AddRemote(remoteName, forkedRepoCloneURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add remote: %w", err)
|
||||
}
|
||||
|
||||
if connectedToTerminal(cmd) {
|
||||
fmt.Fprintf(stderr, "%s Added remote %s\n", greenCheck, utils.Bold(remoteName))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
cloneDesired := clonePref == "true"
|
||||
if clonePref == "prompt" {
|
||||
err = prompt.Confirm("Would you like to clone the fork?", &cloneDesired)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to prompt: %w", err)
|
||||
}
|
||||
}
|
||||
if cloneDesired {
|
||||
forkedRepoCloneURL := formatRemoteURL(cmd, forkedRepo)
|
||||
cloneDir, err := git.RunClone(forkedRepoCloneURL, []string{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to clone fork: %w", err)
|
||||
}
|
||||
|
||||
// TODO This is overly wordy and I'd like to streamline this.
|
||||
cfg, err := ctx.Config()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
protocol, err := cfg.Get("", "git_protocol")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
upstreamURL := ghrepo.FormatRemoteURL(repoToFork, protocol)
|
||||
|
||||
err = git.AddUpstreamRemote(upstreamURL, cloneDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if connectedToTerminal(cmd) {
|
||||
fmt.Fprintf(stderr, "%s Cloned fork\n", greenCheck)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func repoCredits(cmd *cobra.Command, args []string) error {
|
||||
return credits(cmd, args)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,415 +0,0 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/briandowns/spinner"
|
||||
|
||||
"github.com/cli/cli/context"
|
||||
"github.com/cli/cli/internal/run"
|
||||
"github.com/cli/cli/pkg/prompt"
|
||||
"github.com/cli/cli/test"
|
||||
"github.com/cli/cli/utils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func stubSpinner() {
|
||||
// not bothering with teardown since we never want spinners when doing tests
|
||||
utils.StartSpinner = func(_ *spinner.Spinner) {
|
||||
}
|
||||
utils.StopSpinner = func(_ *spinner.Spinner) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepoFork_nontty_insufficient_flags(t *testing.T) {
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
defer stubSince(2 * time.Second)()
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
defer stubTerminal(false)()
|
||||
|
||||
_, err := RunCommand("repo fork")
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
|
||||
assert.Equal(t, "--remote or --clone must be explicitly set when not attached to tty", err.Error())
|
||||
}
|
||||
|
||||
func TestRepoFork_in_parent_nontty(t *testing.T) {
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
defer stubSince(2 * time.Second)()
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
defer http.StubWithFixture(200, "forkResult.json")()
|
||||
defer stubTerminal(false)()
|
||||
|
||||
cs, restore := test.InitCmdStubber()
|
||||
defer restore()
|
||||
|
||||
cs.Stub("") // git remote rename
|
||||
cs.Stub("") // git remote add
|
||||
|
||||
output, err := RunCommand("repo fork --remote")
|
||||
if err != nil {
|
||||
t.Fatalf("error running command `repo fork`: %v", err)
|
||||
}
|
||||
|
||||
eq(t, strings.Join(cs.Calls[0].Args, " "), "git remote rename origin upstream")
|
||||
eq(t, strings.Join(cs.Calls[1].Args, " "), "git remote add -f origin https://github.com/someone/REPO.git")
|
||||
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, "", output.Stderr())
|
||||
}
|
||||
|
||||
func TestRepoFork_outside_parent_nontty(t *testing.T) {
|
||||
defer stubSince(2 * time.Second)()
|
||||
http := initFakeHTTP()
|
||||
defer http.StubWithFixture(200, "forkResult.json")()
|
||||
defer stubTerminal(false)()
|
||||
|
||||
cs, restore := test.InitCmdStubber()
|
||||
defer restore()
|
||||
|
||||
cs.Stub("") // git clone
|
||||
cs.Stub("") // git remote add
|
||||
|
||||
output, err := RunCommand("repo fork --clone OWNER/REPO")
|
||||
if err != nil {
|
||||
t.Errorf("error running command `repo fork`: %v", err)
|
||||
}
|
||||
|
||||
eq(t, output.String(), "")
|
||||
|
||||
eq(t, strings.Join(cs.Calls[0].Args, " "), "git clone https://github.com/someone/REPO.git")
|
||||
eq(t, strings.Join(cs.Calls[1].Args, " "), "git -C REPO remote add -f upstream https://github.com/OWNER/REPO.git")
|
||||
|
||||
assert.Equal(t, output.Stderr(), "")
|
||||
}
|
||||
|
||||
func TestRepoFork_already_forked(t *testing.T) {
|
||||
stubSpinner()
|
||||
initContext = func() context.Context {
|
||||
ctx := context.NewBlank()
|
||||
ctx.SetBaseRepo("OWNER/REPO")
|
||||
ctx.SetBranch("master")
|
||||
ctx.SetRemotes(map[string]string{
|
||||
"origin": "OWNER/REPO",
|
||||
})
|
||||
return ctx
|
||||
}
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
defer http.StubWithFixture(200, "forkResult.json")()
|
||||
defer stubTerminal(true)()
|
||||
|
||||
output, err := RunCommand("repo fork --remote=false")
|
||||
if err != nil {
|
||||
t.Errorf("got unexpected error: %v", err)
|
||||
}
|
||||
r := regexp.MustCompile(`someone/REPO.*already exists`)
|
||||
if !r.MatchString(output.Stderr()) {
|
||||
t.Errorf("output did not match regexp /%s/\n> output\n%s\n", r, output.Stderr())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepoFork_reuseRemote(t *testing.T) {
|
||||
stubSpinner()
|
||||
initContext = func() context.Context {
|
||||
ctx := context.NewBlank()
|
||||
ctx.SetBaseRepo("OWNER/REPO")
|
||||
ctx.SetBranch("master")
|
||||
ctx.SetRemotes(map[string]string{
|
||||
"upstream": "OWNER/REPO",
|
||||
"origin": "someone/REPO",
|
||||
})
|
||||
return ctx
|
||||
}
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
defer http.StubWithFixture(200, "forkResult.json")()
|
||||
defer stubTerminal(true)()
|
||||
|
||||
output, err := RunCommand("repo fork")
|
||||
if err != nil {
|
||||
t.Errorf("got unexpected error: %v", err)
|
||||
}
|
||||
r := regexp.MustCompile(`Using existing remote.*origin`)
|
||||
if !r.MatchString(output.Stderr()) {
|
||||
t.Errorf("output did not match: %q", output.Stderr())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func stubSince(d time.Duration) func() {
|
||||
originalSince := Since
|
||||
Since = func(t time.Time) time.Duration {
|
||||
return d
|
||||
}
|
||||
return func() {
|
||||
Since = originalSince
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepoFork_in_parent(t *testing.T) {
|
||||
stubSpinner()
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
defer stubSince(2 * time.Second)()
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
defer http.StubWithFixture(200, "forkResult.json")()
|
||||
defer stubTerminal(true)()
|
||||
|
||||
output, err := RunCommand("repo fork --remote=false")
|
||||
if err != nil {
|
||||
t.Errorf("error running command `repo fork`: %v", err)
|
||||
}
|
||||
|
||||
eq(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
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepoFork_outside(t *testing.T) {
|
||||
stubSpinner()
|
||||
tests := []struct {
|
||||
name string
|
||||
args string
|
||||
}{
|
||||
{
|
||||
name: "url arg",
|
||||
args: "repo fork --clone=false http://github.com/OWNER/REPO.git",
|
||||
},
|
||||
{
|
||||
name: "full name arg",
|
||||
args: "repo fork --clone=false OWNER/REPO",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
defer stubSince(2 * time.Second)()
|
||||
http := initFakeHTTP()
|
||||
defer http.StubWithFixture(200, "forkResult.json")()
|
||||
defer stubTerminal(true)()
|
||||
|
||||
output, err := RunCommand(tt.args)
|
||||
if err != nil {
|
||||
t.Errorf("error running command `repo fork`: %v", err)
|
||||
}
|
||||
|
||||
eq(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
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepoFork_in_parent_yes(t *testing.T) {
|
||||
stubSpinner()
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
defer stubSince(2 * time.Second)()
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
defer http.StubWithFixture(200, "forkResult.json")()
|
||||
defer stubTerminal(true)()
|
||||
|
||||
var seenCmds []*exec.Cmd
|
||||
defer run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
seenCmds = append(seenCmds, cmd)
|
||||
return &test.OutputStub{}
|
||||
})()
|
||||
|
||||
output, err := RunCommand("repo fork --remote")
|
||||
if err != nil {
|
||||
t.Errorf("error running command `repo fork`: %v", err)
|
||||
}
|
||||
|
||||
expectedCmds := []string{
|
||||
"git remote rename origin upstream",
|
||||
"git remote add -f origin https://github.com/someone/REPO.git",
|
||||
}
|
||||
|
||||
for x, cmd := range seenCmds {
|
||||
eq(t, strings.Join(cmd.Args, " "), expectedCmds[x])
|
||||
}
|
||||
|
||||
eq(t, output.String(), "")
|
||||
|
||||
test.ExpectLines(t, output.Stderr(),
|
||||
"Created fork.*someone/REPO",
|
||||
"Added remote.*origin")
|
||||
}
|
||||
|
||||
func TestRepoFork_outside_yes(t *testing.T) {
|
||||
stubSpinner()
|
||||
defer stubSince(2 * time.Second)()
|
||||
http := initFakeHTTP()
|
||||
defer http.StubWithFixture(200, "forkResult.json")()
|
||||
defer stubTerminal(true)()
|
||||
|
||||
cs, restore := test.InitCmdStubber()
|
||||
defer restore()
|
||||
|
||||
cs.Stub("") // git clone
|
||||
cs.Stub("") // git remote add
|
||||
|
||||
output, err := RunCommand("repo fork --clone OWNER/REPO")
|
||||
if err != nil {
|
||||
t.Errorf("error running command `repo fork`: %v", err)
|
||||
}
|
||||
|
||||
eq(t, output.String(), "")
|
||||
|
||||
eq(t, strings.Join(cs.Calls[0].Args, " "), "git clone https://github.com/someone/REPO.git")
|
||||
eq(t, strings.Join(cs.Calls[1].Args, " "), "git -C REPO remote add -f upstream https://github.com/OWNER/REPO.git")
|
||||
|
||||
test.ExpectLines(t, output.Stderr(),
|
||||
"Created fork.*someone/REPO",
|
||||
"Cloned fork")
|
||||
}
|
||||
|
||||
func TestRepoFork_outside_survey_yes(t *testing.T) {
|
||||
stubSpinner()
|
||||
defer stubSince(2 * time.Second)()
|
||||
http := initFakeHTTP()
|
||||
defer http.StubWithFixture(200, "forkResult.json")()
|
||||
defer stubTerminal(true)()
|
||||
|
||||
cs, restore := test.InitCmdStubber()
|
||||
defer restore()
|
||||
|
||||
cs.Stub("") // git clone
|
||||
cs.Stub("") // git remote add
|
||||
|
||||
defer prompt.StubConfirm(true)()
|
||||
|
||||
output, err := RunCommand("repo fork OWNER/REPO")
|
||||
if err != nil {
|
||||
t.Errorf("error running command `repo fork`: %v", err)
|
||||
}
|
||||
|
||||
eq(t, output.String(), "")
|
||||
|
||||
eq(t, strings.Join(cs.Calls[0].Args, " "), "git clone https://github.com/someone/REPO.git")
|
||||
eq(t, strings.Join(cs.Calls[1].Args, " "), "git -C REPO remote add -f upstream https://github.com/OWNER/REPO.git")
|
||||
|
||||
test.ExpectLines(t, output.Stderr(),
|
||||
"Created fork.*someone/REPO",
|
||||
"Cloned fork")
|
||||
}
|
||||
|
||||
func TestRepoFork_outside_survey_no(t *testing.T) {
|
||||
stubSpinner()
|
||||
defer stubSince(2 * time.Second)()
|
||||
http := initFakeHTTP()
|
||||
defer http.StubWithFixture(200, "forkResult.json")()
|
||||
defer stubTerminal(true)()
|
||||
|
||||
cmdRun := false
|
||||
defer run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
cmdRun = true
|
||||
return &test.OutputStub{}
|
||||
})()
|
||||
|
||||
defer prompt.StubConfirm(false)()
|
||||
|
||||
output, err := RunCommand("repo fork OWNER/REPO")
|
||||
if err != nil {
|
||||
t.Errorf("error running command `repo fork`: %v", err)
|
||||
}
|
||||
|
||||
eq(t, output.String(), "")
|
||||
|
||||
eq(t, cmdRun, false)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepoFork_in_parent_survey_yes(t *testing.T) {
|
||||
stubSpinner()
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
defer stubSince(2 * time.Second)()
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
defer http.StubWithFixture(200, "forkResult.json")()
|
||||
defer stubTerminal(true)()
|
||||
|
||||
var seenCmds []*exec.Cmd
|
||||
defer run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
seenCmds = append(seenCmds, cmd)
|
||||
return &test.OutputStub{}
|
||||
})()
|
||||
|
||||
defer prompt.StubConfirm(true)()
|
||||
|
||||
output, err := RunCommand("repo fork")
|
||||
if err != nil {
|
||||
t.Errorf("error running command `repo fork`: %v", err)
|
||||
}
|
||||
|
||||
expectedCmds := []string{
|
||||
"git remote rename origin upstream",
|
||||
"git remote add -f origin https://github.com/someone/REPO.git",
|
||||
}
|
||||
|
||||
for x, cmd := range seenCmds {
|
||||
eq(t, strings.Join(cmd.Args, " "), expectedCmds[x])
|
||||
}
|
||||
|
||||
eq(t, output.String(), "")
|
||||
|
||||
test.ExpectLines(t, output.Stderr(),
|
||||
"Created fork.*someone/REPO",
|
||||
"Renamed.*origin.*remote to.*upstream",
|
||||
"Added remote.*origin")
|
||||
}
|
||||
|
||||
func TestRepoFork_in_parent_survey_no(t *testing.T) {
|
||||
stubSpinner()
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
defer stubSince(2 * time.Second)()
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
defer http.StubWithFixture(200, "forkResult.json")()
|
||||
defer stubTerminal(true)()
|
||||
|
||||
cmdRun := false
|
||||
defer run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
cmdRun = true
|
||||
return &test.OutputStub{}
|
||||
})()
|
||||
|
||||
defer prompt.StubConfirm(false)()
|
||||
|
||||
output, err := RunCommand("repo fork")
|
||||
if err != nil {
|
||||
t.Errorf("error running command `repo fork`: %v", err)
|
||||
}
|
||||
|
||||
eq(t, output.String(), "")
|
||||
|
||||
eq(t, cmdRun, false)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -23,6 +23,7 @@ import (
|
|||
gistCreateCmd "github.com/cli/cli/pkg/cmd/gist/create"
|
||||
repoCloneCmd "github.com/cli/cli/pkg/cmd/repo/clone"
|
||||
repoCreateCmd "github.com/cli/cli/pkg/cmd/repo/create"
|
||||
repoForkCmd "github.com/cli/cli/pkg/cmd/repo/fork"
|
||||
repoViewCmd "github.com/cli/cli/pkg/cmd/repo/view"
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
"github.com/cli/cli/pkg/iostreams"
|
||||
|
|
@ -97,6 +98,10 @@ func init() {
|
|||
ctx := context.New()
|
||||
return ctx.BaseRepo()
|
||||
},
|
||||
Remotes: func() (context.Remotes, error) {
|
||||
ctx := context.New()
|
||||
return ctx.Remotes()
|
||||
},
|
||||
Config: func() (config.Config, error) {
|
||||
cfg, err := config.ParseDefaultConfig()
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
|
|
@ -148,6 +153,7 @@ func init() {
|
|||
|
||||
RootCmd.AddCommand(repoCmd)
|
||||
repoCmd.AddCommand(repoViewCmd.NewCmdView(&repoResolvingCmdFactory, nil))
|
||||
repoCmd.AddCommand(repoForkCmd.NewCmdFork(&repoResolvingCmdFactory, nil))
|
||||
repoCmd.AddCommand(repoCloneCmd.NewCmdClone(cmdFactory, nil))
|
||||
repoCmd.AddCommand(repoCreateCmd.NewCmdCreate(cmdFactory, nil))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ func TestChangelogURL(t *testing.T) {
|
|||
|
||||
func TestRemoteURLFormatting_no_config(t *testing.T) {
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
result := formatRemoteURL(repoForkCmd, ghrepo.New("OWNER", "REPO"))
|
||||
result := formatRemoteURL(prCheckoutCmd, ghrepo.New("OWNER", "REPO"))
|
||||
eq(t, result, "https://github.com/OWNER/REPO.git")
|
||||
}
|
||||
|
||||
|
|
@ -58,6 +58,6 @@ hosts:
|
|||
git_protocol: ssh
|
||||
`
|
||||
initBlankContext(cfg, "OWNER/REPO", "master")
|
||||
result := formatRemoteURL(repoForkCmd, ghrepo.New("OWNER", "REPO"))
|
||||
result := formatRemoteURL(prCheckoutCmd, ghrepo.New("OWNER", "REPO"))
|
||||
eq(t, result, "git@github.com:OWNER/REPO.git")
|
||||
}
|
||||
|
|
|
|||
292
pkg/cmd/repo/fork/fork.go
Normal file
292
pkg/cmd/repo/fork/fork.go
Normal file
|
|
@ -0,0 +1,292 @@
|
|||
package fork
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/context"
|
||||
"github.com/cli/cli/git"
|
||||
"github.com/cli/cli/internal/config"
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/cli/cli/internal/run"
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
"github.com/cli/cli/pkg/iostreams"
|
||||
"github.com/cli/cli/pkg/prompt"
|
||||
"github.com/cli/cli/utils"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type ForkOptions struct {
|
||||
HttpClient func() (*http.Client, error)
|
||||
Config func() (config.Config, error)
|
||||
IO *iostreams.IOStreams
|
||||
BaseRepo func() (ghrepo.Interface, error)
|
||||
Remotes func() (context.Remotes, error)
|
||||
Since func(t time.Time) time.Duration
|
||||
|
||||
Repository string
|
||||
Clone bool
|
||||
Remote bool
|
||||
PromptClone bool
|
||||
PromptRemote bool
|
||||
}
|
||||
|
||||
// TODO decide if this should be injected via a factory.
|
||||
var Since = func(t time.Time) time.Duration {
|
||||
return time.Since(t)
|
||||
}
|
||||
|
||||
func NewCmdFork(f *cmdutil.Factory, runF func(*ForkOptions) error) *cobra.Command {
|
||||
opts := &ForkOptions{
|
||||
IO: f.IOStreams,
|
||||
HttpClient: f.HttpClient,
|
||||
Config: f.Config,
|
||||
BaseRepo: f.BaseRepo,
|
||||
Remotes: f.Remotes,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "fork [<repository>]",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Short: "Create a fork of a repository",
|
||||
Long: `Create a fork of a repository.
|
||||
|
||||
With no argument, creates a fork of the current repository. Otherwise, forks the specified repository.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
promptOk := opts.IO.IsStdoutTTY() && opts.IO.IsStdinTTY()
|
||||
if len(args) > 0 {
|
||||
opts.Repository = args[0]
|
||||
}
|
||||
|
||||
if cmd.Flags().Changed("clone") && cmd.Flags().Changed("no-clone") {
|
||||
return errors.New("--clone and --no-clone are mutually excusive")
|
||||
}
|
||||
|
||||
if cmd.Flags().Changed("remote") && cmd.Flags().Changed("no-remote") {
|
||||
return errors.New("--remote and --no-remote are mutually excusive")
|
||||
}
|
||||
|
||||
if promptOk && (!cmd.Flags().Changed("clone") && !cmd.Flags().Changed("no-clone")) {
|
||||
opts.PromptClone = true
|
||||
}
|
||||
|
||||
if promptOk && (!cmd.Flags().Changed("remote") && !cmd.Flags().Changed("no-remote")) {
|
||||
opts.PromptRemote = true
|
||||
}
|
||||
|
||||
if cmd.Flags().Changed("no-clone") {
|
||||
opts.Clone = false
|
||||
}
|
||||
|
||||
if cmd.Flags().Changed("no-remote") {
|
||||
opts.Remote = false
|
||||
}
|
||||
|
||||
if runF != nil {
|
||||
return runF(opts)
|
||||
}
|
||||
return forkRun(opts)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVar(&opts.Clone, "clone", false, "Clone the fork")
|
||||
cmd.Flags().BoolVar(&opts.Remote, "remote", false, "Add remote for fork")
|
||||
cmd.Flags().Bool("no-clone", false, "Do not clone the fork")
|
||||
cmd.Flags().Bool("no-remote", false, "Do not add remote for fork")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func forkRun(opts *ForkOptions) error {
|
||||
var repoToFork ghrepo.Interface
|
||||
var err error
|
||||
inParent := false // whether or not we're forking the repo we're currently "in"
|
||||
if opts.Repository == "" {
|
||||
baseRepo, err := opts.BaseRepo()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to determine base repository: %w", err)
|
||||
}
|
||||
inParent = true
|
||||
repoToFork = baseRepo
|
||||
} else {
|
||||
repoArg := opts.Repository
|
||||
|
||||
if utils.IsURL(repoArg) {
|
||||
parsedURL, err := url.Parse(repoArg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("did not understand argument: %w", err)
|
||||
}
|
||||
|
||||
repoToFork, err = ghrepo.FromURL(parsedURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("did not understand argument: %w", err)
|
||||
}
|
||||
|
||||
} else if strings.HasPrefix(repoArg, "git@") {
|
||||
parsedURL, err := git.ParseURL(repoArg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("did not understand argument: %w", err)
|
||||
}
|
||||
repoToFork, err = ghrepo.FromURL(parsedURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("did not understand argument: %w", err)
|
||||
}
|
||||
} else {
|
||||
repoToFork, err = ghrepo.FromFullName(repoArg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("argument error: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
connectedToTerminal := opts.IO.IsStdoutTTY() && opts.IO.IsStderrTTY() && opts.IO.IsStdinTTY()
|
||||
|
||||
greenCheck := utils.Green("✓")
|
||||
stderr := opts.IO.ErrOut
|
||||
s := utils.Spinner(stderr)
|
||||
stopSpinner := func() {}
|
||||
|
||||
if connectedToTerminal {
|
||||
loading := utils.Gray("Forking ") + utils.Bold(utils.Gray(ghrepo.FullName(repoToFork))) + utils.Gray("...")
|
||||
s.Suffix = " " + loading
|
||||
s.FinalMSG = utils.Gray(fmt.Sprintf("- %s\n", loading))
|
||||
utils.StartSpinner(s)
|
||||
stopSpinner = func() {
|
||||
utils.StopSpinner(s)
|
||||
}
|
||||
}
|
||||
|
||||
httpClient, err := opts.HttpClient()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create client: %w", err)
|
||||
}
|
||||
|
||||
apiClient := api.NewClientFromHTTP(httpClient)
|
||||
|
||||
forkedRepo, err := api.ForkRepo(apiClient, repoToFork)
|
||||
if err != nil {
|
||||
stopSpinner()
|
||||
return fmt.Errorf("failed to fork: %w", err)
|
||||
}
|
||||
|
||||
stopSpinner()
|
||||
|
||||
// This is weird. There is not an efficient way to determine via the GitHub API whether or not a
|
||||
// given user has forked a given repo. We noticed, also, that the create fork API endpoint just
|
||||
// returns the fork repo data even if it already exists -- with no change in status code or
|
||||
// anything. We thus check the created time to see if the repo is brand new or not; if it's not,
|
||||
// we assume the fork already existed and report an error.
|
||||
createdAgo := Since(forkedRepo.CreatedAt)
|
||||
if createdAgo > time.Minute {
|
||||
if connectedToTerminal {
|
||||
fmt.Fprintf(stderr, "%s %s %s\n",
|
||||
utils.Yellow("!"),
|
||||
utils.Bold(ghrepo.FullName(forkedRepo)),
|
||||
"already exists")
|
||||
} else {
|
||||
fmt.Fprintf(stderr, "%s already exists", ghrepo.FullName(forkedRepo))
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
if connectedToTerminal {
|
||||
fmt.Fprintf(stderr, "%s Created fork %s\n", greenCheck, utils.Bold(ghrepo.FullName(forkedRepo)))
|
||||
}
|
||||
}
|
||||
|
||||
if (inParent && (!opts.Remote && !opts.PromptRemote)) || (!inParent && (!opts.Clone && !opts.PromptClone)) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO This is overly wordy and I'd like to streamline this.
|
||||
cfg, err := opts.Config()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
protocol, err := cfg.Get("", "git_protocol")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if inParent {
|
||||
remotes, err := opts.Remotes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if remote, err := remotes.FindByRepo(forkedRepo.RepoOwner(), forkedRepo.RepoName()); err == nil {
|
||||
if connectedToTerminal {
|
||||
fmt.Fprintf(stderr, "%s Using existing remote %s\n", greenCheck, utils.Bold(remote.Name))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
remoteDesired := opts.Remote
|
||||
if opts.PromptRemote {
|
||||
err = prompt.Confirm("Would you like to add a remote for the fork?", &remoteDesired)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to prompt: %w", err)
|
||||
}
|
||||
}
|
||||
if remoteDesired {
|
||||
remoteName := "origin"
|
||||
|
||||
remotes, err := opts.Remotes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := remotes.FindByName(remoteName); err == nil {
|
||||
renameTarget := "upstream"
|
||||
renameCmd := git.GitCommand("remote", "rename", remoteName, renameTarget)
|
||||
err = run.PrepareCmd(renameCmd).Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if connectedToTerminal {
|
||||
fmt.Fprintf(stderr, "%s Renamed %s remote to %s\n", greenCheck, utils.Bold(remoteName), utils.Bold(renameTarget))
|
||||
}
|
||||
}
|
||||
|
||||
forkedRepoCloneURL := ghrepo.FormatRemoteURL(forkedRepo, protocol)
|
||||
|
||||
_, err = git.AddRemote(remoteName, forkedRepoCloneURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add remote: %w", err)
|
||||
}
|
||||
|
||||
if connectedToTerminal {
|
||||
fmt.Fprintf(stderr, "%s Added remote %s\n", greenCheck, utils.Bold(remoteName))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
cloneDesired := opts.Clone
|
||||
if opts.PromptClone {
|
||||
err = prompt.Confirm("Would you like to clone the fork?", &cloneDesired)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to prompt: %w", err)
|
||||
}
|
||||
}
|
||||
if cloneDesired {
|
||||
forkedRepoURL := ghrepo.FormatRemoteURL(forkedRepo, protocol)
|
||||
cloneDir, err := git.RunClone(forkedRepoURL, []string{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to clone fork: %w", err)
|
||||
}
|
||||
|
||||
upstreamURL := ghrepo.FormatRemoteURL(repoToFork, protocol)
|
||||
err = git.AddUpstreamRemote(upstreamURL, cloneDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if connectedToTerminal {
|
||||
fmt.Fprintf(stderr, "%s Cloned fork\n", greenCheck)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
484
pkg/cmd/repo/fork/fork_test.go
Normal file
484
pkg/cmd/repo/fork/fork_test.go
Normal file
|
|
@ -0,0 +1,484 @@
|
|||
package fork
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/briandowns/spinner"
|
||||
"github.com/cli/cli/context"
|
||||
"github.com/cli/cli/git"
|
||||
"github.com/cli/cli/internal/config"
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/cli/cli/internal/run"
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
"github.com/cli/cli/pkg/httpmock"
|
||||
"github.com/cli/cli/pkg/iostreams"
|
||||
"github.com/cli/cli/pkg/prompt"
|
||||
"github.com/cli/cli/test"
|
||||
"github.com/cli/cli/utils"
|
||||
"github.com/google/shlex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func runCommand(httpClient *http.Client, remotes []*context.Remote, isTTY bool, cli string) (*test.CmdOut, error) {
|
||||
io, stdin, stdout, stderr := iostreams.Test()
|
||||
io.SetStdoutTTY(isTTY)
|
||||
io.SetStdinTTY(isTTY)
|
||||
io.SetStderrTTY(isTTY)
|
||||
fac := &cmdutil.Factory{
|
||||
IOStreams: io,
|
||||
HttpClient: func() (*http.Client, error) {
|
||||
return httpClient, nil
|
||||
},
|
||||
Config: func() (config.Config, error) {
|
||||
return config.NewBlankConfig(), nil
|
||||
},
|
||||
BaseRepo: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.New("OWNER", "REPO"), nil
|
||||
},
|
||||
Remotes: func() (context.Remotes, error) {
|
||||
if remotes == nil {
|
||||
return []*context.Remote{
|
||||
{
|
||||
Remote: &git.Remote{Name: "origin"},
|
||||
Repo: ghrepo.New("OWNER", "REPO"),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
return remotes, nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd := NewCmdFork(fac, nil)
|
||||
|
||||
argv, err := shlex.Split(cli)
|
||||
cmd.SetArgs(argv)
|
||||
|
||||
cmd.SetIn(stdin)
|
||||
cmd.SetOut(stdout)
|
||||
cmd.SetErr(stderr)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
_, err = cmd.ExecuteC()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &test.CmdOut{
|
||||
OutBuf: stdout,
|
||||
ErrBuf: stderr}, nil
|
||||
}
|
||||
|
||||
func TestRepoFork_nontty(t *testing.T) {
|
||||
defer stubSince(2 * time.Second)()
|
||||
reg := &httpmock.Registry{}
|
||||
defer reg.StubWithFixturePath(200, "./forkResult.json")()
|
||||
httpClient := &http.Client{Transport: reg}
|
||||
|
||||
cs, restore := test.InitCmdStubber()
|
||||
defer restore()
|
||||
|
||||
output, err := runCommand(httpClient, nil, false, "")
|
||||
if err != nil {
|
||||
t.Fatalf("error running command `repo fork`: %v", err)
|
||||
}
|
||||
|
||||
assert.Equal(t, 0, len(cs.Calls))
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, "", output.Stderr())
|
||||
reg.Verify(t)
|
||||
}
|
||||
|
||||
func TestRepoFork_in_parent_nontty(t *testing.T) {
|
||||
defer stubSince(2 * time.Second)()
|
||||
reg := &httpmock.Registry{}
|
||||
defer reg.StubWithFixturePath(200, "./forkResult.json")()
|
||||
httpClient := &http.Client{Transport: reg}
|
||||
|
||||
cs, restore := test.InitCmdStubber()
|
||||
defer restore()
|
||||
|
||||
cs.Stub("") // git remote rename
|
||||
cs.Stub("") // git remote add
|
||||
|
||||
output, err := runCommand(httpClient, nil, false, "--remote")
|
||||
if err != nil {
|
||||
t.Fatalf("error running command `repo fork`: %v", err)
|
||||
}
|
||||
|
||||
assert.Equal(t, 2, len(cs.Calls))
|
||||
assert.Equal(t, "git remote rename origin upstream", strings.Join(cs.Calls[0].Args, " "))
|
||||
assert.Equal(t, "git remote add -f origin https://github.com/someone/REPO.git", strings.Join(cs.Calls[1].Args, " "))
|
||||
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, "", output.Stderr())
|
||||
reg.Verify(t)
|
||||
}
|
||||
|
||||
func TestRepoFork_outside_parent_nontty(t *testing.T) {
|
||||
defer stubSince(2 * time.Second)()
|
||||
reg := &httpmock.Registry{}
|
||||
defer reg.StubWithFixturePath(200, "./forkResult.json")()
|
||||
httpClient := &http.Client{Transport: reg}
|
||||
|
||||
cs, restore := test.InitCmdStubber()
|
||||
defer restore()
|
||||
|
||||
cs.Stub("") // git clone
|
||||
cs.Stub("") // git remote add
|
||||
|
||||
output, err := runCommand(httpClient, nil, false, "--clone OWNER/REPO")
|
||||
if err != nil {
|
||||
t.Errorf("error running command `repo fork`: %v", err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "", output.String())
|
||||
|
||||
assert.Equal(t, "git clone https://github.com/someone/REPO.git", strings.Join(cs.Calls[0].Args, " "))
|
||||
assert.Equal(t, "git -C REPO remote add -f upstream https://github.com/OWNER/REPO.git", strings.Join(cs.Calls[1].Args, " "))
|
||||
|
||||
assert.Equal(t, output.Stderr(), "")
|
||||
reg.Verify(t)
|
||||
}
|
||||
|
||||
func TestRepoFork_already_forked(t *testing.T) {
|
||||
stubSpinner()
|
||||
reg := &httpmock.Registry{}
|
||||
defer reg.StubWithFixturePath(200, "./forkResult.json")()
|
||||
httpClient := &http.Client{Transport: reg}
|
||||
|
||||
cs, restore := test.InitCmdStubber()
|
||||
defer restore()
|
||||
|
||||
output, err := runCommand(httpClient, nil, true, "--no-remote")
|
||||
if err != nil {
|
||||
t.Errorf("got unexpected error: %v", err)
|
||||
}
|
||||
|
||||
assert.Equal(t, 0, len(cs.Calls))
|
||||
|
||||
r := regexp.MustCompile(`someone/REPO.*already exists`)
|
||||
if !r.MatchString(output.Stderr()) {
|
||||
t.Errorf("output did not match regexp /%s/\n> output\n%s\n", r, output.Stderr())
|
||||
return
|
||||
}
|
||||
|
||||
reg.Verify(t)
|
||||
}
|
||||
|
||||
func TestRepoFork_reuseRemote(t *testing.T) {
|
||||
stubSpinner()
|
||||
remotes := []*context.Remote{
|
||||
{
|
||||
Remote: &git.Remote{Name: "origin"},
|
||||
Repo: ghrepo.New("someone", "REPO"),
|
||||
},
|
||||
{
|
||||
Remote: &git.Remote{Name: "upstream"},
|
||||
Repo: ghrepo.New("OWNER", "REPO"),
|
||||
},
|
||||
}
|
||||
reg := &httpmock.Registry{}
|
||||
defer reg.StubWithFixturePath(200, "./forkResult.json")()
|
||||
httpClient := &http.Client{Transport: reg}
|
||||
|
||||
output, err := runCommand(httpClient, remotes, true, "--remote")
|
||||
if err != nil {
|
||||
t.Errorf("got unexpected error: %v", err)
|
||||
}
|
||||
r := regexp.MustCompile(`Using existing remote.*origin`)
|
||||
if !r.MatchString(output.Stderr()) {
|
||||
t.Errorf("output did not match: %q", output.Stderr())
|
||||
return
|
||||
}
|
||||
reg.Verify(t)
|
||||
}
|
||||
|
||||
func TestRepoFork_in_parent(t *testing.T) {
|
||||
stubSpinner()
|
||||
reg := &httpmock.Registry{}
|
||||
defer reg.StubWithFixturePath(200, "./forkResult.json")()
|
||||
httpClient := &http.Client{Transport: reg}
|
||||
|
||||
cs, restore := test.InitCmdStubber()
|
||||
defer restore()
|
||||
defer stubSince(2 * time.Second)()
|
||||
|
||||
output, err := runCommand(httpClient, nil, true, "--no-remote")
|
||||
if err != nil {
|
||||
t.Errorf("error running command `repo fork`: %v", err)
|
||||
}
|
||||
|
||||
assert.Equal(t, 0, len(cs.Calls))
|
||||
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
|
||||
}
|
||||
reg.Verify(t)
|
||||
}
|
||||
|
||||
func TestRepoFork_outside(t *testing.T) {
|
||||
stubSpinner()
|
||||
tests := []struct {
|
||||
name string
|
||||
args string
|
||||
}{
|
||||
{
|
||||
name: "url arg",
|
||||
args: "--no-clone http://github.com/OWNER/REPO.git",
|
||||
},
|
||||
{
|
||||
name: "full name arg",
|
||||
args: "--no-clone OWNER/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")()
|
||||
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.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
|
||||
}
|
||||
reg.Verify(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepoFork_in_parent_yes(t *testing.T) {
|
||||
stubSpinner()
|
||||
defer stubSince(2 * time.Second)()
|
||||
reg := &httpmock.Registry{}
|
||||
defer reg.StubWithFixturePath(200, "./forkResult.json")()
|
||||
httpClient := &http.Client{Transport: reg}
|
||||
|
||||
var seenCmds []*exec.Cmd
|
||||
defer run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
seenCmds = append(seenCmds, cmd)
|
||||
return &test.OutputStub{}
|
||||
})()
|
||||
|
||||
output, err := runCommand(httpClient, nil, true, "--remote")
|
||||
if err != nil {
|
||||
t.Errorf("error running command `repo fork`: %v", err)
|
||||
}
|
||||
|
||||
expectedCmds := []string{
|
||||
"git remote rename origin upstream",
|
||||
"git remote add -f origin https://github.com/someone/REPO.git",
|
||||
}
|
||||
|
||||
for x, cmd := range seenCmds {
|
||||
assert.Equal(t, expectedCmds[x], strings.Join(cmd.Args, " "))
|
||||
}
|
||||
|
||||
assert.Equal(t, "", output.String())
|
||||
|
||||
test.ExpectLines(t, output.Stderr(),
|
||||
"Created fork.*someone/REPO",
|
||||
"Added remote.*origin")
|
||||
reg.Verify(t)
|
||||
}
|
||||
|
||||
func TestRepoFork_outside_yes(t *testing.T) {
|
||||
stubSpinner()
|
||||
defer stubSince(2 * time.Second)()
|
||||
reg := &httpmock.Registry{}
|
||||
defer reg.StubWithFixturePath(200, "./forkResult.json")()
|
||||
httpClient := &http.Client{Transport: reg}
|
||||
|
||||
cs, restore := test.InitCmdStubber()
|
||||
defer restore()
|
||||
|
||||
cs.Stub("") // git clone
|
||||
cs.Stub("") // git remote add
|
||||
|
||||
output, err := runCommand(httpClient, nil, true, "--clone OWNER/REPO")
|
||||
if err != nil {
|
||||
t.Errorf("error running command `repo fork`: %v", err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "", output.String())
|
||||
|
||||
assert.Equal(t, "git clone https://github.com/someone/REPO.git", strings.Join(cs.Calls[0].Args, " "))
|
||||
assert.Equal(t, "git -C REPO remote add -f upstream https://github.com/OWNER/REPO.git", strings.Join(cs.Calls[1].Args, " "))
|
||||
|
||||
test.ExpectLines(t, output.Stderr(),
|
||||
"Created fork.*someone/REPO",
|
||||
"Cloned fork")
|
||||
reg.Verify(t)
|
||||
}
|
||||
|
||||
func TestRepoFork_outside_survey_yes(t *testing.T) {
|
||||
stubSpinner()
|
||||
defer stubSince(2 * time.Second)()
|
||||
reg := &httpmock.Registry{}
|
||||
defer reg.StubWithFixturePath(200, "./forkResult.json")()
|
||||
httpClient := &http.Client{Transport: reg}
|
||||
|
||||
cs, restore := test.InitCmdStubber()
|
||||
defer restore()
|
||||
|
||||
cs.Stub("") // git clone
|
||||
cs.Stub("") // git remote add
|
||||
|
||||
defer prompt.StubConfirm(true)()
|
||||
|
||||
output, err := runCommand(httpClient, nil, true, "OWNER/REPO")
|
||||
if err != nil {
|
||||
t.Errorf("error running command `repo fork`: %v", err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "", output.String())
|
||||
|
||||
assert.Equal(t, "git clone https://github.com/someone/REPO.git", strings.Join(cs.Calls[0].Args, " "))
|
||||
assert.Equal(t, "git -C REPO remote add -f upstream https://github.com/OWNER/REPO.git", strings.Join(cs.Calls[1].Args, " "))
|
||||
|
||||
test.ExpectLines(t, output.Stderr(),
|
||||
"Created fork.*someone/REPO",
|
||||
"Cloned fork")
|
||||
reg.Verify(t)
|
||||
}
|
||||
|
||||
func TestRepoFork_outside_survey_no(t *testing.T) {
|
||||
stubSpinner()
|
||||
defer stubSince(2 * time.Second)()
|
||||
reg := &httpmock.Registry{}
|
||||
defer reg.StubWithFixturePath(200, "./forkResult.json")()
|
||||
httpClient := &http.Client{Transport: reg}
|
||||
|
||||
cmdRun := false
|
||||
defer run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
cmdRun = true
|
||||
return &test.OutputStub{}
|
||||
})()
|
||||
|
||||
defer prompt.StubConfirm(false)()
|
||||
|
||||
output, err := runCommand(httpClient, nil, true, "OWNER/REPO")
|
||||
if err != nil {
|
||||
t.Errorf("error running command `repo fork`: %v", err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "", output.String())
|
||||
|
||||
assert.Equal(t, false, cmdRun)
|
||||
|
||||
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
|
||||
}
|
||||
reg.Verify(t)
|
||||
}
|
||||
|
||||
func TestRepoFork_in_parent_survey_yes(t *testing.T) {
|
||||
stubSpinner()
|
||||
reg := &httpmock.Registry{}
|
||||
defer reg.StubWithFixturePath(200, "./forkResult.json")()
|
||||
httpClient := &http.Client{Transport: reg}
|
||||
defer stubSince(2 * time.Second)()
|
||||
|
||||
var seenCmds []*exec.Cmd
|
||||
defer run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
seenCmds = append(seenCmds, cmd)
|
||||
return &test.OutputStub{}
|
||||
})()
|
||||
|
||||
defer prompt.StubConfirm(true)()
|
||||
|
||||
output, err := runCommand(httpClient, nil, true, "")
|
||||
if err != nil {
|
||||
t.Errorf("error running command `repo fork`: %v", err)
|
||||
}
|
||||
|
||||
expectedCmds := []string{
|
||||
"git remote rename origin upstream",
|
||||
"git remote add -f origin https://github.com/someone/REPO.git",
|
||||
}
|
||||
|
||||
for x, cmd := range seenCmds {
|
||||
assert.Equal(t, expectedCmds[x], strings.Join(cmd.Args, " "))
|
||||
}
|
||||
|
||||
assert.Equal(t, "", output.String())
|
||||
|
||||
test.ExpectLines(t, output.Stderr(),
|
||||
"Created fork.*someone/REPO",
|
||||
"Renamed.*origin.*remote to.*upstream",
|
||||
"Added remote.*origin")
|
||||
reg.Verify(t)
|
||||
}
|
||||
|
||||
func TestRepoFork_in_parent_survey_no(t *testing.T) {
|
||||
stubSpinner()
|
||||
reg := &httpmock.Registry{}
|
||||
defer reg.StubWithFixturePath(200, "./forkResult.json")()
|
||||
httpClient := &http.Client{Transport: reg}
|
||||
defer stubSince(2 * time.Second)()
|
||||
|
||||
cmdRun := false
|
||||
defer run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
cmdRun = true
|
||||
return &test.OutputStub{}
|
||||
})()
|
||||
|
||||
defer prompt.StubConfirm(false)()
|
||||
|
||||
output, err := runCommand(httpClient, nil, true, "")
|
||||
if err != nil {
|
||||
t.Errorf("error running command `repo fork`: %v", err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "", output.String())
|
||||
|
||||
assert.Equal(t, false, cmdRun)
|
||||
|
||||
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
|
||||
}
|
||||
reg.Verify(t)
|
||||
}
|
||||
|
||||
func stubSpinner() {
|
||||
// not bothering with teardown since we never want spinners when doing tests
|
||||
utils.StartSpinner = func(_ *spinner.Spinner) {
|
||||
}
|
||||
utils.StopSpinner = func(_ *spinner.Spinner) {
|
||||
}
|
||||
}
|
||||
|
||||
func stubSince(d time.Duration) func() {
|
||||
originalSince := Since
|
||||
Since = func(t time.Time) time.Duration {
|
||||
return d
|
||||
}
|
||||
return func() {
|
||||
Since = originalSince
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package cmdutil
|
|||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/cli/cli/context"
|
||||
"github.com/cli/cli/internal/config"
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/cli/cli/pkg/iostreams"
|
||||
|
|
@ -12,5 +13,6 @@ type Factory struct {
|
|||
IOStreams *iostreams.IOStreams
|
||||
HttpClient func() (*http.Client, error)
|
||||
BaseRepo func() (ghrepo.Interface, error)
|
||||
Remotes func() (context.Remotes, error)
|
||||
Config func() (config.Config, error)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,21 @@ func (r *Registry) StubWithFixture(status int, fixtureFileName string) func() {
|
|||
}
|
||||
}
|
||||
|
||||
func (r *Registry) StubWithFixturePath(status int, fixturePath string) func() {
|
||||
fixtureFile, err := os.Open(fixturePath)
|
||||
r.Register(MatchAny, func(req *http.Request) (*http.Response, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return httpResponse(200, req, fixtureFile), nil
|
||||
})
|
||||
return func() {
|
||||
if err == nil {
|
||||
fixtureFile.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Registry) StubRepoResponse(owner, repo string) {
|
||||
r.StubRepoResponseWithPermission(owner, repo, "WRITE")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ type IOStreams struct {
|
|||
stdinIsTTY bool
|
||||
stdoutTTYOverride bool
|
||||
stdoutIsTTY bool
|
||||
stderrTTYOverride bool
|
||||
stderrIsTTY bool
|
||||
}
|
||||
|
||||
func (s *IOStreams) ColorEnabled() bool {
|
||||
|
|
@ -57,6 +59,21 @@ func (s *IOStreams) IsStdoutTTY() bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func (s *IOStreams) SetStderrTTY(isTTY bool) {
|
||||
s.stderrTTYOverride = true
|
||||
s.stderrIsTTY = isTTY
|
||||
}
|
||||
|
||||
func (s *IOStreams) IsStderrTTY() bool {
|
||||
if s.stderrTTYOverride {
|
||||
return s.stderrIsTTY
|
||||
}
|
||||
if stderr, ok := s.ErrOut.(*os.File); ok {
|
||||
return isTerminal(stderr)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func System() *IOStreams {
|
||||
var out io.Writer = os.Stdout
|
||||
var colorEnabled bool
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue