v0.35.1 2aca1df

hookclient.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package githttp
4
5import (
6	"bufio"
7	"context"
8	"encoding/json"
9	"errors"
10	"fmt"
11	"io"
12	"net"
13	"strings"
14	"time"
15
16	"bnema.dev/ermite/internal/ports"
17)
18
19// hookDialTimeout bounds connecting to the socket and the whole exchange.
20const (
21	hookDialTimeout = 5 * time.Second
22	hookCallTimeout = 2 * time.Minute
23)
24
25// ErrHookRejected is returned by RunHook when ermite refuses the push.
26var ErrHookRejected = errors.New("push refused")
27
28// RunHook is `ermite hook <hook>`: it reads the ref updates git feeds a
29// receive hook ("<old> <new> <ref>" per line) from in, sends them with the
30// token to ermite over the socket, and writes ermite's message to errOut.
31//
32// For pre-receive it fails closed: a missing socket or token, an unreachable
33// or erroring server, or a refusal all return an error (git then rejects the
34// push). For post-receive the push already happened: failures are reported on
35// errOut and RunHook returns nil.
36func RunHook(ctx context.Context, hook string, in io.Reader, errOut io.Writer, socket, token string, objects ports.PushObjects) error {
37	err := callHook(ctx, hook, in, errOut, socket, token, objects)
38	if err == nil {
39		return nil
40	}
41	if hook == HookPostReceive {
42		_, _ = fmt.Fprintf(errOut, "ermite: %s: %v\n", hook, err)
43		return nil
44	}
45	return err
46}
47
48func callHook(ctx context.Context, hook string, in io.Reader, errOut io.Writer, socket, token string, objects ports.PushObjects) error {
49	if hook != HookPreReceive && hook != HookPostReceive {
50		return fmt.Errorf("unknown hook %q", hook)
51	}
52	if socket == "" || token == "" {
53		return fmt.Errorf("%w: %s and %s are not set (is this push going through ermite?)", ErrHookRejected, EnvHookSocket, EnvHookToken)
54	}
55	updates, err := readUpdates(in)
56	if err != nil {
57		return fmt.Errorf("%w: %v", ErrHookRejected, err)
58	}
59
60	ctx, cancel := context.WithTimeout(ctx, hookCallTimeout)
61	defer cancel()
62	d := net.Dialer{Timeout: hookDialTimeout}
63	conn, err := d.DialContext(ctx, "unix", socket)
64	if err != nil {
65		return fmt.Errorf("%w: ermite is unreachable: %v", ErrHookRejected, err)
66	}
67	defer func() { _ = conn.Close() }()
68	if dl, ok := ctx.Deadline(); ok {
69		_ = conn.SetDeadline(dl)
70	}
71
72	req, err := encodeLine(HookRequest{Token: token, Hook: hook, Updates: updates, ObjectsDir: objects.Dir, Alternates: objects.Alternates})
73	if err != nil {
74		return err
75	}
76	if _, err := conn.Write(req); err != nil {
77		return fmt.Errorf("%w: send to ermite: %v", ErrHookRejected, err)
78	}
79	line, err := readLine(conn)
80	if err != nil {
81		return fmt.Errorf("%w: no reply from ermite: %v", ErrHookRejected, err)
82	}
83	var reply HookReply
84	if err := json.Unmarshal(line, &reply); err != nil {
85		return fmt.Errorf("%w: malformed reply from ermite", ErrHookRejected)
86	}
87	if reply.Message != "" {
88		_, _ = fmt.Fprintln(errOut, reply.Message)
89	}
90	if !reply.OK {
91		return ErrHookRejected
92	}
93	return nil
94}
95
96// readUpdates parses the lines git writes to a receive hook.
97func readUpdates(in io.Reader) ([]HookUpdate, error) {
98	var out []HookUpdate
99	sc := bufio.NewScanner(in)
100	sc.Buffer(make([]byte, 0, 64<<10), 1<<20)
101	for sc.Scan() {
102		line := strings.TrimSpace(sc.Text())
103		if line == "" {
104			continue
105		}
106		f := strings.SplitN(line, " ", 3)
107		if len(f) != 3 {
108			return nil, fmt.Errorf("unexpected hook input %q", line)
109		}
110		out = append(out, HookUpdate{Old: f[0], New: f[1], Ref: f[2]})
111	}
112	if err := sc.Err(); err != nil {
113		return nil, fmt.Errorf("read hook input: %w", err)
114	}
115	return out, nil
116}

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 45

↑↓ moveenter openesc close