cli/pkg/cmd/auth/switch/switch.go

171 lines
4.5 KiB
Go

package authswitch
import (
"errors"
"fmt"
"slices"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/pkg/cmd/auth/shared"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/spf13/cobra"
)
type SwitchOptions struct {
IO *iostreams.IOStreams
Config func() (config.Config, error)
Prompter shared.Prompt
Hostname string
Username string
}
func NewCmdSwitch(f *cmdutil.Factory, runF func(*SwitchOptions) error) *cobra.Command {
opts := SwitchOptions{
IO: f.IOStreams,
Config: f.Config,
Prompter: f.Prompter,
}
cmd := &cobra.Command{
Use: "switch",
Args: cobra.ExactArgs(0),
Short: "Switch active GitHub account",
Long: heredoc.Doc(`
Switch the active account for a GitHub host.
This command changes the authentication configuration that will
be used when running commands targeting the specified GitHub host.
`),
Example: heredoc.Doc(`
# Select what host and account to switch to via a prompt
$ gh auth switch
# Switch to a specific host and specific account
$ gh auth logout --hostname enterprise.internal --user monalisa
`),
RunE: func(c *cobra.Command, args []string) error {
if runF != nil {
return runF(&opts)
}
return switchRun(&opts)
},
}
cmd.Flags().StringVarP(&opts.Hostname, "hostname", "h", "", "The hostname of the GitHub instance to switch account for")
cmd.Flags().StringVarP(&opts.Username, "user", "u", "", "The account to switch to")
return cmd
}
type hostUser struct {
host string
user string
active bool
}
type candidates []hostUser
func switchRun(opts *SwitchOptions) error {
hostname := opts.Hostname
username := opts.Username
cfg, err := opts.Config()
if err != nil {
return err
}
authCfg := cfg.Authentication()
knownHosts := authCfg.Hosts()
if len(knownHosts) == 0 {
return fmt.Errorf("not logged in to any hosts")
}
if hostname != "" {
if !slices.Contains(knownHosts, hostname) {
return fmt.Errorf("not logged in to %s", hostname)
}
if username != "" {
knownUsers := cfg.Authentication().UsersForHost(hostname)
if !slices.Contains(knownUsers, username) {
return fmt.Errorf("not logged in to %s account %s", hostname, username)
}
}
}
var candidates candidates
for _, host := range knownHosts {
if hostname != "" && host != hostname {
continue
}
hostActiveUser, err := authCfg.ActiveUser(host)
if err != nil {
return err
}
knownUsers := cfg.Authentication().UsersForHost(host)
for _, user := range knownUsers {
if username != "" && user != username {
continue
}
candidates = append(candidates, hostUser{host: host, user: user, active: user == hostActiveUser})
}
}
if len(candidates) == 0 {
return errors.New("no accounts matched that criteria")
} else if len(candidates) == 1 {
hostname = candidates[0].host
username = candidates[0].user
} else if len(candidates) == 2 &&
candidates[0].host == candidates[1].host {
// If there is a single host with two users, automatically swith to the
// inactive user without prompting.
hostname = candidates[0].host
username = candidates[0].user
if candidates[0].active {
username = candidates[1].user
}
} else if !opts.IO.CanPrompt() {
return errors.New("unable to determine which account to switch to, please specify `--hostname` and `--user`")
} else {
prompts := make([]string, len(candidates))
for i, c := range candidates {
prompt := fmt.Sprintf("%s (%s)", c.user, c.host)
if c.active {
prompt += " - active"
}
prompts[i] = prompt
}
selected, err := opts.Prompter.Select(
"What account do you want to switch to?", "", prompts)
if err != nil {
return fmt.Errorf("could not prompt: %w", err)
}
hostname = candidates[selected].host
username = candidates[selected].user
}
if src, writeable := shared.AuthTokenWriteable(authCfg, hostname); !writeable {
fmt.Fprintf(opts.IO.ErrOut, "The value of the %s environment variable is being used for authentication.\n", src)
fmt.Fprint(opts.IO.ErrOut, "To have GitHub CLI manage credentials instead, first clear the value from the environment.\n")
return cmdutil.SilentError
}
cs := opts.IO.ColorScheme()
if err := authCfg.SwitchUser(hostname, username); err != nil {
fmt.Fprintf(opts.IO.ErrOut, "%s Failed to switch account for %s to %s\n",
cs.FailureIcon(), hostname, cs.Bold(username))
return err
}
fmt.Fprintf(opts.IO.ErrOut, "%s Switched active account for %s to %s\n",
cs.SuccessIcon(), hostname, cs.Bold(username))
return nil
}