shithub: furgit

Download patch

ref: 1d2912f952ccc11f8b6346aa8160efd1e58acfe0
parent: 61e4da6697606ea176564f50dc83a9f3b1438af2
author: Runxi Yu <runxiyu@umich.edu>
date: Mon Mar 30 11:53:50 EDT 2026

object/store/packed: Add quarantine

--- /dev/null
+++ b/object/store/packed/quarantine.go
@@ -1,0 +1,19 @@
+package packed
+
+import (
+	"os"
+
+	objectstore "codeberg.org/lindenii/furgit/object/store"
+)
+
+var _ objectstore.PackQuarantiner = (*Store)(nil)
+
+type packQuarantine struct {
+	*Store
+
+	parent   *Store
+	tempName string
+	tempRoot *os.Root
+}
+
+var _ objectstore.PackQuarantine = (*packQuarantine)(nil)
--- /dev/null
+++ b/object/store/packed/quarantine_begin.go
@@ -1,0 +1,63 @@
+package packed
+
+import (
+	"crypto/rand"
+	"errors"
+	"fmt"
+	"io/fs"
+	"os"
+
+	objectstore "codeberg.org/lindenii/furgit/object/store"
+)
+
+// BeginPackQuarantine creates one quarantined packed store rooted privately
+// beneath the destination pack root.
+//
+// Labels: Deps-Borrowed, Life-Parent, Close-No.
+func (store *Store) BeginPackQuarantine(_ objectstore.PackQuarantineOptions) (objectstore.PackQuarantine, error) {
+	tempName, tempRoot, err := createPackQuarantineRoot(store.root)
+	if err != nil {
+		return nil, err
+	}
+
+	quarantineStore, err := New(tempRoot, store.algo, store.opts)
+	if err != nil {
+		_ = tempRoot.Close()
+		_ = store.root.RemoveAll(tempName)
+
+		return nil, err
+	}
+
+	return &packQuarantine{
+		Store:    quarantineStore,
+		parent:   store,
+		tempName: tempName,
+		tempRoot: tempRoot,
+	}, nil
+}
+
+func createPackQuarantineRoot(parent *os.Root) (string, *os.Root, error) {
+	for range 32 {
+		name := "tmp_packq_" + rand.Text()
+
+		err := parent.Mkdir(name, 0o700)
+		if err == nil {
+			root, err := parent.OpenRoot(name)
+			if err == nil {
+				return name, root, nil
+			}
+
+			_ = parent.RemoveAll(name)
+
+			return "", nil, err
+		}
+
+		if errors.Is(err, fs.ErrExist) {
+			continue
+		}
+
+		return "", nil, err
+	}
+
+	return "", nil, fmt.Errorf("packed: unable to create quarantine directory")
+}
--- /dev/null
+++ b/object/store/packed/quarantine_discard.go
@@ -1,0 +1,18 @@
+package packed
+
+// Discard removes the quarantine and invalidates the receiver.
+func (quarantine *packQuarantine) Discard() error {
+	closeErr := quarantine.Close()
+	tempRootErr := quarantine.tempRoot.Close()
+	removeErr := quarantine.parent.root.RemoveAll(quarantine.tempName)
+
+	if closeErr != nil {
+		return closeErr
+	}
+
+	if tempRootErr != nil {
+		return tempRootErr
+	}
+
+	return removeErr
+}
--- /dev/null
+++ b/object/store/packed/quarantine_promote.go
@@ -1,0 +1,89 @@
+package packed
+
+import (
+	"errors"
+	"fmt"
+	"io/fs"
+	"os"
+	"slices"
+	"strings"
+)
+
+// Promote publishes all finalized pack artifacts in the quarantine into the
+// parent packed store and invalidates the receiver.
+func (quarantine *packQuarantine) Promote() error {
+	closeErr := quarantine.Close()
+	promoteErr := promotePackQuarantine(quarantine.parent.root, quarantine.tempName, quarantine.tempRoot)
+	tempRootErr := quarantine.tempRoot.Close()
+	removeErr := quarantine.parent.root.RemoveAll(quarantine.tempName)
+
+	if closeErr != nil {
+		return closeErr
+	}
+
+	if tempRootErr != nil {
+		return tempRootErr
+	}
+
+	if promoteErr != nil {
+		return promoteErr
+	}
+
+	return removeErr
+}
+
+func promotePackQuarantine(parent *os.Root, tempName string, tempRoot *os.Root) error {
+	entries, err := fs.ReadDir(tempRoot.FS(), ".")
+	if err != nil && !errors.Is(err, fs.ErrNotExist) {
+		return err
+	}
+
+	slices.SortFunc(entries, func(left, right fs.DirEntry) int {
+		return packPromotionPriority(left.Name()) - packPromotionPriority(right.Name())
+	})
+
+	for _, entry := range entries {
+		if entry.IsDir() {
+			return fmt.Errorf("packed: quarantine contains unexpected directory %q", entry.Name())
+		}
+
+		err := promotePackQuarantineFile(parent, tempName, entry.Name())
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func promotePackQuarantineFile(parent *os.Root, tempName, name string) error {
+	src := tempName + "/" + name
+
+	err := parent.Link(src, name)
+	if err == nil {
+		_ = parent.Remove(src)
+
+		return nil
+	}
+
+	if errors.Is(err, fs.ErrExist) {
+		_ = parent.Remove(src)
+
+		return nil
+	}
+
+	return fmt.Errorf("packed: promote quarantine %q -> %q: %w", src, name, err)
+}
+
+func packPromotionPriority(name string) int {
+	switch {
+	case strings.HasPrefix(name, "pack-") && strings.HasSuffix(name, ".pack"):
+		return 1
+	case strings.HasPrefix(name, "pack-") && strings.HasSuffix(name, ".rev"):
+		return 2
+	case strings.HasPrefix(name, "pack-") && strings.HasSuffix(name, ".idx"):
+		return 3
+	default:
+		return 0
+	}
+}
--- /dev/null
+++ b/object/store/packed/quarantine_test.go
@@ -1,0 +1,215 @@
+package packed_test
+
+import (
+	"bytes"
+	"os"
+	"path/filepath"
+	"strings"
+	"testing"
+
+	"codeberg.org/lindenii/furgit/internal/testgit"
+	objectid "codeberg.org/lindenii/furgit/object/id"
+	objectstore "codeberg.org/lindenii/furgit/object/store"
+	"codeberg.org/lindenii/furgit/object/store/packed"
+	objecttype "codeberg.org/lindenii/furgit/object/type"
+)
+
+func fixturePath(t *testing.T, algo objectid.Algorithm, name string) string {
+	t.Helper()
+
+	return filepath.Join("internal", "ingest", "testdata", "fixtures", algo.String(), name)
+}
+
+func fixtureBytes(t *testing.T, algo objectid.Algorithm, name string) []byte {
+	t.Helper()
+
+	path := fixturePath(t, algo, name)
+	dir := filepath.Dir(path)
+	base := filepath.Base(path)
+
+	root, err := os.OpenRoot(dir)
+	if err != nil {
+		t.Fatalf("open fixture root %q: %v", dir, err)
+	}
+
+	defer func() {
+		err := root.Close()
+		if err != nil {
+			t.Fatalf("close fixture root %q: %v", dir, err)
+		}
+	}()
+
+	data, err := root.ReadFile(base)
+	if err != nil {
+		t.Fatalf("read fixture %q: %v", base, err)
+	}
+
+	return data
+}
+
+func fixtureMetadata(t *testing.T, algo objectid.Algorithm) map[string]string {
+	t.Helper()
+
+	data := fixtureBytes(t, algo, "METADATA.txt")
+	out := make(map[string]string)
+
+	for line := range strings.SplitSeq(strings.TrimSpace(string(data)), "\n") {
+		line = strings.TrimSpace(line)
+		if line == "" {
+			continue
+		}
+
+		key, value, ok := strings.Cut(line, "=")
+		if !ok {
+			t.Fatalf("invalid fixture metadata line %q", line)
+		}
+
+		out[strings.TrimSpace(key)] = strings.TrimSpace(value)
+	}
+
+	return out
+}
+
+func fixtureOID(t *testing.T, algo objectid.Algorithm, key string) objectid.ObjectID {
+	t.Helper()
+
+	meta := fixtureMetadata(t, algo)
+
+	hex, ok := meta[key]
+	if !ok {
+		t.Fatalf("missing fixture metadata key %q", key)
+	}
+
+	id, err := objectid.ParseHex(algo, hex)
+	if err != nil {
+		t.Fatalf("parse fixture metadata oid %q: %v", hex, err)
+	}
+
+	return id
+}
+
+func TestPackQuarantinePromotePublishesWrittenObjects(t *testing.T) {
+	t.Parallel()
+
+	testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
+		head := fixtureOID(t, algo, "head")
+		packBytes := fixtureBytes(t, algo, "nonthin.pack")
+
+		repo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true})
+		packRoot := repo.OpenPackRoot(t)
+
+		store, err := packed.New(packRoot, algo, packed.Options{WriteRev: true})
+		if err != nil {
+			t.Fatalf("packed.New: %v", err)
+		}
+
+		defer func() {
+			err := store.Close()
+			if err != nil {
+				t.Fatalf("store.Close: %v", err)
+			}
+		}()
+
+		quarantiner, ok := any(store).(objectstore.PackQuarantiner)
+		if !ok {
+			t.Fatal("packed store does not implement PackQuarantiner")
+		}
+
+		quarantine, err := quarantiner.BeginPackQuarantine(objectstore.PackQuarantineOptions{})
+		if err != nil {
+			t.Fatalf("BeginPackQuarantine: %v", err)
+		}
+
+		err = quarantine.WritePack(bytes.NewReader(packBytes), objectstore.PackWriteOptions{RequireTrailingEOF: true})
+		if err != nil {
+			t.Fatalf("quarantine.WritePack: %v", err)
+		}
+
+		ty, _, err := quarantine.ReadHeader(head)
+		if err != nil {
+			t.Fatalf("quarantine.ReadHeader: %v", err)
+		}
+
+		if ty != objecttype.TypeCommit {
+			t.Fatalf("quarantine.ReadHeader type = %v, want commit", ty)
+		}
+
+		_, _, err = store.ReadHeader(head)
+		if err == nil {
+			t.Fatal("store.ReadHeader unexpectedly saw quarantined object before promote")
+		}
+
+		err = quarantine.Promote()
+		if err != nil {
+			t.Fatalf("quarantine.Promote: %v", err)
+		}
+
+		err = store.Refresh()
+		if err != nil {
+			t.Fatalf("store.Refresh: %v", err)
+		}
+
+		ty, _, err = store.ReadHeader(head)
+		if err != nil {
+			t.Fatalf("store.ReadHeader after promote: %v", err)
+		}
+
+		if ty != objecttype.TypeCommit {
+			t.Fatalf("store.ReadHeader type = %v, want commit", ty)
+		}
+	})
+}
+
+func TestPackQuarantineDiscardDropsWrittenObjects(t *testing.T) {
+	t.Parallel()
+
+	testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
+		head := fixtureOID(t, algo, "head")
+		packBytes := fixtureBytes(t, algo, "nonthin.pack")
+
+		repo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true})
+		packRoot := repo.OpenPackRoot(t)
+
+		store, err := packed.New(packRoot, algo, packed.Options{WriteRev: true})
+		if err != nil {
+			t.Fatalf("packed.New: %v", err)
+		}
+
+		defer func() {
+			err := store.Close()
+			if err != nil {
+				t.Fatalf("store.Close: %v", err)
+			}
+		}()
+
+		quarantiner, ok := any(store).(objectstore.PackQuarantiner)
+		if !ok {
+			t.Fatalf("expected objectstore.PackQuarantiner")
+		}
+
+		quarantine, err := quarantiner.BeginPackQuarantine(objectstore.PackQuarantineOptions{})
+		if err != nil {
+			t.Fatalf("BeginPackQuarantine: %v", err)
+		}
+
+		err = quarantine.WritePack(bytes.NewReader(packBytes), objectstore.PackWriteOptions{RequireTrailingEOF: true})
+		if err != nil {
+			t.Fatalf("quarantine.WritePack: %v", err)
+		}
+
+		err = quarantine.Discard()
+		if err != nil {
+			t.Fatalf("quarantine.Discard: %v", err)
+		}
+
+		err = store.Refresh()
+		if err != nil {
+			t.Fatalf("store.Refresh: %v", err)
+		}
+
+		_, _, err = store.ReadHeader(head)
+		if err == nil {
+			t.Fatal("store.ReadHeader unexpectedly saw discarded object")
+		}
+	})
+}
--