detached c1925b1

feat(remote): add Resolve() with name-first lookup

Replaces divergent resolution paths. Accepts saved remote names
or ad-hoc URLs. Token resolved from flag > env > pass > TOML.
commit
c1925b17ee4c349f1a6b71350af888df037d671c
author
brice · · 2026-04-04 11:24 UTC
parent
4051035
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +299 -0
M internal/adapters/in/cli/remote/config.go +94 -0
@@ -5,6 +5,7 @@ import (
55 "os"
66 "path/filepath"
77 "strconv"
8+ "strings"
89
910 "github.com/pelletier/go-toml/v2"
1011
@@ -305,6 +306,99 @@ func resolveInsecureTLS(flagInsecure bool, config *ClientConfig, remotes *Client
305306 return false
306307 }
307308
309+// ResolvedRemote holds the fully resolved remote target.
310+// Name is empty for ad-hoc URLs passed directly via flag or env.
311+type ResolvedRemote struct {
312+ Name string
313+ URL string
314+ Token string
315+ InsecureTLS bool
316+}
317+
318+// Resolve resolves the remote target from flags, environment, and config.
319+// Returns nil, false when no remote is configured (local mode).
320+//
321+// URL precedence: flag > GORDON_REMOTE env > active remote.
322+// The flag and env values are treated as a saved remote name first;
323+// if no match and the value starts with http:// or https://, used as ad-hoc URL.
324+//
325+// Token precedence: flag > GORDON_TOKEN env > named remote (pass > TOML token > token_env).
326+// InsecureTLS precedence: flag > GORDON_INSECURE env > remote entry field.
327+func Resolve(flagRemote, flagToken string, flagInsecure bool) (*ResolvedRemote, bool) {
328+ remotes, _ := LoadRemotes("")
329+
330+ name, url, found := resolveTarget(flagRemote, remotes)
331+ if !found {
332+ if envRemote := os.Getenv("GORDON_REMOTE"); envRemote != "" {
333+ name, url, found = resolveTarget(envRemote, remotes)
334+ }
335+ }
336+ if !found {
337+ if remotes != nil && remotes.Active != "" {
338+ if entry, ok := remotes.Remotes[remotes.Active]; ok {
339+ name = remotes.Active
340+ url = entry.URL
341+ found = true
342+ }
343+ }
344+ }
345+ if !found {
346+ return nil, false
347+ }
348+
349+ r := &ResolvedRemote{
350+ Name: name,
351+ URL: url,
352+ InsecureTLS: flagInsecure,
353+ }
354+
355+ // Token: flag > env > stored
356+ switch {
357+ case flagToken != "":
358+ r.Token = flagToken
359+ case os.Getenv("GORDON_TOKEN") != "":
360+ r.Token = os.Getenv("GORDON_TOKEN")
361+ case name != "":
362+ if remotes != nil {
363+ if entry, ok := remotes.Remotes[name]; ok {
364+ r.Token = ResolveTokenForRemote(name, entry)
365+ }
366+ }
367+ }
368+
369+ // InsecureTLS: flag already set; check env then entry
370+ if !r.InsecureTLS {
371+ if env := os.Getenv("GORDON_INSECURE"); env != "" {
372+ if v, err := strconv.ParseBool(env); err == nil {
373+ r.InsecureTLS = v
374+ }
375+ }
376+ }
377+ if !r.InsecureTLS && name != "" && remotes != nil {
378+ if entry, ok := remotes.Remotes[name]; ok {
379+ r.InsecureTLS = entry.InsecureTLS
380+ }
381+ }
382+
383+ return r, true
384+}
385+
386+// resolveTarget checks if value is a saved remote name or an ad-hoc URL.
387+func resolveTarget(value string, remotes *ClientConfig) (name, url string, found bool) {
388+ if value == "" {
389+ return "", "", false
390+ }
391+ if remotes != nil {
392+ if entry, ok := remotes.Remotes[value]; ok {
393+ return value, entry.URL, true
394+ }
395+ }
396+ if strings.HasPrefix(value, "http://") || strings.HasPrefix(value, "https://") {
397+ return "", value, true
398+ }
399+ return "", "", false
400+}
401+
308402 // AddRemote adds a new remote to the remotes configuration.
309403 func AddRemote(name, url, token string, insecureTLS bool) error {
310404 config, err := LoadRemotes("")
@@ -5,6 +5,7 @@ import (
5 "os"5 "os"
6 "path/filepath"6 "path/filepath"
7 "strconv"7 "strconv"
8+ "strings"
8 9
9 "github.com/pelletier/go-toml/v2"10 "github.com/pelletier/go-toml/v2"
10 11
@@ -305,6 +306,99 @@ func resolveInsecureTLS(flagInsecure bool, config *ClientConfig, remotes *Client
305 return false306 return false
306 }307 }
307 308
309+// ResolvedRemote holds the fully resolved remote target.
310+// Name is empty for ad-hoc URLs passed directly via flag or env.
311+type ResolvedRemote struct {
312+ Name string
313+ URL string
314+ Token string
315+ InsecureTLS bool
316+}
317+
318+// Resolve resolves the remote target from flags, environment, and config.
319+// Returns nil, false when no remote is configured (local mode).
320+//
321+// URL precedence: flag > GORDON_REMOTE env > active remote.
322+// The flag and env values are treated as a saved remote name first;
323+// if no match and the value starts with http:// or https://, used as ad-hoc URL.
324+//
325+// Token precedence: flag > GORDON_TOKEN env > named remote (pass > TOML token > token_env).
326+// InsecureTLS precedence: flag > GORDON_INSECURE env > remote entry field.
327+func Resolve(flagRemote, flagToken string, flagInsecure bool) (*ResolvedRemote, bool) {
328+ remotes, _ := LoadRemotes("")
329+
330+ name, url, found := resolveTarget(flagRemote, remotes)
331+ if !found {
332+ if envRemote := os.Getenv("GORDON_REMOTE"); envRemote != "" {
333+ name, url, found = resolveTarget(envRemote, remotes)
334+ }
335+ }
336+ if !found {
337+ if remotes != nil && remotes.Active != "" {
338+ if entry, ok := remotes.Remotes[remotes.Active]; ok {
339+ name = remotes.Active
340+ url = entry.URL
341+ found = true
342+ }
343+ }
344+ }
345+ if !found {
346+ return nil, false
347+ }
348+
349+ r := &ResolvedRemote{
350+ Name: name,
351+ URL: url,
352+ InsecureTLS: flagInsecure,
353+ }
354+
355+ // Token: flag > env > stored
356+ switch {
357+ case flagToken != "":
358+ r.Token = flagToken
359+ case os.Getenv("GORDON_TOKEN") != "":
360+ r.Token = os.Getenv("GORDON_TOKEN")
361+ case name != "":
362+ if remotes != nil {
363+ if entry, ok := remotes.Remotes[name]; ok {
364+ r.Token = ResolveTokenForRemote(name, entry)
365+ }
366+ }
367+ }
368+
369+ // InsecureTLS: flag already set; check env then entry
370+ if !r.InsecureTLS {
371+ if env := os.Getenv("GORDON_INSECURE"); env != "" {
372+ if v, err := strconv.ParseBool(env); err == nil {
373+ r.InsecureTLS = v
374+ }
375+ }
376+ }
377+ if !r.InsecureTLS && name != "" && remotes != nil {
378+ if entry, ok := remotes.Remotes[name]; ok {
379+ r.InsecureTLS = entry.InsecureTLS
380+ }
381+ }
382+
383+ return r, true
384+}
385+
386+// resolveTarget checks if value is a saved remote name or an ad-hoc URL.
387+func resolveTarget(value string, remotes *ClientConfig) (name, url string, found bool) {
388+ if value == "" {
389+ return "", "", false
390+ }
391+ if remotes != nil {
392+ if entry, ok := remotes.Remotes[value]; ok {
393+ return value, entry.URL, true
394+ }
395+ }
396+ if strings.HasPrefix(value, "http://") || strings.HasPrefix(value, "https://") {
397+ return "", value, true
398+ }
399+ return "", "", false
400+}
401+
308 // AddRemote adds a new remote to the remotes configuration.402 // AddRemote adds a new remote to the remotes configuration.
309 func AddRemote(name, url, token string, insecureTLS bool) error {403 func AddRemote(name, url, token string, insecureTLS bool) error {
310 config, err := LoadRemotes("")404 config, err := LoadRemotes("")
A internal/adapters/in/cli/remote/resolve_test.go +205 -0
@@ -0,0 +1,205 @@
1+package remote
2+
3+import (
4+ "testing"
5+
6+ "github.com/stretchr/testify/assert"
7+ "github.com/stretchr/testify/require"
8+)
9+
10+func TestResolve_FlagNameLookup(t *testing.T) {
11+ t.Setenv("XDG_CONFIG_HOME", t.TempDir())
12+ t.Setenv("GORDON_REMOTE", "")
13+ t.Setenv("GORDON_TOKEN", "")
14+ t.Setenv("GORDON_INSECURE", "")
15+
16+ config := &ClientConfig{
17+ Remotes: map[string]RemoteEntry{
18+ "prod": {URL: "https://gordon.example.com", Token: "tok123"},
19+ },
20+ }
21+ require.NoError(t, SaveRemotes("", config))
22+
23+ resolved, ok := Resolve("prod", "", false)
24+ require.True(t, ok)
25+ assert.Equal(t, "prod", resolved.Name)
26+ assert.Equal(t, "https://gordon.example.com", resolved.URL)
27+ assert.Equal(t, "tok123", resolved.Token)
28+ assert.False(t, resolved.InsecureTLS)
29+}
30+
31+func TestResolve_FlagURLPassthrough(t *testing.T) {
32+ t.Setenv("XDG_CONFIG_HOME", t.TempDir())
33+ t.Setenv("GORDON_REMOTE", "")
34+ t.Setenv("GORDON_TOKEN", "")
35+
36+ resolved, ok := Resolve("https://one-off.example.com", "mytoken", false)
37+ require.True(t, ok)
38+ assert.Equal(t, "", resolved.Name)
39+ assert.Equal(t, "https://one-off.example.com", resolved.URL)
40+ assert.Equal(t, "mytoken", resolved.Token)
41+}
42+
43+func TestResolve_ActiveRemoteFallback(t *testing.T) {
44+ t.Setenv("XDG_CONFIG_HOME", t.TempDir())
45+ t.Setenv("GORDON_REMOTE", "")
46+ t.Setenv("GORDON_TOKEN", "")
47+
48+ config := &ClientConfig{
49+ Active: "staging",
50+ Remotes: map[string]RemoteEntry{
51+ "staging": {URL: "https://staging.example.com", Token: "stok"},
52+ },
53+ }
54+ require.NoError(t, SaveRemotes("", config))
55+
56+ resolved, ok := Resolve("", "", false)
57+ require.True(t, ok)
58+ assert.Equal(t, "staging", resolved.Name)
59+ assert.Equal(t, "https://staging.example.com", resolved.URL)
60+ assert.Equal(t, "stok", resolved.Token)
61+}
62+
63+func TestResolve_NoRemote_LocalMode(t *testing.T) {
64+ t.Setenv("XDG_CONFIG_HOME", t.TempDir())
65+ t.Setenv("GORDON_REMOTE", "")
66+ t.Setenv("GORDON_TOKEN", "")
67+
68+ resolved, ok := Resolve("", "", false)
69+ assert.False(t, ok)
70+ assert.Nil(t, resolved)
71+}
72+
73+func TestResolve_EnvRemoteName(t *testing.T) {
74+ t.Setenv("XDG_CONFIG_HOME", t.TempDir())
75+ t.Setenv("GORDON_REMOTE", "prod")
76+ t.Setenv("GORDON_TOKEN", "")
77+
78+ config := &ClientConfig{
79+ Remotes: map[string]RemoteEntry{
80+ "prod": {URL: "https://gordon.example.com", Token: "tok"},
81+ },
82+ }
83+ require.NoError(t, SaveRemotes("", config))
84+
85+ resolved, ok := Resolve("", "", false)
86+ require.True(t, ok)
87+ assert.Equal(t, "prod", resolved.Name)
88+ assert.Equal(t, "https://gordon.example.com", resolved.URL)
89+}
90+
91+func TestResolve_EnvRemoteURL(t *testing.T) {
92+ t.Setenv("XDG_CONFIG_HOME", t.TempDir())
93+ t.Setenv("GORDON_REMOTE", "https://env.example.com")
94+ t.Setenv("GORDON_TOKEN", "envtok")
95+
96+ resolved, ok := Resolve("", "", false)
97+ require.True(t, ok)
98+ assert.Equal(t, "", resolved.Name)
99+ assert.Equal(t, "https://env.example.com", resolved.URL)
100+ assert.Equal(t, "envtok", resolved.Token)
101+}
102+
103+func TestResolve_FlagTokenOverridesStored(t *testing.T) {
104+ t.Setenv("XDG_CONFIG_HOME", t.TempDir())
105+ t.Setenv("GORDON_REMOTE", "")
106+ t.Setenv("GORDON_TOKEN", "")
107+
108+ config := &ClientConfig{
109+ Active: "prod",
110+ Remotes: map[string]RemoteEntry{
111+ "prod": {URL: "https://gordon.example.com", Token: "stored"},
112+ },
113+ }
114+ require.NoError(t, SaveRemotes("", config))
115+
116+ resolved, ok := Resolve("", "override", false)
117+ require.True(t, ok)
118+ assert.Equal(t, "override", resolved.Token)
119+}
120+
121+func TestResolve_EnvTokenOverridesStored(t *testing.T) {
122+ t.Setenv("XDG_CONFIG_HOME", t.TempDir())
123+ t.Setenv("GORDON_REMOTE", "")
124+ t.Setenv("GORDON_TOKEN", "envtoken")
125+
126+ config := &ClientConfig{
127+ Active: "prod",
128+ Remotes: map[string]RemoteEntry{
129+ "prod": {URL: "https://gordon.example.com", Token: "stored"},
130+ },
131+ }
132+ require.NoError(t, SaveRemotes("", config))
133+
134+ resolved, ok := Resolve("", "", false)
135+ require.True(t, ok)
136+ assert.Equal(t, "envtoken", resolved.Token)
137+}
138+
139+func TestResolve_InsecureTLSFlag(t *testing.T) {
140+ t.Setenv("XDG_CONFIG_HOME", t.TempDir())
141+ t.Setenv("GORDON_REMOTE", "")
142+ t.Setenv("GORDON_TOKEN", "")
143+ t.Setenv("GORDON_INSECURE", "")
144+
145+ config := &ClientConfig{
146+ Active: "prod",
147+ Remotes: map[string]RemoteEntry{
148+ "prod": {URL: "https://gordon.example.com"},
149+ },
150+ }
151+ require.NoError(t, SaveRemotes("", config))
152+
153+ resolved, ok := Resolve("", "", true)
154+ require.True(t, ok)
155+ assert.True(t, resolved.InsecureTLS)
156+}
157+
158+func TestResolve_InsecureTLSFromEntry(t *testing.T) {
159+ t.Setenv("XDG_CONFIG_HOME", t.TempDir())
160+ t.Setenv("GORDON_REMOTE", "")
161+ t.Setenv("GORDON_TOKEN", "")
162+ t.Setenv("GORDON_INSECURE", "")
163+
164+ config := &ClientConfig{
165+ Active: "prod",
166+ Remotes: map[string]RemoteEntry{
167+ "prod": {URL: "https://gordon.example.com", InsecureTLS: true},
168+ },
169+ }
170+ require.NoError(t, SaveRemotes("", config))
171+
172+ resolved, ok := Resolve("", "", false)
173+ require.True(t, ok)
174+ assert.True(t, resolved.InsecureTLS)
175+}
176+
177+func TestResolve_TokenEnvVar(t *testing.T) {
178+ t.Setenv("XDG_CONFIG_HOME", t.TempDir())
179+ t.Setenv("GORDON_REMOTE", "")
180+ t.Setenv("GORDON_TOKEN", "")
181+ t.Setenv("MY_TOKEN", "fromenv")
182+
183+ config := &ClientConfig{
184+ Active: "prod",
185+ Remotes: map[string]RemoteEntry{
186+ "prod": {URL: "https://gordon.example.com", TokenEnv: "MY_TOKEN"},
187+ },
188+ }
189+ require.NoError(t, SaveRemotes("", config))
190+
191+ resolved, ok := Resolve("", "", false)
192+ require.True(t, ok)
193+ assert.Equal(t, "fromenv", resolved.Token)
194+}
195+
196+func TestResolve_UnknownNameReturnsNotFound(t *testing.T) {
197+ t.Setenv("XDG_CONFIG_HOME", t.TempDir())
198+ t.Setenv("GORDON_REMOTE", "")
199+ t.Setenv("GORDON_TOKEN", "")
200+
201+ resolved, ok := Resolve("nonexistent", "", false)
202+ assert.False(t, ok)
203+ assert.Nil(t, resolved)
204+}
205+
@@ -0,0 +1,205 @@
1+package remote
2+
3+import (
4+ "testing"
5+
6+ "github.com/stretchr/testify/assert"
7+ "github.com/stretchr/testify/require"
8+)
9+
10+func TestResolve_FlagNameLookup(t *testing.T) {
11+ t.Setenv("XDG_CONFIG_HOME", t.TempDir())
12+ t.Setenv("GORDON_REMOTE", "")
13+ t.Setenv("GORDON_TOKEN", "")
14+ t.Setenv("GORDON_INSECURE", "")
15+
16+ config := &ClientConfig{
17+ Remotes: map[string]RemoteEntry{
18+ "prod": {URL: "https://gordon.example.com", Token: "tok123"},
19+ },
20+ }
21+ require.NoError(t, SaveRemotes("", config))
22+
23+ resolved, ok := Resolve("prod", "", false)
24+ require.True(t, ok)
25+ assert.Equal(t, "prod", resolved.Name)
26+ assert.Equal(t, "https://gordon.example.com", resolved.URL)
27+ assert.Equal(t, "tok123", resolved.Token)
28+ assert.False(t, resolved.InsecureTLS)
29+}
30+
31+func TestResolve_FlagURLPassthrough(t *testing.T) {
32+ t.Setenv("XDG_CONFIG_HOME", t.TempDir())
33+ t.Setenv("GORDON_REMOTE", "")
34+ t.Setenv("GORDON_TOKEN", "")
35+
36+ resolved, ok := Resolve("https://one-off.example.com", "mytoken", false)
37+ require.True(t, ok)
38+ assert.Equal(t, "", resolved.Name)
39+ assert.Equal(t, "https://one-off.example.com", resolved.URL)
40+ assert.Equal(t, "mytoken", resolved.Token)
41+}
42+
43+func TestResolve_ActiveRemoteFallback(t *testing.T) {
44+ t.Setenv("XDG_CONFIG_HOME", t.TempDir())
45+ t.Setenv("GORDON_REMOTE", "")
46+ t.Setenv("GORDON_TOKEN", "")
47+
48+ config := &ClientConfig{
49+ Active: "staging",
50+ Remotes: map[string]RemoteEntry{
51+ "staging": {URL: "https://staging.example.com", Token: "stok"},
52+ },
53+ }
54+ require.NoError(t, SaveRemotes("", config))
55+
56+ resolved, ok := Resolve("", "", false)
57+ require.True(t, ok)
58+ assert.Equal(t, "staging", resolved.Name)
59+ assert.Equal(t, "https://staging.example.com", resolved.URL)
60+ assert.Equal(t, "stok", resolved.Token)
61+}
62+
63+func TestResolve_NoRemote_LocalMode(t *testing.T) {
64+ t.Setenv("XDG_CONFIG_HOME", t.TempDir())
65+ t.Setenv("GORDON_REMOTE", "")
66+ t.Setenv("GORDON_TOKEN", "")
67+
68+ resolved, ok := Resolve("", "", false)
69+ assert.False(t, ok)
70+ assert.Nil(t, resolved)
71+}
72+
73+func TestResolve_EnvRemoteName(t *testing.T) {
74+ t.Setenv("XDG_CONFIG_HOME", t.TempDir())
75+ t.Setenv("GORDON_REMOTE", "prod")
76+ t.Setenv("GORDON_TOKEN", "")
77+
78+ config := &ClientConfig{
79+ Remotes: map[string]RemoteEntry{
80+ "prod": {URL: "https://gordon.example.com", Token: "tok"},
81+ },
82+ }
83+ require.NoError(t, SaveRemotes("", config))
84+
85+ resolved, ok := Resolve("", "", false)
86+ require.True(t, ok)
87+ assert.Equal(t, "prod", resolved.Name)
88+ assert.Equal(t, "https://gordon.example.com", resolved.URL)
89+}
90+
91+func TestResolve_EnvRemoteURL(t *testing.T) {
92+ t.Setenv("XDG_CONFIG_HOME", t.TempDir())
93+ t.Setenv("GORDON_REMOTE", "https://env.example.com")
94+ t.Setenv("GORDON_TOKEN", "envtok")
95+
96+ resolved, ok := Resolve("", "", false)
97+ require.True(t, ok)
98+ assert.Equal(t, "", resolved.Name)
99+ assert.Equal(t, "https://env.example.com", resolved.URL)
100+ assert.Equal(t, "envtok", resolved.Token)
101+}
102+
103+func TestResolve_FlagTokenOverridesStored(t *testing.T) {
104+ t.Setenv("XDG_CONFIG_HOME", t.TempDir())
105+ t.Setenv("GORDON_REMOTE", "")
106+ t.Setenv("GORDON_TOKEN", "")
107+
108+ config := &ClientConfig{
109+ Active: "prod",
110+ Remotes: map[string]RemoteEntry{
111+ "prod": {URL: "https://gordon.example.com", Token: "stored"},
112+ },
113+ }
114+ require.NoError(t, SaveRemotes("", config))
115+
116+ resolved, ok := Resolve("", "override", false)
117+ require.True(t, ok)
118+ assert.Equal(t, "override", resolved.Token)
119+}
120+
121+func TestResolve_EnvTokenOverridesStored(t *testing.T) {
122+ t.Setenv("XDG_CONFIG_HOME", t.TempDir())
123+ t.Setenv("GORDON_REMOTE", "")
124+ t.Setenv("GORDON_TOKEN", "envtoken")
125+
126+ config := &ClientConfig{
127+ Active: "prod",
128+ Remotes: map[string]RemoteEntry{
129+ "prod": {URL: "https://gordon.example.com", Token: "stored"},
130+ },
131+ }
132+ require.NoError(t, SaveRemotes("", config))
133+
134+ resolved, ok := Resolve("", "", false)
135+ require.True(t, ok)
136+ assert.Equal(t, "envtoken", resolved.Token)
137+}
138+
139+func TestResolve_InsecureTLSFlag(t *testing.T) {
140+ t.Setenv("XDG_CONFIG_HOME", t.TempDir())
141+ t.Setenv("GORDON_REMOTE", "")
142+ t.Setenv("GORDON_TOKEN", "")
143+ t.Setenv("GORDON_INSECURE", "")
144+
145+ config := &ClientConfig{
146+ Active: "prod",
147+ Remotes: map[string]RemoteEntry{
148+ "prod": {URL: "https://gordon.example.com"},
149+ },
150+ }
151+ require.NoError(t, SaveRemotes("", config))
152+
153+ resolved, ok := Resolve("", "", true)
154+ require.True(t, ok)
155+ assert.True(t, resolved.InsecureTLS)
156+}
157+
158+func TestResolve_InsecureTLSFromEntry(t *testing.T) {
159+ t.Setenv("XDG_CONFIG_HOME", t.TempDir())
160+ t.Setenv("GORDON_REMOTE", "")
161+ t.Setenv("GORDON_TOKEN", "")
162+ t.Setenv("GORDON_INSECURE", "")
163+
164+ config := &ClientConfig{
165+ Active: "prod",
166+ Remotes: map[string]RemoteEntry{
167+ "prod": {URL: "https://gordon.example.com", InsecureTLS: true},
168+ },
169+ }
170+ require.NoError(t, SaveRemotes("", config))
171+
172+ resolved, ok := Resolve("", "", false)
173+ require.True(t, ok)
174+ assert.True(t, resolved.InsecureTLS)
175+}
176+
177+func TestResolve_TokenEnvVar(t *testing.T) {
178+ t.Setenv("XDG_CONFIG_HOME", t.TempDir())
179+ t.Setenv("GORDON_REMOTE", "")
180+ t.Setenv("GORDON_TOKEN", "")
181+ t.Setenv("MY_TOKEN", "fromenv")
182+
183+ config := &ClientConfig{
184+ Active: "prod",
185+ Remotes: map[string]RemoteEntry{
186+ "prod": {URL: "https://gordon.example.com", TokenEnv: "MY_TOKEN"},
187+ },
188+ }
189+ require.NoError(t, SaveRemotes("", config))
190+
191+ resolved, ok := Resolve("", "", false)
192+ require.True(t, ok)
193+ assert.Equal(t, "fromenv", resolved.Token)
194+}
195+
196+func TestResolve_UnknownNameReturnsNotFound(t *testing.T) {
197+ t.Setenv("XDG_CONFIG_HOME", t.TempDir())
198+ t.Setenv("GORDON_REMOTE", "")
199+ t.Setenv("GORDON_TOKEN", "")
200+
201+ resolved, ok := Resolve("nonexistent", "", false)
202+ assert.False(t, ok)
203+ assert.Nil(t, resolved)
204+}
205+

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