fierj
PublicTiny personal git forge
a86159dee9300f1f5b3ab7b1aeae32410526651f
diff --git a/git.go b/git.go
index cc0a8f5..0ff67d7 100644
--- a/git.go
+++ b/git.go
@@ -1,6 +1,7 @@
package main
import (
+ "bufio"
"bytes"
"encoding/json"
"fmt"
@@ -11,49 +12,63 @@ import (
"strings"
)
-// preReceiveHook is a git pre-receive hook that rejects force-pushes to protected
-// branches listed in .fierj.json. It uses only standard git plumbing and POSIX sh.
-const preReceiveHook = `#!/bin/sh
-# pre-receive hook — managed by fierj, do not edit.
-# Rejects force-pushes and deletions of protected branches.
-set -e
-
-CONF="$(dirname "$0")/../../.fierj.json"
-[ -f "$CONF" ] || exit 0
-
-PROTECTED=$(grep -o '"protected_branches"[[:space:]]*:[[:space:]]*\[[^]]*\]' "$CONF" | \
- sed 's/.*\[//;s/\]//;s/"//g;s/, */ /g')
-[ -n "$PROTECTED" ] || exit 0
-
-zero=0000000000000000000000000000000000000000
-while read old new ref; do
- branch=$(echo "$ref" | sed 's|refs/heads/||')
- for p in $PROTECTED; do
- [ "$branch" = "$p" ] || continue
- if [ "$new" = "$zero" ]; then
- echo "ERROR: Cannot delete protected branch '$branch'."
- exit 1
- fi
- [ "$old" = "$zero" ] && continue
- if ! git merge-base --is-ancestor "$old" "$new" 2>/dev/null; then
- echo "ERROR: Force push to protected branch '$branch' rejected."
- echo " Use a feature branch and submit a patch instead."
- exit 1
- fi
- done
-done
-exit 0
-`
+const zeroSHA = "0000000000000000000000000000000000000000"
+
+// runPreReceive is invoked as a git pre-receive hook. It reads protected
+// branches from .fierj.json in the current directory (the repo root) and
+// rejects direct pushes to them.
+func runPreReceive() int {
+ data, err := os.ReadFile(".fierj.json")
+ if err != nil {
+ return 0
+ }
+ var meta RepoMeta
+ if err := json.Unmarshal(data, &meta); err != nil || len(meta.ProtectedBranches) == 0 {
+ return 0
+ }
+ protected := make(map[string]bool, len(meta.ProtectedBranches))
+ for _, b := range meta.ProtectedBranches {
+ protected[b] = true
+ }
+
+ sc := bufio.NewScanner(os.Stdin)
+ for sc.Scan() {
+ parts := strings.Fields(sc.Text())
+ if len(parts) != 3 {
+ continue
+ }
+ old, new_, ref := parts[0], parts[1], parts[2]
+ branch := strings.TrimPrefix(ref, "refs/heads/")
+ if !protected[branch] {
+ continue
+ }
+ if new_ == zeroSHA {
+ fmt.Fprintf(os.Stderr, "ERROR: Cannot delete protected branch %q.\n", branch)
+ return 1
+ }
+ if old == zeroSHA {
+ continue // new branch, allow
+ }
+ fmt.Fprintf(os.Stderr, "ERROR: Direct push to protected branch %q rejected.\n", branch)
+ fmt.Fprintf(os.Stderr, " Use a feature branch and submit a patch instead.\n")
+ return 1
+ }
+ return 0
+}
// writePreReceiveHook installs the pre-receive hook into a bare repository.
func writePreReceiveHook(repoPath string) error {
+ exe, err := os.Executable()
+ if err != nil {
+ return fmt.Errorf("find executable: %w", err)
+ }
+ script := fmt.Sprintf("#!/bin/sh\nexec %s hook pre-receive\n", exe)
hookPath := filepath.Join(repoPath, "hooks", "pre-receive")
- if err := os.WriteFile(hookPath, []byte(preReceiveHook), 0755); err != nil {
+ if err := os.WriteFile(hookPath, []byte(script), 0755); err != nil {
return fmt.Errorf("write pre-receive hook: %w", err)
}
return nil
}
-
type TreeEntry struct {
Mode string
Type string // "blob" or "tree"
diff --git a/main.go b/main.go
index 92d0671..f297d2b 100644
--- a/main.go
+++ b/main.go
@@ -18,6 +18,10 @@ var templateFS embed.FS
type contextKey string
func main() {
+ if len(os.Args) >= 2 && os.Args[1] == "hook" {
+ os.Exit(hookCmd(os.Args[2:]))
+ }
+
cfgPath := "config.json"
if len(os.Args) > 1 {
cfgPath = os.Args[1]
@@ -75,7 +79,6 @@ func main() {
mux.HandleFunc("POST /{repo}/threads/{threadId}/close", ThreadClosePost(cfg, tmpl))
mux.HandleFunc("POST /{repo}/threads/{threadId}/reopen", ThreadReopenPost(cfg, tmpl))
- // Patches
mux.HandleFunc("GET /{repo}/patches", PatchesList(cfg, tmpl))
mux.HandleFunc("GET /{repo}/patches/new", PatchNewGet(cfg, tmpl))
mux.HandleFunc("POST /{repo}/patches/new", PatchNewPost(cfg, tmpl))
@@ -126,6 +129,18 @@ func main() {
slog.Info("stopped")
}
+// hookCmd dispatches git hook invocations.
+func hookCmd(args []string) int {
+ if len(args) < 1 {
+ return 0
+ }
+ switch args[0] {
+ case "pre-receive":
+ return runPreReceive()
+ }
+ return 0
+}
+
// User returns the logged-in username from the request context, or "" if anonymous.
func User(r *http.Request) string {
u, _ := r.Context().Value(contextKey("user")).(string)