aboutsummaryrefslogtreecommitdiff
path: root/receivepack/hooks
diff options
context:
space:
mode:
authorGravatar Runxi Yu2026-03-07 21:54:33 +0800
committerGravatar Runxi Yu2026-03-07 21:54:33 +0800
commitab312b309bf0403241f8278a9f50daa270ea3d76 (patch)
tree17d325c68392a68da1808cf37646a22d0af7c4ee /receivepack/hooks
parentrefstore/files: Fix lints (diff)
signatureNo signature
receivepack/hooks: Add pre-defined hooks
Diffstat (limited to 'receivepack/hooks')
-rw-r--r--receivepack/hooks/chain.go51
-rw-r--r--receivepack/hooks/reject_force_push.go64
2 files changed, 115 insertions, 0 deletions
diff --git a/receivepack/hooks/chain.go b/receivepack/hooks/chain.go
new file mode 100644
index 00000000..4ce65064
--- /dev/null
+++ b/receivepack/hooks/chain.go
@@ -0,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
+ }
+}
diff --git a/receivepack/hooks/reject_force_push.go b/receivepack/hooks/reject_force_push.go
new file mode 100644
index 00000000..cf5ddaea
--- /dev/null
+++ b/receivepack/hooks/reject_force_push.go
@@ -0,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
+ }
+}