v3.4.0 593c1c3

remote_inference.go RawHistoryFolder

1package cli
2
3import (
4	"context"
5	"errors"
6	"fmt"
7	"net/http"
8	"os"
9	"strconv"
10	"strings"
11
12	"github.com/bnema/gordon/internal/adapters/in/cli/remote"
13	"github.com/bnema/gordon/internal/domain"
14)
15
16type remoteInferenceProbe func(context.Context, *remote.Client) (bool, error)
17
18func inferPushRemote(ctx context.Context, imageArg, _, dockerfile string) (*remote.ResolvedRemote, error) {
19	if imageArg == "" {
20		detected, err := detectImageName(dockerfile)
21		if err != nil {
22			return nil, err
23		}
24		imageArg = detected
25	}
26	classified := classifyPushArgument(imageArg)
27	return inferRemoteForImage(ctx, classified.repository)
28}
29
30func inferRemoteForImage(ctx context.Context, imageName string) (*remote.ResolvedRemote, error) {
31	return inferSavedRemote(ctx, "image", imageName, func(ctx context.Context, client *remote.Client) (bool, error) {
32		tags, err := client.ListTags(ctx, imageName)
33		if err != nil {
34			if isRemoteNotFoundError(err) {
35				return false, nil
36			}
37			return false, err
38		}
39		return len(tags) > 0, nil
40	})
41}
42
43func inferRemoteForRepository(ctx context.Context, repository string) (*remote.ResolvedRemote, error) {
44	return inferSavedRemote(ctx, "repository", repository, func(ctx context.Context, client *remote.Client) (bool, error) {
45		tags, err := client.ListTags(ctx, repository)
46		if err != nil {
47			if isRemoteNotFoundError(err) {
48				return false, nil
49			}
50			return false, err
51		}
52		return len(tags) > 0, nil
53	})
54}
55
56func inferSavedRemote(ctx context.Context, targetKind, target string, probe remoteInferenceProbe) (*remote.ResolvedRemote, error) {
57	remotes, ok := loadInferenceCandidateRemotes()
58	if !ok {
59		return nil, nil
60	}
61
62	names := sortedRemoteNames(remotes)
63	matches := make([]*remote.ResolvedRemote, 0, len(names))
64	probeFailures := make([]string, 0)
65
66	for _, name := range names {
67		entry := remotes[name]
68		matched, err := probe(ctx, newInferenceTargetClient(name, entry))
69		if err != nil {
70			probeFailures = append(probeFailures, fmt.Sprintf("%s (%v)", name, err))
71			continue
72		}
73		if matched {
74			matches = append(matches, resolvedRemoteFromEntry(name, entry))
75		}
76	}
77
78	if len(probeFailures) > 0 {
79		return nil, fmt.Errorf("could not safely infer remote for %s %q because probing failed for: %s; use --remote", targetKind, target, strings.Join(probeFailures, ", "))
80	}
81
82	if len(matches) > 1 {
83		names := make([]string, 0, len(matches))
84		for _, match := range matches {
85			names = append(names, match.DisplayName())
86		}
87		return nil, fmt.Errorf("multiple saved remotes match %s %q: %s; use --remote", targetKind, target, strings.Join(names, ", "))
88	}
89
90	if len(matches) == 1 {
91		return matches[0], nil
92	}
93
94	return nil, nil
95}
96
97func loadInferenceCandidateRemotes() (map[string]remote.RemoteEntry, bool) {
98	if _, ok := resolveExplicitTargetName(); ok {
99		return nil, false
100	}
101
102	remotes, err := remote.LoadRemotes("")
103	if err != nil || remotes == nil {
104		return nil, false
105	}
106	if remotes.Active != "" {
107		return nil, false
108	}
109	if len(remotes.Remotes) == 0 {
110		return nil, false
111	}
112
113	return remotes.Remotes, true
114}
115
116func resolvedRemoteFromEntry(name string, entry remote.RemoteEntry) *remote.ResolvedRemote {
117	return &remote.ResolvedRemote{
118		Name:        name,
119		URL:         entry.URL,
120		Token:       resolveTokenForTarget(name, entry),
121		InsecureTLS: resolveInsecureForTarget(name, entry),
122	}
123}
124
125func isRemoteNotFoundError(err error) bool {
126	if httpErr, ok := errors.AsType[*remote.HTTPError](err); ok {
127		return httpErr.StatusCode == http.StatusNotFound
128	}
129	return errors.Is(err, domain.ErrRouteNotFound)
130}
131
132func resolveExplicitTargetName() (string, bool) {
133	if target := strings.TrimSpace(remoteFlag); target != "" {
134		return target, true
135	}
136	if target := strings.TrimSpace(os.Getenv("GORDON_REMOTE")); target != "" {
137		return target, true
138	}
139	return "", false
140}
141
142func resolveTokenForTarget(name string, entry remote.RemoteEntry) string {
143	if token := strings.TrimSpace(tokenFlag); token != "" {
144		return token
145	}
146	if token := strings.TrimSpace(os.Getenv("GORDON_TOKEN")); token != "" {
147		return token
148	}
149	if name != "" {
150		return remote.ResolveTokenForRemote(name, entry)
151	}
152	return ""
153}
154
155func resolveInsecureForTarget(name string, entry remote.RemoteEntry) bool {
156	if insecureTLSFlag {
157		return true
158	}
159	if env := strings.TrimSpace(os.Getenv("GORDON_INSECURE")); env != "" {
160		if value, err := strconv.ParseBool(env); err == nil {
161			return value
162		}
163	}
164	if name != "" {
165		return entry.InsecureTLS
166	}
167	return false
168}
169
170func newInferenceTargetClient(name string, entry remote.RemoteEntry) *remote.Client {
171	return remote.NewClient(entry.URL, remoteClientOptions(resolveTokenForTarget(name, entry), resolveInsecureForTarget(name, entry))...)
172}

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