shithub: furgit

Download patch

ref: bf394989bad0242c6596520b7528c1a06563efa7
parent: a93a872a8ef5b66bb92f00bfe50a6664139654b3
author: Runxi Yu <runxiyu@umich.edu>
date: Mon Mar 30 13:34:16 EDT 2026

object/store/loose: Add quarantine

--- /dev/null
+++ b/object/store/loose/quarantine.go
@@ -1,0 +1,19 @@
+package loose
+
+import (
+	"os"
+
+	objectstore "codeberg.org/lindenii/furgit/object/store"
+)
+
+var _ objectstore.ObjectQuarantiner = (*Store)(nil)
+
+type objectQuarantine struct {
+	*Store
+
+	parent   *Store
+	tempName string
+	tempRoot *os.Root
+}
+
+var _ objectstore.ObjectQuarantine = (*objectQuarantine)(nil)
--- /dev/null
+++ b/object/store/loose/quarantine_begin.go
@@ -1,0 +1,63 @@
+package loose
+
+import (
+	"crypto/rand"
+	"errors"
+	"fmt"
+	"io/fs"
+	"os"
+
+	objectstore "codeberg.org/lindenii/furgit/object/store"
+)
+
+// BeginObjectQuarantine creates one quarantined loose store rooted privately
+// beneath the destination loose root.
+//
+// Labels: Deps-Borrowed, Life-Parent, Close-No.
+func (store *Store) BeginObjectQuarantine(_ objectstore.ObjectQuarantineOptions) (objectstore.ObjectQuarantine, error) {
+	tempName, tempRoot, err := createLooseQuarantineRoot(store.root)
+	if err != nil {
+		return nil, err
+	}
+
+	quarantineStore, err := New(tempRoot, store.algo)
+	if err != nil {
+		_ = tempRoot.Close()
+		_ = store.root.RemoveAll(tempName)
+
+		return nil, err
+	}
+
+	return &objectQuarantine{
+		Store:    quarantineStore,
+		parent:   store,
+		tempName: tempName,
+		tempRoot: tempRoot,
+	}, nil
+}
+
+func createLooseQuarantineRoot(parent *os.Root) (string, *os.Root, error) {
+	for range 32 {
+		name := "tmp_looseq_" + 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("objectstore/loose: unable to create quarantine directory")
+}
--- /dev/null
+++ b/object/store/loose/quarantine_discard.go
@@ -1,0 +1,18 @@
+package loose
+
+// Discard removes the quarantine and invalidates the receiver.
+func (quarantine *objectQuarantine) 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/loose/quarantine_promote.go
@@ -1,0 +1,116 @@
+package loose
+
+import (
+	"errors"
+	"fmt"
+	"io/fs"
+	"os"
+	"path/filepath"
+)
+
+// Promote publishes all quarantined loose objects into the parent loose store
+// and invalidates the receiver.
+func (quarantine *objectQuarantine) Promote() error {
+	closeErr := quarantine.Close()
+	promoteErr := promoteLooseQuarantine(quarantine.parent, quarantine.tempName, quarantine.tempRoot)
+	tempRootErr := quarantine.tempRoot.Close()
+	removeErr := quarantine.parent.root.RemoveAll(quarantine.tempName)
+
+	if closeErr != nil {
+		return closeErr
+	}
+
+	if promoteErr != nil {
+		return promoteErr
+	}
+
+	if tempRootErr != nil {
+		return tempRootErr
+	}
+
+	return removeErr
+}
+
+func promoteLooseQuarantine(parent *Store, tempName string, tempRoot *os.Root) error {
+	entries, err := fs.ReadDir(tempRoot.FS(), ".")
+	if err != nil && !errors.Is(err, fs.ErrNotExist) {
+		return err
+	}
+
+	for _, entry := range entries {
+		if !entry.IsDir() {
+			return fmt.Errorf("objectstore/loose: quarantine contains unexpected file %q", entry.Name())
+		}
+
+		if len(entry.Name()) == 2 && isHexString(entry.Name()) {
+			return fmt.Errorf("objectstore/loose: quarantine contains invalid shard %q", entry.Name())
+		}
+
+		err := promoteLooseQuarantineShard(parent, tempName, tempRoot, entry.Name())
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func promoteLooseQuarantineShard(parent *Store, tempName string, tempRoot *os.Root, shard string) error {
+	entries, err := fs.ReadDir(tempRoot.FS(), shard)
+	if err != nil {
+		return err
+	}
+
+	err = parent.root.MkdirAll(shard, 0o755)
+	if err != nil {
+		return err
+	}
+
+	wantNameLen := parent.algo.HexLen() - 2
+
+	for _, entry := range entries {
+		if entry.IsDir() {
+			return fmt.Errorf("objectstore/loose: quarantine shard %q contains unexpected directory %q", shard, entry.Name())
+		}
+
+		if len(entry.Name()) != wantNameLen || !isHexString(entry.Name()) {
+			return fmt.Errorf("objectstore/loose: quarantine shard %q contains invalid object path %q", shard, entry.Name())
+		}
+
+		err := promoteLooseQuarantineObject(parent.root, filepath.Join(tempName, shard, entry.Name()), filepath.Join(shard, entry.Name()))
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func promoteLooseQuarantineObject(root *os.Root, src, dst string) error {
+	err := root.Link(src, dst)
+	if err == nil {
+		_ = root.Remove(src)
+
+		return nil
+	}
+
+	if errors.Is(err, fs.ErrExist) {
+		_ = root.Remove(src)
+
+		return nil
+	}
+
+	return fmt.Errorf("objectstore/loose: promote quarantine %q -> %q: %w", src, dst, err)
+}
+
+func isHexString(s string) bool {
+	for _, ch := range s {
+		if ('0' <= ch && ch <= '9') || ('a' <= ch && ch <= 'f') || ('A' <= ch && ch <= 'F') {
+			continue
+		}
+
+		return false
+	}
+
+	return true
+}
--- /dev/null
+++ b/object/store/loose/quarantine_test.go
@@ -1,0 +1,119 @@
+package loose_test
+
+import (
+	"bytes"
+	"testing"
+
+	"codeberg.org/lindenii/furgit/internal/testgit"
+	objectid "codeberg.org/lindenii/furgit/object/id"
+	objectstore "codeberg.org/lindenii/furgit/object/store"
+	objecttype "codeberg.org/lindenii/furgit/object/type"
+)
+
+func TestLooseQuarantinePromotePublishesWrittenObjects(t *testing.T) {
+	t.Parallel()
+
+	testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
+		testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true})
+		store := openLooseStore(t, testRepo, algo)
+
+		quarantiner, ok := any(store).(objectstore.ObjectQuarantiner)
+		if !ok {
+			t.Fatal("loose store does not implement ObjectQuarantiner")
+		}
+
+		quarantine, err := quarantiner.BeginObjectQuarantine(objectstore.ObjectQuarantineOptions{})
+		if err != nil {
+			t.Fatalf("BeginObjectQuarantine: %v", err)
+		}
+
+		content := []byte("quarantined loose object\n")
+
+		id, err := quarantine.WriteBytesContent(objecttype.TypeBlob, content)
+		if err != nil {
+			t.Fatalf("quarantine.WriteBytesContent: %v", err)
+		}
+
+		ty, got, err := quarantine.ReadBytesContent(id)
+		if err != nil {
+			t.Fatalf("quarantine.ReadBytesContent: %v", err)
+		}
+
+		if ty != objecttype.TypeBlob {
+			t.Fatalf("quarantine.ReadBytesContent type = %v, want %v", ty, objecttype.TypeBlob)
+		}
+
+		if !bytes.Equal(got, content) {
+			t.Fatal("quarantine.ReadBytesContent mismatch")
+		}
+
+		_, _, err = store.ReadBytesContent(id)
+		if err == nil {
+			t.Fatal("store.ReadBytesContent 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, got, err = store.ReadBytesContent(id)
+		if err != nil {
+			t.Fatalf("store.ReadBytesContent after promote: %v", err)
+		}
+
+		if ty != objecttype.TypeBlob {
+			t.Fatalf("store.ReadBytesContent type = %v, want %v", ty, objecttype.TypeBlob)
+		}
+
+		if !bytes.Equal(got, content) {
+			t.Fatal("store.ReadBytesContent mismatch")
+		}
+	})
+}
+
+func TestLooseQuarantineDiscardDropsWrittenObjects(t *testing.T) {
+	t.Parallel()
+
+	testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
+		testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true})
+		store := openLooseStore(t, testRepo, algo)
+
+		quarantiner, ok := any(store).(objectstore.ObjectQuarantiner)
+		if !ok {
+			t.Fatal("expected objectstore.ObjectQuarantiner")
+		}
+
+		quarantine, err := quarantiner.BeginObjectQuarantine(objectstore.ObjectQuarantineOptions{})
+		if err != nil {
+			t.Fatalf("BeginObjectQuarantine: %v", err)
+		}
+
+		content := []byte("discarded loose object\n")
+
+		id, err := quarantine.WriteBytesContent(objecttype.TypeBlob, content)
+		if err != nil {
+			t.Fatalf("quarantine.WriteBytesContent: %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.ReadBytesContent(id)
+		if err == nil {
+			t.Fatal("store.ReadBytesContent unexpectedly saw discarded object")
+		}
+	})
+}
--