shithub: furgit

Download patch

ref: 777db8470909b16411cb54a6c0bbf927be778952
parent: 52be24ec3be4290ad3be421a448907b0edcb368b
author: Runxi Yu <me@runxiyu.org>
date: Sat Feb 21 17:55:15 EST 2026

objectstore/*, repository: Add ReadSize

For cases where knowing the type is unnecessary and incurs extra
overhead.

--- a/objectstore/chain/chain.go
+++ b/objectstore/chain/chain.go
@@ -96,6 +96,24 @@
 	return objecttype.TypeInvalid, 0, nil, objectstore.ErrObjectNotFound
 }
 
+// ReadSize reads object content length from the first backend that has it.
+func (chain *Chain) ReadSize(id objectid.ObjectID) (int64, error) {
+	for i, backend := range chain.backends {
+		if backend == nil {
+			continue
+		}
+		size, err := backend.ReadSize(id)
+		if err == nil {
+			return size, nil
+		}
+		if errors.Is(err, objectstore.ErrObjectNotFound) {
+			continue
+		}
+		return 0, fmt.Errorf("objectstore: backend %d read size: %w", i, err)
+	}
+	return 0, objectstore.ErrObjectNotFound
+}
+
 // ReadHeader reads object header data from the first backend that has it.
 func (chain *Chain) ReadHeader(id objectid.ObjectID) (objecttype.Type, int64, error) {
 	for i, backend := range chain.backends {
--- /dev/null
+++ b/objectstore/loose/read_size.go
@@ -1,0 +1,9 @@
+package loose
+
+import "codeberg.org/lindenii/furgit/objectid"
+
+// ReadSize reads an object's declared content length.
+func (store *Store) ReadSize(id objectid.ObjectID) (int64, error) {
+	_, size, err := store.ReadHeader(id)
+	return size, err
+}
--- a/objectstore/objectstore.go
+++ b/objectstore/objectstore.go
@@ -30,6 +30,11 @@
 	// and content stream.
 	// Caller must close the returned reader.
 	ReadReaderContent(id objectid.ObjectID) (objecttype.Type, int64, io.ReadCloser, error)
+	// ReadSize reads an object's declared content length.
+	//
+	// This is equivalent to ReadHeader(...).size and may be cheaper than
+	// ReadHeader when callers do not need object type.
+	ReadSize(id objectid.ObjectID) (int64, error)
 	// ReadHeader reads an object's type and declared content length.
 	ReadHeader(id objectid.ObjectID) (objecttype.Type, int64, error)
 	// Close releases resources associated with the backend.
--- /dev/null
+++ b/objectstore/packed/read_size.go
@@ -1,0 +1,39 @@
+package packed
+
+import (
+	"fmt"
+
+	packfmt "codeberg.org/lindenii/furgit/format/pack"
+	"codeberg.org/lindenii/furgit/objectid"
+	"codeberg.org/lindenii/furgit/objecttype"
+)
+
+// ReadSize reads an object's declared content size.
+func (store *Store) ReadSize(id objectid.ObjectID) (int64, error) {
+	loc, err := store.lookup(id)
+	if err != nil {
+		return 0, err
+	}
+	return store.resolveSizeAt(loc)
+}
+
+// resolveSizeAt resolves one object's declared content size from location.
+func (store *Store) resolveSizeAt(start location) (int64, error) {
+	pack, meta, err := store.entryMetaAt(start)
+	if err != nil {
+		return 0, err
+	}
+	if packfmt.IsBaseObjectType(meta.ty) {
+		return meta.size, nil
+	}
+	switch meta.ty {
+	case objecttype.TypeRefDelta, objecttype.TypeOfsDelta:
+		return deltaDeclaredSizeAt(pack, meta.dataOffset)
+	case objecttype.TypeInvalid, objecttype.TypeFuture:
+		return 0, fmt.Errorf("objectstore/packed: unsupported pack type %d", meta.ty)
+	case objecttype.TypeCommit, objecttype.TypeTree, objecttype.TypeBlob, objecttype.TypeTag:
+		return 0, fmt.Errorf("objectstore/packed: internal invariant violation for base type %d", meta.ty)
+	default:
+		return 0, fmt.Errorf("objectstore/packed: unsupported pack type %d", meta.ty)
+	}
+}
--- a/objectstore/packed/read_test.go
+++ b/objectstore/packed/read_test.go
@@ -36,6 +36,13 @@
 				if gotHeaderSize != int64(len(wantBody)) {
 					t.Fatalf("ReadHeader size = %d, want %d", gotHeaderSize, len(wantBody))
 				}
+				gotSize, err := store.ReadSize(id)
+				if err != nil {
+					t.Fatalf("ReadSize: %v", err)
+				}
+				if gotSize != int64(len(wantBody)) {
+					t.Fatalf("ReadSize = %d, want %d", gotSize, len(wantBody))
+				}
 
 				gotRaw, err := store.ReadBytesFull(id)
 				if err != nil {
@@ -108,6 +115,9 @@
 		if _, _, err := store.ReadHeader(notFoundID); !errors.Is(err, objectstore.ErrObjectNotFound) {
 			t.Fatalf("ReadHeader not-found error = %v", err)
 		}
+		if _, err := store.ReadSize(notFoundID); !errors.Is(err, objectstore.ErrObjectNotFound) {
+			t.Fatalf("ReadSize not-found error = %v", err)
+		}
 
 		var otherAlgo objectid.Algorithm
 		for _, candidate := range objectid.SupportedAlgorithms() {
@@ -181,6 +191,13 @@
 		}
 		if gotSize != wantResolvedSize {
 			t.Fatalf("ReadHeader(%s) size = %d, want resolved size %d", deltaID, gotSize, wantResolvedSize)
+		}
+		gotReadSize, err := store.ReadSize(deltaID)
+		if err != nil {
+			t.Fatalf("ReadSize(%s): %v", deltaID, err)
+		}
+		if gotReadSize != wantResolvedSize {
+			t.Fatalf("ReadSize(%s) = %d, want resolved size %d", deltaID, gotReadSize, wantResolvedSize)
 		}
 	})
 }
--- a/repository/read_stored_passthrough.go
+++ b/repository/read_stored_passthrough.go
@@ -12,6 +12,11 @@
 	return repo.objects.ReadHeader(id)
 }
 
+// ReadStoredSize reads an object's declared content length.
+func (repo *Repository) ReadStoredSize(id objectid.ObjectID) (int64, error) {
+	return repo.objects.ReadSize(id)
+}
+
 // ReadStoredBytesFull reads a full serialized object as "type size\\x00content".
 func (repo *Repository) ReadStoredBytesFull(id objectid.ObjectID) ([]byte, error) {
 	return repo.objects.ReadBytesFull(id)
--