From dc7ce00cbe3c300caac3c13b6701240126b99e00 Mon Sep 17 00:00:00 2001 From: Runxi Yu Date: Sat, 7 Mar 2026 14:24:22 +0800 Subject: receivepack: Add service semantics thingy --- receivepack/internal/service/command.go | 23 ++++++ receivepack/internal/service/command_result.go | 7 ++ receivepack/internal/service/doc.go | 2 + receivepack/internal/service/execute.go | 88 +++++++++++++++++++++++ receivepack/internal/service/options.go | 18 +++++ receivepack/internal/service/quarantine.go | 26 +++++++ receivepack/internal/service/request.go | 12 ++++ receivepack/internal/service/result.go | 14 ++++ receivepack/internal/service/service.go | 11 +++ receivepack/internal/service/service_test.go | 99 ++++++++++++++++++++++++++ receivepack/internal/service/update.go | 12 ++++ 11 files changed, 312 insertions(+) create mode 100644 receivepack/internal/service/command.go create mode 100644 receivepack/internal/service/command_result.go create mode 100644 receivepack/internal/service/doc.go create mode 100644 receivepack/internal/service/execute.go create mode 100644 receivepack/internal/service/options.go create mode 100644 receivepack/internal/service/quarantine.go create mode 100644 receivepack/internal/service/request.go create mode 100644 receivepack/internal/service/result.go create mode 100644 receivepack/internal/service/service.go create mode 100644 receivepack/internal/service/service_test.go create mode 100644 receivepack/internal/service/update.go diff --git a/receivepack/internal/service/command.go b/receivepack/internal/service/command.go new file mode 100644 index 00000000..f51461ff --- /dev/null +++ b/receivepack/internal/service/command.go @@ -0,0 +1,23 @@ +package service + +import "codeberg.org/lindenii/furgit/objectid" + +// Command is one protocol-independent requested ref update. +type Command struct { + OldID objectid.ObjectID + NewID objectid.ObjectID + Name string +} + +func fillCommandErrors(result *Result, commands []Command, errText string) { + for _, command := range commands { + result.Commands = append(result.Commands, CommandResult{ + Name: command.Name, + Error: errText, + }) + } +} + +func isDelete(command Command) bool { + return command.NewID == objectid.Zero(command.NewID.Algorithm()) +} diff --git a/receivepack/internal/service/command_result.go b/receivepack/internal/service/command_result.go new file mode 100644 index 00000000..1234c8ef --- /dev/null +++ b/receivepack/internal/service/command_result.go @@ -0,0 +1,7 @@ +package service + +// CommandResult is one per-command execution result. +type CommandResult struct { + Name string + Error string +} diff --git a/receivepack/internal/service/doc.go b/receivepack/internal/service/doc.go new file mode 100644 index 00000000..2bb15a38 --- /dev/null +++ b/receivepack/internal/service/doc.go @@ -0,0 +1,2 @@ +// Package service implements the protocol-independent receive-pack service. +package service diff --git a/receivepack/internal/service/execute.go b/receivepack/internal/service/execute.go new file mode 100644 index 00000000..b3d47d29 --- /dev/null +++ b/receivepack/internal/service/execute.go @@ -0,0 +1,88 @@ +package service + +import ( + "context" + "log" + + "codeberg.org/lindenii/furgit/format/pack/ingest" +) + +// 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. +// TODO: Apply planned ref updates with one atomic compare-and-swap ref +// transaction once ref writing exists. +func (service *Service) Execute(ctx context.Context, req *Request) (*Result, error) { + _ = ctx + + result := &Result{ + Commands: make([]CommandResult, 0, len(req.Commands)), + } + + if req.PackExpected { + if req.Pack == nil { + result.UnpackError = "missing pack stream" + fillCommandErrors(result, req.Commands, "missing pack stream") + + return result, nil + } + + if service.opts.ObjectsRoot == nil { + result.UnpackError = "objects root not configured" + fillCommandErrors(result, req.Commands, "objects root not configured") + + return result, nil + } + + quarantineName, quarantineRoot, err := service.createQuarantineRoot() + if err != nil { + result.UnpackError = err.Error() + fillCommandErrors(result, req.Commands, err.Error()) + + return result, nil + } + + defer func() { + _ = quarantineRoot.Close() + // TODO: Promote accepted quarantined objects into the permanent object + // store once atomic ref application exists. + _ = service.opts.ObjectsRoot.RemoveAll(quarantineName) + }() + + ingested, err := ingest.Ingest( + req.Pack, + quarantineRoot, + service.opts.Algorithm, + true, + true, + service.opts.ExistingObjects, + ) + if err != nil { + result.UnpackError = err.Error() + fillCommandErrors(result, req.Commands, err.Error()) + + return result, nil + } + + result.Ingest = &ingested + } + + for _, command := range req.Commands { + result.Planned = append(result.Planned, PlannedUpdate{ + Name: command.Name, + OldID: command.OldID, + NewID: command.NewID, + Delete: isDelete(command), + }) + } + + fillCommandErrors(result, req.Commands, "ref updates not implemented yet") + log.Printf( + "receivepack: planned %d ref updates, but hook/policy checks and atomic ref writes are not implemented yet", + len(result.Planned), + ) + + return result, nil +} diff --git a/receivepack/internal/service/options.go b/receivepack/internal/service/options.go new file mode 100644 index 00000000..2bc70058 --- /dev/null +++ b/receivepack/internal/service/options.go @@ -0,0 +1,18 @@ +package service + +import ( + "os" + + "codeberg.org/lindenii/furgit/objectid" + "codeberg.org/lindenii/furgit/objectstore" + "codeberg.org/lindenii/furgit/refstore" +) + +// Options configures one protocol-independent receive-pack service. +type Options struct { + Algorithm objectid.Algorithm + Refs refstore.ReadingStore + ExistingObjects objectstore.Store + ObjectsRoot *os.Root + // TODO: Hook and such callbacks. +} diff --git a/receivepack/internal/service/quarantine.go b/receivepack/internal/service/quarantine.go new file mode 100644 index 00000000..17ff6279 --- /dev/null +++ b/receivepack/internal/service/quarantine.go @@ -0,0 +1,26 @@ +package service + +import ( + "crypto/rand" + "os" +) + +// createQuarantineRoot creates one per-push quarantine directory beneath the +// permanent objects root. +func (service *Service) createQuarantineRoot() (string, *os.Root, error) { + name := "tmp_objdir-incoming-" + rand.Text() + + err := service.opts.ObjectsRoot.Mkdir(name, 0o700) + if err != nil { + return "", nil, err + } + + root, err := service.opts.ObjectsRoot.OpenRoot(name) + if err != nil { + _ = service.opts.ObjectsRoot.RemoveAll(name) + + return "", nil, err + } + + return name, root, nil +} diff --git a/receivepack/internal/service/request.go b/receivepack/internal/service/request.go new file mode 100644 index 00000000..62764501 --- /dev/null +++ b/receivepack/internal/service/request.go @@ -0,0 +1,12 @@ +package service + +import "io" + +// Request is one protocol-independent receive-pack execution request. +type Request struct { + Commands []Command + PushOptions []string + DeleteOnly bool + PackExpected bool + Pack io.Reader +} diff --git a/receivepack/internal/service/result.go b/receivepack/internal/service/result.go new file mode 100644 index 00000000..7db7dcb1 --- /dev/null +++ b/receivepack/internal/service/result.go @@ -0,0 +1,14 @@ +package service + +import ( + "codeberg.org/lindenii/furgit/format/pack/ingest" +) + +// Result is one receive-pack execution result. +type Result struct { + UnpackError string + Commands []CommandResult + Ingest *ingest.Result + Planned []PlannedUpdate + Applied bool +} diff --git a/receivepack/internal/service/service.go b/receivepack/internal/service/service.go new file mode 100644 index 00000000..d204e9aa --- /dev/null +++ b/receivepack/internal/service/service.go @@ -0,0 +1,11 @@ +package service + +// Service executes protocol-independent receive-pack requests. +type Service struct { + opts Options +} + +// New creates one receive-pack service. +func New(opts Options) *Service { + return &Service{opts: opts} +} diff --git a/receivepack/internal/service/service_test.go b/receivepack/internal/service/service_test.go new file mode 100644 index 00000000..a29e71de --- /dev/null +++ b/receivepack/internal/service/service_test.go @@ -0,0 +1,99 @@ +package service_test + +import ( + "context" + "io/fs" + "os" + "strings" + "testing" + + "codeberg.org/lindenii/furgit/internal/testgit" + "codeberg.org/lindenii/furgit/objectid" + "codeberg.org/lindenii/furgit/objectstore/memory" + "codeberg.org/lindenii/furgit/receivepack/internal/service" +) + +func TestExecutePackExpectedWithoutObjectsRoot(t *testing.T) { + t.Parallel() + + //nolint:thelper + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { + t.Parallel() + + store := memory.New(algo) + svc := service.New(service.Options{ + Algorithm: algo, + ExistingObjects: store, + }) + + result, err := svc.Execute(context.Background(), &service.Request{ + Commands: []service.Command{{ + Name: "refs/heads/main", + OldID: objectid.Zero(algo), + NewID: objectid.Zero(algo), + }}, + PackExpected: true, + Pack: strings.NewReader("not a pack"), + }) + if err != nil { + t.Fatalf("Execute: %v", err) + } + + if result.UnpackError != "objects root not configured" { + t.Fatalf("unexpected unpack error %q", result.UnpackError) + } + }) +} + +func TestExecuteRemovesDerivedQuarantineAfterIngestFailure(t *testing.T) { + t.Parallel() + + //nolint:thelper + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { + t.Parallel() + + store := memory.New(algo) + objectsDir := t.TempDir() + + objectsRoot, err := os.OpenRoot(objectsDir) + if err != nil { + t.Fatalf("os.OpenRoot: %v", err) + } + + t.Cleanup(func() { + _ = objectsRoot.Close() + }) + + svc := service.New(service.Options{ + Algorithm: algo, + ExistingObjects: store, + ObjectsRoot: objectsRoot, + }) + + result, err := svc.Execute(context.Background(), &service.Request{ + Commands: []service.Command{{ + Name: "refs/heads/main", + OldID: objectid.Zero(algo), + NewID: objectid.Zero(algo), + }}, + PackExpected: true, + Pack: strings.NewReader("not a pack"), + }) + if err != nil { + t.Fatalf("Execute: %v", err) + } + + if result.UnpackError == "" { + t.Fatal("Execute returned empty unpack error for invalid pack") + } + + entries, err := fs.ReadDir(objectsRoot.FS(), ".") + if err != nil { + t.Fatalf("fs.ReadDir: %v", err) + } + + if len(entries) != 0 { + t.Fatalf("objects root still has entries after failed ingest: %d", len(entries)) + } + }) +} diff --git a/receivepack/internal/service/update.go b/receivepack/internal/service/update.go new file mode 100644 index 00000000..c73b73a5 --- /dev/null +++ b/receivepack/internal/service/update.go @@ -0,0 +1,12 @@ +package service + +import "codeberg.org/lindenii/furgit/objectid" + +// PlannedUpdate is one ref update that would be applied once ref writing +// exists. +type PlannedUpdate struct { + Name string + OldID objectid.ObjectID + NewID objectid.ObjectID + Delete bool +} -- cgit v1.3.1-10-gc9f91