v2.14.0 5e11a00

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	config, _ := LoadClientConfig("")
142	remotes, _ := LoadRemotes("")
143
144	url, remoteName, isRemote := resolveRemoteURL(flagRemote, config, remotes)
145	if isRemote {
146		token = resolveToken(flagToken, config, remotes)
147		insecureTLS = resolveInsecureTLS(flagInsecure, config, remotes, remoteName)
148	}
149
150	return url, token, insecureTLS, isRemote
151}
152
153// resolveRemoteURL resolves the remote URL from various sources.
154func resolveRemoteURL(flagRemote string, config *ClientConfig, remotes *ClientConfig) (url, remoteName string, isRemote bool) {
155	// 1. Check flag
156	if flagRemote != "" {
157		return flagRemote, "", true
158	}
159
160	// 2. Check environment variable
161	if envRemote := os.Getenv("GORDON_REMOTE"); envRemote != "" {
162		return envRemote, "", true
163	}
164
165	// 3. Check client config
166	if config != nil && config.Client.Mode == "remote" && config.Client.Remote != "" {
167		return config.Client.Remote, "", true
168	}
169
170	// 4. Check active remote
171	if remotes != nil && remotes.Active != "" {
172		if remote, ok := remotes.Remotes[remotes.Active]; ok {
173			return remote.URL, remotes.Active, true
174		}
175	}
176
177	return "", "", false
178}
179
180// resolveToken resolves the authentication token from various sources.
181func resolveToken(flagToken string, config *ClientConfig, remotes *ClientConfig) string {
182	// 1. Flag token takes precedence
183	if flagToken != "" {
184		return flagToken
185	}
186
187	// 2. Environment variable
188	if envToken := os.Getenv("GORDON_TOKEN"); envToken != "" {
189		return envToken
190	}
191
192	// 3. Config token
193	if config != nil {
194		if config.Client.Token != "" {
195			return config.Client.Token
196		}
197		if config.Client.TokenEnv != "" {
198			return os.Getenv(config.Client.TokenEnv)
199		}
200	}
201
202	// 4. Active remote token
203	if remotes != nil && remotes.Active != "" {
204		if remote, ok := remotes.Remotes[remotes.Active]; ok {
205			if remote.Token != "" {
206				return remote.Token
207			}
208			if remote.TokenEnv != "" {
209				return os.Getenv(remote.TokenEnv)
210			}
211		}
212	}
213
214	return ""
215}
216
217// ResolveInsecureTLSForRemote resolves insecure TLS behavior for a named remote.
218// Precedence: flag > env > client config > specific remote config.
219func ResolveInsecureTLSForRemote(flagInsecure bool, remoteName string) bool {
220	config, _ := LoadClientConfig("")
221	remotes, _ := LoadRemotes("")
222	return resolveInsecureTLS(flagInsecure, config, remotes, remoteName)
223}
224
225func resolveInsecureTLS(flagInsecure bool, config *ClientConfig, remotes *ClientConfig, remoteName string) bool {
226	if flagInsecure {
227		return true
228	}
229
230	if env := os.Getenv("GORDON_INSECURE"); env != "" {
231		if value, err := strconv.ParseBool(env); err == nil {
232			return value
233		}
234		fmt.Fprintf(os.Stderr, "WARNING: invalid GORDON_INSECURE value %q, ignoring\n", env)
235	}
236
237	if config != nil && config.Client.InsecureTLS {
238		return true
239	}
240
241	if remoteName != "" && remotes != nil {
242		if remote, ok := remotes.Remotes[remoteName]; ok && remote.InsecureTLS {
243			return true
244		}
245	}
246
247	return false
248}
249
250// AddRemote adds a new remote to the remotes configuration.
251func AddRemote(name, url, token string, insecureTLS bool) error {
252	config, err := LoadRemotes("")
253	if err != nil {
254		return err
255	}
256
257	config.Remotes[name] = RemoteEntry{
258		URL:         url,
259		Token:       token,
260		InsecureTLS: insecureTLS,
261	}
262
263	return SaveRemotes("", config)
264}
265
266// RemoveRemote removes a remote from the remotes configuration.
267func RemoveRemote(name string) error {
268	config, err := LoadRemotes("")
269	if err != nil {
270		return err
271	}
272
273	delete(config.Remotes, name)
274
275	// Clear active if it was the removed remote
276	if config.Active == name {
277		config.Active = ""
278	}
279
280	return SaveRemotes("", config)
281}
282
283// SetActiveRemote sets the active remote.
284func SetActiveRemote(name string) error {
285	config, err := LoadRemotes("")
286	if err != nil {
287		return err
288	}
289
290	// Verify remote exists
291	if _, ok := config.Remotes[name]; !ok {
292		return fmt.Errorf("remote '%s' not found", name)
293	}
294
295	config.Active = name
296
297	return SaveRemotes("", config)
298}
299
300// ListRemotes returns all saved remotes.
301func ListRemotes() (map[string]RemoteEntry, string, error) {
302	config, err := LoadRemotes("")
303	if err != nil {
304		return nil, "", err
305	}
306
307	return config.Remotes, config.Active, nil
308}

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 8

Tags 106

↑↓ moveenter openesc close