From c9eefd50557a5436da84e0a38ee96c812d453336 Mon Sep 17 00:00:00 2001 From: Runxi Yu Date: Sat, 21 Feb 2026 15:39:10 +0800 Subject: repository: Add loose object writing --- repository/repository.go | 59 ++++++++++------- repository/write_loose.go | 50 ++++++++++++++ repository/write_loose_test.go | 145 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 229 insertions(+), 25 deletions(-) create mode 100644 repository/write_loose.go create mode 100644 repository/write_loose_test.go diff --git a/repository/repository.go b/repository/repository.go index a1efe92f..bd5ac8f3 100644 --- a/repository/repository.go +++ b/repository/repository.go @@ -27,8 +27,9 @@ type Repository struct { config *config.Config algo objectid.Algorithm - objects objectstore.Store - refs refstore.Store + objects objectstore.Store + objectsLooseForWritingOnly *objectloose.Store + refs refstore.Store } // Open opens a repository and wires object/ref stores from its on-disk format. @@ -58,11 +59,12 @@ func Open(path string) (repo *Repository, err error) { } repo.algo = algo - objects, err := openObjectStore(path, algo) + objects, objectsLooseForWritingOnly, err := openObjectStore(path, algo) if err != nil { return nil, err } repo.objects = objects + repo.objectsLooseForWritingOnly = objectsLooseForWritingOnly refs, err := openRefStore(path, algo) if err != nil { @@ -111,6 +113,11 @@ func (repo *Repository) Close() error { errs = append(errs, err) } } + if repo.objectsLooseForWritingOnly != nil { + if err := repo.objectsLooseForWritingOnly.Close(); err != nil { + errs = append(errs, err) + } + } return errors.Join(errs...) } @@ -141,51 +148,53 @@ func detectObjectAlgorithm(cfg *config.Config) (objectid.Algorithm, error) { return algo, nil } -func openObjectStore(path string, algo objectid.Algorithm) (out objectstore.Store, err error) { +func openObjectStore(path string, algo objectid.Algorithm) (objectstore.Store, *objectloose.Store, error) { repoRoot, err := os.OpenRoot(path) if err != nil { - return nil, fmt.Errorf("repository: open root: %w", err) + return nil, nil, fmt.Errorf("repository: open root: %w", err) } defer func() { _ = repoRoot.Close() }() objectsRoot, err := repoRoot.OpenRoot("objects") if err != nil { - return nil, fmt.Errorf("repository: open objects: %w", err) + return nil, nil, fmt.Errorf("repository: open objects: %w", err) } - var packRoot *os.Root - defer func() { - if err != nil { - if out != nil { - _ = out.Close() - } - if packRoot != nil { - _ = packRoot.Close() - } - _ = objectsRoot.Close() - } - }() looseStore, err := objectloose.New(objectsRoot, algo) if err != nil { - return nil, err + return nil, nil, err } backends := []objectstore.Store{looseStore} - packRoot, err = objectsRoot.OpenRoot("pack") + packRoot, err := objectsRoot.OpenRoot("pack") if err == nil { var packedStore *objectpacked.Store packedStore, err = objectpacked.New(packRoot, algo) if err != nil { - return nil, err + _ = looseStore.Close() + return nil, nil, err } backends = append(backends, packedStore) } else if !errors.Is(err, os.ErrNotExist) { - return nil, fmt.Errorf("repository: open objects/pack: %w", err) + _ = looseStore.Close() + return nil, nil, fmt.Errorf("repository: open objects/pack: %w", err) + } + + objectsChain := objectchain.New(backends...) + + objectsRootForWriting, err := repoRoot.OpenRoot("objects") + if err != nil { + _ = objectsChain.Close() + return nil, nil, fmt.Errorf("repository: open objects for loose writing: %w", err) + } + objectsLooseForWritingOnly, err := objectloose.New(objectsRootForWriting, algo) + if err != nil { + _ = objectsRootForWriting.Close() + _ = objectsChain.Close() + return nil, nil, err } - err = nil - out = objectchain.New(backends...) - return out, nil + return objectsChain, objectsLooseForWritingOnly, nil } func openRefStore(path string, algo objectid.Algorithm) (out refstore.Store, err error) { diff --git a/repository/write_loose.go b/repository/write_loose.go new file mode 100644 index 00000000..c9f42fea --- /dev/null +++ b/repository/write_loose.go @@ -0,0 +1,50 @@ +package repository + +import ( + "fmt" + "io" + + "codeberg.org/lindenii/furgit/objectid" + "codeberg.org/lindenii/furgit/objecttype" +) + +// WriteLooseBytesFull writes one loose object from raw "type size\0content". +func (repo *Repository) WriteLooseBytesFull(raw []byte) (objectid.ObjectID, error) { + id, err := repo.objectsLooseForWritingOnly.WriteBytesFull(raw) + if err != nil { + return objectid.ObjectID{}, fmt.Errorf("repository: write loose full bytes: %w", err) + } + return id, nil +} + +// WriteLooseBytesContent writes one loose object from typed content bytes. +func (repo *Repository) WriteLooseBytesContent(ty objecttype.Type, content []byte) (objectid.ObjectID, error) { + id, err := repo.objectsLooseForWritingOnly.WriteBytesContent(ty, content) + if err != nil { + return objectid.ObjectID{}, fmt.Errorf("repository: write loose content bytes: %w", err) + } + return id, nil +} + +// WriteLooseWriterFull returns a writer for one full serialized object stream. +// +// The caller must close the writer, then call finalize to publish the object. +func (repo *Repository) WriteLooseWriterFull() (io.WriteCloser, func() (objectid.ObjectID, error), error) { + writer, finalize, err := repo.objectsLooseForWritingOnly.WriteWriterFull() + if err != nil { + return nil, nil, fmt.Errorf("repository: create loose full writer: %w", err) + } + return writer, finalize, nil +} + +// WriteLooseWriterContent returns a writer for one typed object content stream. +// +// The caller must write exactly size bytes, close the writer, then call +// finalize to publish the object. +func (repo *Repository) WriteLooseWriterContent(ty objecttype.Type, size int64) (io.WriteCloser, func() (objectid.ObjectID, error), error) { + writer, finalize, err := repo.objectsLooseForWritingOnly.WriteWriterContent(ty, size) + if err != nil { + return nil, nil, fmt.Errorf("repository: create loose content writer: %w", err) + } + return writer, finalize, nil +} diff --git a/repository/write_loose_test.go b/repository/write_loose_test.go new file mode 100644 index 00000000..a09cbbde --- /dev/null +++ b/repository/write_loose_test.go @@ -0,0 +1,145 @@ +package repository_test + +import ( + "bytes" + "io" + "testing" + + "codeberg.org/lindenii/furgit/internal/testgit" + "codeberg.org/lindenii/furgit/objectid" + "codeberg.org/lindenii/furgit/objecttype" + "codeberg.org/lindenii/furgit/repository" +) + +func TestWriteLooseBytesContent(t *testing.T) { + t.Parallel() + + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper + repoHarness := testgit.NewRepo(t, testgit.RepoOptions{ + ObjectFormat: algo, + Bare: true, + RefFormat: "files", + }) + + repo, err := repository.Open(repoHarness.Dir()) + if err != nil { + t.Fatalf("repository.Open: %v", err) + } + defer func() { _ = repo.Close() }() + + content := []byte("write-loose-bytes-content\n") + gotID, err := repo.WriteLooseBytesContent(objecttype.TypeBlob, content) + if err != nil { + t.Fatalf("WriteLooseBytesContent: %v", err) + } + + wantID := repoHarness.HashObject(t, "blob", content) + if gotID != wantID { + t.Fatalf("WriteLooseBytesContent id = %s, want %s", gotID, wantID) + } + + ty, gotContent, err := repo.ReadStoredBytesContent(gotID) + if err != nil { + t.Fatalf("ReadStoredBytesContent: %v", err) + } + if ty != objecttype.TypeBlob { + t.Fatalf("ReadStoredBytesContent type = %v, want %v", ty, objecttype.TypeBlob) + } + if !bytes.Equal(gotContent, content) { + t.Fatalf("ReadStoredBytesContent content mismatch") + } + }) +} + +func TestWriteLooseWriterContent(t *testing.T) { + t.Parallel() + + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper + repoHarness := testgit.NewRepo(t, testgit.RepoOptions{ + ObjectFormat: algo, + Bare: true, + RefFormat: "files", + }) + + repo, err := repository.Open(repoHarness.Dir()) + if err != nil { + t.Fatalf("repository.Open: %v", err) + } + defer func() { _ = repo.Close() }() + + content := []byte("write-loose-writer-content\n") + writer, finalize, err := repo.WriteLooseWriterContent(objecttype.TypeBlob, int64(len(content))) + if err != nil { + t.Fatalf("WriteLooseWriterContent: %v", err) + } + + if _, err := writer.Write(content[:6]); err != nil { + t.Fatalf("WriteLooseWriterContent first write: %v", err) + } + if _, err := writer.Write(content[6:]); err != nil { + t.Fatalf("WriteLooseWriterContent second write: %v", err) + } + if err := writer.Close(); err != nil { + t.Fatalf("WriteLooseWriterContent close: %v", err) + } + gotID, err := finalize() + if err != nil { + t.Fatalf("WriteLooseWriterContent finalize: %v", err) + } + + wantID := repoHarness.HashObject(t, "blob", content) + if gotID != wantID { + t.Fatalf("WriteLooseWriterContent id = %s, want %s", gotID, wantID) + } + }) +} + +func TestWriteLooseFull(t *testing.T) { + t.Parallel() + + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper + repoHarness := testgit.NewRepo(t, testgit.RepoOptions{ + ObjectFormat: algo, + Bare: true, + RefFormat: "files", + }) + _, _, commitID := repoHarness.MakeCommit(t, "write-loose-full") + + repo, err := repository.Open(repoHarness.Dir()) + if err != nil { + t.Fatalf("repository.Open: %v", err) + } + defer func() { _ = repo.Close() }() + + raw, err := repo.ReadStoredBytesFull(commitID) + if err != nil { + t.Fatalf("ReadStoredBytesFull: %v", err) + } + + idFromBytes, err := repo.WriteLooseBytesFull(raw) + if err != nil { + t.Fatalf("WriteLooseBytesFull: %v", err) + } + if idFromBytes != commitID { + t.Fatalf("WriteLooseBytesFull id = %s, want %s", idFromBytes, commitID) + } + + writer, finalize, err := repo.WriteLooseWriterFull() + if err != nil { + t.Fatalf("WriteLooseWriterFull: %v", err) + } + if _, err := io.Copy(writer, bytes.NewReader(raw)); err != nil { + t.Fatalf("WriteLooseWriterFull copy: %v", err) + } + if err := writer.Close(); err != nil { + t.Fatalf("WriteLooseWriterFull close: %v", err) + } + idFromWriter, err := finalize() + if err != nil { + t.Fatalf("WriteLooseWriterFull finalize: %v", err) + } + if idFromWriter != commitID { + t.Fatalf("WriteLooseWriterFull id = %s, want %s", idFromWriter, commitID) + } + }) +} -- cgit v1.3.1-10-gc9f91