v0.13.0 dc42452

errors.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package client
4
5import (
6	"errors"
7	"regexp"
8	"strings"
9
10	"connectrpc.com/connect"
11)
12
13// redirected matches the message Connect gives to a 3xx answer.
14var redirected = regexp.MustCompile(`\b30[1-8]\b`)
15
16// maxMessage caps a server message echoed to the terminal.
17const maxMessage = 300
18
19// Error is a failed call described for a person: one line, with a hint when
20// the code points to a likely cause. It wraps the original error.
21type Error struct {
22	msg string
23	err error
24}
25
26// Error implements error.
27func (e *Error) Error() string { return e.msg }
28
29// Unwrap returns the original error, a *connect.Error for server answers.
30func (e *Error) Unwrap() error { return e.err }
31
32// Describe turns an error of a call into a friendly one-line *Error. Errors
33// that are not Connect errors are returned unchanged, nil stays nil.
34func (c *Client) Describe(err error) error {
35	var ce *connect.Error
36	if err == nil || !errors.As(err, &ce) {
37		return err
38	}
39	msg := oneLine(ce.Message())
40	with := func(prefix string) string {
41		if msg == "" {
42			return prefix
43		}
44		return prefix + ": " + msg
45	}
46	var text string
47	switch ce.Code() {
48	case connect.CodeUnauthenticated:
49		if c.cfg.Token == "" {
50			text = "authentication required: set " + EnvToken + " to a valid token (create one with `ermite token create`)"
51		} else {
52			text = "not authenticated: " + EnvToken + " is invalid, expired or revoked (create a new one with `ermite token create`)"
53		}
54	case connect.CodePermissionDenied:
55		text = with("permission denied") + " (the token lacks a required scope, see `ermite token list`)"
56	case connect.CodeNotFound:
57		text = with("not found")
58	case connect.CodeAlreadyExists:
59		text = with("already exists")
60	case connect.CodeInvalidArgument:
61		if !connect.IsWireError(err) {
62			// Raised by the client library: the answer was cut or garbled.
63			text = "the connection to " + c.cfg.URL + " broke or the answer was malformed"
64			if msg != "" {
65				text += ": " + msg
66			}
67			break
68		}
69		text = with("invalid request")
70	case connect.CodeFailedPrecondition:
71		text = with("precondition failed")
72	case connect.CodeUnavailable:
73		text = "cannot reach the server at " + c.cfg.URL
74		if msg != "" {
75			text += ": " + msg
76		}
77	case connect.CodeDeadlineExceeded:
78		text = "the request to " + c.cfg.URL + " timed out"
79	case connect.CodeCanceled:
80		text = "canceled"
81	case connect.CodeUnimplemented:
82		text = "the server does not implement this call: check that " + EnvURL + " points to an ermite server, and its version with `ermite version`"
83	case connect.CodeResourceExhausted:
84		text = with("too many requests")
85	case connect.CodeInternal:
86		text = with("server error")
87	default:
88		text = with("unexpected server response")
89		if redirected.MatchString(msg) {
90			text += " (the server redirected the request: set " + EnvURL + " to the final address, probably https://)"
91		}
92	}
93	return &Error{msg: text, err: err}
94}
95
96// Clean makes a server-provided string safe for a terminal: line breaks and
97// tabs become spaces; other control characters (escape sequences) and bidi
98// override or isolate characters are replaced by "?".
99func Clean(s string) string {
100	return strings.Map(func(r rune) rune {
101		switch {
102		case r == '\t' || r == '\n' || r == '\r':
103			return ' '
104		case r < 0x20 || r == 0x7f || (r >= 0x80 && r < 0xa0),
105			r >= 0x202a && r <= 0x202e, r >= 0x2066 && r <= 0x2069:
106			return '?'
107		}
108		return r
109	}, s)
110}
111
112// oneLine collapses whitespace and bounds the length of a server message.
113func oneLine(s string) string {
114	s = strings.Join(strings.Fields(Clean(s)), " ")
115	if len(s) > maxMessage {
116		s = strings.ToValidUTF8(s[:maxMessage], "") + "…"
117	}
118	return s
119}

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