aboutsummaryrefslogtreecommitdiff
path: root/receivepack/internal
diff options
context:
space:
mode:
authorGravatar Runxi Yu2026-03-07 21:15:54 +0800
committerGravatar Runxi Yu2026-03-07 21:16:32 +0800
commitb82515530f10dfebbf99dca501890570f3466910 (patch)
tree9574dc49fa7239f7f0c131471f4a6708fd7041d5 /receivepack/internal
parentreceivepack: Set permissions properly (diff)
signatureNo signature
receivepack: Add hooks
Diffstat (limited to 'receivepack/internal')
-rw-r--r--receivepack/internal/service/execute.go96
-rw-r--r--receivepack/internal/service/hook.go30
-rw-r--r--receivepack/internal/service/hook_apply.go44
-rw-r--r--receivepack/internal/service/options.go12
-rw-r--r--receivepack/internal/service/quarantine_objects.go50
5 files changed, 218 insertions, 14 deletions
diff --git a/receivepack/internal/service/execute.go b/receivepack/internal/service/execute.go
index ebba9003..8febfc79 100644
--- a/receivepack/internal/service/execute.go
+++ b/receivepack/internal/service/execute.go
@@ -8,13 +8,8 @@ import (
)
// Execute validates one receive-pack request, optionally ingests its pack into
-// quarantine, and plans ref updates.
-//
-// TODO: Invoke hook or policy callbacks to decide whether each planned update
-// should be allowed.
+// quarantine, runs the optional hook, and applies allowed ref updates.
func (service *Service) Execute(ctx context.Context, req *Request) (*Result, error) {
- _ = ctx
-
result := &Result{
Commands: make([]CommandResult, 0, len(req.Commands)),
}
@@ -95,6 +90,83 @@ func (service *Service) Execute(ctx context.Context, req *Request) (*Result, err
return result, nil
}
+ allowedCommands := append([]Command(nil), req.Commands...)
+ allowedIndices := make([]int, 0, len(req.Commands))
+ for index := range req.Commands {
+ allowedIndices = append(allowedIndices, index)
+ }
+ rejected := make(map[int]string)
+
+ if service.opts.Hook != nil {
+ quarantinedObjects, err := service.openQuarantinedObjects(quarantineName)
+ if err != nil {
+ fillCommandErrors(result, req.Commands, err.Error())
+
+ return result, nil
+ }
+
+ defer func() {
+ _ = quarantinedObjects.Close()
+ }()
+
+ decisions, err := service.opts.Hook(ctx, HookRequest{
+ Refs: service.opts.Refs,
+ ExistingObjects: service.opts.ExistingObjects,
+ QuarantinedObjects: quarantinedObjects,
+ Updates: buildHookUpdates(req.Commands),
+ PushOptions: append([]string(nil), req.PushOptions...),
+ })
+ if err != nil {
+ fillCommandErrors(result, req.Commands, err.Error())
+
+ return result, nil
+ }
+
+ if len(decisions) != len(req.Commands) {
+ fillCommandErrors(result, req.Commands, "hook returned wrong number of update decisions")
+
+ return result, nil
+ }
+
+ allowedCommands = allowedCommands[:0]
+ allowedIndices = allowedIndices[:0]
+ for index, decision := range decisions {
+ if decision.Accept {
+ allowedCommands = append(allowedCommands, req.Commands[index])
+ allowedIndices = append(allowedIndices, index)
+
+ continue
+ }
+
+ message := decision.Message
+ if message == "" {
+ message = "rejected by hook"
+ }
+
+ rejected[index] = message
+ }
+
+ if req.Atomic && len(rejected) != 0 {
+ result.Commands = make([]CommandResult, 0, len(req.Commands))
+ for index, command := range req.Commands {
+ message := rejected[index]
+ if message == "" {
+ message = "atomic push rejected by hook"
+ }
+
+ result.Commands = append(result.Commands, resultForHookRejection(command, message))
+ }
+
+ return result, nil
+ }
+ }
+
+ if len(allowedCommands) == 0 {
+ result.Commands = mergeCommandResults(req.Commands, rejected, nil, nil)
+
+ return result, nil
+ }
+
if req.PackExpected {
// Git migrates quarantined objects into permanent storage immediately
// before starting ref updates.
@@ -108,18 +180,26 @@ func (service *Service) Execute(ctx context.Context, req *Request) (*Result, err
}
if req.Atomic {
- err := service.applyAtomic(result, req.Commands)
+ subresult := &Result{}
+ err := service.applyAtomic(subresult, allowedCommands)
if err != nil {
return result, err
}
+ result.Commands = mergeCommandResults(req.Commands, rejected, subresult.Commands, allowedIndices)
+ result.Applied = subresult.Applied
+
return result, nil
}
- err = service.applyBatch(result, req.Commands)
+ subresult := &Result{}
+ err = service.applyBatch(subresult, allowedCommands)
if err != nil {
return result, err
}
+ result.Commands = mergeCommandResults(req.Commands, rejected, subresult.Commands, allowedIndices)
+ result.Applied = subresult.Applied
+
return result, nil
}
diff --git a/receivepack/internal/service/hook.go b/receivepack/internal/service/hook.go
new file mode 100644
index 00000000..85295e15
--- /dev/null
+++ b/receivepack/internal/service/hook.go
@@ -0,0 +1,30 @@
+package service
+
+import (
+ "context"
+
+ "codeberg.org/lindenii/furgit/objectid"
+ "codeberg.org/lindenii/furgit/objectstore"
+ "codeberg.org/lindenii/furgit/refstore"
+)
+
+type RefUpdate struct {
+ Name string
+ OldID objectid.ObjectID
+ NewID objectid.ObjectID
+}
+
+type UpdateDecision struct {
+ Accept bool
+ Message string
+}
+
+type HookRequest struct {
+ Refs refstore.ReadingStore
+ ExistingObjects objectstore.Store
+ QuarantinedObjects objectstore.Store
+ Updates []RefUpdate
+ PushOptions []string
+}
+
+type Hook func(context.Context, HookRequest) ([]UpdateDecision, error)
diff --git a/receivepack/internal/service/hook_apply.go b/receivepack/internal/service/hook_apply.go
new file mode 100644
index 00000000..5bd8f596
--- /dev/null
+++ b/receivepack/internal/service/hook_apply.go
@@ -0,0 +1,44 @@
+package service
+
+func buildHookUpdates(commands []Command) []RefUpdate {
+ updates := make([]RefUpdate, 0, len(commands))
+ for _, command := range commands {
+ updates = append(updates, RefUpdate{
+ Name: command.Name,
+ OldID: command.OldID,
+ NewID: command.NewID,
+ })
+ }
+
+ return updates
+}
+
+func resultForHookRejection(command Command, message string) CommandResult {
+ result := successCommandResult(command)
+ result.Error = message
+
+ return result
+}
+
+func mergeCommandResults(
+ commands []Command,
+ rejected map[int]string,
+ applied []CommandResult,
+ appliedIndices []int,
+) []CommandResult {
+ out := make([]CommandResult, len(commands))
+
+ for index, message := range rejected {
+ out[index] = resultForHookRejection(commands[index], message)
+ }
+
+ for i, appliedResult := range applied {
+ if i >= len(appliedIndices) {
+ break
+ }
+
+ out[appliedIndices[i]] = appliedResult
+ }
+
+ return out
+}
diff --git a/receivepack/internal/service/options.go b/receivepack/internal/service/options.go
index c3d6c961..d9f8a265 100644
--- a/receivepack/internal/service/options.go
+++ b/receivepack/internal/service/options.go
@@ -16,10 +16,10 @@ type PromotedObjectPermissions struct {
// Options configures one protocol-independent receive-pack service.
type Options struct {
- Algorithm objectid.Algorithm
- Refs refstore.ReadWriteStore
- ExistingObjects objectstore.Store
- ObjectsRoot *os.Root
- PromotedObjectPermissions *PromotedObjectPermissions
- // TODO: Hook and such callbacks.
+ Algorithm objectid.Algorithm
+ Refs refstore.ReadWriteStore
+ ExistingObjects objectstore.Store
+ ObjectsRoot *os.Root
+ PromotedObjectPermissions *PromotedObjectPermissions
+ Hook Hook
}
diff --git a/receivepack/internal/service/quarantine_objects.go b/receivepack/internal/service/quarantine_objects.go
new file mode 100644
index 00000000..0c03be51
--- /dev/null
+++ b/receivepack/internal/service/quarantine_objects.go
@@ -0,0 +1,50 @@
+package service
+
+import (
+ "os"
+
+ "codeberg.org/lindenii/furgit/objectstore"
+ objectmix "codeberg.org/lindenii/furgit/objectstore/mix"
+ "codeberg.org/lindenii/furgit/objectstore/memory"
+ "codeberg.org/lindenii/furgit/objectstore/loose"
+ "codeberg.org/lindenii/furgit/objectstore/packed"
+)
+
+func (service *Service) openQuarantinedObjects(quarantineName string) (objectstore.Store, error) {
+ if quarantineName == "" {
+ return memory.New(service.opts.Algorithm), nil
+ }
+
+ looseRoot, err := service.opts.ObjectsRoot.OpenRoot(quarantineName)
+ if err != nil {
+ return nil, err
+ }
+
+ looseStore, err := loose.New(looseRoot, service.opts.Algorithm)
+ if err != nil {
+ _ = looseRoot.Close()
+
+ return nil, err
+ }
+
+ packRoot, err := looseRoot.OpenRoot("pack")
+ if err == nil {
+ packedStore, packedErr := packed.New(packRoot, service.opts.Algorithm)
+ if packedErr != nil {
+ _ = packRoot.Close()
+ _ = looseStore.Close()
+
+ return nil, packedErr
+ }
+
+ return objectmix.New(looseStore, packedStore), nil
+ }
+
+ if !os.IsNotExist(err) {
+ _ = looseStore.Close()
+
+ return nil, err
+ }
+
+ return looseStore, nil
+}