Merge pull request #4859 from bchadwic/base-cmd
`gh config base` a way to configure the base repo
This commit is contained in:
commit
59812d429a
6 changed files with 738 additions and 44 deletions
|
|
@ -5,6 +5,7 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/cli/cli/v2/api"
|
||||
"github.com/cli/cli/v2/git"
|
||||
|
|
@ -85,36 +86,18 @@ func (r *ResolvedRemotes) BaseRepo(io *iostreams.IOStreams, p iprompter) (ghrepo
|
|||
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]
|
||||
|
|
@ -130,7 +113,8 @@ func (r *ResolvedRemotes) BaseRepo(io *iostreams.IOStreams, p iprompter) (ghrepo
|
|||
}
|
||||
|
||||
// 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 {
|
||||
|
|
@ -140,7 +124,7 @@ func (r *ResolvedRemotes) BaseRepo(io *iostreams.IOStreams, p iprompter) (ghrepo
|
|||
|
||||
// cache the result to git config
|
||||
c := &git.Client{}
|
||||
err := c.SetRemoteResolution(context.Background(), remote.Name, resolution)
|
||||
err = c.SetRemoteResolution(context.Background(), remote.Name, resolution)
|
||||
return selectedRepo, err
|
||||
}
|
||||
|
||||
|
|
@ -161,6 +145,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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -489,6 +489,36 @@ func (c *Client) AddRemote(ctx context.Context, name, urlStr string, trackingBra
|
|||
return remote, nil
|
||||
}
|
||||
|
||||
func (c *Client) InGitDirectory(ctx context.Context) bool {
|
||||
showCmd, err := c.Command(ctx, "rev-parse", "--is-inside-work-tree")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
out, err := showCmd.Output()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
split := strings.Split(string(out), "\n")
|
||||
if len(split) > 0 {
|
||||
return split[0] == "true"
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *Client) UnsetRemoteResolution(ctx context.Context, name string) error {
|
||||
args := []string{"config", "--unset", fmt.Sprintf("remote.%s.gh-resolved", name)}
|
||||
cmd, err := c.Command(ctx, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = cmd.Output()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func resolveGitPath() (string, error) {
|
||||
path, err := safeexec.LookPath("git")
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -13,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"
|
||||
|
|
@ -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.NewCmdSetDefault(f, nil))
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
|
|||
232
pkg/cmd/repo/setdefault/setdefault.go
Normal file
232
pkg/cmd/repo/setdefault/setdefault.go
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
package base
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
ctx "context"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
func explainer() string {
|
||||
return heredoc.Doc(`
|
||||
This command sets the default remote repository to use when querying the
|
||||
GitHub API for the locally cloned repository.
|
||||
|
||||
gh uses the default repository for things like:
|
||||
|
||||
- viewing and creating pull requests
|
||||
- viewing and creating issues
|
||||
- viewing and creating releases
|
||||
- working with Actions
|
||||
- adding repository and environment secrets`)
|
||||
}
|
||||
|
||||
type iprompter interface {
|
||||
Select(string, string, []string) (int, error)
|
||||
}
|
||||
|
||||
type SetDefaultOptions struct {
|
||||
IO *iostreams.IOStreams
|
||||
Remotes func() (context.Remotes, error)
|
||||
HttpClient func() (*http.Client, error)
|
||||
Prompter iprompter
|
||||
GitClient *git.Client
|
||||
|
||||
Repo ghrepo.Interface
|
||||
ViewMode bool
|
||||
}
|
||||
|
||||
func NewCmdSetDefault(f *cmdutil.Factory, runF func(*SetDefaultOptions) error) *cobra.Command {
|
||||
opts := &SetDefaultOptions{
|
||||
IO: f.IOStreams,
|
||||
HttpClient: f.HttpClient,
|
||||
Remotes: f.Remotes,
|
||||
Prompter: f.Prompter,
|
||||
GitClient: f.GitClient,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "set-default [<repository>]",
|
||||
Short: "Configure default repository for this directory",
|
||||
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 {
|
||||
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")
|
||||
}
|
||||
|
||||
c := &git.Client{}
|
||||
|
||||
if !c.InGitDirectory(ctx.Background()) {
|
||||
return errors.New("must be run from inside a git repository")
|
||||
}
|
||||
|
||||
if runF != nil {
|
||||
return runF(opts)
|
||||
}
|
||||
|
||||
return setDefaultRun(opts)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVarP(&opts.ViewMode, "view", "v", false, "view the current default repository")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func setDefaultRun(opts *SetDefaultOptions) error {
|
||||
remotes, err := opts.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))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
httpClient, err := opts.HttpClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
apiClient := api.NewClientFromHTTP(httpClient)
|
||||
|
||||
resolvedRemotes, err := context.ResolveRemotesToRepos(remotes, apiClient, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
knownRepos, err := resolvedRemotes.NetworkRepos()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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))
|
||||
}
|
||||
}
|
||||
cs := opts.IO.ColorScheme()
|
||||
|
||||
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()))
|
||||
} else {
|
||||
var repoNames []string
|
||||
current := ""
|
||||
if currentDefaultRepo != nil {
|
||||
current = ghrepo.FullName(currentDefaultRepo)
|
||||
}
|
||||
|
||||
for _, knownRepo := range knownRepos {
|
||||
repoNames = append(repoNames, ghrepo.FullName(knownRepo))
|
||||
}
|
||||
|
||||
fmt.Fprintln(opts.IO.Out, explainer())
|
||||
fmt.Fprintln(opts.IO.Out)
|
||||
|
||||
selected, err := opts.Prompter.Select("Which repository should be the default?", current, repoNames)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not prompt: %w", err)
|
||||
}
|
||||
selectedName := repoNames[selected]
|
||||
|
||||
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 := opts.GitClient.UnsetRemoteResolution(
|
||||
ctx.Background(), currentDefaultRepo.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err = opts.GitClient.SetRemoteResolution(
|
||||
ctx.Background(), selectedRemote.Name, resolution); 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)
|
||||
}
|
||||
405
pkg/cmd/repo/setdefault/setdefault_test.go
Normal file
405
pkg/cmd/repo/setdefault/setdefault_test.go
Normal file
|
|
@ -0,0 +1,405 @@
|
|||
package base
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"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/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/google/shlex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewCmdSetDefault(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
gitStubs func(*run.CommandStubber)
|
||||
input string
|
||||
output SetDefaultOptions
|
||||
wantErr bool
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
name: "no argument",
|
||||
gitStubs: func(cs *run.CommandStubber) {
|
||||
cs.Register(`git rev-parse --is-inside-work-tree`, 0, "true")
|
||||
},
|
||||
input: "",
|
||||
output: SetDefaultOptions{},
|
||||
},
|
||||
{
|
||||
name: "repo argument",
|
||||
gitStubs: func(cs *run.CommandStubber) {
|
||||
cs.Register(`git rev-parse --is-inside-work-tree`, 0, "true")
|
||||
},
|
||||
input: "cli/cli",
|
||||
output: SetDefaultOptions{Repo: ghrepo.New("cli", "cli")},
|
||||
},
|
||||
{
|
||||
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")
|
||||
},
|
||||
input: "--view",
|
||||
output: SetDefaultOptions{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 *SetDefaultOptions
|
||||
cmd := NewCmdSetDefault(f, func(opts *SetDefaultOptions) 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) {
|
||||
argv, err := shlex.Split(tt.input)
|
||||
assert.NoError(t, err)
|
||||
|
||||
cmd.SetArgs(argv)
|
||||
|
||||
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 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 SetDefaultOptions
|
||||
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",
|
||||
opts: SetDefaultOptions{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: SetDefaultOptions{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: SetDefaultOptions{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: SetDefaultOptions{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: SetDefaultOptions{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: SetDefaultOptions{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: SetDefaultOptions{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: SetDefaultOptions{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: SetDefaultOptions{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: SetDefaultOptions{},
|
||||
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, "")
|
||||
},
|
||||
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: "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",
|
||||
tty: true,
|
||||
opts: SetDefaultOptions{},
|
||||
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: "Found only one known remote repo, OWNER2/REPO2 on github.com.\n✓ 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)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
tt.opts.GitClient = &git.Client{}
|
||||
|
||||
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)
|
||||
if tt.gitStubs != nil {
|
||||
tt.gitStubs(cs)
|
||||
}
|
||||
defer reg.Verify(t)
|
||||
err := setDefaultRun(&tt.opts)
|
||||
if tt.wantErr {
|
||||
assert.EqualError(t, err, tt.errMsg)
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.wantStdout, stdout.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue