krz/gitbay
A CLI-first git forge.
clone: git clone https://gitbay.org/krz/gitbay.git
repo-descriptions: cmd/gitbayd/hook.go · raw
1package main
2
3import (
4 "bufio"
5 "fmt"
6 "os"
7 "os/exec"
8 "strconv"
9 "strings"
10
11 "github.com/spf13/cobra"
12
13 "gitbay.org/gitbay/internal/gitutil"
14 "gitbay.org/gitbay/internal/hookd"
15 "gitbay.org/gitbay/internal/policy"
16)
17
18// collectIncomingCommits lists the commits this push introduces and reads
19// their raw objects. It runs in the hook process, which inherits git's
20// quarantine environment — the daemon cannot see these objects yet.
21func collectIncomingCommits(updates []policy.RefUpdate) (hookd.CommitsPayload, error) {
22 seen := map[string]bool{}
23 var payload hookd.CommitsPayload
24 for _, u := range updates {
25 if u.IsDelete {
26 continue
27 }
28 // Everything reachable from the new tip that no existing ref has.
29 out, err := exec.Command("git", "rev-list", u.New, "--not", "--all").Output()
30 if err != nil {
31 return payload, fmt.Errorf("rev-list %s: %w", u.New, err)
32 }
33 for _, sha := range strings.Fields(string(out)) {
34 if seen[sha] {
35 continue
36 }
37 seen[sha] = true
38 raw, err := exec.Command("git", "cat-file", "commit", sha).Output()
39 if err != nil {
40 return payload, fmt.Errorf("cat-file %s: %w", sha, err)
41 }
42 payload.Commits = append(payload.Commits, hookd.RawCommit{SHA: sha, Raw: raw})
43 }
44 }
45 return payload, nil
46}
47
48// hookCmd runs inside a git hook. It computes git facts here — the hook
49// process inherits git's quarantine environment, so incoming objects are
50// visible — and asks the daemon for a policy decision over the unix socket.
51func hookCmd() *cobra.Command {
52 return &cobra.Command{
53 Use: "hook <pre-receive|post-receive>",
54 Hidden: true,
55 Args: cobra.ExactArgs(1),
56 RunE: func(cmd *cobra.Command, args []string) error {
57 sock := os.Getenv(hookd.EnvSocket)
58 repoID, err1 := strconv.ParseInt(os.Getenv(hookd.EnvRepoID), 10, 64)
59 userID, err2 := strconv.ParseInt(os.Getenv(hookd.EnvUserID), 10, 64)
60 if sock == "" || err1 != nil || err2 != nil {
61 return fmt.Errorf("missing GITBAY_* environment; this command only runs as a git hook")
62 }
63
64 var updates []policy.RefUpdate
65 scanner := bufio.NewScanner(os.Stdin)
66 for scanner.Scan() {
67 fields := strings.Fields(scanner.Text())
68 if len(fields) != 3 {
69 continue
70 }
71 u := policy.RefUpdate{Old: fields[0], New: fields[1], Ref: fields[2]}
72 u.IsDelete = gitutil.ZeroSHA(u.New)
73 if !u.IsDelete && !gitutil.ZeroSHA(u.Old) {
74 anc, err := gitutil.IsAncestor(".", u.Old, u.New)
75 if err != nil {
76 return fmt.Errorf("checking ancestry for %s: %w", u.Ref, err)
77 }
78 u.IsForce = !anc
79 }
80 updates = append(updates, u)
81 }
82 if err := scanner.Err(); err != nil {
83 return err
84 }
85
86 resp, err := hookd.Ask(sock, hookd.Request{
87 Hook: args[0],
88 RepoID: repoID,
89 UserID: userID,
90 Updates: updates,
91 }, func() (hookd.CommitsPayload, error) {
92 return collectIncomingCommits(updates)
93 })
94 if err != nil {
95 return fmt.Errorf("gitbay daemon unreachable: %w", err)
96 }
97 if !resp.Allow {
98 fmt.Fprintln(os.Stderr, resp.Message)
99 os.Exit(1)
100 }
101 return nil
102 },
103 }
104}