1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package githttp
4
5import (
6 "encoding/json"
7 "errors"
8 "fmt"
9
10 "bnema.dev/ermite/internal/domain"
11)
12
13// Hook names, as used by git and by `ermite hook`.
14const (
15 HookPreReceive = "pre-receive"
16 HookPostReceive = "post-receive"
17)
18
19// maxHookMessage bounds one line of the hook protocol, request or reply.
20const maxHookMessage = 32 << 20
21
22// HookRequest is the one JSON line a hook sends over the socket.
23//
24// {"token":"<hook token>","hook":"pre-receive","updates":[{"old":"<oid>","new":"<oid>","ref":"refs/heads/main"}]}
25//
26// The repository is not sent: the token is bound to it.
27type HookRequest struct {
28 Token string `json:"token"`
29 Hook string `json:"hook"`
30 Updates []HookUpdate `json:"updates"`
31}
32
33// HookUpdate is one ref update, as git feeds it to the hook.
34type HookUpdate struct {
35 Old string `json:"old"`
36 New string `json:"new"`
37 Ref string `json:"ref"`
38}
39
40// HookReply is the one JSON line ermite answers. Message is shown to the
41// pusher (on stderr of the hook, so git prefixes it with "remote:").
42//
43// {"ok":true} {"ok":false,"message":"push rejected: ..."}
44type HookReply struct {
45 OK bool `json:"ok"`
46 Message string `json:"message,omitempty"`
47}
48
49// refUpdates checks the request fields that do not need the token.
50func (r HookRequest) refUpdates() ([]domain.RefUpdate, error) {
51 switch r.Hook {
52 case HookPreReceive, HookPostReceive:
53 default:
54 return nil, fmt.Errorf("unknown hook %q", r.Hook)
55 }
56 if r.Token == "" {
57 return nil, errors.New("missing token")
58 }
59 out := make([]domain.RefUpdate, 0, len(r.Updates))
60 for _, u := range r.Updates {
61 up, err := domain.NewRefUpdate(u.Old, u.New, u.Ref)
62 if err != nil {
63 return nil, err
64 }
65 out = append(out, up)
66 }
67 return out, nil
68}
69
70func encodeLine(v any) ([]byte, error) {
71 b, err := json.Marshal(v)
72 if err != nil {
73 return nil, err
74 }
75 return append(b, '\n'), nil
76}