aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Runxi Yu2026-03-07 14:24:22 +0800
committerGravatar Runxi Yu2026-03-07 17:17:14 +0800
commitdc7ce00cbe3c300caac3c13b6701240126b99e00 (patch)
treea00d516403fa7f7c5662937f7391e5d843b286d7
parentrefstore/files: Add new files backend (diff)
signatureNo signature
receivepack: Add service semantics thingy
-rw-r--r--receivepack/internal/service/command.go23
-rw-r--r--receivepack/internal/service/command_result.go7
-rw-r--r--receivepack/internal/service/doc.go2
-rw-r--r--receivepack/internal/service/execute.go88
-rw-r--r--receivepack/internal/service/options.go18
-rw-r--r--receivepack/internal/service/quarantine.go26
-rw-r--r--receivepack/internal/service/request.go12
-rw-r--r--receivepack/internal/service/result.go14
-rw-r--r--receivepack/internal/service/service.go11
-rw-r--r--receivepack/internal/service/service_test.go99
-rw-r--r--receivepack/internal/service/update.go12
11 files changed, 312 insertions, 0 deletions
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
+}