From 38097e8cad324122d4b0af37528c0d04bc89d340 Mon Sep 17 00:00:00 2001 From: bchadwic Date: Mon, 6 Dec 2021 00:47:55 -0800 Subject: [PATCH 01/20] created `gh config base` a way to configure the base repo --- git/remote.go | 8 +++ pkg/cmd/config/base/base.go | 117 +++++++++++++++++++++++++++++++ pkg/cmd/config/base/base_test.go | 1 + pkg/cmd/config/config.go | 2 + 4 files changed, 128 insertions(+) create mode 100644 pkg/cmd/config/base/base.go create mode 100644 pkg/cmd/config/base/base_test.go diff --git a/git/remote.go b/git/remote.go index bea81da90..676027b3b 100644 --- a/git/remote.go +++ b/git/remote.go @@ -167,3 +167,11 @@ func SetRemoteResolution(name, resolution string) error { } return run.PrepareCmd(addCmd).Run() } + +func RemoveRemoteResolution(name string) error { + addCmd, err := GitCommand("config", "--unset-all", fmt.Sprintf("remote.%s.gh-resolved", name)) + if err != nil { + return err + } + return run.PrepareCmd(addCmd).Run() +} diff --git a/pkg/cmd/config/base/base.go b/pkg/cmd/config/base/base.go new file mode 100644 index 000000000..c0e9e8b46 --- /dev/null +++ b/pkg/cmd/config/base/base.go @@ -0,0 +1,117 @@ +package base + +import ( + "fmt" + "net/http" + + "github.com/MakeNowJust/heredoc" + "github.com/cli/cli/v2/api" + "github.com/cli/cli/v2/context" + "github.com/cli/cli/v2/git" + "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/spf13/cobra" +) + +type BaseOptions struct { + IO *iostreams.IOStreams + Remotes func() (context.Remotes, error) + BaseRepo func() (ghrepo.Interface, error) + HttpClient func() (*http.Client, error) + + RemoteName string + ViewFlag bool +} + +func NewCmdConfigBase(f *cmdutil.Factory, runF func(*BaseOptions) error) *cobra.Command { + opts := &BaseOptions{ + IO: f.IOStreams, + HttpClient: f.HttpClient, + BaseRepo: f.BaseRepo, + Remotes: f.Remotes, + } + + cmd := &cobra.Command{ + Use: "base ", + Short: "Configure the base repository used for various commands", + Long: heredoc.Doc(` + The base repository is used to determine which repository gh + should automatically query for the commands: + issue, pr, browse, run, repo rename, secret, workflow + `), + Example: heredoc.Doc(` + $ gh base upstream + `), + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) > 0 { + opts.RemoteName = args[0] + } + if err := cmdutil.MutuallyExclusive( + "args cannot be passed in with --view", + opts.RemoteName != "", + opts.ViewFlag, + ); err != nil { + return err + } + + if runF != nil { + return runF(opts) + } + return baseRun(opts) + }, + } + + cmd.Flags().BoolVarP(&opts.ViewFlag, "view", "v", false, "View the current configured default repository") + return cmd +} + +func baseRun(opts *BaseOptions) error { + remotes, err := opts.Remotes() + if err != nil { + return err + } + + if opts.ViewFlag { + for _, remote := range remotes { + if remote.Resolved == "base" { + fmt.Println(remote.Remote.Name) + return nil + } + } + return fmt.Errorf("base repo has not been specified yet") + } + + if opts.RemoteName != "" { + for _, remote := range remotes { + if opts.RemoteName == remote.Remote.Name { + removeBaseRepo(remotes) + git.SetRemoteResolution(remote.Name, "base") + return nil + } + } + return fmt.Errorf("could not find local remote name %s", opts.RemoteName) + } + removeBaseRepo(remotes) + httpClient, err := opts.HttpClient() + if err != nil { + return err + } + apiClient := api.NewClientFromHTTP(httpClient) + repoContext, err := context.ResolveRemotesToRepos(remotes, apiClient, "") + if err != nil { + return err + } + _, err = repoContext.BaseRepo(opts.IO) + return err +} + +func removeBaseRepo(remotes context.Remotes) { + for _, remote := range remotes { + if remote.Resolved == "base" { + remote.Resolved = "" + git.RemoveRemoteResolution(remote.Remote.Name) + } + } +} diff --git a/pkg/cmd/config/base/base_test.go b/pkg/cmd/config/base/base_test.go new file mode 100644 index 000000000..22f111144 --- /dev/null +++ b/pkg/cmd/config/base/base_test.go @@ -0,0 +1 @@ +package base diff --git a/pkg/cmd/config/config.go b/pkg/cmd/config/config.go index 2168516d3..03b8700ee 100644 --- a/pkg/cmd/config/config.go +++ b/pkg/cmd/config/config.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/cli/cli/v2/internal/config" + cmdBase "github.com/cli/cli/v2/pkg/cmd/config/base" cmdGet "github.com/cli/cli/v2/pkg/cmd/config/get" cmdList "github.com/cli/cli/v2/pkg/cmd/config/list" cmdSet "github.com/cli/cli/v2/pkg/cmd/config/set" @@ -32,6 +33,7 @@ func NewCmdConfig(f *cmdutil.Factory) *cobra.Command { cmdutil.DisableAuthCheck(cmd) + cmd.AddCommand(cmdBase.NewCmdConfigBase(f, nil)) cmd.AddCommand(cmdGet.NewCmdConfigGet(f, nil)) cmd.AddCommand(cmdSet.NewCmdConfigSet(f, nil)) cmd.AddCommand(cmdList.NewCmdConfigList(f, nil)) From a98395f4e8e0ecb42bc55a70f649e49241e41dbb Mon Sep 17 00:00:00 2001 From: bchadwic Date: Tue, 18 Jan 2022 22:35:36 -0800 Subject: [PATCH 02/20] headed in the right direction. Needs tests and to be cleaned up. This is a working example of the teams design combined with https://github.com/cli/cli/pull/4859#issuecomment-1012776582 --- git/remote.go | 2 +- pkg/cmd/config/config.go | 2 - .../base/base.go => repo/default/default.go} | 246 +++++++++--------- .../default/default_test.go} | 2 +- pkg/cmd/repo/repo.go | 2 + 5 files changed, 133 insertions(+), 121 deletions(-) rename pkg/cmd/{config/base/base.go => repo/default/default.go} (60%) rename pkg/cmd/{config/base/base_test.go => repo/default/default_test.go} (92%) diff --git a/git/remote.go b/git/remote.go index 676027b3b..df3a37201 100644 --- a/git/remote.go +++ b/git/remote.go @@ -168,7 +168,7 @@ func SetRemoteResolution(name, resolution string) error { return run.PrepareCmd(addCmd).Run() } -func RemoveRemoteResolution(name string) error { +func UnsetRemoteResolution(name string) error { addCmd, err := GitCommand("config", "--unset-all", fmt.Sprintf("remote.%s.gh-resolved", name)) if err != nil { return err diff --git a/pkg/cmd/config/config.go b/pkg/cmd/config/config.go index 03b8700ee..2168516d3 100644 --- a/pkg/cmd/config/config.go +++ b/pkg/cmd/config/config.go @@ -5,7 +5,6 @@ import ( "strings" "github.com/cli/cli/v2/internal/config" - cmdBase "github.com/cli/cli/v2/pkg/cmd/config/base" cmdGet "github.com/cli/cli/v2/pkg/cmd/config/get" cmdList "github.com/cli/cli/v2/pkg/cmd/config/list" cmdSet "github.com/cli/cli/v2/pkg/cmd/config/set" @@ -33,7 +32,6 @@ func NewCmdConfig(f *cmdutil.Factory) *cobra.Command { cmdutil.DisableAuthCheck(cmd) - cmd.AddCommand(cmdBase.NewCmdConfigBase(f, nil)) cmd.AddCommand(cmdGet.NewCmdConfigGet(f, nil)) cmd.AddCommand(cmdSet.NewCmdConfigSet(f, nil)) cmd.AddCommand(cmdList.NewCmdConfigList(f, nil)) diff --git a/pkg/cmd/config/base/base.go b/pkg/cmd/repo/default/default.go similarity index 60% rename from pkg/cmd/config/base/base.go rename to pkg/cmd/repo/default/default.go index c0e9e8b46..0481c39eb 100644 --- a/pkg/cmd/config/base/base.go +++ b/pkg/cmd/repo/default/default.go @@ -1,117 +1,129 @@ -package base - -import ( - "fmt" - "net/http" - - "github.com/MakeNowJust/heredoc" - "github.com/cli/cli/v2/api" - "github.com/cli/cli/v2/context" - "github.com/cli/cli/v2/git" - "github.com/cli/cli/v2/internal/ghrepo" - "github.com/cli/cli/v2/pkg/cmdutil" - "github.com/cli/cli/v2/pkg/iostreams" - "github.com/spf13/cobra" -) - -type BaseOptions struct { - IO *iostreams.IOStreams - Remotes func() (context.Remotes, error) - BaseRepo func() (ghrepo.Interface, error) - HttpClient func() (*http.Client, error) - - RemoteName string - ViewFlag bool -} - -func NewCmdConfigBase(f *cmdutil.Factory, runF func(*BaseOptions) error) *cobra.Command { - opts := &BaseOptions{ - IO: f.IOStreams, - HttpClient: f.HttpClient, - BaseRepo: f.BaseRepo, - Remotes: f.Remotes, - } - - cmd := &cobra.Command{ - Use: "base ", - Short: "Configure the base repository used for various commands", - Long: heredoc.Doc(` - The base repository is used to determine which repository gh - should automatically query for the commands: - issue, pr, browse, run, repo rename, secret, workflow - `), - Example: heredoc.Doc(` - $ gh base upstream - `), - Args: cobra.MaximumNArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - if len(args) > 0 { - opts.RemoteName = args[0] - } - if err := cmdutil.MutuallyExclusive( - "args cannot be passed in with --view", - opts.RemoteName != "", - opts.ViewFlag, - ); err != nil { - return err - } - - if runF != nil { - return runF(opts) - } - return baseRun(opts) - }, - } - - cmd.Flags().BoolVarP(&opts.ViewFlag, "view", "v", false, "View the current configured default repository") - return cmd -} - -func baseRun(opts *BaseOptions) error { - remotes, err := opts.Remotes() - if err != nil { - return err - } - - if opts.ViewFlag { - for _, remote := range remotes { - if remote.Resolved == "base" { - fmt.Println(remote.Remote.Name) - return nil - } - } - return fmt.Errorf("base repo has not been specified yet") - } - - if opts.RemoteName != "" { - for _, remote := range remotes { - if opts.RemoteName == remote.Remote.Name { - removeBaseRepo(remotes) - git.SetRemoteResolution(remote.Name, "base") - return nil - } - } - return fmt.Errorf("could not find local remote name %s", opts.RemoteName) - } - removeBaseRepo(remotes) - httpClient, err := opts.HttpClient() - if err != nil { - return err - } - apiClient := api.NewClientFromHTTP(httpClient) - repoContext, err := context.ResolveRemotesToRepos(remotes, apiClient, "") - if err != nil { - return err - } - _, err = repoContext.BaseRepo(opts.IO) - return err -} - -func removeBaseRepo(remotes context.Remotes) { - for _, remote := range remotes { - if remote.Resolved == "base" { - remote.Resolved = "" - git.RemoveRemoteResolution(remote.Remote.Name) - } - } -} +package base + +import ( + "fmt" + "net/http" + "strings" + + "github.com/MakeNowJust/heredoc" + "github.com/cli/cli/v2/api" + "github.com/cli/cli/v2/context" + "github.com/cli/cli/v2/git" + "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/spf13/cobra" +) + +type DefaultOptions struct { + IO *iostreams.IOStreams + Remotes func() (context.Remotes, error) + BaseRepo func() (ghrepo.Interface, error) + HttpClient func() (*http.Client, error) + + RemoteName string + ListFlag bool +} + +func NewCmdDefault(f *cmdutil.Factory, runF func(*DefaultOptions) error) *cobra.Command { + opts := &DefaultOptions{ + IO: f.IOStreams, + HttpClient: f.HttpClient, + BaseRepo: f.BaseRepo, + Remotes: f.Remotes, + } + + cmd := &cobra.Command{ + Use: "default ", + Short: "Configure the default repository used for various commands", + Long: heredoc.Doc(` + The default repository is used to determine which repository gh + should automatically query for the commands: + issue, pr, browse, run, repo rename, secret, workflow + `), + Example: heredoc.Doc(` + $ gh repo default upstream + `), + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) > 0 { + opts.RemoteName = args[0] + } + if !opts.IO.CanPrompt() && !opts.ListFlag && opts.RemoteName == "" { + return cmdutil.FlagErrorf("remote name is required when not running interactively, use `--list` to see available remotes") + } + if err := cmdutil.MutuallyExclusive( + "args cannot be passed in with --list", + opts.RemoteName != "", + opts.ListFlag, + ); err != nil { + return err + } + + if runF != nil { + return runF(opts) + } + return defaultRun(opts) + }, + } + + cmd.Flags().BoolVarP(&opts.ListFlag, "list", "l", false, "list the current and available repositories") + return cmd +} + +func defaultRun(opts *DefaultOptions) error { + remotes, err := opts.Remotes() + if err != nil { + return err + } + + if opts.ListFlag { + found := false + list := &strings.Builder{} + for _, remote := range remotes { + if remote.Resolved == "base" { + list.WriteString(fmt.Sprintf("* %s\n", remote.Remote.Name)) + found = true + } else { + list.WriteString(fmt.Sprintf(" %s\n", remote.Remote.Name)) + } + } + if !found { + fmt.Fprint(opts.IO.Out, "the default repo has not been set\n") + } + fmt.Fprint(opts.IO.Out, list.String()) + return nil + } + + if opts.RemoteName != "" { + for _, remote := range remotes { + if opts.RemoteName == remote.Remote.Name { + removeBaseRepo(remotes) + git.SetRemoteResolution(remote.Name, "base") + return nil + } + } + return fmt.Errorf("could not find local remote name %s", opts.RemoteName) + } + removeBaseRepo(remotes) + httpClient, err := opts.HttpClient() + if err != nil { + return err + } + apiClient := api.NewClientFromHTTP(httpClient) + repoContext, err := context.ResolveRemotesToRepos(remotes, apiClient, "") + if err != nil { + return err + } + _, err = repoContext.BaseRepo(opts.IO) + return err +} + +func removeBaseRepo(remotes context.Remotes) { + for _, remote := range remotes { + if remote.Resolved == "base" { + remote.Resolved = "" + git.UnsetRemoteResolution(remote.Remote.Name) + } + } +} diff --git a/pkg/cmd/config/base/base_test.go b/pkg/cmd/repo/default/default_test.go similarity index 92% rename from pkg/cmd/config/base/base_test.go rename to pkg/cmd/repo/default/default_test.go index 22f111144..66fa1b529 100644 --- a/pkg/cmd/config/base/base_test.go +++ b/pkg/cmd/repo/default/default_test.go @@ -1 +1 @@ -package base +package base diff --git a/pkg/cmd/repo/repo.go b/pkg/cmd/repo/repo.go index e8696d7d9..d3dce190d 100644 --- a/pkg/cmd/repo/repo.go +++ b/pkg/cmd/repo/repo.go @@ -6,6 +6,7 @@ import ( repoCloneCmd "github.com/cli/cli/v2/pkg/cmd/repo/clone" repoCreateCmd "github.com/cli/cli/v2/pkg/cmd/repo/create" creditsCmd "github.com/cli/cli/v2/pkg/cmd/repo/credits" + repoDefaultCmd "github.com/cli/cli/v2/pkg/cmd/repo/default" repoDeleteCmd "github.com/cli/cli/v2/pkg/cmd/repo/delete" deployKeyCmd "github.com/cli/cli/v2/pkg/cmd/repo/deploy-key" repoEditCmd "github.com/cli/cli/v2/pkg/cmd/repo/edit" @@ -52,6 +53,7 @@ func NewCmdRepo(f *cmdutil.Factory) *cobra.Command { cmd.AddCommand(repoRenameCmd.NewCmdRename(f, nil)) cmd.AddCommand(repoDeleteCmd.NewCmdDelete(f, nil)) cmd.AddCommand(repoArchiveCmd.NewCmdArchive(f, nil)) + cmd.AddCommand(repoDefaultCmd.NewCmdDefault(f, nil)) return cmd } From ca5e9a49b58164256ab95bdb0d024cd65f4cb648 Mon Sep 17 00:00:00 2001 From: bchadwic Date: Mon, 7 Mar 2022 21:30:58 -0800 Subject: [PATCH 03/20] thinking of a better solution, wip. BaseRepo() might be better split up --- context/context.go | 87 +++++++++++++ git/remote.go | 4 +- pkg/cmd/base/base.go | 43 +++++++ pkg/cmd/repo/default/default.go | 216 +++++++++++++------------------- 4 files changed, 219 insertions(+), 131 deletions(-) create mode 100644 pkg/cmd/base/base.go diff --git a/context/context.go b/context/context.go index 40a9383a3..7b6863def 100644 --- a/context/context.go +++ b/context/context.go @@ -59,6 +59,93 @@ type ResolvedRemotes struct { apiClient *api.Client } +func GetBaseRepo(remotes Remotes) (ghrepo.Interface, error) { + for _, r := range remotes { + if r.Resolved == "base" { + return r, nil + } else if r.Resolved != "" { + repo, err := ghrepo.FromFullName(r.Resolved) + if err != nil { + return nil, err + } + return ghrepo.NewWithHost(repo.RepoOwner(), repo.RepoName(), r.RepoHost()), nil + } + } + return nil, errors.New("a default repo has not been set, use `gh repo default` to set a default repo") +} + +func RemoveBaseRepo(remotes Remotes) { + for _, remote := range remotes { + if remote.Resolved == "base" { + remote.Resolved = "" + git.UnsetRemoteResolution(remote.Remote.Name) + } + } +} + +func (r *ResolvedRemotes) SetGitConfigBaseRepo(io *iostreams.IOStreams) error { + resolution := "base" + if !io.CanPrompt() { + git.SetRemoteResolution(r.remotes[0].Name, resolution) + return nil + } + + // from here on, consult the API + if r.network == nil { + err := resolveNetwork(r) + if err != nil { + return err + } + } + + var repoNames []string + repoMap := map[string]*api.Repository{} + add := func(r *api.Repository) { + fn := ghrepo.FullName(r) + if _, ok := repoMap[fn]; !ok { + repoMap[fn] = r + repoNames = append(repoNames, fn) + } + } + + for _, repo := range r.network.Repositories { + if repo == nil { + continue + } + if repo.Parent != nil { + add(repo.Parent) + } + add(repo) + } + + if len(repoNames) == 0 { + git.SetRemoteResolution(r.remotes[0].Name, resolution) + return nil + } + + baseName := repoNames[0] + if len(repoNames) > 1 { + err := prompt.SurveyAskOne(&survey.Select{ + Message: "Which should be the base repository (used for e.g. querying issues) for this directory?", + Options: repoNames, + }, &baseName) + if err != nil { + return err + } + } + + // determine corresponding git remote + selectedRepo := repoMap[baseName] + remote, _ := r.RemoteForRepo(selectedRepo) + if remote == nil { + remote = r.remotes[0] + resolution = ghrepo.FullName(selectedRepo) + } + + // cache the result to git config + return git.SetRemoteResolution(remote.Name, resolution) +} + func (r *ResolvedRemotes) BaseRepo(io *iostreams.IOStreams) (ghrepo.Interface, error) { if r.baseOverride != nil { return r.baseOverride, nil diff --git a/git/remote.go b/git/remote.go index df3a37201..423bd17cb 100644 --- a/git/remote.go +++ b/git/remote.go @@ -169,9 +169,9 @@ func SetRemoteResolution(name, resolution string) error { } func UnsetRemoteResolution(name string) error { - addCmd, err := GitCommand("config", "--unset-all", fmt.Sprintf("remote.%s.gh-resolved", name)) + unsetCmd, err := GitCommand("config", "--unset-all", fmt.Sprintf("remote.%s.gh-resolved", name)) if err != nil { return err } - return run.PrepareCmd(addCmd).Run() + return run.PrepareCmd(unsetCmd).Run() } diff --git a/pkg/cmd/base/base.go b/pkg/cmd/base/base.go new file mode 100644 index 000000000..d7650a36d --- /dev/null +++ b/pkg/cmd/base/base.go @@ -0,0 +1,43 @@ +package base + +import ( + "net/http" + + "github.com/AlecAivazis/survey/v2" + "github.com/cli/cli/v2/context" + "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/cli/cli/v2/pkg/prompt" + + "github.com/cli/cli/v2/api" +) + +func GetAllResolvedRemotes(Remotes func() (context.Remotes, error), HttpClient func() (*http.Client, error)) ([]func(ghrepo.Interface, error), error) { + // return []func() (ghrepo.Interface, error) { + httpClient, err := HttpClient() + if err != nil { + return nil, err + } + + apiClient := api.NewClientFromHTTP(httpClient) + + remotes, err := Remotes() + if err != nil { + return nil, err + } + + repoContext, err := context.ResolveRemotesToRepos(remotes, apiClient, "") + if err != nil { + return nil, err + } + + repoContext.GetBaseRepo() + // baseRepo, err := repoContext.BaseRepo(f.IOStreams) + // if err != nil { + // return nil, err + // } + + // return baseRepo, nil + // } + // urn nil +} diff --git a/pkg/cmd/repo/default/default.go b/pkg/cmd/repo/default/default.go index 0481c39eb..6b1926fd2 100644 --- a/pkg/cmd/repo/default/default.go +++ b/pkg/cmd/repo/default/default.go @@ -1,129 +1,87 @@ -package base - -import ( - "fmt" - "net/http" - "strings" - - "github.com/MakeNowJust/heredoc" - "github.com/cli/cli/v2/api" - "github.com/cli/cli/v2/context" - "github.com/cli/cli/v2/git" - "github.com/cli/cli/v2/internal/ghrepo" - "github.com/cli/cli/v2/pkg/cmdutil" - "github.com/cli/cli/v2/pkg/iostreams" - "github.com/spf13/cobra" -) - -type DefaultOptions struct { - IO *iostreams.IOStreams - Remotes func() (context.Remotes, error) - BaseRepo func() (ghrepo.Interface, error) - HttpClient func() (*http.Client, error) - - RemoteName string - ListFlag bool -} - -func NewCmdDefault(f *cmdutil.Factory, runF func(*DefaultOptions) error) *cobra.Command { - opts := &DefaultOptions{ - IO: f.IOStreams, - HttpClient: f.HttpClient, - BaseRepo: f.BaseRepo, - Remotes: f.Remotes, - } - - cmd := &cobra.Command{ - Use: "default ", - Short: "Configure the default repository used for various commands", - Long: heredoc.Doc(` - The default repository is used to determine which repository gh - should automatically query for the commands: - issue, pr, browse, run, repo rename, secret, workflow - `), - Example: heredoc.Doc(` - $ gh repo default upstream - `), - Args: cobra.MaximumNArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - if len(args) > 0 { - opts.RemoteName = args[0] - } - if !opts.IO.CanPrompt() && !opts.ListFlag && opts.RemoteName == "" { - return cmdutil.FlagErrorf("remote name is required when not running interactively, use `--list` to see available remotes") - } - if err := cmdutil.MutuallyExclusive( - "args cannot be passed in with --list", - opts.RemoteName != "", - opts.ListFlag, - ); err != nil { - return err - } - - if runF != nil { - return runF(opts) - } - return defaultRun(opts) - }, - } - - cmd.Flags().BoolVarP(&opts.ListFlag, "list", "l", false, "list the current and available repositories") - return cmd -} - -func defaultRun(opts *DefaultOptions) error { - remotes, err := opts.Remotes() - if err != nil { - return err - } - - if opts.ListFlag { - found := false - list := &strings.Builder{} - for _, remote := range remotes { - if remote.Resolved == "base" { - list.WriteString(fmt.Sprintf("* %s\n", remote.Remote.Name)) - found = true - } else { - list.WriteString(fmt.Sprintf(" %s\n", remote.Remote.Name)) - } - } - if !found { - fmt.Fprint(opts.IO.Out, "the default repo has not been set\n") - } - fmt.Fprint(opts.IO.Out, list.String()) - return nil - } - - if opts.RemoteName != "" { - for _, remote := range remotes { - if opts.RemoteName == remote.Remote.Name { - removeBaseRepo(remotes) - git.SetRemoteResolution(remote.Name, "base") - return nil - } - } - return fmt.Errorf("could not find local remote name %s", opts.RemoteName) - } - removeBaseRepo(remotes) - httpClient, err := opts.HttpClient() - if err != nil { - return err - } - apiClient := api.NewClientFromHTTP(httpClient) - repoContext, err := context.ResolveRemotesToRepos(remotes, apiClient, "") - if err != nil { - return err - } - _, err = repoContext.BaseRepo(opts.IO) - return err -} - -func removeBaseRepo(remotes context.Remotes) { - for _, remote := range remotes { - if remote.Resolved == "base" { - remote.Resolved = "" - git.UnsetRemoteResolution(remote.Remote.Name) - } - } -} +package base + +import ( + "fmt" + "net/http" + + "github.com/MakeNowJust/heredoc" + "github.com/cli/cli/v2/api" + "github.com/cli/cli/v2/context" + "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/spf13/cobra" +) + +type DefaultOptions struct { + IO *iostreams.IOStreams + BaseRepo func() (ghrepo.Interface, error) + Remotes func() (context.Remotes, error) + HttpClient func() (*http.Client, error) + + ViewFlag bool +} + +func NewCmdDefault(f *cmdutil.Factory, runF func(*DefaultOptions) error) *cobra.Command { + opts := &DefaultOptions{ + IO: f.IOStreams, + HttpClient: f.HttpClient, + BaseRepo: f.BaseRepo, + Remotes: f.Remotes, + } + + cmd := &cobra.Command{ + Use: "default", + Short: "Configure the default repository used for various commands", + Long: heredoc.Doc(` + The default repository is used to determine which repository gh + should automatically query for the commands: + issue, pr, browse, run, repo rename, secret, workflow + `), + Example: heredoc.Doc(` + $ gh repo default cli/cli + `), + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + if !opts.IO.CanPrompt() && !opts.ViewFlag { + return cmdutil.FlagErrorf("a repository name is required when not running interactively") + } + + if runF != nil { + return runF(opts) + } + return defaultRun(opts) + }, + } + + cmd.Flags().BoolVarP(&opts.ViewFlag, "view", "v", false, "view the default repository used for various commands") + return cmd +} + +func defaultRun(opts *DefaultOptions) error { + remotes, err := opts.Remotes() + if err != nil { + return err + } + + if opts.ViewFlag { + baseRepo, err := context.GetBaseRepo(remotes) + if err != nil { + return err + } + fmt.Fprintln(opts.IO.Out, ghrepo.FullName(baseRepo)) + return nil + } + + httpClient, err := opts.HttpClient() + if err != nil { + return err + } + apiClient := api.NewClientFromHTTP(httpClient) + context.RemoveBaseRepo(remotes) + repoContext, err := context.ResolveRemotesToRepos(remotes, apiClient, "") + if err != nil { + return err + } + return repoContext.SetGitConfigBaseRepo(opts.IO) +} From a140bea9bf145698e6ed8822339c7d27406f455e Mon Sep 17 00:00:00 2001 From: Benjamin Chadwick <73488828+bchadwic@users.noreply.github.com> Date: Mon, 7 Mar 2022 21:32:51 -0800 Subject: [PATCH 04/20] deleted scratch pad of commit --- pkg/cmd/base/base.go | 43 ------------------------------------------- 1 file changed, 43 deletions(-) delete mode 100644 pkg/cmd/base/base.go diff --git a/pkg/cmd/base/base.go b/pkg/cmd/base/base.go deleted file mode 100644 index d7650a36d..000000000 --- a/pkg/cmd/base/base.go +++ /dev/null @@ -1,43 +0,0 @@ -package base - -import ( - "net/http" - - "github.com/AlecAivazis/survey/v2" - "github.com/cli/cli/v2/context" - "github.com/cli/cli/v2/internal/ghrepo" - "github.com/cli/cli/v2/pkg/iostreams" - "github.com/cli/cli/v2/pkg/prompt" - - "github.com/cli/cli/v2/api" -) - -func GetAllResolvedRemotes(Remotes func() (context.Remotes, error), HttpClient func() (*http.Client, error)) ([]func(ghrepo.Interface, error), error) { - // return []func() (ghrepo.Interface, error) { - httpClient, err := HttpClient() - if err != nil { - return nil, err - } - - apiClient := api.NewClientFromHTTP(httpClient) - - remotes, err := Remotes() - if err != nil { - return nil, err - } - - repoContext, err := context.ResolveRemotesToRepos(remotes, apiClient, "") - if err != nil { - return nil, err - } - - repoContext.GetBaseRepo() - // baseRepo, err := repoContext.BaseRepo(f.IOStreams) - // if err != nil { - // return nil, err - // } - - // return baseRepo, nil - // } - // urn nil -} From 08b02da3fc4a39064e8efe8716280017886f81c0 Mon Sep 17 00:00:00 2001 From: bchadwic Date: Wed, 23 Mar 2022 21:29:57 -0700 Subject: [PATCH 05/20] beginning testing --- context/context.go | 19 +++--- pkg/cmd/repo/default/default.go | 24 +++----- pkg/cmd/repo/default/default_test.go | 87 +++++++++++++++++++++++++++- pkg/cmd/repo/repo.go | 2 +- 4 files changed, 103 insertions(+), 29 deletions(-) diff --git a/context/context.go b/context/context.go index 7b6863def..f596d8810 100644 --- a/context/context.go +++ b/context/context.go @@ -74,16 +74,7 @@ func GetBaseRepo(remotes Remotes) (ghrepo.Interface, error) { return nil, errors.New("a default repo has not been set, use `gh repo default` to set a default repo") } -func RemoveBaseRepo(remotes Remotes) { - for _, remote := range remotes { - if remote.Resolved == "base" { - remote.Resolved = "" - git.UnsetRemoteResolution(remote.Remote.Name) - } - } -} - -func (r *ResolvedRemotes) SetGitConfigBaseRepo(io *iostreams.IOStreams) error { +func (r *ResolvedRemotes) SetBaseRepo(io *iostreams.IOStreams) error { resolution := "base" if !io.CanPrompt() { git.SetRemoteResolution(r.remotes[0].Name, resolution) @@ -146,6 +137,14 @@ func (r *ResolvedRemotes) SetGitConfigBaseRepo(io *iostreams.IOStreams) error { return git.SetRemoteResolution(remote.Name, resolution) } +func RemoveBaseRepo(remotes Remotes) { + for _, remote := range remotes { + if remote.Resolved == "base" { + git.UnsetRemoteResolution(remote.Remote.Name) + } + } +} + func (r *ResolvedRemotes) BaseRepo(io *iostreams.IOStreams) (ghrepo.Interface, error) { if r.baseOverride != nil { return r.baseOverride, nil diff --git a/pkg/cmd/repo/default/default.go b/pkg/cmd/repo/default/default.go index 6b1926fd2..22b59bdf6 100644 --- a/pkg/cmd/repo/default/default.go +++ b/pkg/cmd/repo/default/default.go @@ -15,18 +15,16 @@ import ( type DefaultOptions struct { IO *iostreams.IOStreams - BaseRepo func() (ghrepo.Interface, error) Remotes func() (context.Remotes, error) HttpClient func() (*http.Client, error) ViewFlag bool } -func NewCmdDefault(f *cmdutil.Factory, runF func(*DefaultOptions) error) *cobra.Command { +func NewCmdDefault(f *cmdutil.Factory) *cobra.Command { opts := &DefaultOptions{ IO: f.IOStreams, HttpClient: f.HttpClient, - BaseRepo: f.BaseRepo, Remotes: f.Remotes, } @@ -34,23 +32,15 @@ func NewCmdDefault(f *cmdutil.Factory, runF func(*DefaultOptions) error) *cobra. Use: "default", Short: "Configure the default repository used for various commands", Long: heredoc.Doc(` - The default repository is used to determine which repository gh - should automatically query for the commands: - issue, pr, browse, run, repo rename, secret, workflow + The default repository is used to determine which remote + repository gh should automatically point to. `), Example: heredoc.Doc(` $ gh repo default cli/cli `), Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - if !opts.IO.CanPrompt() && !opts.ViewFlag { - return cmdutil.FlagErrorf("a repository name is required when not running interactively") - } - - if runF != nil { - return runF(opts) - } - return defaultRun(opts) + return runDefault(opts) }, } @@ -58,7 +48,7 @@ func NewCmdDefault(f *cmdutil.Factory, runF func(*DefaultOptions) error) *cobra. return cmd } -func defaultRun(opts *DefaultOptions) error { +func runDefault(opts *DefaultOptions) error { remotes, err := opts.Remotes() if err != nil { return err @@ -78,10 +68,10 @@ func defaultRun(opts *DefaultOptions) error { return err } apiClient := api.NewClientFromHTTP(httpClient) - context.RemoveBaseRepo(remotes) repoContext, err := context.ResolveRemotesToRepos(remotes, apiClient, "") if err != nil { return err } - return repoContext.SetGitConfigBaseRepo(opts.IO) + context.RemoveBaseRepo(remotes) + return repoContext.SetBaseRepo(opts.IO) } diff --git a/pkg/cmd/repo/default/default_test.go b/pkg/cmd/repo/default/default_test.go index 66fa1b529..5bc6db460 100644 --- a/pkg/cmd/repo/default/default_test.go +++ b/pkg/cmd/repo/default/default_test.go @@ -1 +1,86 @@ -package base +package base + +import ( + "errors" + "fmt" + "net/http" + "testing" + + "github.com/cli/cli/v2/context" + "github.com/cli/cli/v2/git" + "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/pkg/httpmock" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/stretchr/testify/assert" +) + +func Test_defaultRun(t *testing.T) { + tests := []struct { + name string + opts DefaultOptions + wantedErr error + wantedStdOut string + }{ + { + opts: DefaultOptions{ + Remotes: func() (context.Remotes, error) { + return []*context.Remote{ + { + Remote: &git.Remote{ + Name: "origin", + Resolved: "base", + }, + Repo: ghrepo.New("hubot", "Spoon-Knife"), + }, + }, nil + }, + ViewFlag: true, + }, + wantedStdOut: "hubot/Spoon-Knife", + }, + { + opts: DefaultOptions{ + Remotes: func() (context.Remotes, error) { + return []*context.Remote{ + { + Remote: &git.Remote{ + Name: "origin", + }, + Repo: ghrepo.New("hubot", "Spoon-Knife"), + }, + }, nil + }, + ViewFlag: true, + }, + wantedErr: errors.New("a default repo has not been set, use `gh repo default` to set a default repo"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + io, _, stdout, stderr := iostreams.Test() + + reg := httpmock.Registry{} + defer reg.Verify(t) + + opts := tt.opts + opts.IO = io + opts.HttpClient = func() (*http.Client, error) { + return &http.Client{Transport: ®}, nil + } + + err := runDefault(&opts) + if tt.wantedErr != nil { + assert.EqualError(t, tt.wantedErr, err.Error()) + } else { + assert.NoError(t, err) + if opts.ViewFlag { + assert.Equal(t, fmt.Sprintf("%s\n", tt.wantedStdOut), stdout.String()) + } else { + assert.Equal(t, "", stdout.String()) + assert.Equal(t, "", stderr.String()) + } + } + }) + } +} diff --git a/pkg/cmd/repo/repo.go b/pkg/cmd/repo/repo.go index d3dce190d..01cae54b4 100644 --- a/pkg/cmd/repo/repo.go +++ b/pkg/cmd/repo/repo.go @@ -53,7 +53,7 @@ func NewCmdRepo(f *cmdutil.Factory) *cobra.Command { cmd.AddCommand(repoRenameCmd.NewCmdRename(f, nil)) cmd.AddCommand(repoDeleteCmd.NewCmdDelete(f, nil)) cmd.AddCommand(repoArchiveCmd.NewCmdArchive(f, nil)) - cmd.AddCommand(repoDefaultCmd.NewCmdDefault(f, nil)) + cmd.AddCommand(repoDefaultCmd.NewCmdDefault(f)) return cmd } From 86658513b1d1d478b5569cefdf56a6e3d4fed769 Mon Sep 17 00:00:00 2001 From: bchadwic Date: Tue, 5 Apr 2022 18:07:27 -0700 Subject: [PATCH 06/20] Created more test cases, added in env section --- git/fixtures/simple.git/config | 6 +++ pkg/cmd/repo/default/default.go | 16 ++++++- pkg/cmd/repo/default/default_test.go | 65 +++++++++++++++++++++++----- 3 files changed, 74 insertions(+), 13 deletions(-) diff --git a/git/fixtures/simple.git/config b/git/fixtures/simple.git/config index f0858dd73..54c13e66c 100644 --- a/git/fixtures/simple.git/config +++ b/git/fixtures/simple.git/config @@ -7,3 +7,9 @@ [user] name = Mona the Cat email = monalisa@github.com +[remote "origin"] + url = git@github.com:monathecat/cli.git + fetch = +refs/heads/*:refs/remotes/origin/* +[remote "upstream"] + url = git@github.com:cli/cli.git + fetch = +refs/heads/trunk:refs/remotes/upstream/trunk diff --git a/pkg/cmd/repo/default/default.go b/pkg/cmd/repo/default/default.go index 22b59bdf6..3b3002034 100644 --- a/pkg/cmd/repo/default/default.go +++ b/pkg/cmd/repo/default/default.go @@ -36,8 +36,22 @@ func NewCmdDefault(f *cmdutil.Factory) *cobra.Command { repository gh should automatically point to. `), Example: heredoc.Doc(` - $ gh repo default cli/cli + $ gh repo default + #=> prompts remote options + + $ gh repo default -v + #=> cli/cli `), + Annotations: map[string]string{ + "help:environment": heredoc.Doc(` + To manually configure a remote for gh to use, modify your local repo's git config + + ; Ex: setting gh to use the upstream remote + [remote "upstream"] + gh-resolved = base + ... + `), + }, Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { return runDefault(opts) diff --git a/pkg/cmd/repo/default/default_test.go b/pkg/cmd/repo/default/default_test.go index 5bc6db460..fa32255f6 100644 --- a/pkg/cmd/repo/default/default_test.go +++ b/pkg/cmd/repo/default/default_test.go @@ -4,24 +4,27 @@ import ( "errors" "fmt" "net/http" + "os" "testing" "github.com/cli/cli/v2/context" "github.com/cli/cli/v2/git" "github.com/cli/cli/v2/internal/ghrepo" - "github.com/cli/cli/v2/pkg/httpmock" "github.com/cli/cli/v2/pkg/iostreams" "github.com/stretchr/testify/assert" ) func Test_defaultRun(t *testing.T) { + setGitDir(t, "../../../../git/fixtures/simple.git") tests := []struct { - name string - opts DefaultOptions - wantedErr error - wantedStdOut string + name string + opts DefaultOptions + wantedErr error + wantedStdOut string + wantedResolvedName string }{ { + name: "Base repo set with view option", opts: DefaultOptions{ Remotes: func() (context.Remotes, error) { return []*context.Remote{ @@ -39,6 +42,7 @@ func Test_defaultRun(t *testing.T) { wantedStdOut: "hubot/Spoon-Knife", }, { + name: "Base repo not set with view option", opts: DefaultOptions{ Remotes: func() (context.Remotes, error) { return []*context.Remote{ @@ -46,7 +50,6 @@ func Test_defaultRun(t *testing.T) { Remote: &git.Remote{ Name: "origin", }, - Repo: ghrepo.New("hubot", "Spoon-Knife"), }, }, nil }, @@ -54,20 +57,35 @@ func Test_defaultRun(t *testing.T) { }, wantedErr: errors.New("a default repo has not been set, use `gh repo default` to set a default repo"), }, + { + name: "Base repo not set, assign non-interactively", + opts: DefaultOptions{ + Remotes: func() (context.Remotes, error) { + return []*context.Remote{ + { + Remote: &git.Remote{ + Name: "origin", + }, + }, + { + Remote: &git.Remote{ + Name: "upstream", + }, + }, + }, nil + }, + }, + wantedResolvedName: "upstream", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { io, _, stdout, stderr := iostreams.Test() - reg := httpmock.Registry{} - defer reg.Verify(t) - opts := tt.opts opts.IO = io - opts.HttpClient = func() (*http.Client, error) { - return &http.Client{Transport: ®}, nil - } + opts.HttpClient = func() (*http.Client, error) { return nil, nil } err := runDefault(&opts) if tt.wantedErr != nil { @@ -81,6 +99,29 @@ func Test_defaultRun(t *testing.T) { assert.Equal(t, "", stderr.String()) } } + if tt.wantedResolvedName != "" { + resolvedAmount := 0 + remotes, err := git.Remotes() + if err != nil { + panic(err) + } + for _, r := range remotes { + if r.Resolved == "base" { + assert.Equal(t, r.Name, tt.wantedResolvedName) + resolvedAmount++ + } + } + assert.Equal(t, 1, resolvedAmount) + } }) } } + +func setGitDir(t *testing.T, dir string) { + old_GIT_DIR := os.Getenv("GIT_DIR") + os.Setenv("GIT_DIR", dir) + t.Cleanup(func() { + git.UnsetRemoteResolution("upstream") + os.Setenv("GIT_DIR", old_GIT_DIR) + }) +} From 66a69bdec5dfc0a2a3d2522f4252904e20672870 Mon Sep 17 00:00:00 2001 From: bchadwic Date: Tue, 5 Apr 2022 21:35:38 -0700 Subject: [PATCH 07/20] fixing lint and formatting --- context/context.go | 13 +++++++------ git/fixtures/simple.git/config | 4 ++-- pkg/cmd/repo/default/default_test.go | 2 +- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/context/context.go b/context/context.go index f596d8810..d2006f97b 100644 --- a/context/context.go +++ b/context/context.go @@ -77,8 +77,7 @@ func GetBaseRepo(remotes Remotes) (ghrepo.Interface, error) { func (r *ResolvedRemotes) SetBaseRepo(io *iostreams.IOStreams) error { resolution := "base" if !io.CanPrompt() { - git.SetRemoteResolution(r.remotes[0].Name, resolution) - return nil + return git.SetRemoteResolution(r.remotes[0].Name, resolution) } // from here on, consult the API @@ -110,8 +109,7 @@ func (r *ResolvedRemotes) SetBaseRepo(io *iostreams.IOStreams) error { } if len(repoNames) == 0 { - git.SetRemoteResolution(r.remotes[0].Name, resolution) - return nil + return git.SetRemoteResolution(r.remotes[0].Name, resolution) } baseName := repoNames[0] @@ -137,12 +135,15 @@ func (r *ResolvedRemotes) SetBaseRepo(io *iostreams.IOStreams) error { return git.SetRemoteResolution(remote.Name, resolution) } -func RemoveBaseRepo(remotes Remotes) { +func RemoveBaseRepo(remotes Remotes) error { for _, remote := range remotes { if remote.Resolved == "base" { - git.UnsetRemoteResolution(remote.Remote.Name) + if err := git.UnsetRemoteResolution(remote.Remote.Name); err != nil { + return err + } } } + return nil } func (r *ResolvedRemotes) BaseRepo(io *iostreams.IOStreams) (ghrepo.Interface, error) { diff --git a/git/fixtures/simple.git/config b/git/fixtures/simple.git/config index 54c13e66c..51cb0d228 100644 --- a/git/fixtures/simple.git/config +++ b/git/fixtures/simple.git/config @@ -9,7 +9,7 @@ email = monalisa@github.com [remote "origin"] url = git@github.com:monathecat/cli.git - fetch = +refs/heads/*:refs/remotes/origin/* + fetch = +refs/heads/*:refs/remotes/origin/* [remote "upstream"] url = git@github.com:cli/cli.git - fetch = +refs/heads/trunk:refs/remotes/upstream/trunk + fetch = +refs/heads/trunk:refs/remotes/upstream/trunk diff --git a/pkg/cmd/repo/default/default_test.go b/pkg/cmd/repo/default/default_test.go index fa32255f6..f4fa9bdbc 100644 --- a/pkg/cmd/repo/default/default_test.go +++ b/pkg/cmd/repo/default/default_test.go @@ -121,7 +121,7 @@ func setGitDir(t *testing.T, dir string) { old_GIT_DIR := os.Getenv("GIT_DIR") os.Setenv("GIT_DIR", dir) t.Cleanup(func() { - git.UnsetRemoteResolution("upstream") + _ = git.UnsetRemoteResolution("upstream") os.Setenv("GIT_DIR", old_GIT_DIR) }) } From e5ebbcb66abd594845db4e883832fe491ff5935c Mon Sep 17 00:00:00 2001 From: bchadwic Date: Tue, 5 Apr 2022 21:39:51 -0700 Subject: [PATCH 08/20] lint test update --- pkg/cmd/repo/default/default_test.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/repo/default/default_test.go b/pkg/cmd/repo/default/default_test.go index f4fa9bdbc..e76734ff0 100644 --- a/pkg/cmd/repo/default/default_test.go +++ b/pkg/cmd/repo/default/default_test.go @@ -121,7 +121,9 @@ func setGitDir(t *testing.T, dir string) { old_GIT_DIR := os.Getenv("GIT_DIR") os.Setenv("GIT_DIR", dir) t.Cleanup(func() { - _ = git.UnsetRemoteResolution("upstream") + if err := git.UnsetRemoteResolution("upstream"); err != nil { + panic(err) + } os.Setenv("GIT_DIR", old_GIT_DIR) }) } From 9060b44e6d469f82817a758be56331aa3f5f69ff Mon Sep 17 00:00:00 2001 From: bchadwic Date: Tue, 5 Apr 2022 21:43:57 -0700 Subject: [PATCH 09/20] default lint fix --- pkg/cmd/repo/default/default.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/repo/default/default.go b/pkg/cmd/repo/default/default.go index 3b3002034..128494210 100644 --- a/pkg/cmd/repo/default/default.go +++ b/pkg/cmd/repo/default/default.go @@ -86,6 +86,8 @@ func runDefault(opts *DefaultOptions) error { if err != nil { return err } - context.RemoveBaseRepo(remotes) + if err = context.RemoveBaseRepo(remotes); err != nil { + return err + } return repoContext.SetBaseRepo(opts.IO) } From 22a5d2abf8dbc15b4c72ada25cc751a24efdc9ec Mon Sep 17 00:00:00 2001 From: Sam Coe Date: Tue, 19 Apr 2022 13:31:59 +0200 Subject: [PATCH 10/20] Refactor code to live inside default pkg --- context/context.go | 161 +++------- context/remote.go | 39 ++- git/fixtures/simple.git/config | 6 - git/git.go | 14 +- git/remote.go | 2 +- pkg/cmd/repo/default/default.go | 183 ++++++++--- pkg/cmd/repo/default/default_test.go | 450 +++++++++++++++++++++------ pkg/cmd/repo/repo.go | 2 +- 8 files changed, 589 insertions(+), 268 deletions(-) diff --git a/context/context.go b/context/context.go index d2006f97b..f331e5c78 100644 --- a/context/context.go +++ b/context/context.go @@ -4,6 +4,7 @@ package context import ( "errors" "sort" + "strings" "github.com/AlecAivazis/survey/v2" "github.com/cli/cli/v2/api" @@ -59,93 +60,6 @@ type ResolvedRemotes struct { apiClient *api.Client } -func GetBaseRepo(remotes Remotes) (ghrepo.Interface, error) { - for _, r := range remotes { - if r.Resolved == "base" { - return r, nil - } else if r.Resolved != "" { - repo, err := ghrepo.FromFullName(r.Resolved) - if err != nil { - return nil, err - } - return ghrepo.NewWithHost(repo.RepoOwner(), repo.RepoName(), r.RepoHost()), nil - } - } - return nil, errors.New("a default repo has not been set, use `gh repo default` to set a default repo") -} - -func (r *ResolvedRemotes) SetBaseRepo(io *iostreams.IOStreams) error { - resolution := "base" - if !io.CanPrompt() { - return git.SetRemoteResolution(r.remotes[0].Name, resolution) - } - - // from here on, consult the API - if r.network == nil { - err := resolveNetwork(r) - if err != nil { - return err - } - } - - var repoNames []string - repoMap := map[string]*api.Repository{} - add := func(r *api.Repository) { - fn := ghrepo.FullName(r) - if _, ok := repoMap[fn]; !ok { - repoMap[fn] = r - repoNames = append(repoNames, fn) - } - } - - for _, repo := range r.network.Repositories { - if repo == nil { - continue - } - if repo.Parent != nil { - add(repo.Parent) - } - add(repo) - } - - if len(repoNames) == 0 { - return git.SetRemoteResolution(r.remotes[0].Name, resolution) - } - - baseName := repoNames[0] - if len(repoNames) > 1 { - err := prompt.SurveyAskOne(&survey.Select{ - Message: "Which should be the base repository (used for e.g. querying issues) for this directory?", - Options: repoNames, - }, &baseName) - if err != nil { - return err - } - } - - // determine corresponding git remote - selectedRepo := repoMap[baseName] - remote, _ := r.RemoteForRepo(selectedRepo) - if remote == nil { - remote = r.remotes[0] - resolution = ghrepo.FullName(selectedRepo) - } - - // cache the result to git config - return git.SetRemoteResolution(remote.Name, resolution) -} - -func RemoveBaseRepo(remotes Remotes) error { - for _, remote := range remotes { - if remote.Resolved == "base" { - if err := git.UnsetRemoteResolution(remote.Remote.Name); err != nil { - return err - } - } - } - return nil -} - func (r *ResolvedRemotes) BaseRepo(io *iostreams.IOStreams) (ghrepo.Interface, error) { if r.baseOverride != nil { return r.baseOverride, nil @@ -169,36 +83,18 @@ func (r *ResolvedRemotes) BaseRepo(io *iostreams.IOStreams) (ghrepo.Interface, e return r.remotes[0], nil } - // from here on, consult the API - if r.network == nil { - err := resolveNetwork(r) - if err != nil { - return nil, err - } + repos, err := r.NetworkRepos() + if err != nil { + return nil, err + } + + if len(repos) == 0 { + return r.remotes[0], nil } var repoNames []string - repoMap := map[string]*api.Repository{} - add := func(r *api.Repository) { - fn := ghrepo.FullName(r) - if _, ok := repoMap[fn]; !ok { - repoMap[fn] = r - repoNames = append(repoNames, fn) - } - } - - for _, repo := range r.network.Repositories { - if repo == nil { - continue - } - if repo.Parent != nil { - add(repo.Parent) - } - add(repo) - } - - if len(repoNames) == 0 { - return r.remotes[0], nil + for _, r := range repos { + repoNames = append(repoNames, ghrepo.FullName(r)) } baseName := repoNames[0] @@ -216,7 +112,8 @@ func (r *ResolvedRemotes) BaseRepo(io *iostreams.IOStreams) (ghrepo.Interface, e } // determine corresponding git remote - selectedRepo := repoMap[baseName] + owner, repo, _ := strings.Cut(baseName, "/") + selectedRepo := ghrepo.New(owner, repo) resolution := "base" remote, _ := r.RemoteForRepo(selectedRepo) if remote == nil { @@ -225,7 +122,7 @@ func (r *ResolvedRemotes) BaseRepo(io *iostreams.IOStreams) (ghrepo.Interface, e } // cache the result to git config - err := git.SetRemoteResolution(remote.Name, resolution) + err = git.SetRemoteResolution(remote.Name, resolution) return selectedRepo, err } @@ -246,6 +143,38 @@ func (r *ResolvedRemotes) HeadRepos() ([]*api.Repository, error) { return results, nil } +func (r *ResolvedRemotes) NetworkRepos() ([]*api.Repository, error) { + if r.network == nil { + err := resolveNetwork(r) + if err != nil { + return nil, err + } + } + + var repos []*api.Repository + repoMap := map[string]bool{} + + add := func(r *api.Repository) { + fn := ghrepo.FullName(r) + if _, ok := repoMap[fn]; !ok { + repoMap[fn] = true + repos = append(repos, r) + } + } + + for _, repo := range r.network.Repositories { + if repo == nil { + continue + } + if repo.Parent != nil { + add(repo.Parent) + } + add(repo) + } + + return repos, nil +} + // RemoteForRepo finds the git remote that points to a repository func (r *ResolvedRemotes) RemoteForRepo(repo ghrepo.Interface) (*Remote, error) { for _, remote := range r.remotes { diff --git a/context/remote.go b/context/remote.go index b88f1cfa3..ddc149cd1 100644 --- a/context/remote.go +++ b/context/remote.go @@ -21,7 +21,7 @@ func (r Remotes) FindByName(names ...string) (*Remote, error) { } } } - return nil, fmt.Errorf("no GitHub remotes found") + return nil, fmt.Errorf("no matching remote found") } // FindByRepo returns the first Remote that points to a specific GitHub repository @@ -34,6 +34,29 @@ func (r Remotes) FindByRepo(owner, name string) (*Remote, error) { return nil, fmt.Errorf("no matching remote found") } +// Filter remotes by given hostnames, maintains original order +func (r Remotes) FilterByHosts(hosts []string) Remotes { + filtered := make(Remotes, 0) + for _, rr := range r { + for _, host := range hosts { + if strings.EqualFold(rr.RepoHost(), host) { + filtered = append(filtered, rr) + break + } + } + } + return filtered +} + +func (r Remotes) ResolvedRemote() (*Remote, error) { + for _, rr := range r { + if rr.Resolved != "" { + return rr, nil + } + } + return nil, fmt.Errorf("no resolved remote found") +} + func remoteNameSortScore(name string) int { switch strings.ToLower(name) { case "upstream": @@ -54,20 +77,6 @@ func (r Remotes) Less(i, j int) bool { return remoteNameSortScore(r[i].Name) > remoteNameSortScore(r[j].Name) } -// Filter remotes by given hostnames, maintains original order -func (r Remotes) FilterByHosts(hosts []string) Remotes { - filtered := make(Remotes, 0) - for _, rr := range r { - for _, host := range hosts { - if strings.EqualFold(rr.RepoHost(), host) { - filtered = append(filtered, rr) - break - } - } - } - return filtered -} - // Remote represents a git remote mapped to a GitHub repository type Remote struct { *git.Remote diff --git a/git/fixtures/simple.git/config b/git/fixtures/simple.git/config index 51cb0d228..f0858dd73 100644 --- a/git/fixtures/simple.git/config +++ b/git/fixtures/simple.git/config @@ -7,9 +7,3 @@ [user] name = Mona the Cat email = monalisa@github.com -[remote "origin"] - url = git@github.com:monathecat/cli.git - fetch = +refs/heads/*:refs/remotes/origin/* -[remote "upstream"] - url = git@github.com:cli/cli.git - fetch = +refs/heads/trunk:refs/remotes/upstream/trunk diff --git a/git/git.go b/git/git.go index 31fea6233..1b6ea6f91 100644 --- a/git/git.go +++ b/git/git.go @@ -394,7 +394,6 @@ func ToplevelDir() (string, error) { } output, err := run.PrepareCmd(showCmd).Output() return firstLine(output), err - } // ToplevelDirFromPath returns the top-level given path of the current repository @@ -439,3 +438,16 @@ func getBranchShortName(output []byte) string { branch := firstLine(output) return strings.TrimPrefix(branch, "refs/heads/") } + +func IsGitDirectory() bool { + showCmd, err := GitCommand("rev-parse", "--is-inside-work-tree") + if err != nil { + return false + } + output, err := run.PrepareCmd(showCmd).Output() + if err != nil { + return false + } + out := firstLine(output) + return out == "true" +} diff --git a/git/remote.go b/git/remote.go index 423bd17cb..6e70396f7 100644 --- a/git/remote.go +++ b/git/remote.go @@ -169,7 +169,7 @@ func SetRemoteResolution(name, resolution string) error { } func UnsetRemoteResolution(name string) error { - unsetCmd, err := GitCommand("config", "--unset-all", fmt.Sprintf("remote.%s.gh-resolved", name)) + unsetCmd, err := GitCommand("config", "--unset", fmt.Sprintf("remote.%s.gh-resolved", name)) if err != nil { return err } diff --git a/pkg/cmd/repo/default/default.go b/pkg/cmd/repo/default/default.go index 128494210..0bc53217a 100644 --- a/pkg/cmd/repo/default/default.go +++ b/pkg/cmd/repo/default/default.go @@ -1,15 +1,21 @@ package base import ( + "errors" "fmt" "net/http" + "sort" + "strings" + "github.com/AlecAivazis/survey/v2" "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/api" "github.com/cli/cli/v2/context" + "github.com/cli/cli/v2/git" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/iostreams" + "github.com/cli/cli/v2/pkg/prompt" "github.com/spf13/cobra" ) @@ -18,10 +24,11 @@ type DefaultOptions struct { Remotes func() (context.Remotes, error) HttpClient func() (*http.Client, error) - ViewFlag bool + Repo ghrepo.Interface + ViewMode bool } -func NewCmdDefault(f *cmdutil.Factory) *cobra.Command { +func NewCmdDefault(f *cmdutil.Factory, runF func(*DefaultOptions) error) *cobra.Command { opts := &DefaultOptions{ IO: f.IOStreams, HttpClient: f.HttpClient, @@ -29,51 +36,60 @@ func NewCmdDefault(f *cmdutil.Factory) *cobra.Command { } cmd := &cobra.Command{ - Use: "default", - Short: "Configure the default repository used for various commands", - Long: heredoc.Doc(` - The default repository is used to determine which remote - repository gh should automatically point to. - `), - Example: heredoc.Doc(` - $ gh repo default - #=> prompts remote options + Use: "default []", + Short: "Configure default repository", + Long: heredoc.Docf(` + Set default repository for current directory. - $ gh repo default -v - #=> cli/cli - `), - Annotations: map[string]string{ - "help:environment": heredoc.Doc(` - To manually configure a remote for gh to use, modify your local repo's git config - - ; Ex: setting gh to use the upstream remote - [remote "upstream"] - gh-resolved = base - ... - `), - }, - Args: cobra.NoArgs, + The default repository is used as the target + repository for various commands such as %[1]spr%[1]s, %[1]sissue%[1]s, + and %[1]srepo%[1]s. + `, "`"), + Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - return runDefault(opts) + if len(args) > 0 { + var err error + opts.Repo, err = ghrepo.FromFullName(args[0]) + if err != nil { + return err + } + } + + if !opts.IO.CanPrompt() && opts.Repo == nil { + return cmdutil.FlagErrorf("repository required when not running interactively") + } + + if !git.IsGitDirectory() { + return errors.New("must be run from inside a git repository") + } + + if runF != nil { + return runF(opts) + } + + return defaultRun(opts) }, } - cmd.Flags().BoolVarP(&opts.ViewFlag, "view", "v", false, "view the default repository used for various commands") + cmd.Flags().BoolVarP(&opts.ViewMode, "view", "v", false, "view the current default repository") + return cmd } -func runDefault(opts *DefaultOptions) error { +func defaultRun(opts *DefaultOptions) error { remotes, err := opts.Remotes() if err != nil { return err } - if opts.ViewFlag { - baseRepo, err := context.GetBaseRepo(remotes) - if err != nil { - return err + currentDefaultRepo, _ := remotes.ResolvedRemote() + + if opts.ViewMode { + if currentDefaultRepo == nil { + fmt.Fprintln(opts.IO.Out, "no default repo has been set; use `gh repo default` to select one") + } else { + fmt.Fprintln(opts.IO.Out, displayRemoteRepoName(currentDefaultRepo)) } - fmt.Fprintln(opts.IO.Out, ghrepo.FullName(baseRepo)) return nil } @@ -82,12 +98,107 @@ func runDefault(opts *DefaultOptions) error { return err } apiClient := api.NewClientFromHTTP(httpClient) - repoContext, err := context.ResolveRemotesToRepos(remotes, apiClient, "") + + resolvedRemotes, err := context.ResolveRemotesToRepos(remotes, apiClient, "") if err != nil { return err } - if err = context.RemoveBaseRepo(remotes); err != nil { + + knownRepos, err := resolvedRemotes.NetworkRepos() + if err != nil { return err } - return repoContext.SetBaseRepo(opts.IO) + if len(knownRepos) == 0 { + return errors.New("none of the git remotes correspond to a valid remote repository") + } + + var selectedRepo ghrepo.Interface + + if opts.Repo != nil { + for _, knownRepo := range knownRepos { + if ghrepo.IsSame(opts.Repo, knownRepo) { + selectedRepo = opts.Repo + break + } + } + if selectedRepo == nil { + return fmt.Errorf("%s does not correspond to any git remotes", ghrepo.FullName(opts.Repo)) + } + } + + if selectedRepo == nil { + if len(knownRepos) == 1 { + selectedRepo = knownRepos[0] + } else { + var repoNames []string + var selectedName string + current := "" + if currentDefaultRepo != nil { + current = ghrepo.FullName(currentDefaultRepo) + } + + for _, knownRepo := range knownRepos { + repoNames = append(repoNames, ghrepo.FullName(knownRepo)) + } + + err := prompt.SurveyAskOne(&survey.Select{ + Message: "Which should be the default repository (used for e.g. querying issues) for this directory?", + Options: repoNames, + Default: current, + }, &selectedName) + if err != nil { + return err + } + + owner, repo, _ := strings.Cut(selectedName, "/") + selectedRepo = ghrepo.New(owner, repo) + } + } + + resolution := "base" + selectedRemote, _ := resolvedRemotes.RemoteForRepo(selectedRepo) + if selectedRemote == nil { + sort.Stable(remotes) + selectedRemote = remotes[0] + resolution = ghrepo.FullName(selectedRepo) + } + + if currentDefaultRepo != nil { + if err := unsetDefaultRepo(currentDefaultRepo); err != nil { + return err + } + } + + err = setDefaultRepo(selectedRemote, resolution) + if err != nil { + return err + } + + if opts.IO.IsStdoutTTY() { + cs := opts.IO.ColorScheme() + fmt.Fprintf(opts.IO.Out, "%s Set %s as the default repository for the current directory\n", cs.SuccessIcon(), ghrepo.FullName(selectedRepo)) + } + + return nil +} + +func displayRemoteRepoName(remote *context.Remote) string { + if remote.Resolved == "" || remote.Resolved == "base" { + return ghrepo.FullName(remote) + } + + repo, err := ghrepo.FromFullName(remote.Resolved) + if err != nil { + return ghrepo.FullName(remote) + } + + return ghrepo.FullName(repo) +} + +func setDefaultRepo(remote *context.Remote, resolution string) error { + return git.SetRemoteResolution(remote.Name, resolution) +} + +func unsetDefaultRepo(remote *context.Remote) error { + return git.UnsetRemoteResolution(remote.Name) } diff --git a/pkg/cmd/repo/default/default_test.go b/pkg/cmd/repo/default/default_test.go index e76734ff0..a057b28c9 100644 --- a/pkg/cmd/repo/default/default_test.go +++ b/pkg/cmd/repo/default/default_test.go @@ -1,129 +1,395 @@ package base import ( - "errors" - "fmt" + "bytes" "net/http" - "os" "testing" "github.com/cli/cli/v2/context" "github.com/cli/cli/v2/git" "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/internal/run" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/httpmock" "github.com/cli/cli/v2/pkg/iostreams" + "github.com/cli/cli/v2/pkg/prompt" + "github.com/google/shlex" "github.com/stretchr/testify/assert" ) -func Test_defaultRun(t *testing.T) { - setGitDir(t, "../../../../git/fixtures/simple.git") +func TestNewCmdDefault(t *testing.T) { tests := []struct { - name string - opts DefaultOptions - wantedErr error - wantedStdOut string - wantedResolvedName string + name string + gitStubs func(*run.CommandStubber) + input string + output DefaultOptions + wantErr bool + errMsg string }{ { - name: "Base repo set with view option", - opts: DefaultOptions{ - Remotes: func() (context.Remotes, error) { - return []*context.Remote{ - { - Remote: &git.Remote{ - Name: "origin", - Resolved: "base", - }, - Repo: ghrepo.New("hubot", "Spoon-Knife"), - }, - }, nil - }, - ViewFlag: true, + name: "no argument", + gitStubs: func(cs *run.CommandStubber) { + cs.Register(`git rev-parse --is-inside-work-tree`, 0, "true") }, - wantedStdOut: "hubot/Spoon-Knife", + input: "", + output: DefaultOptions{}, }, { - name: "Base repo not set with view option", - opts: DefaultOptions{ - Remotes: func() (context.Remotes, error) { - return []*context.Remote{ - { - Remote: &git.Remote{ - Name: "origin", - }, - }, - }, nil - }, - ViewFlag: true, + name: "repo argument", + gitStubs: func(cs *run.CommandStubber) { + cs.Register(`git rev-parse --is-inside-work-tree`, 0, "true") }, - wantedErr: errors.New("a default repo has not been set, use `gh repo default` to set a default repo"), + input: "cli/cli", + output: DefaultOptions{Repo: ghrepo.New("cli", "cli")}, }, { - name: "Base repo not set, assign non-interactively", - opts: DefaultOptions{ - Remotes: func() (context.Remotes, error) { - return []*context.Remote{ - { - Remote: &git.Remote{ - Name: "origin", - }, - }, - { - Remote: &git.Remote{ - Name: "upstream", - }, - }, - }, nil - }, + name: "invalid repo argument", + gitStubs: func(cs *run.CommandStubber) {}, + input: "some_invalid_format", + wantErr: true, + errMsg: `expected the "[HOST/]OWNER/REPO" format, got "some_invalid_format"`, + }, + { + name: "view flag", + gitStubs: func(cs *run.CommandStubber) { + cs.Register(`git rev-parse --is-inside-work-tree`, 0, "true") }, - wantedResolvedName: "upstream", + input: "--view", + output: DefaultOptions{ViewMode: true}, + }, + { + name: "run from non-git directory", + gitStubs: func(cs *run.CommandStubber) { + cs.Register(`git rev-parse --is-inside-work-tree`, 1, "") + }, + input: "", + wantErr: true, + errMsg: "must be run from inside a git repository", }, } for _, tt := range tests { + io, _, _, _ := iostreams.Test() + io.SetStdoutTTY(true) + io.SetStdinTTY(true) + io.SetStderrTTY(true) + f := &cmdutil.Factory{ + IOStreams: io, + } + + var gotOpts *DefaultOptions + cmd := NewCmdDefault(f, func(opts *DefaultOptions) error { + gotOpts = opts + return nil + }) + cmd.SetIn(&bytes.Buffer{}) + cmd.SetOut(&bytes.Buffer{}) + cmd.SetErr(&bytes.Buffer{}) + t.Run(tt.name, func(t *testing.T) { - io, _, stdout, stderr := iostreams.Test() + argv, err := shlex.Split(tt.input) + assert.NoError(t, err) - opts := tt.opts - opts.IO = io - opts.HttpClient = func() (*http.Client, error) { return nil, nil } + cmd.SetArgs(argv) - err := runDefault(&opts) - if tt.wantedErr != nil { - assert.EqualError(t, tt.wantedErr, err.Error()) - } else { - assert.NoError(t, err) - if opts.ViewFlag { - assert.Equal(t, fmt.Sprintf("%s\n", tt.wantedStdOut), stdout.String()) - } else { - assert.Equal(t, "", stdout.String()) - assert.Equal(t, "", stderr.String()) - } - } - if tt.wantedResolvedName != "" { - resolvedAmount := 0 - remotes, err := git.Remotes() - if err != nil { - panic(err) - } - for _, r := range remotes { - if r.Resolved == "base" { - assert.Equal(t, r.Name, tt.wantedResolvedName) - resolvedAmount++ - } - } - assert.Equal(t, 1, resolvedAmount) + cs, teardown := run.Stub() + defer teardown(t) + tt.gitStubs(cs) + + _, err = cmd.ExecuteC() + if tt.wantErr { + assert.EqualError(t, err, tt.errMsg) + return } + + assert.NoError(t, err) + assert.Equal(t, tt.output.Repo, gotOpts.Repo) + assert.Equal(t, tt.output.ViewMode, gotOpts.ViewMode) }) } } -func setGitDir(t *testing.T, dir string) { - old_GIT_DIR := os.Getenv("GIT_DIR") - os.Setenv("GIT_DIR", dir) - t.Cleanup(func() { - if err := git.UnsetRemoteResolution("upstream"); err != nil { - panic(err) +func TestDefaultRun(t *testing.T) { + repo1, _ := ghrepo.FromFullName("OWNER/REPO") + repo2, _ := ghrepo.FromFullName("OWNER2/REPO2") + repo3, _ := ghrepo.FromFullName("OWNER3/REPO3") + + tests := []struct { + name string + tty bool + opts DefaultOptions + remotes []*context.Remote + httpStubs func(*httpmock.Registry) + gitStubs func(*run.CommandStubber) + askStubs func(*prompt.AskStubber) + wantStdout string + wantErr bool + errMsg string + }{ + { + name: "view mode no current default", + opts: DefaultOptions{ViewMode: true}, + remotes: []*context.Remote{ + { + Remote: &git.Remote{Name: "origin"}, + Repo: repo1, + }, + }, + wantStdout: "no default repo has been set; use `gh repo default` to select one\n", + }, + { + name: "view mode with base resolved current default", + opts: DefaultOptions{ViewMode: true}, + remotes: []*context.Remote{ + { + Remote: &git.Remote{Name: "origin", Resolved: "base"}, + Repo: repo1, + }, + }, + wantStdout: "OWNER/REPO\n", + }, + { + name: "view mode with non-base resolved current default", + opts: DefaultOptions{ViewMode: true}, + remotes: []*context.Remote{ + { + Remote: &git.Remote{Name: "origin", Resolved: "PARENT/REPO"}, + Repo: repo1, + }, + }, + wantStdout: "PARENT/REPO\n", + }, + { + name: "tty non-interactive mode no current default", + tty: true, + opts: DefaultOptions{Repo: repo2}, + remotes: []*context.Remote{ + { + Remote: &git.Remote{Name: "origin"}, + Repo: repo1, + }, + { + Remote: &git.Remote{Name: "upstream"}, + Repo: repo2, + }, + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.GraphQL(`query RepositoryNetwork\b`), + httpmock.StringResponse(`{"data":{"repo_000":{"name":"REPO2","owner":{"login":"OWNER2"}}}}`), + ) + }, + gitStubs: func(cs *run.CommandStubber) { + cs.Register(`git config --add remote.upstream.gh-resolved base`, 0, "") + }, + wantStdout: "✓ Set OWNER2/REPO2 as the default repository for the current directory\n", + }, + { + name: "tty non-interactive mode set non-base default", + tty: true, + opts: DefaultOptions{Repo: repo2}, + remotes: []*context.Remote{ + { + Remote: &git.Remote{Name: "origin"}, + Repo: repo1, + }, + { + Remote: &git.Remote{Name: "upstream"}, + Repo: repo3, + }, + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.GraphQL(`query RepositoryNetwork\b`), + httpmock.StringResponse(`{"data":{"repo_000":{"name":"REPO","owner":{"login":"OWNER"},"parent":{"name":"REPO2","owner":{"login":"OWNER2"}}}}}`), + ) + }, + gitStubs: func(cs *run.CommandStubber) { + cs.Register(`git config --add remote.upstream.gh-resolved OWNER2/REPO2`, 0, "") + }, + wantStdout: "✓ Set OWNER2/REPO2 as the default repository for the current directory\n", + }, + { + name: "non-tty non-interactive mode no current default", + opts: DefaultOptions{Repo: repo2}, + remotes: []*context.Remote{ + { + Remote: &git.Remote{Name: "origin"}, + Repo: repo1, + }, + { + Remote: &git.Remote{Name: "upstream"}, + Repo: repo2, + }, + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.GraphQL(`query RepositoryNetwork\b`), + httpmock.StringResponse(`{"data":{"repo_000":{"name":"REPO2","owner":{"login":"OWNER2"}}}}`), + ) + }, + gitStubs: func(cs *run.CommandStubber) { + cs.Register(`git config --add remote.upstream.gh-resolved base`, 0, "") + }, + wantStdout: "", + }, + { + name: "non-interactive mode with current default", + tty: true, + opts: DefaultOptions{Repo: repo2}, + remotes: []*context.Remote{ + { + Remote: &git.Remote{Name: "origin", Resolved: "base"}, + Repo: repo1, + }, + { + Remote: &git.Remote{Name: "upstream"}, + Repo: repo2, + }, + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.GraphQL(`query RepositoryNetwork\b`), + httpmock.StringResponse(`{"data":{"repo_000":{"name":"REPO2","owner":{"login":"OWNER2"}}}}`), + ) + }, + gitStubs: func(cs *run.CommandStubber) { + cs.Register(`git config --unset remote.origin.gh-resolved`, 0, "") + cs.Register(`git config --add remote.upstream.gh-resolved base`, 0, "") + }, + wantStdout: "✓ Set OWNER2/REPO2 as the default repository for the current directory\n", + }, + { + name: "non-interactive mode no known hosts", + opts: DefaultOptions{Repo: repo2}, + remotes: []*context.Remote{ + { + Remote: &git.Remote{Name: "origin"}, + Repo: repo1, + }, + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.GraphQL(`query RepositoryNetwork\b`), + httpmock.StringResponse(`{"data":{}}`), + ) + }, + wantErr: true, + errMsg: "none of the git remotes correspond to a valid remote repository", + }, + { + name: "non-interactive mode no matching remotes", + opts: DefaultOptions{Repo: repo2}, + remotes: []*context.Remote{ + { + Remote: &git.Remote{Name: "origin"}, + Repo: repo1, + }, + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.GraphQL(`query RepositoryNetwork\b`), + httpmock.StringResponse(`{"data":{"repo_000":{"name":"REPO","owner":{"login":"OWNER"}}}}`), + ) + }, + wantErr: true, + errMsg: "OWNER2/REPO2 does not correspond to any git remotes", + }, + { + name: "interactive mode", + tty: true, + opts: DefaultOptions{}, + remotes: []*context.Remote{ + { + Remote: &git.Remote{Name: "origin"}, + Repo: repo1, + }, + { + Remote: &git.Remote{Name: "upstream"}, + Repo: repo2, + }, + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.GraphQL(`query RepositoryNetwork\b`), + httpmock.StringResponse(`{"data":{"repo_000":{"name":"REPO","owner":{"login":"OWNER"}},"repo_001":{"name":"REPO2","owner":{"login":"OWNER2"}}}}`), + ) + }, + gitStubs: func(cs *run.CommandStubber) { + cs.Register(`git config --add remote.upstream.gh-resolved base`, 0, "") + }, + askStubs: func(as *prompt.AskStubber) { + as.StubPrompt("Which should be the default repository (used for e.g. querying issues) for this directory?"). + AssertOptions([]string{"OWNER/REPO", "OWNER2/REPO2"}). + AnswerWith("OWNER2/REPO2") + }, + wantStdout: "✓ Set OWNER2/REPO2 as the default repository for the current directory\n", + }, + { + name: "interactive mode only one known host", + tty: true, + opts: DefaultOptions{}, + remotes: []*context.Remote{ + { + Remote: &git.Remote{Name: "origin"}, + Repo: repo1, + }, + { + Remote: &git.Remote{Name: "upstream"}, + Repo: repo2, + }, + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.GraphQL(`query RepositoryNetwork\b`), + httpmock.StringResponse(`{"data":{"repo_000":{"name":"REPO2","owner":{"login":"OWNER2"}}}}`), + ) + }, + gitStubs: func(cs *run.CommandStubber) { + cs.Register(`git config --add remote.upstream.gh-resolved base`, 0, "") + }, + wantStdout: "✓ Set OWNER2/REPO2 as the default repository for the current directory\n", + }, + } + + for _, tt := range tests { + reg := &httpmock.Registry{} + if tt.httpStubs != nil { + tt.httpStubs(reg) } - os.Setenv("GIT_DIR", old_GIT_DIR) - }) + tt.opts.HttpClient = func() (*http.Client, error) { + return &http.Client{Transport: reg}, nil + } + + io, _, stdout, _ := iostreams.Test() + io.SetStdinTTY(tt.tty) + io.SetStdoutTTY(tt.tty) + io.SetStderrTTY(tt.tty) + tt.opts.IO = io + + tt.opts.Remotes = func() (context.Remotes, error) { + return tt.remotes, nil + } + + as := prompt.NewAskStubber(t) + if tt.askStubs != nil { + tt.askStubs(as) + } + + t.Run(tt.name, func(t *testing.T) { + cs, teardown := run.Stub() + defer teardown(t) + if tt.gitStubs != nil { + tt.gitStubs(cs) + } + defer reg.Verify(t) + err := defaultRun(&tt.opts) + if tt.wantErr { + assert.EqualError(t, err, tt.errMsg) + return + } + assert.NoError(t, err) + assert.Equal(t, tt.wantStdout, stdout.String()) + }) + } } diff --git a/pkg/cmd/repo/repo.go b/pkg/cmd/repo/repo.go index 01cae54b4..d3dce190d 100644 --- a/pkg/cmd/repo/repo.go +++ b/pkg/cmd/repo/repo.go @@ -53,7 +53,7 @@ func NewCmdRepo(f *cmdutil.Factory) *cobra.Command { cmd.AddCommand(repoRenameCmd.NewCmdRename(f, nil)) cmd.AddCommand(repoDeleteCmd.NewCmdDelete(f, nil)) cmd.AddCommand(repoArchiveCmd.NewCmdArchive(f, nil)) - cmd.AddCommand(repoDefaultCmd.NewCmdDefault(f)) + cmd.AddCommand(repoDefaultCmd.NewCmdDefault(f, nil)) return cmd } From c89d8cb7596e8596f4e1dfb96f8f275b2a4f9597 Mon Sep 17 00:00:00 2001 From: Sam Coe Date: Wed, 20 Apr 2022 09:36:07 +0200 Subject: [PATCH 11/20] Backport strings.Cut --- context/context.go | 9 ++++++++- pkg/cmd/repo/default/default.go | 9 ++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/context/context.go b/context/context.go index f331e5c78..a00cd4ebe 100644 --- a/context/context.go +++ b/context/context.go @@ -112,7 +112,7 @@ func (r *ResolvedRemotes) BaseRepo(io *iostreams.IOStreams) (ghrepo.Interface, e } // determine corresponding git remote - owner, repo, _ := strings.Cut(baseName, "/") + owner, repo, _ := cut(baseName, "/") selectedRepo := ghrepo.New(owner, repo) resolution := "base" remote, _ := r.RemoteForRepo(selectedRepo) @@ -184,3 +184,10 @@ func (r *ResolvedRemotes) RemoteForRepo(repo ghrepo.Interface) (*Remote, error) } return nil, errors.New("not found") } + +func cut(s, sep string) (before, after string, found bool) { + if i := strings.Index(s, sep); i >= 0 { + return s[:i], s[i+len(sep):], true + } + return s, "", false +} diff --git a/pkg/cmd/repo/default/default.go b/pkg/cmd/repo/default/default.go index 0bc53217a..0418adaf0 100644 --- a/pkg/cmd/repo/default/default.go +++ b/pkg/cmd/repo/default/default.go @@ -150,7 +150,7 @@ func defaultRun(opts *DefaultOptions) error { return err } - owner, repo, _ := strings.Cut(selectedName, "/") + owner, repo, _ := cut(selectedName, "/") selectedRepo = ghrepo.New(owner, repo) } } @@ -202,3 +202,10 @@ func setDefaultRepo(remote *context.Remote, resolution string) error { func unsetDefaultRepo(remote *context.Remote) error { return git.UnsetRemoteResolution(remote.Name) } + +func cut(s, sep string) (before, after string, found bool) { + if i := strings.Index(s, sep); i >= 0 { + return s[:i], s[i+len(sep):], true + } + return s, "", false +} From 63a3adf2126ce05f27a53fb8d6b41268277744d1 Mon Sep 17 00:00:00 2001 From: Sam Coe Date: Wed, 18 May 2022 08:42:14 +0200 Subject: [PATCH 12/20] Use strings.Cut --- context/context.go | 9 +-------- pkg/cmd/repo/default/default.go | 9 +-------- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/context/context.go b/context/context.go index a00cd4ebe..f331e5c78 100644 --- a/context/context.go +++ b/context/context.go @@ -112,7 +112,7 @@ func (r *ResolvedRemotes) BaseRepo(io *iostreams.IOStreams) (ghrepo.Interface, e } // determine corresponding git remote - owner, repo, _ := cut(baseName, "/") + owner, repo, _ := strings.Cut(baseName, "/") selectedRepo := ghrepo.New(owner, repo) resolution := "base" remote, _ := r.RemoteForRepo(selectedRepo) @@ -184,10 +184,3 @@ func (r *ResolvedRemotes) RemoteForRepo(repo ghrepo.Interface) (*Remote, error) } return nil, errors.New("not found") } - -func cut(s, sep string) (before, after string, found bool) { - if i := strings.Index(s, sep); i >= 0 { - return s[:i], s[i+len(sep):], true - } - return s, "", false -} diff --git a/pkg/cmd/repo/default/default.go b/pkg/cmd/repo/default/default.go index 0418adaf0..0bc53217a 100644 --- a/pkg/cmd/repo/default/default.go +++ b/pkg/cmd/repo/default/default.go @@ -150,7 +150,7 @@ func defaultRun(opts *DefaultOptions) error { return err } - owner, repo, _ := cut(selectedName, "/") + owner, repo, _ := strings.Cut(selectedName, "/") selectedRepo = ghrepo.New(owner, repo) } } @@ -202,10 +202,3 @@ func setDefaultRepo(remote *context.Remote, resolution string) error { func unsetDefaultRepo(remote *context.Remote) error { return git.UnsetRemoteResolution(remote.Name) } - -func cut(s, sep string) (before, after string, found bool) { - if i := strings.Index(s, sep); i >= 0 { - return s[:i], s[i+len(sep):], true - } - return s, "", false -} From 76c58930d96c897c799318f0d65f3dc0f9ed88fa Mon Sep 17 00:00:00 2001 From: vilmibm Date: Tue, 13 Dec 2022 10:47:57 -0800 Subject: [PATCH 13/20] tests pass --- git/client.go | 11 +++++------ pkg/cmd/repo/default/default.go | 9 +++------ 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/git/client.go b/git/client.go index 472f73274..0bcbe4aeb 100644 --- a/git/client.go +++ b/git/client.go @@ -489,22 +489,21 @@ func (c *Client) AddRemote(ctx context.Context, name, urlStr string, trackingBra return remote, nil } -func (c *Client) InGitDirectory(ctx context.Context) (bool, error) { +func (c *Client) InGitDirectory(ctx context.Context) bool { showCmd, err := c.Command(ctx, "rev-parse", "--is-inside-work-tree") if err != nil { - return false, err + return false } out, err := showCmd.Output() if err != nil { - return false, err + return false } split := strings.Split(string(out), "\n") if len(split) > 0 { - return split[0] == "true", nil + return split[0] == "true" } - - return false, nil + return false } func (c *Client) UnsetRemoteResolution(ctx context.Context, name string) error { diff --git a/pkg/cmd/repo/default/default.go b/pkg/cmd/repo/default/default.go index eea3ee4ba..b3df69e21 100644 --- a/pkg/cmd/repo/default/default.go +++ b/pkg/cmd/repo/default/default.go @@ -62,12 +62,8 @@ func NewCmdDefault(f *cmdutil.Factory, runF func(*DefaultOptions) error) *cobra. } c := &git.Client{} - inGitDir, err := c.InGitDirectory(ctx.Background()) - if err != nil { - return err - } - if !inGitDir { + if !c.InGitDirectory(ctx.Background()) { return errors.New("must be run from inside a git repository") } @@ -204,7 +200,8 @@ func displayRemoteRepoName(remote *context.Remote) string { } func setDefaultRepo(remote *context.Remote, resolution string) error { - return git.SetRemoteResolution(remote.Name, resolution) + c := &git.Client{} + return c.SetRemoteResolution(ctx.Background(), remote.Name, resolution) } func unsetDefaultRepo(remote *context.Remote) error { From 2090e1111f2ce7262c365b5c1b0005c601679581 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Tue, 13 Dec 2022 11:57:05 -0800 Subject: [PATCH 14/20] tweak UX and switch to prompter --- pkg/cmd/repo/default/default.go | 41 ++++++++++++++++++------ pkg/cmd/repo/default/default_test.go | 48 ++++++++++++++++------------ 2 files changed, 60 insertions(+), 29 deletions(-) diff --git a/pkg/cmd/repo/default/default.go b/pkg/cmd/repo/default/default.go index b3df69e21..43b13c6db 100644 --- a/pkg/cmd/repo/default/default.go +++ b/pkg/cmd/repo/default/default.go @@ -9,7 +9,6 @@ import ( ctx "context" - "github.com/AlecAivazis/survey/v2" "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/api" "github.com/cli/cli/v2/context" @@ -17,14 +16,18 @@ import ( "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/iostreams" - "github.com/cli/cli/v2/pkg/prompt" "github.com/spf13/cobra" ) +type iprompter interface { + Select(string, string, []string) (int, error) +} + type DefaultOptions struct { IO *iostreams.IOStreams Remotes func() (context.Remotes, error) HttpClient func() (*http.Client, error) + Prompter iprompter Repo ghrepo.Interface ViewMode bool @@ -35,6 +38,7 @@ func NewCmdDefault(f *cmdutil.Factory, runF func(*DefaultOptions) error) *cobra. IO: f.IOStreams, HttpClient: f.HttpClient, Remotes: f.Remotes, + Prompter: f.Prompter, } cmd := &cobra.Command{ @@ -129,13 +133,21 @@ func defaultRun(opts *DefaultOptions) error { return fmt.Errorf("%s does not correspond to any git remotes", ghrepo.FullName(opts.Repo)) } } + cs := opts.IO.ColorScheme() + + hintMsg := cs.Gray("(hint: for gh to see more remote repositories, add them with `git remote`)") if selectedRepo == nil { if len(knownRepos) == 1 { selectedRepo = knownRepos[0] + + fmt.Fprintf(opts.IO.Out, "Found only one known remote repo, %s on %s.\n", + cs.Bold(ghrepo.FullName(selectedRepo)), + cs.Bold(selectedRepo.RepoHost())) + fmt.Fprintln(opts.IO.Out, hintMsg) + fmt.Fprintln(opts.IO.Out) } else { var repoNames []string - var selectedName string current := "" if currentDefaultRepo != nil { current = ghrepo.FullName(currentDefaultRepo) @@ -145,14 +157,25 @@ func defaultRun(opts *DefaultOptions) error { repoNames = append(repoNames, ghrepo.FullName(knownRepo)) } - err := prompt.SurveyAskOne(&survey.Select{ - Message: "Which should be the default repository (used for e.g. querying issues) for this directory?", - Options: repoNames, - Default: current, - }, &selectedName) + defaultExplainer := heredoc.Doc(` + gh uses the default repository for things like: + + - viewing, creating, and setting the default base for pull requests + - viewing and creating issues + - viewing and creating releases + - working with Actions + - adding secrets + `) + + fmt.Fprintln(opts.IO.Out, defaultExplainer) + fmt.Fprintln(opts.IO.Out, hintMsg) + fmt.Fprintln(opts.IO.Out) + + selected, err := opts.Prompter.Select("Which repository should be the default?", current, repoNames) if err != nil { - return err + return fmt.Errorf("could not prompt: %w", err) } + selectedName := repoNames[selected] owner, repo, _ := strings.Cut(selectedName, "/") selectedRepo = ghrepo.New(owner, repo) diff --git a/pkg/cmd/repo/default/default_test.go b/pkg/cmd/repo/default/default_test.go index a057b28c9..b8c129da8 100644 --- a/pkg/cmd/repo/default/default_test.go +++ b/pkg/cmd/repo/default/default_test.go @@ -8,11 +8,11 @@ import ( "github.com/cli/cli/v2/context" "github.com/cli/cli/v2/git" "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/internal/prompter" "github.com/cli/cli/v2/internal/run" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/httpmock" "github.com/cli/cli/v2/pkg/iostreams" - "github.com/cli/cli/v2/pkg/prompt" "github.com/google/shlex" "github.com/stretchr/testify/assert" ) @@ -115,16 +115,16 @@ func TestDefaultRun(t *testing.T) { repo3, _ := ghrepo.FromFullName("OWNER3/REPO3") tests := []struct { - name string - tty bool - opts DefaultOptions - remotes []*context.Remote - httpStubs func(*httpmock.Registry) - gitStubs func(*run.CommandStubber) - askStubs func(*prompt.AskStubber) - wantStdout string - wantErr bool - errMsg string + name string + tty bool + opts DefaultOptions + remotes []*context.Remote + httpStubs func(*httpmock.Registry) + gitStubs func(*run.CommandStubber) + prompterStubs func(*prompter.PrompterMock) + wantStdout string + wantErr bool + errMsg string }{ { name: "view mode no current default", @@ -318,12 +318,18 @@ func TestDefaultRun(t *testing.T) { gitStubs: func(cs *run.CommandStubber) { cs.Register(`git config --add remote.upstream.gh-resolved base`, 0, "") }, - askStubs: func(as *prompt.AskStubber) { - as.StubPrompt("Which should be the default repository (used for e.g. querying issues) for this directory?"). - AssertOptions([]string{"OWNER/REPO", "OWNER2/REPO2"}). - AnswerWith("OWNER2/REPO2") + prompterStubs: func(pm *prompter.PrompterMock) { + pm.SelectFunc = func(p, d string, opts []string) (int, error) { + switch p { + case "Which repository should be the default?": + prompter.AssertOptions(t, []string{"OWNER/REPO", "OWNER2/REPO2"}, opts) + return prompter.IndexFor(opts, "OWNER2/REPO2") + default: + return -1, prompter.NoSuchPromptErr(p) + } + } }, - wantStdout: "✓ Set OWNER2/REPO2 as the default repository for the current directory\n", + wantStdout: "gh uses the default repository for things like:\n\n - viewing, creating, and setting the default base for pull requests\n - viewing and creating issues\n - viewing and creating releases\n - working with Actions\n - adding secrets\n\n(hint: for gh to see more remote repositories, add them with `git remote`)\n\n✓ Set OWNER2/REPO2 as the default repository for the current directory\n", }, { name: "interactive mode only one known host", @@ -348,7 +354,7 @@ func TestDefaultRun(t *testing.T) { gitStubs: func(cs *run.CommandStubber) { cs.Register(`git config --add remote.upstream.gh-resolved base`, 0, "") }, - wantStdout: "✓ Set OWNER2/REPO2 as the default repository for the current directory\n", + wantStdout: "Found only one known remote repo, OWNER2/REPO2 on github.com.\n(hint: for gh to see more remote repositories, add them with `git remote`)\n\n✓ Set OWNER2/REPO2 as the default repository for the current directory\n", }, } @@ -371,11 +377,13 @@ func TestDefaultRun(t *testing.T) { return tt.remotes, nil } - as := prompt.NewAskStubber(t) - if tt.askStubs != nil { - tt.askStubs(as) + pm := &prompter.PrompterMock{} + if tt.prompterStubs != nil { + tt.prompterStubs(pm) } + tt.opts.Prompter = pm + t.Run(tt.name, func(t *testing.T) { cs, teardown := run.Stub() defer teardown(t) From 230d89be938d64c0f760d6b3e29d00845f641409 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Tue, 13 Dec 2022 12:06:57 -0800 Subject: [PATCH 15/20] add more to usage --- pkg/cmd/repo/default/default.go | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/pkg/cmd/repo/default/default.go b/pkg/cmd/repo/default/default.go index 43b13c6db..2f0b34aad 100644 --- a/pkg/cmd/repo/default/default.go +++ b/pkg/cmd/repo/default/default.go @@ -44,13 +44,30 @@ func NewCmdDefault(f *cmdutil.Factory, runF func(*DefaultOptions) error) *cobra. cmd := &cobra.Command{ Use: "default []", Short: "Configure default repository", - Long: heredoc.Docf(` + Long: heredoc.Doc(` Set default repository for current directory. - The default repository is used as the target - repository for various commands such as %[1]spr%[1]s, %[1]sissue%[1]s, - and %[1]srepo%[1]s. - `, "`"), + gh uses the default repository for things like: + + - viewing, creating, and setting the default base for pull requests + - viewing and creating issues + - viewing and creating releases + - working with Actions + - adding secrets + `), + Example: heredoc.Doc(` + Interactively select a default repository: + + $ gh repo default + + Set a repository explicitly: + + $ gh repo default owner/repo + + View the current default repository: + + $ gh repo default --view + `), Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { if len(args) > 0 { From 941dc26cc83f4c0cdebdd00246e6233746f446fb Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Wed, 14 Dec 2022 16:15:37 -0800 Subject: [PATCH 16/20] Update git/client.go Co-authored-by: Sam Coe --- git/client.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/git/client.go b/git/client.go index 0bcbe4aeb..c9533094f 100644 --- a/git/client.go +++ b/git/client.go @@ -507,11 +507,16 @@ func (c *Client) InGitDirectory(ctx context.Context) bool { } func (c *Client) UnsetRemoteResolution(ctx context.Context, name string) error { - unsetCmd, err := c.Command(ctx, "config", "--unset", fmt.Sprintf("remote.%s.gh-resolved", name)) + args := []string{"config", "--unset", fmt.Sprintf("remote.%s.gh-resolved", name)} + cmd, err := c.Command(ctx, args...) if err != nil { return err } - return unsetCmd.Run() + _, err = cmd.Output() + if err != nil { + return err + } + return nil } func resolveGitPath() (string, error) { From 41bdaa426a010eb989a30ab1d084206f170633a8 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Wed, 14 Dec 2022 16:04:35 -0800 Subject: [PATCH 17/20] tweak wording --- pkg/cmd/repo/default/default.go | 50 +++++++++++++-------------------- 1 file changed, 20 insertions(+), 30 deletions(-) diff --git a/pkg/cmd/repo/default/default.go b/pkg/cmd/repo/default/default.go index 2f0b34aad..8b618d0f0 100644 --- a/pkg/cmd/repo/default/default.go +++ b/pkg/cmd/repo/default/default.go @@ -19,6 +19,20 @@ import ( "github.com/spf13/cobra" ) +func explainer() string { + return heredoc.Doc(` + This command sets the default remote repository to use when querying the + GitHub API for a locally cloned repository. + + gh uses the default repository for things like: + + - viewing, creating, and setting the default base for pull requests + - viewing and creating issues + - viewing and creating releases + - working with Actions + - adding secrets`) +} + type iprompter interface { Select(string, string, []string) (int, error) } @@ -44,29 +58,20 @@ func NewCmdDefault(f *cmdutil.Factory, runF func(*DefaultOptions) error) *cobra. cmd := &cobra.Command{ Use: "default []", Short: "Configure default repository", - Long: heredoc.Doc(` - Set default repository for current directory. - - gh uses the default repository for things like: - - - viewing, creating, and setting the default base for pull requests - - viewing and creating issues - - viewing and creating releases - - working with Actions - - adding secrets - `), + Long: explainer(), Example: heredoc.Doc(` Interactively select a default repository: - $ gh repo default Set a repository explicitly: - $ gh repo default owner/repo View the current default repository: - $ gh repo default --view + + Show more repository options in the interactive picker: + $ git remote add newrepo https://github.com/owner/repo + $ gh repo default `), Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { @@ -152,8 +157,6 @@ func defaultRun(opts *DefaultOptions) error { } cs := opts.IO.ColorScheme() - hintMsg := cs.Gray("(hint: for gh to see more remote repositories, add them with `git remote`)") - if selectedRepo == nil { if len(knownRepos) == 1 { selectedRepo = knownRepos[0] @@ -161,8 +164,6 @@ func defaultRun(opts *DefaultOptions) error { fmt.Fprintf(opts.IO.Out, "Found only one known remote repo, %s on %s.\n", cs.Bold(ghrepo.FullName(selectedRepo)), cs.Bold(selectedRepo.RepoHost())) - fmt.Fprintln(opts.IO.Out, hintMsg) - fmt.Fprintln(opts.IO.Out) } else { var repoNames []string current := "" @@ -174,18 +175,7 @@ func defaultRun(opts *DefaultOptions) error { repoNames = append(repoNames, ghrepo.FullName(knownRepo)) } - defaultExplainer := heredoc.Doc(` - gh uses the default repository for things like: - - - viewing, creating, and setting the default base for pull requests - - viewing and creating issues - - viewing and creating releases - - working with Actions - - adding secrets - `) - - fmt.Fprintln(opts.IO.Out, defaultExplainer) - fmt.Fprintln(opts.IO.Out, hintMsg) + fmt.Fprintln(opts.IO.Out, explainer()) fmt.Fprintln(opts.IO.Out) selected, err := opts.Prompter.Select("Which repository should be the default?", current, repoNames) From db4de603075090c9ec59d9c5ea5b00664325a3ac Mon Sep 17 00:00:00 2001 From: vilmibm Date: Wed, 14 Dec 2022 16:08:50 -0800 Subject: [PATCH 18/20] rename command and fix tests --- pkg/cmd/repo/repo.go | 4 +- .../default.go => setdefault/setdefault.go} | 14 +++--- .../setdefault_test.go} | 44 +++++++++---------- 3 files changed, 31 insertions(+), 31 deletions(-) rename pkg/cmd/repo/{default/default.go => setdefault/setdefault.go} (94%) rename pkg/cmd/repo/{default/default_test.go => setdefault/setdefault_test.go} (87%) diff --git a/pkg/cmd/repo/repo.go b/pkg/cmd/repo/repo.go index d3dce190d..bfaaf2ce4 100644 --- a/pkg/cmd/repo/repo.go +++ b/pkg/cmd/repo/repo.go @@ -6,7 +6,6 @@ import ( repoCloneCmd "github.com/cli/cli/v2/pkg/cmd/repo/clone" repoCreateCmd "github.com/cli/cli/v2/pkg/cmd/repo/create" creditsCmd "github.com/cli/cli/v2/pkg/cmd/repo/credits" - repoDefaultCmd "github.com/cli/cli/v2/pkg/cmd/repo/default" repoDeleteCmd "github.com/cli/cli/v2/pkg/cmd/repo/delete" deployKeyCmd "github.com/cli/cli/v2/pkg/cmd/repo/deploy-key" repoEditCmd "github.com/cli/cli/v2/pkg/cmd/repo/edit" @@ -14,6 +13,7 @@ import ( gardenCmd "github.com/cli/cli/v2/pkg/cmd/repo/garden" repoListCmd "github.com/cli/cli/v2/pkg/cmd/repo/list" repoRenameCmd "github.com/cli/cli/v2/pkg/cmd/repo/rename" + repoDefaultCmd "github.com/cli/cli/v2/pkg/cmd/repo/setdefault" repoSyncCmd "github.com/cli/cli/v2/pkg/cmd/repo/sync" repoViewCmd "github.com/cli/cli/v2/pkg/cmd/repo/view" "github.com/cli/cli/v2/pkg/cmdutil" @@ -53,7 +53,7 @@ func NewCmdRepo(f *cmdutil.Factory) *cobra.Command { cmd.AddCommand(repoRenameCmd.NewCmdRename(f, nil)) cmd.AddCommand(repoDeleteCmd.NewCmdDelete(f, nil)) cmd.AddCommand(repoArchiveCmd.NewCmdArchive(f, nil)) - cmd.AddCommand(repoDefaultCmd.NewCmdDefault(f, nil)) + cmd.AddCommand(repoDefaultCmd.NewCmdSetDefault(f, nil)) return cmd } diff --git a/pkg/cmd/repo/default/default.go b/pkg/cmd/repo/setdefault/setdefault.go similarity index 94% rename from pkg/cmd/repo/default/default.go rename to pkg/cmd/repo/setdefault/setdefault.go index 8b618d0f0..fdb1465ae 100644 --- a/pkg/cmd/repo/default/default.go +++ b/pkg/cmd/repo/setdefault/setdefault.go @@ -37,7 +37,7 @@ type iprompter interface { Select(string, string, []string) (int, error) } -type DefaultOptions struct { +type SetDefaultOptions struct { IO *iostreams.IOStreams Remotes func() (context.Remotes, error) HttpClient func() (*http.Client, error) @@ -47,8 +47,8 @@ type DefaultOptions struct { ViewMode bool } -func NewCmdDefault(f *cmdutil.Factory, runF func(*DefaultOptions) error) *cobra.Command { - opts := &DefaultOptions{ +func NewCmdSetDefault(f *cmdutil.Factory, runF func(*SetDefaultOptions) error) *cobra.Command { + opts := &SetDefaultOptions{ IO: f.IOStreams, HttpClient: f.HttpClient, Remotes: f.Remotes, @@ -56,8 +56,8 @@ func NewCmdDefault(f *cmdutil.Factory, runF func(*DefaultOptions) error) *cobra. } cmd := &cobra.Command{ - Use: "default []", - Short: "Configure default repository", + Use: "set-default []", + Short: "Configure default repository for this directory", Long: explainer(), Example: heredoc.Doc(` Interactively select a default repository: @@ -97,7 +97,7 @@ func NewCmdDefault(f *cmdutil.Factory, runF func(*DefaultOptions) error) *cobra. return runF(opts) } - return defaultRun(opts) + return setDefaultRun(opts) }, } @@ -106,7 +106,7 @@ func NewCmdDefault(f *cmdutil.Factory, runF func(*DefaultOptions) error) *cobra. return cmd } -func defaultRun(opts *DefaultOptions) error { +func setDefaultRun(opts *SetDefaultOptions) error { remotes, err := opts.Remotes() if err != nil { return err diff --git a/pkg/cmd/repo/default/default_test.go b/pkg/cmd/repo/setdefault/setdefault_test.go similarity index 87% rename from pkg/cmd/repo/default/default_test.go rename to pkg/cmd/repo/setdefault/setdefault_test.go index b8c129da8..efd893756 100644 --- a/pkg/cmd/repo/default/default_test.go +++ b/pkg/cmd/repo/setdefault/setdefault_test.go @@ -17,12 +17,12 @@ import ( "github.com/stretchr/testify/assert" ) -func TestNewCmdDefault(t *testing.T) { +func TestNewCmdSetDefault(t *testing.T) { tests := []struct { name string gitStubs func(*run.CommandStubber) input string - output DefaultOptions + output SetDefaultOptions wantErr bool errMsg string }{ @@ -32,7 +32,7 @@ func TestNewCmdDefault(t *testing.T) { cs.Register(`git rev-parse --is-inside-work-tree`, 0, "true") }, input: "", - output: DefaultOptions{}, + output: SetDefaultOptions{}, }, { name: "repo argument", @@ -40,7 +40,7 @@ func TestNewCmdDefault(t *testing.T) { cs.Register(`git rev-parse --is-inside-work-tree`, 0, "true") }, input: "cli/cli", - output: DefaultOptions{Repo: ghrepo.New("cli", "cli")}, + output: SetDefaultOptions{Repo: ghrepo.New("cli", "cli")}, }, { name: "invalid repo argument", @@ -55,7 +55,7 @@ func TestNewCmdDefault(t *testing.T) { cs.Register(`git rev-parse --is-inside-work-tree`, 0, "true") }, input: "--view", - output: DefaultOptions{ViewMode: true}, + output: SetDefaultOptions{ViewMode: true}, }, { name: "run from non-git directory", @@ -77,8 +77,8 @@ func TestNewCmdDefault(t *testing.T) { IOStreams: io, } - var gotOpts *DefaultOptions - cmd := NewCmdDefault(f, func(opts *DefaultOptions) error { + var gotOpts *SetDefaultOptions + cmd := NewCmdSetDefault(f, func(opts *SetDefaultOptions) error { gotOpts = opts return nil }) @@ -117,7 +117,7 @@ func TestDefaultRun(t *testing.T) { tests := []struct { name string tty bool - opts DefaultOptions + opts SetDefaultOptions remotes []*context.Remote httpStubs func(*httpmock.Registry) gitStubs func(*run.CommandStubber) @@ -128,7 +128,7 @@ func TestDefaultRun(t *testing.T) { }{ { name: "view mode no current default", - opts: DefaultOptions{ViewMode: true}, + opts: SetDefaultOptions{ViewMode: true}, remotes: []*context.Remote{ { Remote: &git.Remote{Name: "origin"}, @@ -139,7 +139,7 @@ func TestDefaultRun(t *testing.T) { }, { name: "view mode with base resolved current default", - opts: DefaultOptions{ViewMode: true}, + opts: SetDefaultOptions{ViewMode: true}, remotes: []*context.Remote{ { Remote: &git.Remote{Name: "origin", Resolved: "base"}, @@ -150,7 +150,7 @@ func TestDefaultRun(t *testing.T) { }, { name: "view mode with non-base resolved current default", - opts: DefaultOptions{ViewMode: true}, + opts: SetDefaultOptions{ViewMode: true}, remotes: []*context.Remote{ { Remote: &git.Remote{Name: "origin", Resolved: "PARENT/REPO"}, @@ -162,7 +162,7 @@ func TestDefaultRun(t *testing.T) { { name: "tty non-interactive mode no current default", tty: true, - opts: DefaultOptions{Repo: repo2}, + opts: SetDefaultOptions{Repo: repo2}, remotes: []*context.Remote{ { Remote: &git.Remote{Name: "origin"}, @@ -187,7 +187,7 @@ func TestDefaultRun(t *testing.T) { { name: "tty non-interactive mode set non-base default", tty: true, - opts: DefaultOptions{Repo: repo2}, + opts: SetDefaultOptions{Repo: repo2}, remotes: []*context.Remote{ { Remote: &git.Remote{Name: "origin"}, @@ -211,7 +211,7 @@ func TestDefaultRun(t *testing.T) { }, { name: "non-tty non-interactive mode no current default", - opts: DefaultOptions{Repo: repo2}, + opts: SetDefaultOptions{Repo: repo2}, remotes: []*context.Remote{ { Remote: &git.Remote{Name: "origin"}, @@ -236,7 +236,7 @@ func TestDefaultRun(t *testing.T) { { name: "non-interactive mode with current default", tty: true, - opts: DefaultOptions{Repo: repo2}, + opts: SetDefaultOptions{Repo: repo2}, remotes: []*context.Remote{ { Remote: &git.Remote{Name: "origin", Resolved: "base"}, @@ -261,7 +261,7 @@ func TestDefaultRun(t *testing.T) { }, { name: "non-interactive mode no known hosts", - opts: DefaultOptions{Repo: repo2}, + opts: SetDefaultOptions{Repo: repo2}, remotes: []*context.Remote{ { Remote: &git.Remote{Name: "origin"}, @@ -279,7 +279,7 @@ func TestDefaultRun(t *testing.T) { }, { name: "non-interactive mode no matching remotes", - opts: DefaultOptions{Repo: repo2}, + opts: SetDefaultOptions{Repo: repo2}, remotes: []*context.Remote{ { Remote: &git.Remote{Name: "origin"}, @@ -298,7 +298,7 @@ func TestDefaultRun(t *testing.T) { { name: "interactive mode", tty: true, - opts: DefaultOptions{}, + opts: SetDefaultOptions{}, remotes: []*context.Remote{ { Remote: &git.Remote{Name: "origin"}, @@ -329,12 +329,12 @@ func TestDefaultRun(t *testing.T) { } } }, - wantStdout: "gh uses the default repository for things like:\n\n - viewing, creating, and setting the default base for pull requests\n - viewing and creating issues\n - viewing and creating releases\n - working with Actions\n - adding secrets\n\n(hint: for gh to see more remote repositories, add them with `git remote`)\n\n✓ Set OWNER2/REPO2 as the default repository for the current directory\n", + wantStdout: "This command sets the default remote repository to use when querying the\nGitHub API for a locally cloned repository.\n\ngh uses the default repository for things like:\n\n - viewing, creating, and setting the default base for pull requests\n - viewing and creating issues\n - viewing and creating releases\n - working with Actions\n - adding secrets\n\n✓ Set OWNER2/REPO2 as the default repository for the current directory\n", }, { name: "interactive mode only one known host", tty: true, - opts: DefaultOptions{}, + opts: SetDefaultOptions{}, remotes: []*context.Remote{ { Remote: &git.Remote{Name: "origin"}, @@ -354,7 +354,7 @@ func TestDefaultRun(t *testing.T) { gitStubs: func(cs *run.CommandStubber) { cs.Register(`git config --add remote.upstream.gh-resolved base`, 0, "") }, - wantStdout: "Found only one known remote repo, OWNER2/REPO2 on github.com.\n(hint: for gh to see more remote repositories, add them with `git remote`)\n\n✓ Set OWNER2/REPO2 as the default repository for the current directory\n", + wantStdout: "Found only one known remote repo, OWNER2/REPO2 on github.com.\n✓ Set OWNER2/REPO2 as the default repository for the current directory\n", }, } @@ -391,7 +391,7 @@ func TestDefaultRun(t *testing.T) { tt.gitStubs(cs) } defer reg.Verify(t) - err := defaultRun(&tt.opts) + err := setDefaultRun(&tt.opts) if tt.wantErr { assert.EqualError(t, err, tt.errMsg) return From 65323979637eec714be1b82ea73d56f027358651 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Wed, 14 Dec 2022 16:21:14 -0800 Subject: [PATCH 19/20] use factory git client --- pkg/cmd/repo/setdefault/setdefault.go | 20 ++++++-------------- pkg/cmd/repo/setdefault/setdefault_test.go | 2 ++ 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/pkg/cmd/repo/setdefault/setdefault.go b/pkg/cmd/repo/setdefault/setdefault.go index fdb1465ae..71248e1ae 100644 --- a/pkg/cmd/repo/setdefault/setdefault.go +++ b/pkg/cmd/repo/setdefault/setdefault.go @@ -42,6 +42,7 @@ type SetDefaultOptions struct { Remotes func() (context.Remotes, error) HttpClient func() (*http.Client, error) Prompter iprompter + GitClient *git.Client Repo ghrepo.Interface ViewMode bool @@ -53,6 +54,7 @@ func NewCmdSetDefault(f *cmdutil.Factory, runF func(*SetDefaultOptions) error) * HttpClient: f.HttpClient, Remotes: f.Remotes, Prompter: f.Prompter, + GitClient: f.GitClient, } cmd := &cobra.Command{ @@ -198,13 +200,13 @@ func setDefaultRun(opts *SetDefaultOptions) error { } if currentDefaultRepo != nil { - if err := unsetDefaultRepo(currentDefaultRepo); err != nil { + if err := opts.GitClient.UnsetRemoteResolution( + ctx.Background(), currentDefaultRepo.Name); err != nil { return err } } - - err = setDefaultRepo(selectedRemote, resolution) - if err != nil { + if err = opts.GitClient.SetRemoteResolution( + ctx.Background(), selectedRemote.Name, resolution); err != nil { return err } @@ -228,13 +230,3 @@ func displayRemoteRepoName(remote *context.Remote) string { return ghrepo.FullName(repo) } - -func setDefaultRepo(remote *context.Remote, resolution string) error { - c := &git.Client{} - return c.SetRemoteResolution(ctx.Background(), remote.Name, resolution) -} - -func unsetDefaultRepo(remote *context.Remote) error { - c := &git.Client{} - return c.UnsetRemoteResolution(ctx.Background(), remote.Name) -} diff --git a/pkg/cmd/repo/setdefault/setdefault_test.go b/pkg/cmd/repo/setdefault/setdefault_test.go index efd893756..004dd69d4 100644 --- a/pkg/cmd/repo/setdefault/setdefault_test.go +++ b/pkg/cmd/repo/setdefault/setdefault_test.go @@ -377,6 +377,8 @@ func TestDefaultRun(t *testing.T) { return tt.remotes, nil } + tt.opts.GitClient = &git.Client{} + pm := &prompter.PrompterMock{} if tt.prompterStubs != nil { tt.prompterStubs(pm) From 5461d15b743d606856a193cd03332b2a1969d3c4 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Wed, 14 Dec 2022 16:24:21 -0800 Subject: [PATCH 20/20] final bit of feedback --- pkg/cmd/repo/setdefault/setdefault.go | 6 +++--- pkg/cmd/repo/setdefault/setdefault_test.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/cmd/repo/setdefault/setdefault.go b/pkg/cmd/repo/setdefault/setdefault.go index 71248e1ae..a86ca8960 100644 --- a/pkg/cmd/repo/setdefault/setdefault.go +++ b/pkg/cmd/repo/setdefault/setdefault.go @@ -22,15 +22,15 @@ import ( func explainer() string { return heredoc.Doc(` This command sets the default remote repository to use when querying the - GitHub API for a locally cloned repository. + GitHub API for the locally cloned repository. gh uses the default repository for things like: - - viewing, creating, and setting the default base for pull requests + - viewing and creating pull requests - viewing and creating issues - viewing and creating releases - working with Actions - - adding secrets`) + - adding repository and environment secrets`) } type iprompter interface { diff --git a/pkg/cmd/repo/setdefault/setdefault_test.go b/pkg/cmd/repo/setdefault/setdefault_test.go index 004dd69d4..707edf5b6 100644 --- a/pkg/cmd/repo/setdefault/setdefault_test.go +++ b/pkg/cmd/repo/setdefault/setdefault_test.go @@ -329,7 +329,7 @@ func TestDefaultRun(t *testing.T) { } } }, - wantStdout: "This command sets the default remote repository to use when querying the\nGitHub API for a locally cloned repository.\n\ngh uses the default repository for things like:\n\n - viewing, creating, and setting the default base for pull requests\n - viewing and creating issues\n - viewing and creating releases\n - working with Actions\n - adding secrets\n\n✓ Set OWNER2/REPO2 as the default repository for the current directory\n", + wantStdout: "This command sets the default remote repository to use when querying the\nGitHub API for the locally cloned repository.\n\ngh uses the default repository for things like:\n\n - viewing and creating pull requests\n - viewing and creating issues\n - viewing and creating releases\n - working with Actions\n - adding repository and environment secrets\n\n✓ Set OWNER2/REPO2 as the default repository for the current directory\n", }, { name: "interactive mode only one known host",