v0.2.0 43088fd

guestforge.go RawHistoryFolder

1// SPDX-License-Identifier: MIT
2
3package host
4
5import (
6	"bytes"
7	"encoding/json"
8	"errors"
9	"fmt"
10	"io"
11	"strings"
12
13	"bnema.dev/ermite-runner/internal/urlorigin"
14)
15
16// serverURLKeys are the keys of the github context that actions use to reach
17// the forge: the server's web URL, its REST API and its GraphQL API. The
18// environment variables GITHUB_SERVER_URL, GITHUB_API_URL and GITHUB_GRAPHQL_URL
19// are derived from them by the executor (State.RuntimeEnv), so rewriting the
20// context rewrites the environment too. Everything else that carries the
21// origin (the URLs inside github.event, recorded as the server built them;
22// repositoryUrl, which is a git:// URL) is left alone.
23var serverURLKeys = []string{"server_url", "api_url", "graphql_url"}
24
25type jsonMember struct {
26	key string
27	val json.RawMessage
28}
29
30// RewriteServerURLs returns the contexts JSON of a job with the origin of
31// github.server_url, github.api_url and github.graphql_url replaced by target
32// (scheme://host[:port], the ERMITE_RUNNER_URL origin) when, and only when, the
33// value has the same origin as github.server_url, the server's public origin.
34// Paths, queries and fragments are kept ("/-/github/api"); values with another
35// origin, with credentials, or that are not strings are untouched, as is the
36// rest of the document (order of keys included). The input is returned as it
37// is when nothing changes.
38func RewriteServerURLs(contexts, target string) (string, error) {
39	if target == "" {
40		return contexts, nil
41	}
42	top, err := parseObject([]byte(contexts))
43	if err != nil {
44		return "", fmt.Errorf("job contexts: %w", err)
45	}
46	gi := -1
47	for i, m := range top {
48		if m.key == "github" {
49			gi = i
50		}
51	}
52	if gi < 0 {
53		return contexts, nil
54	}
55	gh, err := parseObject(top[gi].val)
56	if err != nil {
57		return "", fmt.Errorf("job contexts: github: %w", err)
58	}
59	str := func(raw json.RawMessage) (string, bool) {
60		var s string
61		return s, json.Unmarshal(raw, &s) == nil
62	}
63	var public string
64	for _, m := range gh {
65		if m.key == "server_url" {
66			if s, ok := str(m.val); ok {
67				public = originOf(s)
68			}
69		}
70	}
71	if public == "" || public == originOf(target) {
72		return contexts, nil
73	}
74	changed := false
75	for i, m := range gh {
76		isURL := false
77		for _, k := range serverURLKeys {
78			isURL = isURL || m.key == k
79		}
80		if !isURL {
81			continue
82		}
83		s, ok := str(m.val)
84		if !ok || originOf(s) != public {
85			continue
86		}
87		rest := s[strings.Index(s, "://")+3:]
88		if k := strings.IndexAny(rest, "/?#"); k >= 0 {
89			rest = rest[k:]
90		} else {
91			rest = ""
92		}
93		nv, err := marshalNoEscape(target + rest)
94		if err != nil {
95			return "", err
96		}
97		gh[i].val = nv
98		changed = true
99	}
100	if !changed {
101		return contexts, nil
102	}
103	if top[gi].val, err = encodeObject(gh); err != nil {
104		return "", err
105	}
106	out, err := encodeObject(top)
107	if err != nil {
108		return "", err
109	}
110	return string(out), nil
111}
112
113// originOf returns "scheme://host[:port]" of an http(s) URL, lower-cased and
114// without the default port, an IPv6 host in brackets, or "" for anything else
115// (including URLs with credentials, which are never rewritten). It is
116// urlorigin.Prefix, the form the executor compares by urlorigin.Canonical.
117func originOf(raw string) string { return urlorigin.Prefix(raw) }
118
119func marshalNoEscape(v any) (json.RawMessage, error) {
120	var b bytes.Buffer
121	enc := json.NewEncoder(&b)
122	enc.SetEscapeHTML(false)
123	if err := enc.Encode(v); err != nil {
124		return nil, err
125	}
126	return bytes.TrimRight(b.Bytes(), "\n"), nil
127}
128
129// parseObject reads a JSON object into its members, in order.
130func parseObject(b []byte) ([]jsonMember, error) {
131	dec := json.NewDecoder(bytes.NewReader(b))
132	if t, err := dec.Token(); err != nil || t != json.Delim('{') {
133		return nil, errors.New("not a JSON object")
134	}
135	var out []jsonMember
136	for dec.More() {
137		kt, err := dec.Token()
138		if err != nil {
139			return nil, err
140		}
141		k, ok := kt.(string)
142		if !ok {
143			return nil, errors.New("not a JSON object")
144		}
145		var v json.RawMessage
146		if err := dec.Decode(&v); err != nil {
147			return nil, err
148		}
149		out = append(out, jsonMember{k, v})
150	}
151	if _, err := dec.Token(); err != nil {
152		return nil, err
153	}
154	if _, err := dec.Token(); err != io.EOF {
155		return nil, errors.New("data after the JSON object")
156	}
157	return out, nil
158}
159
160func encodeObject(ms []jsonMember) (json.RawMessage, error) {
161	var b bytes.Buffer
162	b.WriteByte('{')
163	for i, m := range ms {
164		if i > 0 {
165			b.WriteByte(',')
166		}
167		k, err := marshalNoEscape(m.key)
168		if err != nil {
169			return nil, err
170		}
171		b.Write(k)
172		b.WriteByte(':')
173		b.Write(m.val)
174	}
175	b.WriteByte('}')
176	return b.Bytes(), nil
177}

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 4

Tags 11

↑↓ moveenter openesc close