v0.10.1 e4c85c7

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