diff options
| author | 2026-03-07 21:15:54 +0800 | |
|---|---|---|
| committer | 2026-03-07 21:16:32 +0800 | |
| commit | b82515530f10dfebbf99dca501890570f3466910 (patch) | |
| tree | 9574dc49fa7239f7f0c131471f4a6708fd7041d5 /receivepack/internal | |
| parent | receivepack: Set permissions properly (diff) | |
| signature | No signature | |
receivepack: Add hooks
Diffstat (limited to 'receivepack/internal')
| -rw-r--r-- | receivepack/internal/service/execute.go | 96 | ||||
| -rw-r--r-- | receivepack/internal/service/hook.go | 30 | ||||
| -rw-r--r-- | receivepack/internal/service/hook_apply.go | 44 | ||||
| -rw-r--r-- | receivepack/internal/service/options.go | 12 | ||||
| -rw-r--r-- | receivepack/internal/service/quarantine_objects.go | 50 |
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 +} |
