Fast implementation of Git in pure Go codeberg.org/lindenii/furgit
git go
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

object/store/loose: Add quarantine

Runxi Yu bf394989 a93a872a

+335
+19
object/store/loose/quarantine.go
··· 1 + package loose 2 + 3 + import ( 4 + "os" 5 + 6 + objectstore "codeberg.org/lindenii/furgit/object/store" 7 + ) 8 + 9 + var _ objectstore.ObjectQuarantiner = (*Store)(nil) 10 + 11 + type objectQuarantine struct { 12 + *Store 13 + 14 + parent *Store 15 + tempName string 16 + tempRoot *os.Root 17 + } 18 + 19 + var _ objectstore.ObjectQuarantine = (*objectQuarantine)(nil)
+63
object/store/loose/quarantine_begin.go
··· 1 + package loose 2 + 3 + import ( 4 + "crypto/rand" 5 + "errors" 6 + "fmt" 7 + "io/fs" 8 + "os" 9 + 10 + objectstore "codeberg.org/lindenii/furgit/object/store" 11 + ) 12 + 13 + // BeginObjectQuarantine creates one quarantined loose store rooted privately 14 + // beneath the destination loose root. 15 + // 16 + // Labels: Deps-Borrowed, Life-Parent, Close-No. 17 + func (store *Store) BeginObjectQuarantine(_ objectstore.ObjectQuarantineOptions) (objectstore.ObjectQuarantine, error) { 18 + tempName, tempRoot, err := createLooseQuarantineRoot(store.root) 19 + if err != nil { 20 + return nil, err 21 + } 22 + 23 + quarantineStore, err := New(tempRoot, store.algo) 24 + if err != nil { 25 + _ = tempRoot.Close() 26 + _ = store.root.RemoveAll(tempName) 27 + 28 + return nil, err 29 + } 30 + 31 + return &objectQuarantine{ 32 + Store: quarantineStore, 33 + parent: store, 34 + tempName: tempName, 35 + tempRoot: tempRoot, 36 + }, nil 37 + } 38 + 39 + func createLooseQuarantineRoot(parent *os.Root) (string, *os.Root, error) { 40 + for range 32 { 41 + name := "tmp_looseq_" + rand.Text() 42 + 43 + err := parent.Mkdir(name, 0o700) 44 + if err == nil { 45 + root, err := parent.OpenRoot(name) 46 + if err == nil { 47 + return name, root, nil 48 + } 49 + 50 + _ = parent.RemoveAll(name) 51 + 52 + return "", nil, err 53 + } 54 + 55 + if errors.Is(err, fs.ErrExist) { 56 + continue 57 + } 58 + 59 + return "", nil, err 60 + } 61 + 62 + return "", nil, fmt.Errorf("objectstore/loose: unable to create quarantine directory") 63 + }
+18
object/store/loose/quarantine_discard.go
··· 1 + package loose 2 + 3 + // Discard removes the quarantine and invalidates the receiver. 4 + func (quarantine *objectQuarantine) Discard() error { 5 + closeErr := quarantine.Close() 6 + tempRootErr := quarantine.tempRoot.Close() 7 + removeErr := quarantine.parent.root.RemoveAll(quarantine.tempName) 8 + 9 + if closeErr != nil { 10 + return closeErr 11 + } 12 + 13 + if tempRootErr != nil { 14 + return tempRootErr 15 + } 16 + 17 + return removeErr 18 + }
+116
object/store/loose/quarantine_promote.go
··· 1 + package loose 2 + 3 + import ( 4 + "errors" 5 + "fmt" 6 + "io/fs" 7 + "os" 8 + "path/filepath" 9 + ) 10 + 11 + // Promote publishes all quarantined loose objects into the parent loose store 12 + // and invalidates the receiver. 13 + func (quarantine *objectQuarantine) Promote() error { 14 + closeErr := quarantine.Close() 15 + promoteErr := promoteLooseQuarantine(quarantine.parent, quarantine.tempName, quarantine.tempRoot) 16 + tempRootErr := quarantine.tempRoot.Close() 17 + removeErr := quarantine.parent.root.RemoveAll(quarantine.tempName) 18 + 19 + if closeErr != nil { 20 + return closeErr 21 + } 22 + 23 + if promoteErr != nil { 24 + return promoteErr 25 + } 26 + 27 + if tempRootErr != nil { 28 + return tempRootErr 29 + } 30 + 31 + return removeErr 32 + } 33 + 34 + func promoteLooseQuarantine(parent *Store, tempName string, tempRoot *os.Root) error { 35 + entries, err := fs.ReadDir(tempRoot.FS(), ".") 36 + if err != nil && !errors.Is(err, fs.ErrNotExist) { 37 + return err 38 + } 39 + 40 + for _, entry := range entries { 41 + if !entry.IsDir() { 42 + return fmt.Errorf("objectstore/loose: quarantine contains unexpected file %q", entry.Name()) 43 + } 44 + 45 + if len(entry.Name()) == 2 && isHexString(entry.Name()) { 46 + return fmt.Errorf("objectstore/loose: quarantine contains invalid shard %q", entry.Name()) 47 + } 48 + 49 + err := promoteLooseQuarantineShard(parent, tempName, tempRoot, entry.Name()) 50 + if err != nil { 51 + return err 52 + } 53 + } 54 + 55 + return nil 56 + } 57 + 58 + func promoteLooseQuarantineShard(parent *Store, tempName string, tempRoot *os.Root, shard string) error { 59 + entries, err := fs.ReadDir(tempRoot.FS(), shard) 60 + if err != nil { 61 + return err 62 + } 63 + 64 + err = parent.root.MkdirAll(shard, 0o755) 65 + if err != nil { 66 + return err 67 + } 68 + 69 + wantNameLen := parent.algo.HexLen() - 2 70 + 71 + for _, entry := range entries { 72 + if entry.IsDir() { 73 + return fmt.Errorf("objectstore/loose: quarantine shard %q contains unexpected directory %q", shard, entry.Name()) 74 + } 75 + 76 + if len(entry.Name()) != wantNameLen || !isHexString(entry.Name()) { 77 + return fmt.Errorf("objectstore/loose: quarantine shard %q contains invalid object path %q", shard, entry.Name()) 78 + } 79 + 80 + err := promoteLooseQuarantineObject(parent.root, filepath.Join(tempName, shard, entry.Name()), filepath.Join(shard, entry.Name())) 81 + if err != nil { 82 + return err 83 + } 84 + } 85 + 86 + return nil 87 + } 88 + 89 + func promoteLooseQuarantineObject(root *os.Root, src, dst string) error { 90 + err := root.Link(src, dst) 91 + if err == nil { 92 + _ = root.Remove(src) 93 + 94 + return nil 95 + } 96 + 97 + if errors.Is(err, fs.ErrExist) { 98 + _ = root.Remove(src) 99 + 100 + return nil 101 + } 102 + 103 + return fmt.Errorf("objectstore/loose: promote quarantine %q -> %q: %w", src, dst, err) 104 + } 105 + 106 + func isHexString(s string) bool { 107 + for _, ch := range s { 108 + if ('0' <= ch && ch <= '9') || ('a' <= ch && ch <= 'f') || ('A' <= ch && ch <= 'F') { 109 + continue 110 + } 111 + 112 + return false 113 + } 114 + 115 + return true 116 + }
+119
object/store/loose/quarantine_test.go
··· 1 + package loose_test 2 + 3 + import ( 4 + "bytes" 5 + "testing" 6 + 7 + "codeberg.org/lindenii/furgit/internal/testgit" 8 + objectid "codeberg.org/lindenii/furgit/object/id" 9 + objectstore "codeberg.org/lindenii/furgit/object/store" 10 + objecttype "codeberg.org/lindenii/furgit/object/type" 11 + ) 12 + 13 + func TestLooseQuarantinePromotePublishesWrittenObjects(t *testing.T) { 14 + t.Parallel() 15 + 16 + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper 17 + testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) 18 + store := openLooseStore(t, testRepo, algo) 19 + 20 + quarantiner, ok := any(store).(objectstore.ObjectQuarantiner) 21 + if !ok { 22 + t.Fatal("loose store does not implement ObjectQuarantiner") 23 + } 24 + 25 + quarantine, err := quarantiner.BeginObjectQuarantine(objectstore.ObjectQuarantineOptions{}) 26 + if err != nil { 27 + t.Fatalf("BeginObjectQuarantine: %v", err) 28 + } 29 + 30 + content := []byte("quarantined loose object\n") 31 + 32 + id, err := quarantine.WriteBytesContent(objecttype.TypeBlob, content) 33 + if err != nil { 34 + t.Fatalf("quarantine.WriteBytesContent: %v", err) 35 + } 36 + 37 + ty, got, err := quarantine.ReadBytesContent(id) 38 + if err != nil { 39 + t.Fatalf("quarantine.ReadBytesContent: %v", err) 40 + } 41 + 42 + if ty != objecttype.TypeBlob { 43 + t.Fatalf("quarantine.ReadBytesContent type = %v, want %v", ty, objecttype.TypeBlob) 44 + } 45 + 46 + if !bytes.Equal(got, content) { 47 + t.Fatal("quarantine.ReadBytesContent mismatch") 48 + } 49 + 50 + _, _, err = store.ReadBytesContent(id) 51 + if err == nil { 52 + t.Fatal("store.ReadBytesContent unexpectedly saw quarantined object before promote") 53 + } 54 + 55 + err = quarantine.Promote() 56 + if err != nil { 57 + t.Fatalf("quarantine.Promote: %v", err) 58 + } 59 + 60 + err = store.Refresh() 61 + if err != nil { 62 + t.Fatalf("store.Refresh: %v", err) 63 + } 64 + 65 + ty, got, err = store.ReadBytesContent(id) 66 + if err != nil { 67 + t.Fatalf("store.ReadBytesContent after promote: %v", err) 68 + } 69 + 70 + if ty != objecttype.TypeBlob { 71 + t.Fatalf("store.ReadBytesContent type = %v, want %v", ty, objecttype.TypeBlob) 72 + } 73 + 74 + if !bytes.Equal(got, content) { 75 + t.Fatal("store.ReadBytesContent mismatch") 76 + } 77 + }) 78 + } 79 + 80 + func TestLooseQuarantineDiscardDropsWrittenObjects(t *testing.T) { 81 + t.Parallel() 82 + 83 + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper 84 + testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) 85 + store := openLooseStore(t, testRepo, algo) 86 + 87 + quarantiner, ok := any(store).(objectstore.ObjectQuarantiner) 88 + if !ok { 89 + t.Fatal("expected objectstore.ObjectQuarantiner") 90 + } 91 + 92 + quarantine, err := quarantiner.BeginObjectQuarantine(objectstore.ObjectQuarantineOptions{}) 93 + if err != nil { 94 + t.Fatalf("BeginObjectQuarantine: %v", err) 95 + } 96 + 97 + content := []byte("discarded loose object\n") 98 + 99 + id, err := quarantine.WriteBytesContent(objecttype.TypeBlob, content) 100 + if err != nil { 101 + t.Fatalf("quarantine.WriteBytesContent: %v", err) 102 + } 103 + 104 + err = quarantine.Discard() 105 + if err != nil { 106 + t.Fatalf("quarantine.Discard: %v", err) 107 + } 108 + 109 + err = store.Refresh() 110 + if err != nil { 111 + t.Fatalf("store.Refresh: %v", err) 112 + } 113 + 114 + _, _, err = store.ReadBytesContent(id) 115 + if err == nil { 116 + t.Fatal("store.ReadBytesContent unexpectedly saw discarded object") 117 + } 118 + }) 119 + }