shithub: furgit

Download patch

ref: dc7ce00cbe3c300caac3c13b6701240126b99e00
parent: 04b424a6398637dd0f5d29857f489a03fd5e38f5
author: Runxi Yu <me@runxiyu.org>
date: Sat Mar 7 09:24:22 EST 2026

receivepack: Add service semantics thingy

--- /dev/null
+++ b/receivepack/internal/service/command.go
@@ -1,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())
+}
--- /dev/null
+++ b/receivepack/internal/service/command_result.go
@@ -1,0 +1,7 @@
+package service
+
+// CommandResult is one per-command execution result.
+type CommandResult struct {
+	Name  string
+	Error string
+}
--- /dev/null
+++ b/receivepack/internal/service/doc.go
@@ -1,0 +1,2 @@
+// Package service implements the protocol-independent receive-pack service.
+package service
--- /dev/null
+++ b/receivepack/internal/service/execute.go
@@ -1,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
+}
--- /dev/null
+++ b/receivepack/internal/service/options.go
@@ -1,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.
+}
--- /dev/null
+++ b/receivepack/internal/service/quarantine.go
@@ -1,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
+}
--- /dev/null
+++ b/receivepack/internal/service/request.go
@@ -1,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
+}
--- /dev/null
+++ b/receivepack/internal/service/result.go
@@ -1,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
+}
--- /dev/null
+++ b/receivepack/internal/service/service.go
@@ -1,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}
+}
--- /dev/null
+++ b/receivepack/internal/service/service_test.go
@@ -1,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))
+		}
+	})
+}
--- /dev/null
+++ b/receivepack/internal/service/update.go
@@ -1,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
+}
--