aboutsummaryrefslogtreecommitdiff
path: root/object/resolve
diff options
context:
space:
mode:
authorGravatar Runxi Yu2026-03-19 16:19:24 +0000
committerGravatar Runxi Yu2026-03-19 17:17:18 +0000
commit3c7add2cf4154c54c42d348bc462e29198e69338 (patch)
treeab0c7e9359e7e44fe951fa6a09825aa299314172 /object/resolve
parent*: Update call sites (diff)
signatureNo signature
object/resolve: Object resolver v0.1.85
Diffstat (limited to 'object/resolve')
-rw-r--r--object/resolve/doc.go5
-rw-r--r--object/resolve/exact_blob.go24
-rw-r--r--object/resolve/exact_blob_reader.go14
-rw-r--r--object/resolve/exact_commit.go24
-rw-r--r--object/resolve/exact_commit_reader.go16
-rw-r--r--object/resolve/exact_object.go18
-rw-r--r--object/resolve/exact_reader.go26
-rw-r--r--object/resolve/exact_tag.go24
-rw-r--r--object/resolve/exact_tag_reader.go16
-rw-r--r--object/resolve/exact_tree.go24
-rw-r--r--object/resolve/exact_tree_reader.go16
-rw-r--r--object/resolve/object_parse.go28
-rw-r--r--object/resolve/path.go53
-rw-r--r--object/resolve/peel_to_blob.go28
-rw-r--r--object/resolve/peel_to_blob_id.go32
-rw-r--r--object/resolve/peel_to_blob_reader.go18
-rw-r--r--object/resolve/peel_to_commit.go28
-rw-r--r--object/resolve/peel_to_commit_id.go32
-rw-r--r--object/resolve/peel_to_commit_reader.go20
-rw-r--r--object/resolve/peel_to_tag.go12
-rw-r--r--object/resolve/peel_to_tag_id.go8
-rw-r--r--object/resolve/peel_to_tag_reader.go20
-rw-r--r--object/resolve/peel_to_tree.go31
-rw-r--r--object/resolve/peel_to_tree_id.go40
-rw-r--r--object/resolve/peel_to_tree_reader.go20
-rw-r--r--object/resolve/resolver.go17
26 files changed, 594 insertions, 0 deletions
diff --git a/object/resolve/doc.go b/object/resolve/doc.go
new file mode 100644
index 00000000..c4197087
--- /dev/null
+++ b/object/resolve/doc.go
@@ -0,0 +1,5 @@
+// Package resolve resolves stored Git objects by exact type, by peeling
+// tree-ish or commit-ish references, and by path within trees.
+//
+// A Resolver does not take ownership of the underlying object store.
+package resolve
diff --git a/object/resolve/exact_blob.go b/object/resolve/exact_blob.go
new file mode 100644
index 00000000..3b880909
--- /dev/null
+++ b/object/resolve/exact_blob.go
@@ -0,0 +1,24 @@
+package resolve
+
+import (
+ "fmt"
+
+ "codeberg.org/lindenii/furgit/object"
+ "codeberg.org/lindenii/furgit/object/stored"
+ "codeberg.org/lindenii/furgit/objectid"
+)
+
+// ExactBlob reads, parses, and wraps the blob at id.
+func (r *Resolver) ExactBlob(id objectid.ObjectID) (*stored.Stored[*object.Blob], error) {
+ parsed, err := r.parseObject(id)
+ if err != nil {
+ return nil, err
+ }
+
+ blob, ok := parsed.(*object.Blob)
+ if !ok {
+ return nil, fmt.Errorf("object/resolve: expected blob object %s, got %v", id, parsed.ObjectType())
+ }
+
+ return stored.New(id, blob), nil
+}
diff --git a/object/resolve/exact_blob_reader.go b/object/resolve/exact_blob_reader.go
new file mode 100644
index 00000000..760b7d90
--- /dev/null
+++ b/object/resolve/exact_blob_reader.go
@@ -0,0 +1,14 @@
+package resolve
+
+import (
+ "io"
+
+ "codeberg.org/lindenii/furgit/objectid"
+ "codeberg.org/lindenii/furgit/objecttype"
+)
+
+// ExactBlobReader returns a reader for the content of the blob at id,
+// together with its content size in bytes.
+func (r *Resolver) ExactBlobReader(id objectid.ObjectID) (io.ReadCloser, int64, error) {
+ return r.exactReader(id, objecttype.TypeBlob, "blob")
+}
diff --git a/object/resolve/exact_commit.go b/object/resolve/exact_commit.go
new file mode 100644
index 00000000..0d209253
--- /dev/null
+++ b/object/resolve/exact_commit.go
@@ -0,0 +1,24 @@
+package resolve
+
+import (
+ "fmt"
+
+ "codeberg.org/lindenii/furgit/object"
+ "codeberg.org/lindenii/furgit/object/stored"
+ "codeberg.org/lindenii/furgit/objectid"
+)
+
+// ExactCommit reads, parses, and wraps the commit at id.
+func (r *Resolver) ExactCommit(id objectid.ObjectID) (*stored.Stored[*object.Commit], error) {
+ parsed, err := r.parseObject(id)
+ if err != nil {
+ return nil, err
+ }
+
+ commit, ok := parsed.(*object.Commit)
+ if !ok {
+ return nil, fmt.Errorf("object/resolve: expected commit object %s, got %v", id, parsed.ObjectType())
+ }
+
+ return stored.New(id, commit), nil
+}
diff --git a/object/resolve/exact_commit_reader.go b/object/resolve/exact_commit_reader.go
new file mode 100644
index 00000000..b012c485
--- /dev/null
+++ b/object/resolve/exact_commit_reader.go
@@ -0,0 +1,16 @@
+package resolve
+
+import (
+ "io"
+
+ "codeberg.org/lindenii/furgit/objectid"
+ "codeberg.org/lindenii/furgit/objecttype"
+)
+
+// ExactCommitReader returns a reader for the content of the commit at id,
+// together with its content size in bytes.
+//
+// Usage of this method is unusual.
+func (r *Resolver) ExactCommitReader(id objectid.ObjectID) (io.ReadCloser, int64, error) {
+ return r.exactReader(id, objecttype.TypeCommit, "commit")
+}
diff --git a/object/resolve/exact_object.go b/object/resolve/exact_object.go
new file mode 100644
index 00000000..4fac07cd
--- /dev/null
+++ b/object/resolve/exact_object.go
@@ -0,0 +1,18 @@
+package resolve
+
+import (
+ "codeberg.org/lindenii/furgit/object"
+ "codeberg.org/lindenii/furgit/object/stored"
+ "codeberg.org/lindenii/furgit/objectid"
+)
+
+// ExactObject reads, parses, and wraps the object at id without constraining
+// its concrete object kind.
+func (r *Resolver) ExactObject(id objectid.ObjectID) (*stored.Stored[object.Object], error) {
+ parsed, err := r.parseObject(id)
+ if err != nil {
+ return nil, err
+ }
+
+ return stored.New(id, parsed), nil
+}
diff --git a/object/resolve/exact_reader.go b/object/resolve/exact_reader.go
new file mode 100644
index 00000000..10734f5f
--- /dev/null
+++ b/object/resolve/exact_reader.go
@@ -0,0 +1,26 @@
+package resolve
+
+import (
+ "fmt"
+ "io"
+
+ "codeberg.org/lindenii/furgit/objectid"
+ "codeberg.org/lindenii/furgit/objecttype"
+)
+
+// exactReader reads one object's content stream and verifies that its header
+// type matches wantType.
+func (r *Resolver) exactReader(id objectid.ObjectID, wantType objecttype.Type, wantName string) (io.ReadCloser, int64, error) {
+ gotType, size, rc, err := r.store.ReadReaderContent(id)
+ if err != nil {
+ return nil, 0, err
+ }
+
+ if gotType != wantType {
+ _ = rc.Close()
+
+ return nil, 0, fmt.Errorf("object/resolve: expected %s object %s, got %v", wantName, id, gotType)
+ }
+
+ return rc, size, nil
+}
diff --git a/object/resolve/exact_tag.go b/object/resolve/exact_tag.go
new file mode 100644
index 00000000..7af7cb3b
--- /dev/null
+++ b/object/resolve/exact_tag.go
@@ -0,0 +1,24 @@
+package resolve
+
+import (
+ "fmt"
+
+ "codeberg.org/lindenii/furgit/object"
+ "codeberg.org/lindenii/furgit/object/stored"
+ "codeberg.org/lindenii/furgit/objectid"
+)
+
+// ExactTag reads, parses, and wraps the tag at id.
+func (r *Resolver) ExactTag(id objectid.ObjectID) (*stored.Stored[*object.Tag], error) {
+ parsed, err := r.parseObject(id)
+ if err != nil {
+ return nil, err
+ }
+
+ tag, ok := parsed.(*object.Tag)
+ if !ok {
+ return nil, fmt.Errorf("object/resolve: expected tag object %s, got %v", id, parsed.ObjectType())
+ }
+
+ return stored.New(id, tag), nil
+}
diff --git a/object/resolve/exact_tag_reader.go b/object/resolve/exact_tag_reader.go
new file mode 100644
index 00000000..2c057b02
--- /dev/null
+++ b/object/resolve/exact_tag_reader.go
@@ -0,0 +1,16 @@
+package resolve
+
+import (
+ "io"
+
+ "codeberg.org/lindenii/furgit/objectid"
+ "codeberg.org/lindenii/furgit/objecttype"
+)
+
+// ExactTagReader returns a reader for the content of the tag at id,
+// together with its content size in bytes.
+//
+// Usage of this method is unusual.
+func (r *Resolver) ExactTagReader(id objectid.ObjectID) (io.ReadCloser, int64, error) {
+ return r.exactReader(id, objecttype.TypeTag, "tag")
+}
diff --git a/object/resolve/exact_tree.go b/object/resolve/exact_tree.go
new file mode 100644
index 00000000..b4271b52
--- /dev/null
+++ b/object/resolve/exact_tree.go
@@ -0,0 +1,24 @@
+package resolve
+
+import (
+ "fmt"
+
+ "codeberg.org/lindenii/furgit/object"
+ "codeberg.org/lindenii/furgit/object/stored"
+ "codeberg.org/lindenii/furgit/objectid"
+)
+
+// ExactTree reads, parses, and wraps the tree at id.
+func (r *Resolver) ExactTree(id objectid.ObjectID) (*stored.Stored[*object.Tree], error) {
+ parsed, err := r.parseObject(id)
+ if err != nil {
+ return nil, err
+ }
+
+ tree, ok := parsed.(*object.Tree)
+ if !ok {
+ return nil, fmt.Errorf("object/resolve: expected tree object %s, got %v", id, parsed.ObjectType())
+ }
+
+ return stored.New(id, tree), nil
+}
diff --git a/object/resolve/exact_tree_reader.go b/object/resolve/exact_tree_reader.go
new file mode 100644
index 00000000..b66b3d3f
--- /dev/null
+++ b/object/resolve/exact_tree_reader.go
@@ -0,0 +1,16 @@
+package resolve
+
+import (
+ "io"
+
+ "codeberg.org/lindenii/furgit/objectid"
+ "codeberg.org/lindenii/furgit/objecttype"
+)
+
+// ExactTreeReader returns a reader for the content of the tree at id,
+// together with its content size in bytes.
+//
+// Usage of this method is unusual.
+func (r *Resolver) ExactTreeReader(id objectid.ObjectID) (io.ReadCloser, int64, error) {
+ return r.exactReader(id, objecttype.TypeTree, "tree")
+}
diff --git a/object/resolve/object_parse.go b/object/resolve/object_parse.go
new file mode 100644
index 00000000..8b624c5d
--- /dev/null
+++ b/object/resolve/object_parse.go
@@ -0,0 +1,28 @@
+package resolve
+
+import (
+ "fmt"
+
+ "codeberg.org/lindenii/furgit/object"
+ "codeberg.org/lindenii/furgit/objectid"
+ "codeberg.org/lindenii/furgit/objecttype"
+)
+
+func (r *Resolver) parseObject(id objectid.ObjectID) (object.Object, error) {
+ ty, content, err := r.store.ReadBytesContent(id)
+ if err != nil {
+ return nil, err
+ }
+
+ parsed, err := object.ParseObjectWithoutHeader(ty, content, id.Algorithm())
+ if err != nil {
+ tyName, ok := objecttype.Name(ty)
+ if !ok {
+ tyName = fmt.Sprintf("type %d", ty)
+ }
+
+ return nil, fmt.Errorf("object/resolve: parse object %s (%s): %w", id, tyName, err)
+ }
+
+ return parsed, nil
+}
diff --git a/object/resolve/path.go b/object/resolve/path.go
new file mode 100644
index 00000000..b3e2d642
--- /dev/null
+++ b/object/resolve/path.go
@@ -0,0 +1,53 @@
+package resolve
+
+import (
+ "fmt"
+
+ "codeberg.org/lindenii/furgit/object"
+ "codeberg.org/lindenii/furgit/objectid"
+)
+
+// Path resolves parts within the tree identified by root and returns the final
+// tree entry.
+//
+// The root object may be any tree-ish object accepted by PeelToTree.
+//
+// parts must contain at least one path segment. Intermediate path segments
+// must resolve to tree entries. The final entry is returned without loading
+// its object.
+func (r *Resolver) Path(root objectid.ObjectID, parts [][]byte) (object.TreeEntry, error) {
+ if len(parts) == 0 {
+ return object.TreeEntry{}, fmt.Errorf("object/resolve: empty tree path")
+ }
+
+ current, err := r.PeelToTree(root)
+ if err != nil {
+ return object.TreeEntry{}, err
+ }
+
+ for i, part := range parts {
+ if len(part) == 0 {
+ return object.TreeEntry{}, fmt.Errorf("object/resolve: empty tree path segment")
+ }
+
+ entry := current.Object().Entry(part)
+ if entry == nil {
+ return object.TreeEntry{}, fmt.Errorf("object/resolve: tree entry %q not found", part)
+ }
+
+ if i == len(parts)-1 {
+ return *entry, nil
+ }
+
+ if entry.Mode != object.FileModeDir {
+ return object.TreeEntry{}, fmt.Errorf("object/resolve: path segment %q is not a tree", part)
+ }
+
+ current, err = r.ExactTree(entry.ID)
+ if err != nil {
+ return object.TreeEntry{}, err
+ }
+ }
+
+ return object.TreeEntry{}, fmt.Errorf("object/resolve: tree entry not found")
+}
diff --git a/object/resolve/peel_to_blob.go b/object/resolve/peel_to_blob.go
new file mode 100644
index 00000000..59cfcba5
--- /dev/null
+++ b/object/resolve/peel_to_blob.go
@@ -0,0 +1,28 @@
+package resolve
+
+import (
+ "fmt"
+
+ "codeberg.org/lindenii/furgit/object"
+ "codeberg.org/lindenii/furgit/object/stored"
+ "codeberg.org/lindenii/furgit/objectid"
+)
+
+// PeelToBlob peels tags until it reaches a blob.
+func (r *Resolver) PeelToBlob(id objectid.ObjectID) (*stored.Stored[*object.Blob], error) {
+ for {
+ obj, err := r.ExactObject(id)
+ if err != nil {
+ return nil, err
+ }
+
+ switch parsed := obj.Object().(type) {
+ case *object.Blob:
+ return stored.New(id, parsed), nil
+ case *object.Tag:
+ id = parsed.Target
+ default:
+ return nil, fmt.Errorf("object/resolve: expected blob-ish object %s, got %v", id, parsed.ObjectType())
+ }
+ }
+}
diff --git a/object/resolve/peel_to_blob_id.go b/object/resolve/peel_to_blob_id.go
new file mode 100644
index 00000000..d0e28dd9
--- /dev/null
+++ b/object/resolve/peel_to_blob_id.go
@@ -0,0 +1,32 @@
+package resolve
+
+import (
+ "fmt"
+
+ "codeberg.org/lindenii/furgit/objectid"
+ "codeberg.org/lindenii/furgit/objecttype"
+)
+
+// PeelToBlobID peels tags until it reaches a blob object ID.
+func (r *Resolver) PeelToBlobID(id objectid.ObjectID) (objectid.ObjectID, error) {
+ for {
+ ty, _, err := r.store.ReadHeader(id)
+ if err != nil {
+ return objectid.ObjectID{}, err
+ }
+
+ switch ty {
+ case objecttype.TypeBlob:
+ return id, nil
+ case objecttype.TypeTag:
+ tag, err := r.ExactTag(id)
+ if err != nil {
+ return objectid.ObjectID{}, err
+ }
+
+ id = tag.Object().Target
+ default:
+ return objectid.ObjectID{}, fmt.Errorf("object/resolve: expected blob-ish object %s, got %v", id, ty)
+ }
+ }
+}
diff --git a/object/resolve/peel_to_blob_reader.go b/object/resolve/peel_to_blob_reader.go
new file mode 100644
index 00000000..7ea9f3af
--- /dev/null
+++ b/object/resolve/peel_to_blob_reader.go
@@ -0,0 +1,18 @@
+package resolve
+
+import (
+ "io"
+
+ "codeberg.org/lindenii/furgit/objectid"
+)
+
+// PeelToBlobReader returns a reader for the content of the peeled blob at id,
+// together with its content size in bytes.
+func (r *Resolver) PeelToBlobReader(id objectid.ObjectID) (io.ReadCloser, int64, error) {
+ blobID, err := r.PeelToBlobID(id)
+ if err != nil {
+ return nil, 0, err
+ }
+
+ return r.ExactBlobReader(blobID)
+}
diff --git a/object/resolve/peel_to_commit.go b/object/resolve/peel_to_commit.go
new file mode 100644
index 00000000..566fa294
--- /dev/null
+++ b/object/resolve/peel_to_commit.go
@@ -0,0 +1,28 @@
+package resolve
+
+import (
+ "fmt"
+
+ "codeberg.org/lindenii/furgit/object"
+ "codeberg.org/lindenii/furgit/object/stored"
+ "codeberg.org/lindenii/furgit/objectid"
+)
+
+// PeelToCommit peels tags until it reaches a commit.
+func (r *Resolver) PeelToCommit(id objectid.ObjectID) (*stored.Stored[*object.Commit], error) {
+ for {
+ obj, err := r.ExactObject(id)
+ if err != nil {
+ return nil, err
+ }
+
+ switch parsed := obj.Object().(type) {
+ case *object.Commit:
+ return stored.New(id, parsed), nil
+ case *object.Tag:
+ id = parsed.Target
+ default:
+ return nil, fmt.Errorf("object/resolve: expected commit-ish object %s, got %v", id, parsed.ObjectType())
+ }
+ }
+}
diff --git a/object/resolve/peel_to_commit_id.go b/object/resolve/peel_to_commit_id.go
new file mode 100644
index 00000000..3ff98fba
--- /dev/null
+++ b/object/resolve/peel_to_commit_id.go
@@ -0,0 +1,32 @@
+package resolve
+
+import (
+ "fmt"
+
+ "codeberg.org/lindenii/furgit/objectid"
+ "codeberg.org/lindenii/furgit/objecttype"
+)
+
+// PeelToCommitID peels tags until it reaches a commit object ID.
+func (r *Resolver) PeelToCommitID(id objectid.ObjectID) (objectid.ObjectID, error) {
+ for {
+ ty, _, err := r.store.ReadHeader(id)
+ if err != nil {
+ return objectid.ObjectID{}, err
+ }
+
+ switch ty {
+ case objecttype.TypeCommit:
+ return id, nil
+ case objecttype.TypeTag:
+ tag, err := r.ExactTag(id)
+ if err != nil {
+ return objectid.ObjectID{}, err
+ }
+
+ id = tag.Object().Target
+ default:
+ return objectid.ObjectID{}, fmt.Errorf("object/resolve: expected commit-ish object %s, got %v", id, ty)
+ }
+ }
+}
diff --git a/object/resolve/peel_to_commit_reader.go b/object/resolve/peel_to_commit_reader.go
new file mode 100644
index 00000000..ab17fe67
--- /dev/null
+++ b/object/resolve/peel_to_commit_reader.go
@@ -0,0 +1,20 @@
+package resolve
+
+import (
+ "io"
+
+ "codeberg.org/lindenii/furgit/objectid"
+)
+
+// PeelToCommitReader returns a reader for the content of the peeled commit at
+// id, together with its content size in bytes.
+//
+// Usage of this method is unusual.
+func (r *Resolver) PeelToCommitReader(id objectid.ObjectID) (io.ReadCloser, int64, error) {
+ commitID, err := r.PeelToCommitID(id)
+ if err != nil {
+ return nil, 0, err
+ }
+
+ return r.ExactCommitReader(commitID)
+}
diff --git a/object/resolve/peel_to_tag.go b/object/resolve/peel_to_tag.go
new file mode 100644
index 00000000..49fe9108
--- /dev/null
+++ b/object/resolve/peel_to_tag.go
@@ -0,0 +1,12 @@
+package resolve
+
+import (
+ "codeberg.org/lindenii/furgit/object"
+ "codeberg.org/lindenii/furgit/object/stored"
+ "codeberg.org/lindenii/furgit/objectid"
+)
+
+// PeelToTag returns the tag at id without further peeling.
+func (r *Resolver) PeelToTag(id objectid.ObjectID) (*stored.Stored[*object.Tag], error) {
+ return r.ExactTag(id)
+}
diff --git a/object/resolve/peel_to_tag_id.go b/object/resolve/peel_to_tag_id.go
new file mode 100644
index 00000000..8574aaa2
--- /dev/null
+++ b/object/resolve/peel_to_tag_id.go
@@ -0,0 +1,8 @@
+package resolve
+
+import "codeberg.org/lindenii/furgit/objectid"
+
+// PeelToTagID returns id unchanged.
+func (r *Resolver) PeelToTagID(id objectid.ObjectID) (objectid.ObjectID, error) {
+ return id, nil
+}
diff --git a/object/resolve/peel_to_tag_reader.go b/object/resolve/peel_to_tag_reader.go
new file mode 100644
index 00000000..d34ff7ad
--- /dev/null
+++ b/object/resolve/peel_to_tag_reader.go
@@ -0,0 +1,20 @@
+package resolve
+
+import (
+ "io"
+
+ "codeberg.org/lindenii/furgit/objectid"
+)
+
+// PeelToTagReader returns a reader for the content of the tag at id,
+// together with its content size in bytes.
+//
+// Usage of this method is unusual.
+func (r *Resolver) PeelToTagReader(id objectid.ObjectID) (io.ReadCloser, int64, error) {
+ tagID, err := r.PeelToTagID(id)
+ if err != nil {
+ return nil, 0, err
+ }
+
+ return r.ExactTagReader(tagID)
+}
diff --git a/object/resolve/peel_to_tree.go b/object/resolve/peel_to_tree.go
new file mode 100644
index 00000000..bb5d2422
--- /dev/null
+++ b/object/resolve/peel_to_tree.go
@@ -0,0 +1,31 @@
+package resolve
+
+import (
+ "fmt"
+
+ "codeberg.org/lindenii/furgit/object"
+ "codeberg.org/lindenii/furgit/object/stored"
+ "codeberg.org/lindenii/furgit/objectid"
+)
+
+// PeelToTree peels tags until it reaches a tree or commit. If it reaches a
+// commit, it returns the commit's root tree.
+func (r *Resolver) PeelToTree(id objectid.ObjectID) (*stored.Stored[*object.Tree], error) {
+ for {
+ obj, err := r.ExactObject(id)
+ if err != nil {
+ return nil, err
+ }
+
+ switch parsed := obj.Object().(type) {
+ case *object.Tree:
+ return stored.New(id, parsed), nil
+ case *object.Commit:
+ return r.ExactTree(parsed.Tree)
+ case *object.Tag:
+ id = parsed.Target
+ default:
+ return nil, fmt.Errorf("object/resolve: expected tree-ish object %s, got %v", id, parsed.ObjectType())
+ }
+ }
+}
diff --git a/object/resolve/peel_to_tree_id.go b/object/resolve/peel_to_tree_id.go
new file mode 100644
index 00000000..27c15672
--- /dev/null
+++ b/object/resolve/peel_to_tree_id.go
@@ -0,0 +1,40 @@
+package resolve
+
+import (
+ "fmt"
+
+ "codeberg.org/lindenii/furgit/objectid"
+ "codeberg.org/lindenii/furgit/objecttype"
+)
+
+// PeelToTreeID peels tags until it reaches a tree object ID, or a commit whose
+// root tree object ID is then returned.
+func (r *Resolver) PeelToTreeID(id objectid.ObjectID) (objectid.ObjectID, error) {
+ for {
+ ty, _, err := r.store.ReadHeader(id)
+ if err != nil {
+ return objectid.ObjectID{}, err
+ }
+
+ switch ty {
+ case objecttype.TypeTree:
+ return id, nil
+ case objecttype.TypeCommit:
+ commit, err := r.ExactCommit(id)
+ if err != nil {
+ return objectid.ObjectID{}, err
+ }
+
+ return commit.Object().Tree, nil
+ case objecttype.TypeTag:
+ tag, err := r.ExactTag(id)
+ if err != nil {
+ return objectid.ObjectID{}, err
+ }
+
+ id = tag.Object().Target
+ default:
+ return objectid.ObjectID{}, fmt.Errorf("object/resolve: expected tree-ish object %s, got %v", id, ty)
+ }
+ }
+}
diff --git a/object/resolve/peel_to_tree_reader.go b/object/resolve/peel_to_tree_reader.go
new file mode 100644
index 00000000..1f867883
--- /dev/null
+++ b/object/resolve/peel_to_tree_reader.go
@@ -0,0 +1,20 @@
+package resolve
+
+import (
+ "io"
+
+ "codeberg.org/lindenii/furgit/objectid"
+)
+
+// PeelToTreeReader returns a reader for the content of the peeled tree at id,
+// together with its content size in bytes.
+//
+// Usage of this method is unusual.
+func (r *Resolver) PeelToTreeReader(id objectid.ObjectID) (io.ReadCloser, int64, error) {
+ treeID, err := r.PeelToTreeID(id)
+ if err != nil {
+ return nil, 0, err
+ }
+
+ return r.ExactTreeReader(treeID)
+}
diff --git a/object/resolve/resolver.go b/object/resolve/resolver.go
new file mode 100644
index 00000000..0d67d890
--- /dev/null
+++ b/object/resolve/resolver.go
@@ -0,0 +1,17 @@
+package resolve
+
+import "codeberg.org/lindenii/furgit/objectstore"
+
+// Resolver resolves parsed and streamed objects from an object store.
+//
+// A Resolver does not take ownership of the store and does not close it.
+type Resolver struct {
+ store objectstore.Store
+}
+
+// New returns a Resolver that reads objects from store.
+//
+// The returned Resolver does not take ownership of store.
+func New(store objectstore.Store) *Resolver {
+ return &Resolver{store: store}
+}