shithub: furgit

Download patch

ref: ab312b309bf0403241f8278a9f50daa270ea3d76
parent: 26f7f0e33f667a934532e393f40c984172c15889
author: Runxi Yu <me@runxiyu.org>
date: Sat Mar 7 16:54:33 EST 2026

receivepack/hooks: Add pre-defined hooks

--- /dev/null
+++ b/receivepack/hooks/chain.go
@@ -1,0 +1,51 @@
+package hooks
+
+import (
+	"context"
+	"fmt"
+
+	receivepack "codeberg.org/lindenii/furgit/receivepack"
+)
+
+// Chain combines hooks by running them in order and intersecting their
+// decisions. The first rejecting message for each update is preserved.
+func Chain(hooks ...receivepack.Hook) receivepack.Hook {
+	return func(
+		ctx context.Context,
+		req receivepack.HookRequest,
+	) ([]receivepack.UpdateDecision, error) {
+		decisions := make([]receivepack.UpdateDecision, len(req.Updates))
+		for i := range decisions {
+			decisions[i].Accept = true
+		}
+
+		for _, hook := range hooks {
+			if hook == nil {
+				continue
+			}
+
+			hookDecisions, err := hook(ctx, req)
+			if err != nil {
+				return nil, err
+			}
+
+			if len(hookDecisions) != len(req.Updates) {
+				return nil, fmt.Errorf("hook returned %d decisions for %d updates", len(hookDecisions), len(req.Updates))
+			}
+
+			for i, decision := range hookDecisions {
+				if decision.Accept {
+					continue
+				}
+
+				if decisions[i].Accept {
+					decisions[i].Message = decision.Message
+				}
+
+				decisions[i].Accept = false
+			}
+		}
+
+		return decisions, nil
+	}
+}
--- /dev/null
+++ b/receivepack/hooks/reject_force_push.go
@@ -1,0 +1,64 @@
+package hooks
+
+import (
+	"context"
+	"errors"
+	"fmt"
+
+	"codeberg.org/lindenii/furgit/ancestor"
+	"codeberg.org/lindenii/furgit/objectid"
+	objectmix "codeberg.org/lindenii/furgit/objectstore/mix"
+	receivepack "codeberg.org/lindenii/furgit/receivepack"
+	"codeberg.org/lindenii/furgit/refstore"
+)
+
+// RejectForcePush rejects updates whose new value is not a fast-forward of the
+// currently resolved reference.
+func RejectForcePush() receivepack.Hook {
+	return func(
+		ctx context.Context,
+		req receivepack.HookRequest,
+	) ([]receivepack.UpdateDecision, error) {
+		_ = ctx
+
+		objects := objectmix.New(req.QuarantinedObjects, req.ExistingObjects)
+
+		decisions := make([]receivepack.UpdateDecision, len(req.Updates))
+		for i := range decisions {
+			decisions[i].Accept = true
+		}
+
+		for i, update := range req.Updates {
+			if update.OldID == objectid.Zero(update.OldID.Algorithm()) || update.NewID == objectid.Zero(update.NewID.Algorithm()) {
+				continue
+			}
+
+			current, err := req.Refs.ResolveFully(update.Name)
+			switch {
+			case err == nil:
+			case errors.Is(err, refstore.ErrReferenceNotFound):
+				continue
+			default:
+				return nil, fmt.Errorf("resolve %s: %w", update.Name, err)
+			}
+
+			if current.ID == update.NewID {
+				continue
+			}
+
+			ok, err := ancestor.Is(objects, nil, current.ID, update.NewID)
+			if err != nil {
+				return nil, fmt.Errorf("check fast-forward %s: %w", update.Name, err)
+			}
+
+			if !ok {
+				decisions[i] = receivepack.UpdateDecision{
+					Accept:  false,
+					Message: "non-fast-forward",
+				}
+			}
+		}
+
+		return decisions, nil
+	}
+}
--