v0.12.1 7328df5

config.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3// Package client is the Connect client of the remote CLI: it turns the
4// ERMITE_URL and ERMITE_TOKEN settings into authenticated service clients for
5// the ermite-api contract, maps Connect errors to one-line messages and keeps
6// a Watch stream alive across reconnections.
7//
8// It is a driving adapter: it never imports another adapter and never reads
9// the process environment itself, the caller passes a lookup function.
10package client
11
12import (
13	"errors"
14	"fmt"
15	"net"
16	"net/url"
17	"strings"
18	"time"
19)
20
21// Variable names.
22const (
23	// EnvURL is the base URL of the ermite server.
24	EnvURL = "ERMITE_URL"
25	// EnvToken is the bearer token sent with every request. Optional: without
26	// it the calls are anonymous.
27	EnvToken = "ERMITE_TOKEN"
28)
29
30const (
31	// APIPath is where the server mounts the Connect handlers.
32	APIPath = "/-/api"
33	// DefaultTimeout bounds one unary call.
34	DefaultTimeout = 30 * time.Second
35)
36
37// Config configures a Client.
38type Config struct {
39	// URL is the server address, http or https, without credentials. A path
40	// prefix is kept, for a server behind a sub-path proxy.
41	URL string
42	// Token is the bearer token. Empty means anonymous.
43	Token string
44	// Timeout bounds each unary call. Streams are not bounded. Zero means
45	// DefaultTimeout.
46	Timeout time.Duration
47	// UserAgent identifies the CLI, for example "ermite/v0.1.0 (cli)".
48	UserAgent string
49}
50
51// NewConfig validates the server address and the token, however they were
52// found (environment, config file or token command). The token is trimmed and
53// may be empty (anonymous). Timeout and UserAgent are left for the caller.
54func NewConfig(rawURL, token string) (Config, error) {
55	raw := strings.TrimSpace(rawURL)
56	if raw == "" {
57		return Config{}, fmt.Errorf("%s is not set: set it to the address of your ermite server, for example https://git.example.com, or run `ermite config set url <url>`", EnvURL)
58	}
59	base, err := ParseURL(raw)
60	if err != nil {
61		return Config{}, fmt.Errorf("invalid server URL: %w", err)
62	}
63	return Config{URL: base.String(), Token: strings.TrimSpace(token)}, nil
64}
65
66// ParseURL validates a server address: an absolute http(s) URL with a host,
67// without credentials, query or fragment. The result has no trailing slash.
68// Errors never echo a URL that may carry credentials.
69func ParseURL(raw string) (*url.URL, error) {
70	u, err := url.Parse(raw)
71	if err != nil {
72		return nil, errors.New("not a valid URL")
73	}
74	switch {
75	case u.User != nil:
76		return nil, errors.New("must not contain credentials: use ERMITE_TOKEN for the token")
77	case u.Scheme != "http" && u.Scheme != "https":
78		return nil, errors.New("not an http(s) URL: it must start with http:// or https://")
79	case u.Hostname() == "":
80		return nil, errors.New("the URL has no host")
81	case u.RawQuery != "" || u.ForceQuery || u.Fragment != "":
82		return nil, errors.New("the URL must not contain a query or a fragment")
83	}
84	u.Scheme = strings.ToLower(u.Scheme)
85	u.Path = strings.TrimRight(u.Path, "/")
86	u.RawPath = ""
87	return u, nil
88}
89
90// InsecureTokenWarning returns a warning when the token would travel over
91// plain http to a host that is not on the loopback interface, and "" otherwise.
92//
93// It warns instead of refusing: ermite does not terminate TLS itself and is
94// commonly reached over a VPN or a private network, where http is a legitimate
95// deployment; a refusal would make those setups unusable. The warning keeps
96// the risk visible on every call.
97func (c Config) InsecureTokenWarning() string {
98	if c.Token == "" {
99		return ""
100	}
101	u, err := url.Parse(c.URL)
102	if err != nil || u.Scheme != "http" || isLoopback(u.Hostname()) {
103		return ""
104	}
105	return fmt.Sprintf("%s is sent over plain http to %s: anyone on the network path can read it; use an https:// URL unless the network is trusted", EnvToken, u.Hostname())
106}
107
108func isLoopback(host string) bool {
109	if host == "localhost" || strings.HasSuffix(host, ".localhost") {
110		return true
111	}
112	ip := net.ParseIP(host)
113	return ip != nil && ip.IsLoopback()
114}

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 2

Tags 45

↑↓ moveenter openesc close