From f3eb092d7e51ca96c65ae905d302c07c9e7d29aa Mon Sep 17 00:00:00 2001 From: vilmibm Date: Fri, 24 Jul 2020 14:57:03 -0500 Subject: [PATCH] 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. --- command/repo.go | 256 +-------- command/repo_test.go | 415 --------------- command/root.go | 6 + command/root_test.go | 4 +- pkg/cmd/repo/fork/fork.go | 292 +++++++++++ .../cmd/repo/fork}/forkResult.json | 0 pkg/cmd/repo/fork/fork_test.go | 484 ++++++++++++++++++ pkg/cmdutil/factory.go | 2 + pkg/httpmock/legacy.go | 15 + pkg/iostreams/iostreams.go | 17 + 10 files changed, 829 insertions(+), 662 deletions(-) delete mode 100644 command/repo_test.go create mode 100644 pkg/cmd/repo/fork/fork.go rename {test/fixtures => pkg/cmd/repo/fork}/forkResult.json (100%) create mode 100644 pkg/cmd/repo/fork/fork_test.go diff --git a/command/repo.go b/command/repo.go index c5a3934f6..9027b96d7 100644 --- a/command/repo.go +++ b/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 []", - 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 []", 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) } diff --git a/command/repo_test.go b/command/repo_test.go deleted file mode 100644 index 904d4c6bb..000000000 --- a/command/repo_test.go +++ /dev/null @@ -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 - } -} diff --git a/command/root.go b/command/root.go index 046946797..3bcb5b0cb 100644 --- a/command/root.go +++ b/command/root.go @@ -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)) } diff --git a/command/root_test.go b/command/root_test.go index 7ca938d6c..7d93169a4 100644 --- a/command/root_test.go +++ b/command/root_test.go @@ -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") } diff --git a/pkg/cmd/repo/fork/fork.go b/pkg/cmd/repo/fork/fork.go new file mode 100644 index 000000000..8b663857c --- /dev/null +++ b/pkg/cmd/repo/fork/fork.go @@ -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 []", + 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 +} diff --git a/test/fixtures/forkResult.json b/pkg/cmd/repo/fork/forkResult.json similarity index 100% rename from test/fixtures/forkResult.json rename to pkg/cmd/repo/fork/forkResult.json diff --git a/pkg/cmd/repo/fork/fork_test.go b/pkg/cmd/repo/fork/fork_test.go new file mode 100644 index 000000000..a9ef9b5c4 --- /dev/null +++ b/pkg/cmd/repo/fork/fork_test.go @@ -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 + } +} diff --git a/pkg/cmdutil/factory.go b/pkg/cmdutil/factory.go index 0c66a2b10..9a6cd5c6f 100644 --- a/pkg/cmdutil/factory.go +++ b/pkg/cmdutil/factory.go @@ -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) } diff --git a/pkg/httpmock/legacy.go b/pkg/httpmock/legacy.go index 2c69be824..0d42005d6 100644 --- a/pkg/httpmock/legacy.go +++ b/pkg/httpmock/legacy.go @@ -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") } diff --git a/pkg/iostreams/iostreams.go b/pkg/iostreams/iostreams.go index f2e31c439..a1ffdad32 100644 --- a/pkg/iostreams/iostreams.go +++ b/pkg/iostreams/iostreams.go @@ -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