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}