File: switch.go

package info (click to toggle)
gh 2.46.0-4
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 15,548 kB
  • sloc: sh: 227; makefile: 117
file content (177 lines) | stat: -rw-r--r-- 4,935 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
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.Docf(`
			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.

			If the specified host has two accounts, the active account will be switched
			automatically. If there are more than two accounts, disambiguation will be
			required either through the %[1]s--user%[1]s flag or an interactive prompt.

			For a list of authenticated accounts you can run %[1]sgh auth status%[1]s.
		`, "`"),
		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 switch 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
}