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")+ }
+ })
+}
--
⑨