v2.14.0 5e11a00

remotes.go RawHistoryFolder

1package cli
2
3import (
4	"fmt"
5	"strings"
6
7	"github.com/bnema/gordon/internal/adapters/in/cli/remote"
8	"github.com/bnema/gordon/internal/adapters/in/cli/ui/components"
9	"github.com/bnema/gordon/internal/adapters/in/cli/ui/styles"
10
11	"github.com/spf13/cobra"
12)
13
14// newRemotesCmd creates the remotes command group.
15func newRemotesCmd() *cobra.Command {
16	cmd := &cobra.Command{
17		Use:   "remotes",
18		Short: "Manage saved remote Gordon instances",
19		Long: `Manage saved remote Gordon instances.
20
21Remotes allow you to save frequently used remote Gordon instances
22and quickly switch between them.
23
24Configuration is stored in ~/.config/gordon/remotes.toml`,
25	}
26
27	cmd.AddCommand(newRemotesListCmd())
28	cmd.AddCommand(newRemotesAddCmd())
29	cmd.AddCommand(newRemotesRemoveCmd())
30	cmd.AddCommand(newRemotesUseCmd())
31	cmd.AddCommand(newRemotesSetTokenCmd())
32
33	return cmd
34}
35
36// newRemotesListCmd creates the remotes list command.
37func newRemotesListCmd() *cobra.Command {
38	return &cobra.Command{
39		Use:   "list",
40		Short: "List saved remotes",
41		RunE: func(cmd *cobra.Command, args []string) error {
42			remotes, active, err := remote.ListRemotes()
43			if err != nil {
44				return fmt.Errorf("failed to list remotes: %w", err)
45			}
46
47			if len(remotes) == 0 {
48				fmt.Println(styles.Theme.Muted.Render("No remotes configured"))
49				fmt.Println()
50				fmt.Println("Add a remote with:")
51				fmt.Println(styles.Theme.Bold.Render("  gordon remotes add <name> <url>"))
52				return nil
53			}
54
55			fmt.Println(styles.Theme.Title.Render("Saved Remotes"))
56			fmt.Println()
57
58			// Build table rows
59			rows := make([][]string, 0, len(remotes))
60			for name, r := range remotes {
61				status := ""
62				if name == active {
63					status = styles.Theme.Success.Render("active")
64				}
65
66				// Mask token if present
67				tokenStatus := styles.Theme.Muted.Render("none")
68				if r.Token != "" {
69					tokenStatus = styles.Theme.Success.Render("set")
70				} else if r.TokenEnv != "" {
71					tokenStatus = fmt.Sprintf("$%s", r.TokenEnv)
72				}
73
74				tlsStatus := ""
75				if r.InsecureTLS {
76					tlsStatus = styles.Theme.Warning.Render("insecure")
77				}
78
79				rows = append(rows, []string{name, r.URL, tokenStatus, tlsStatus, status})
80			}
81
82			// Render table
83			table := components.NewTable(
84				components.WithColumns([]components.TableColumn{
85					{Title: "Name", Width: 15},
86					{Title: "URL", Width: 35},
87					{Title: "Token", Width: 15},
88					{Title: "TLS", Width: 10},
89					{Title: "Status", Width: 10},
90				}),
91				components.WithRows(rows),
92			)
93
94			fmt.Println(table.View())
95
96			return nil
97		},
98	}
99}
100
101// newRemotesAddCmd creates the remotes add command.
102func newRemotesAddCmd() *cobra.Command {
103	var token string
104	var tokenEnv string
105	var insecureTLS bool
106
107	cmd := &cobra.Command{
108		Use:   "add <name> <url>",
109		Short: "Add a new remote",
110		Long: `Add a new saved remote.
111
112Examples:
113  gordon remotes add prod https://gordon.mydomain.com
114  gordon remotes add prod https://gordon.mydomain.com --token eyJ...
115  gordon remotes add staging https://staging.mydomain.com --token-env STAGING_TOKEN
116  gordon remotes add dev https://dev.internal --insecure`,
117		Args: cobra.ExactArgs(2),
118		RunE: func(cmd *cobra.Command, args []string) error {
119			name := args[0]
120			url := normalizeURL(args[1])
121
122			// If token-env is provided, use that instead of token
123			finalToken := token
124			if tokenEnv != "" {
125				// Store token_env reference instead of actual token
126				if err := addRemoteWithEnv(name, url, tokenEnv, insecureTLS); err != nil {
127					return fmt.Errorf("failed to add remote: %w", err)
128				}
129			} else {
130				if err := remote.AddRemote(name, url, finalToken, insecureTLS); err != nil {
131					return fmt.Errorf("failed to add remote: %w", err)
132				}
133			}
134
135			fmt.Println(styles.RenderSuccess(fmt.Sprintf("Remote added: %s -> %s", name, url)))
136
137			if token == "" && tokenEnv == "" {
138				fmt.Println(styles.Theme.Muted.Render("Tip: Add a token with --token or --token-env for authenticated access"))
139			}
140
141			return nil
142		},
143	}
144
145	cmd.Flags().StringVar(&token, "token", "", "Authentication token")
146	cmd.Flags().StringVar(&tokenEnv, "token-env", "", "Environment variable containing token")
147	cmd.Flags().BoolVar(&insecureTLS, "insecure", false, "Skip TLS certificate verification")
148
149	return cmd
150}
151
152// addRemoteWithEnv adds a remote with a token environment variable reference.
153func addRemoteWithEnv(name, url, tokenEnv string, insecureTLS bool) error {
154	config, err := remote.LoadRemotes("")
155	if err != nil {
156		return err
157	}
158
159	config.Remotes[name] = remote.RemoteEntry{
160		URL:         url,
161		TokenEnv:    tokenEnv,
162		InsecureTLS: insecureTLS,
163	}
164
165	return remote.SaveRemotes("", config)
166}
167
168// newRemotesRemoveCmd creates the remotes remove command.
169func newRemotesRemoveCmd() *cobra.Command {
170	var force bool
171
172	cmd := &cobra.Command{
173		Use:     "remove <name>",
174		Aliases: []string{"rm"},
175		Short:   "Remove a remote",
176		Args:    cobra.ExactArgs(1),
177		RunE: func(cmd *cobra.Command, args []string) error {
178			name := args[0]
179
180			// Check if remote exists
181			remotes, active, err := remote.ListRemotes()
182			if err != nil {
183				return fmt.Errorf("failed to list remotes: %w", err)
184			}
185
186			if _, exists := remotes[name]; !exists {
187				fmt.Println(styles.RenderError(fmt.Sprintf("Remote '%s' not found", name)))
188				return nil
189			}
190
191			// Confirm unless --force
192			if !force {
193				message := fmt.Sprintf("Remove remote '%s'?", name)
194				if name == active {
195					message = fmt.Sprintf("Remove remote '%s'? (currently active)", name)
196				}
197
198				confirmed, err := components.RunConfirm(message)
199				if err != nil {
200					return err
201				}
202				if !confirmed {
203					fmt.Println(styles.Theme.Muted.Render("Cancelled"))
204					return nil
205				}
206			}
207
208			if err := remote.RemoveRemote(name); err != nil {
209				return fmt.Errorf("failed to remove remote: %w", err)
210			}
211
212			fmt.Println(styles.RenderSuccess(fmt.Sprintf("Remote removed: %s", name)))
213			return nil
214		},
215	}
216
217	cmd.Flags().BoolVarP(&force, "force", "f", false, "Skip confirmation")
218
219	return cmd
220}
221
222// newRemotesUseCmd creates the remotes use command.
223func newRemotesUseCmd() *cobra.Command {
224	return &cobra.Command{
225		Use:   "use <name>",
226		Short: "Set the active remote",
227		Long: `Set a remote as the active default.
228
229When a remote is active, it will be used automatically for remote commands
230without needing to specify --remote.
231
232Examples:
233  gordon remotes use prod
234  gordon routes list  # Uses prod remote automatically`,
235		Args: cobra.ExactArgs(1),
236		RunE: func(cmd *cobra.Command, args []string) error {
237			name := args[0]
238
239			if err := remote.SetActiveRemote(name); err != nil {
240				if err.Error() == fmt.Sprintf("remote '%s' not found", name) {
241					fmt.Println(styles.RenderError(fmt.Sprintf("Remote '%s' not found", name)))
242					fmt.Println()
243					fmt.Println("Available remotes:")
244
245					remotes, _, _ := remote.ListRemotes()
246					for remoteName := range remotes {
247						fmt.Printf("  %s\n", remoteName)
248					}
249					return nil
250				}
251				return fmt.Errorf("failed to set active remote: %w", err)
252			}
253
254			fmt.Println(styles.RenderSuccess(fmt.Sprintf("Active remote set to '%s'", name)))
255			return nil
256		},
257	}
258}
259
260// newRemotesSetTokenCmd creates the remotes set-token command.
261func newRemotesSetTokenCmd() *cobra.Command {
262	return &cobra.Command{
263		Use:   "set-token <name> <token>",
264		Short: "Set the token for a remote",
265		Long: `Set or update the authentication token for a saved remote.
266
267This is useful when:
268- The server uses token-based authentication (not password auth)
269- You have a pre-generated token from 'gordon auth token generate'
270- You want to update an expired token
271
272For servers with password authentication, use 'gordon auth login' instead.
273
274Examples:
275  gordon remotes set-token prod eyJhbGciOiJIUzI1NiIs...
276  gordon remotes set-token staging $(cat token.txt)`,
277		Args: cobra.ExactArgs(2),
278		RunE: func(cmd *cobra.Command, args []string) error {
279			name := args[0]
280			token := args[1]
281
282			// Verify remote exists
283			remotes, _, err := remote.ListRemotes()
284			if err != nil {
285				return fmt.Errorf("failed to list remotes: %w", err)
286			}
287
288			if _, exists := remotes[name]; !exists {
289				fmt.Println(styles.RenderError(fmt.Sprintf("Remote '%s' not found", name)))
290				return nil
291			}
292
293			if err := remote.UpdateRemoteToken(name, token); err != nil {
294				return fmt.Errorf("failed to update token: %w", err)
295			}
296
297			fmt.Println(styles.RenderSuccess(fmt.Sprintf("Token updated for remote '%s'", name)))
298			return nil
299		},
300	}
301}
302
303// normalizeURL ensures the URL has a protocol scheme, defaulting to https.
304func normalizeURL(url string) string {
305	if strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://") {
306		return url
307	}
308	return "https://" + url
309}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close