ref: c9eefd50557a5436da84e0a38ee96c812d453336
parent: 673902b14458f32fbf47efa3757279872bdfcb7e
author: Runxi Yu <me@runxiyu.org>
date: Sat Feb 21 10:39:10 EST 2026
repository: Add loose object writing
--- a/repository/repository.go
+++ b/repository/repository.go
@@ -27,8 +27,9 @@
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 @@
}
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 @@
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 @@
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)}
- err = nil
- out = objectchain.New(backends...)
- return out, nil
+ 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
+ }
+
+ return objectsChain, objectsLooseForWritingOnly, nil
}
func openRefStore(path string, algo objectid.Algorithm) (out refstore.Store, err error) {--- /dev/null
+++ b/repository/write_loose.go
@@ -1,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
+}
--- /dev/null
+++ b/repository/write_loose_test.go
@@ -1,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)+ }
+ })
+}
--
⑨