fierj
PublicTiny personal git forge
4da217258f8da7ea43fcfb2d97074edb3ef6e5b8
diff --git a/git.go b/git.go
index 7ca37c4..cc0a8f5 100644
--- a/git.go
+++ b/git.go
@@ -11,6 +11,49 @@ 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
+`
+
+// writePreReceiveHook installs the pre-receive hook into a bare repository.
+func writePreReceiveHook(repoPath string) error {
+ hookPath := filepath.Join(repoPath, "hooks", "pre-receive")
+ if err := os.WriteFile(hookPath, []byte(preReceiveHook), 0755); err != nil {
+ return fmt.Errorf("write pre-receive hook: %w", err)
+ }
+ return nil
+}
+
type TreeEntry struct {
Mode string
Type string // "blob" or "tree"
@@ -132,6 +175,9 @@ func InitRepo(cfg Config, name, description string) error {
if err := cmd.Run(); err != nil {
return fmt.Errorf("git init failed: %v, stderr: %s", err, stderr.String())
}
+ if err := writePreReceiveHook(repoPath); err != nil {
+ return err
+ }
if description != "" {
g := &Git{Dir: absRoot, Name: name}
g.SaveMeta(RepoMeta{Description: description})
@@ -158,6 +204,9 @@ func ImportRepo(cfg Config, cloneURL, description string) (string, error) {
if err := cmd.Run(); err != nil {
return "", fmt.Errorf("clone failed: %v, stderr: %s", err, stderr.String())
}
+ if err := writePreReceiveHook(repoPath); err != nil {
+ return "", err
+ }
if description != "" {
g := &Git{Dir: absRoot, Name: name}
g.SaveMeta(RepoMeta{Description: description})
diff --git a/repo_handlers.go b/repo_handlers.go
index 563c572..bcd16e6 100644
--- a/repo_handlers.go
+++ b/repo_handlers.go
@@ -6,6 +6,7 @@ import (
"log/slog"
"net"
"net/http"
+ "path/filepath"
"sort"
"strings"
"time"
@@ -442,6 +443,11 @@ func SettingsPost(cfg Config, tmpl *template.Template) http.HandlerFunc {
renderError(w, tmpl, http.StatusInternalServerError, "failed to save settings: "+err.Error())
return
}
+ // Re-install hook in case protected branches changed.
+ rp := filepath.Join(cfg.Dir, git.Name+".git")
+ if err := writePreReceiveHook(rp); err != nil {
+ slog.Error("write pre-receive hook", "repo", git.Name, "error", err)
+ }
http.Redirect(w, r, "/"+git.Name+"/settings", http.StatusSeeOther)
}
}