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
+ }
+}
--
⑨