v2.8.0 6aced72

config.go RawHistoryFolder

1package remote
2
3import (
4	"fmt"
5	"os"
6	"path/filepath"
7
8	"github.com/pelletier/go-toml/v2"
9)
10
11// ClientConfig represents the client-mode configuration.
12type ClientConfig struct {
13	Client  ClientSettings         `toml:"client"`
14	Remotes map[string]RemoteEntry `toml:"remotes"`
15	Active  string                 `toml:"active"` // Active remote name
16}
17
18// ClientSettings represents the [client] section.
19type ClientSettings struct {
20	Mode     string `toml:"mode"`      // "local" (default) or "remote"
21	Remote   string `toml:"remote"`    // Remote Gordon URL
22	Token    string `toml:"token"`     // Auth token
23	TokenEnv string `toml:"token_env"` // Env var name for token
24}
25
26// RemoteEntry represents a saved remote in [remotes.*].
27type RemoteEntry struct {
28	URL      string `toml:"url"`
29	Token    string `toml:"token,omitempty"`
30	TokenEnv string `toml:"token_env,omitempty"`
31}
32
33// DefaultClientConfigPath returns the default client config path.
34func DefaultClientConfigPath() string {
35	configDir, err := os.UserConfigDir()
36	if err != nil {
37		configDir = os.Getenv("HOME")
38	}
39	return filepath.Join(configDir, "gordon", "gordon.toml")
40}
41
42// DefaultRemotesPath returns the default remotes config path.
43func DefaultRemotesPath() string {
44	configDir, err := os.UserConfigDir()
45	if err != nil {
46		configDir = os.Getenv("HOME")
47	}
48	return filepath.Join(configDir, "gordon", "remotes.toml")
49}
50
51// LoadClientConfig loads the client configuration.
52func LoadClientConfig(path string) (*ClientConfig, error) {
53	if path == "" {
54		path = DefaultClientConfigPath()
55	}
56
57	data, err := os.ReadFile(path)
58	if err != nil {
59		if os.IsNotExist(err) {
60			// Return default config
61			return &ClientConfig{
62				Client: ClientSettings{
63					Mode: "local",
64				},
65				Remotes: make(map[string]RemoteEntry),
66			}, nil
67		}
68		return nil, fmt.Errorf("failed to read config: %w", err)
69	}
70
71	var config ClientConfig
72	if err := toml.Unmarshal(data, &config); err != nil {
73		return nil, fmt.Errorf("failed to parse config: %w", err)
74	}
75
76	if config.Remotes == nil {
77		config.Remotes = make(map[string]RemoteEntry)
78	}
79
80	return &config, nil
81}
82
83// LoadRemotes loads the remotes configuration.
84func LoadRemotes(path string) (*ClientConfig, error) {
85	if path == "" {
86		path = DefaultRemotesPath()
87	}
88
89	data, err := os.ReadFile(path)
90	if err != nil {
91		if os.IsNotExist(err) {
92			return &ClientConfig{
93				Remotes: make(map[string]RemoteEntry),
94			}, nil
95		}
96		return nil, fmt.Errorf("failed to read remotes: %w", err)
97	}
98
99	var config ClientConfig
100	if err := toml.Unmarshal(data, &config); err != nil {
101		return nil, fmt.Errorf("failed to parse remotes: %w", err)
102	}
103
104	if config.Remotes == nil {
105		config.Remotes = make(map[string]RemoteEntry)
106	}
107
108	return &config, nil
109}
110
111// SaveRemotes saves the remotes configuration.
112func SaveRemotes(path string, config *ClientConfig) error {
113	if path == "" {
114		path = DefaultRemotesPath()
115	}
116
117	// Ensure directory exists
118	dir := filepath.Dir(path)
119	if err := os.MkdirAll(dir, 0700); err != nil {
120		return fmt.Errorf("failed to create config directory: %w", err)
121	}
122
123	data, err := toml.Marshal(config)
124	if err != nil {
125		return fmt.Errorf("failed to marshal remotes: %w", err)
126	}
127
128	if err := os.WriteFile(path, data, 0600); err != nil {
129		return fmt.Errorf("failed to write remotes: %w", err)
130	}
131
132	return nil
133}
134
135// ResolveRemote resolves the remote URL and token from configuration.
136// Precedence: flag > env > config > active remote
137func ResolveRemote(flagRemote, flagToken string) (url, token string, isRemote bool) {
138	config, _ := LoadClientConfig("")
139	remotes, _ := LoadRemotes("")
140
141	url, isRemote = resolveRemoteURL(flagRemote, config, remotes)
142	if isRemote {
143		token = resolveToken(flagToken, config, remotes)
144	}
145
146	return url, token, isRemote
147}
148
149// resolveRemoteURL resolves the remote URL from various sources.
150func resolveRemoteURL(flagRemote string, config *ClientConfig, remotes *ClientConfig) (string, bool) {
151	// 1. Check flag
152	if flagRemote != "" {
153		return flagRemote, true
154	}
155
156	// 2. Check environment variable
157	if envRemote := os.Getenv("GORDON_REMOTE"); envRemote != "" {
158		return envRemote, true
159	}
160
161	// 3. Check client config
162	if config != nil && config.Client.Mode == "remote" && config.Client.Remote != "" {
163		return config.Client.Remote, true
164	}
165
166	// 4. Check active remote
167	if remotes != nil && remotes.Active != "" {
168		if remote, ok := remotes.Remotes[remotes.Active]; ok {
169			return remote.URL, true
170		}
171	}
172
173	return "", false
174}
175
176// resolveToken resolves the authentication token from various sources.
177func resolveToken(flagToken string, config *ClientConfig, remotes *ClientConfig) string {
178	// 1. Flag token takes precedence
179	if flagToken != "" {
180		return flagToken
181	}
182
183	// 2. Environment variable
184	if envToken := os.Getenv("GORDON_TOKEN"); envToken != "" {
185		return envToken
186	}
187
188	// 3. Config token
189	if config != nil {
190		if config.Client.Token != "" {
191			return config.Client.Token
192		}
193		if config.Client.TokenEnv != "" {
194			return os.Getenv(config.Client.TokenEnv)
195		}
196	}
197
198	// 4. Active remote token
199	if remotes != nil && remotes.Active != "" {
200		if remote, ok := remotes.Remotes[remotes.Active]; ok {
201			if remote.Token != "" {
202				return remote.Token
203			}
204			if remote.TokenEnv != "" {
205				return os.Getenv(remote.TokenEnv)
206			}
207		}
208	}
209
210	return ""
211}
212
213// AddRemote adds a new remote to the remotes configuration.
214func AddRemote(name, url, token string) error {
215	config, err := LoadRemotes("")
216	if err != nil {
217		return err
218	}
219
220	config.Remotes[name] = RemoteEntry{
221		URL:   url,
222		Token: token,
223	}
224
225	return SaveRemotes("", config)
226}
227
228// RemoveRemote removes a remote from the remotes configuration.
229func RemoveRemote(name string) error {
230	config, err := LoadRemotes("")
231	if err != nil {
232		return err
233	}
234
235	delete(config.Remotes, name)
236
237	// Clear active if it was the removed remote
238	if config.Active == name {
239		config.Active = ""
240	}
241
242	return SaveRemotes("", config)
243}
244
245// SetActiveRemote sets the active remote.
246func SetActiveRemote(name string) error {
247	config, err := LoadRemotes("")
248	if err != nil {
249		return err
250	}
251
252	// Verify remote exists
253	if _, ok := config.Remotes[name]; !ok {
254		return fmt.Errorf("remote '%s' not found", name)
255	}
256
257	config.Active = name
258
259	return SaveRemotes("", config)
260}
261
262// ListRemotes returns all saved remotes.
263func ListRemotes() (map[string]RemoteEntry, string, error) {
264	config, err := LoadRemotes("")
265	if err != nil {
266		return nil, "", err
267	}
268
269	return config.Remotes, config.Active, nil
270}

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