v2.30.0-pre 7341e0e

config.go RawHistoryFolder

1package remote
2
3import (
4	"fmt"
5	"os"
6	"path/filepath"
7	"strconv"
8
9	"github.com/pelletier/go-toml/v2"
10)
11
12// ClientConfig represents the client-mode configuration.
13type ClientConfig struct {
14	Client  ClientSettings         `toml:"client"`
15	Remotes map[string]RemoteEntry `toml:"remotes"`
16	Active  string                 `toml:"active"` // Active remote name
17}
18
19// ClientSettings represents the [client] section.
20type ClientSettings struct {
21	Mode        string `toml:"mode"`         // "local" (default) or "remote"
22	Remote      string `toml:"remote"`       // Remote Gordon URL
23	Token       string `toml:"token"`        // Auth token
24	TokenEnv    string `toml:"token_env"`    // Env var name for token
25	InsecureTLS bool   `toml:"insecure_tls"` // Skip TLS verification for remote admin API
26}
27
28// RemoteEntry represents a saved remote in [remotes.*].
29type RemoteEntry struct {
30	URL         string `toml:"url"`
31	Token       string `toml:"token,omitempty"`
32	TokenEnv    string `toml:"token_env,omitempty"`
33	InsecureTLS bool   `toml:"insecure_tls,omitempty"`
34}
35
36// DefaultClientConfigPath returns the default client config path.
37func DefaultClientConfigPath() string {
38	configDir, err := os.UserConfigDir()
39	if err != nil {
40		configDir = os.Getenv("HOME")
41	}
42	return filepath.Join(configDir, "gordon", "gordon.toml")
43}
44
45// DefaultRemotesPath returns the default remotes config path.
46func DefaultRemotesPath() string {
47	configDir, err := os.UserConfigDir()
48	if err != nil {
49		configDir = os.Getenv("HOME")
50	}
51	return filepath.Join(configDir, "gordon", "remotes.toml")
52}
53
54// LoadClientConfig loads the client configuration.
55func LoadClientConfig(path string) (*ClientConfig, error) {
56	if path == "" {
57		path = DefaultClientConfigPath()
58	}
59
60	data, err := os.ReadFile(path)
61	if err != nil {
62		if os.IsNotExist(err) {
63			// Return default config
64			return &ClientConfig{
65				Client: ClientSettings{
66					Mode: "local",
67				},
68				Remotes: make(map[string]RemoteEntry),
69			}, nil
70		}
71		return nil, fmt.Errorf("failed to read config: %w", err)
72	}
73
74	var config ClientConfig
75	if err := toml.Unmarshal(data, &config); err != nil {
76		return nil, fmt.Errorf("failed to parse config: %w", err)
77	}
78
79	if config.Remotes == nil {
80		config.Remotes = make(map[string]RemoteEntry)
81	}
82
83	return &config, nil
84}
85
86// LoadRemotes loads the remotes configuration.
87func LoadRemotes(path string) (*ClientConfig, error) {
88	if path == "" {
89		path = DefaultRemotesPath()
90	}
91
92	data, err := os.ReadFile(path)
93	if err != nil {
94		if os.IsNotExist(err) {
95			return &ClientConfig{
96				Remotes: make(map[string]RemoteEntry),
97			}, nil
98		}
99		return nil, fmt.Errorf("failed to read remotes: %w", err)
100	}
101
102	var config ClientConfig
103	if err := toml.Unmarshal(data, &config); err != nil {
104		return nil, fmt.Errorf("failed to parse remotes: %w", err)
105	}
106
107	if config.Remotes == nil {
108		config.Remotes = make(map[string]RemoteEntry)
109	}
110
111	return &config, nil
112}
113
114// SaveRemotes saves the remotes configuration.
115func SaveRemotes(path string, config *ClientConfig) error {
116	if path == "" {
117		path = DefaultRemotesPath()
118	}
119
120	// Ensure directory exists
121	dir := filepath.Dir(path)
122	if err := os.MkdirAll(dir, 0700); err != nil {
123		return fmt.Errorf("failed to create config directory: %w", err)
124	}
125
126	data, err := toml.Marshal(config)
127	if err != nil {
128		return fmt.Errorf("failed to marshal remotes: %w", err)
129	}
130
131	if err := os.WriteFile(path, data, 0600); err != nil {
132		return fmt.Errorf("failed to write remotes: %w", err)
133	}
134
135	return nil
136}
137
138// ResolveRemote resolves the remote URL and token from configuration.
139// Precedence: flag > env > config > active remote
140func ResolveRemote(flagRemote, flagToken string, flagInsecure bool) (url, token string, insecureTLS, isRemote bool) {
141	url, token, insecureTLS, _, isRemote = ResolveRemoteFull(flagRemote, flagToken, flagInsecure)
142	return url, token, insecureTLS, isRemote
143}
144
145// ResolveRemoteFull resolves the remote URL, token, and named remote from configuration.
146// It also returns the remote name for use in token persistence callbacks.
147// Precedence: flag > env > config > active remote
148func ResolveRemoteFull(flagRemote, flagToken string, flagInsecure bool) (url, token string, insecureTLS bool, remoteName string, isRemote bool) {
149	config, _ := LoadClientConfig("")
150	remotes, _ := LoadRemotes("")
151
152	var name string
153	url, name, isRemote = resolveRemoteURL(flagRemote, config, remotes)
154	if isRemote {
155		// resolveToken looks up remotes.Active internally; name == remotes.Active here
156		// because resolveRemoteURL only sets name when it finds the active remote entry.
157		token = resolveToken(flagToken, config, remotes)
158		insecureTLS = resolveInsecureTLS(flagInsecure, config, remotes, name)
159		remoteName = name
160	}
161
162	return url, token, insecureTLS, remoteName, isRemote
163}
164
165// resolveRemoteURL resolves the remote URL from various sources.
166func resolveRemoteURL(flagRemote string, config *ClientConfig, remotes *ClientConfig) (url, remoteName string, isRemote bool) {
167	// 1. Check flag
168	if flagRemote != "" {
169		return flagRemote, "", true
170	}
171
172	// 2. Check environment variable
173	if envRemote := os.Getenv("GORDON_REMOTE"); envRemote != "" {
174		return envRemote, "", true
175	}
176
177	// 3. Check client config
178	if config != nil && config.Client.Mode == "remote" && config.Client.Remote != "" {
179		return config.Client.Remote, "", true
180	}
181
182	// 4. Check active remote
183	if remotes != nil && remotes.Active != "" {
184		if remote, ok := remotes.Remotes[remotes.Active]; ok {
185			return remote.URL, remotes.Active, true
186		}
187	}
188
189	return "", "", false
190}
191
192// resolveToken resolves the authentication token from various sources.
193func resolveToken(flagToken string, config *ClientConfig, remotes *ClientConfig) string {
194	activeRemote := activeRemoteEntry(remotes)
195
196	// 1. Flag token takes precedence
197	if flagToken != "" {
198		return flagToken
199	}
200
201	// 2. Environment variable
202	if envToken := os.Getenv("GORDON_TOKEN"); envToken != "" {
203		return envToken
204	}
205
206	// 3. Config token
207	if config != nil {
208		if config.Client.Token != "" {
209			return config.Client.Token
210		}
211		if config.Client.TokenEnv != "" {
212			if envToken := os.Getenv(config.Client.TokenEnv); envToken != "" {
213				return envToken
214			}
215		}
216	}
217
218	// 4. Try pass store for active remote
219	if remotes != nil && remotes.Active != "" && passAvailable() {
220		if token, err := passReadToken(remotes.Active); err == nil && token != "" {
221			return token
222		}
223	}
224
225	// 5. Active remote token
226	if token := resolveRemoteEntryToken(activeRemote); token != "" {
227		return token
228	}
229
230	return ""
231}
232
233func activeRemoteEntry(remotes *ClientConfig) *RemoteEntry {
234	if remotes == nil || remotes.Active == "" {
235		return nil
236	}
237
238	remote, ok := remotes.Remotes[remotes.Active]
239	if !ok {
240		return nil
241	}
242
243	return &remote
244}
245
246func resolveRemoteEntryToken(remote *RemoteEntry) string {
247	if remote == nil {
248		return ""
249	}
250	if remote.Token != "" {
251		return remote.Token
252	}
253	if remote.TokenEnv != "" {
254		return os.Getenv(remote.TokenEnv)
255	}
256	return ""
257}
258
259// ResolveInsecureTLSForRemote resolves insecure TLS behavior for a named remote.
260// Precedence: flag > env > client config > specific remote config.
261func ResolveInsecureTLSForRemote(flagInsecure bool, remoteName string) bool {
262	config, _ := LoadClientConfig("")
263	remotes, _ := LoadRemotes("")
264	return resolveInsecureTLS(flagInsecure, config, remotes, remoteName)
265}
266
267func resolveInsecureTLS(flagInsecure bool, config *ClientConfig, remotes *ClientConfig, remoteName string) bool {
268	if flagInsecure {
269		return true
270	}
271
272	if env := os.Getenv("GORDON_INSECURE"); env != "" {
273		if value, err := strconv.ParseBool(env); err == nil {
274			return value
275		}
276		fmt.Fprintf(os.Stderr, "WARNING: invalid GORDON_INSECURE value %q, ignoring\n", env)
277	}
278
279	if config != nil && config.Client.InsecureTLS {
280		return true
281	}
282
283	if remoteName != "" && remotes != nil {
284		if remote, ok := remotes.Remotes[remoteName]; ok && remote.InsecureTLS {
285			return true
286		}
287	}
288
289	return false
290}
291
292// AddRemote adds a new remote to the remotes configuration.
293func AddRemote(name, url, token string, insecureTLS bool) error {
294	config, err := LoadRemotes("")
295	if err != nil {
296		return err
297	}
298
299	config.Remotes[name] = RemoteEntry{
300		URL:         url,
301		Token:       token,
302		InsecureTLS: insecureTLS,
303	}
304
305	return SaveRemotes("", config)
306}
307
308// RemoveRemote removes a remote from the remotes configuration.
309func RemoveRemote(name string) error {
310	config, err := LoadRemotes("")
311	if err != nil {
312		return err
313	}
314
315	delete(config.Remotes, name)
316
317	// Clear active if it was the removed remote
318	if config.Active == name {
319		config.Active = ""
320	}
321
322	return SaveRemotes("", config)
323}
324
325// SetActiveRemote sets the active remote.
326func SetActiveRemote(name string) error {
327	config, err := LoadRemotes("")
328	if err != nil {
329		return err
330	}
331
332	// Verify remote exists
333	if _, ok := config.Remotes[name]; !ok {
334		return fmt.Errorf("remote '%s' not found", name)
335	}
336
337	config.Active = name
338
339	return SaveRemotes("", config)
340}
341
342// ListRemotes returns all saved remotes.
343func ListRemotes() (map[string]RemoteEntry, string, error) {
344	config, err := LoadRemotes("")
345	if err != nil {
346		return nil, "", err
347	}
348
349	return config.Remotes, config.Active, nil
350}

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