From 1aa5cad4c8d6455eeb1f10893549e18bcca11996 Mon Sep 17 00:00:00 2001 From: Runxi Yu Date: Wed, 25 Mar 2026 19:33:32 +0000 Subject: object/fetch: Rename from object/resolve --- README.md | 6 +- object/fetch/doc.go | 5 ++ object/fetch/exact_blob.go | 24 +++++++ object/fetch/exact_blob_reader.go | 14 ++++ object/fetch/exact_commit.go | 24 +++++++ object/fetch/exact_commit_reader.go | 16 +++++ object/fetch/exact_object.go | 18 +++++ object/fetch/exact_reader.go | 26 +++++++ object/fetch/exact_tag.go | 24 +++++++ object/fetch/exact_tag_reader.go | 16 +++++ object/fetch/exact_tree.go | 24 +++++++ object/fetch/exact_tree_reader.go | 16 +++++ object/fetch/fetcher.go | 17 +++++ object/fetch/object_parse.go | 28 ++++++++ object/fetch/path.go | 103 +++++++++++++++++++++++++++ object/fetch/peel_to_blob.go | 29 ++++++++ object/fetch/peel_to_blob_id.go | 39 ++++++++++ object/fetch/peel_to_blob_reader.go | 18 +++++ object/fetch/peel_to_commit.go | 29 ++++++++ object/fetch/peel_to_commit_id.go | 39 ++++++++++ object/fetch/peel_to_commit_reader.go | 20 ++++++ object/fetch/peel_to_tag.go | 12 ++++ object/fetch/peel_to_tag_id.go | 8 +++ object/fetch/peel_to_tag_reader.go | 20 ++++++ object/fetch/peel_to_tree.go | 33 +++++++++ object/fetch/peel_to_tree_id.go | 46 ++++++++++++ object/fetch/peel_to_tree_reader.go | 20 ++++++ object/fetch/treefs.go | 30 ++++++++ object/fetch/treefs_entry.go | 90 +++++++++++++++++++++++ object/fetch/treefs_info.go | 75 ++++++++++++++++++++ object/fetch/treefs_new.go | 17 +++++ object/fetch/treefs_op.go | 28 ++++++++ object/fetch/treefs_open.go | 122 ++++++++++++++++++++++++++++++++ object/fetch/treefs_path.go | 29 ++++++++ object/fetch/treefs_readdir.go | 20 ++++++ object/fetch/treefs_readfile.go | 40 +++++++++++ object/fetch/treefs_stat.go | 22 ++++++ object/fetch/treefs_sub.go | 22 ++++++ object/fetch/treefs_test.go | 111 +++++++++++++++++++++++++++++ object/resolve/doc.go | 5 -- object/resolve/exact_blob.go | 24 ------- object/resolve/exact_blob_reader.go | 14 ---- object/resolve/exact_commit.go | 24 ------- object/resolve/exact_commit_reader.go | 16 ----- object/resolve/exact_object.go | 18 ----- object/resolve/exact_reader.go | 26 ------- object/resolve/exact_tag.go | 24 ------- object/resolve/exact_tag_reader.go | 16 ----- object/resolve/exact_tree.go | 24 ------- object/resolve/exact_tree_reader.go | 16 ----- object/resolve/object_parse.go | 28 -------- object/resolve/path.go | 103 --------------------------- object/resolve/peel_to_blob.go | 29 -------- object/resolve/peel_to_blob_id.go | 39 ---------- object/resolve/peel_to_blob_reader.go | 18 ----- object/resolve/peel_to_commit.go | 29 -------- object/resolve/peel_to_commit_id.go | 39 ---------- object/resolve/peel_to_commit_reader.go | 20 ------ object/resolve/peel_to_tag.go | 12 ---- object/resolve/peel_to_tag_id.go | 8 --- object/resolve/peel_to_tag_reader.go | 20 ------ object/resolve/peel_to_tree.go | 33 --------- object/resolve/peel_to_tree_id.go | 46 ------------ object/resolve/peel_to_tree_reader.go | 20 ------ object/resolve/resolver.go | 17 ----- object/resolve/treefs.go | 30 -------- object/resolve/treefs_entry.go | 90 ----------------------- object/resolve/treefs_info.go | 75 -------------------- object/resolve/treefs_new.go | 17 ----- object/resolve/treefs_op.go | 28 -------- object/resolve/treefs_open.go | 122 -------------------------------- object/resolve/treefs_path.go | 29 -------- object/resolve/treefs_readdir.go | 20 ------ object/resolve/treefs_readfile.go | 40 ----------- object/resolve/treefs_stat.go | 22 ------ object/resolve/treefs_sub.go | 22 ------ object/resolve/treefs_test.go | 111 ----------------------------- repository/resolver.go | 6 +- 78 files changed, 1280 insertions(+), 1280 deletions(-) create mode 100644 object/fetch/doc.go create mode 100644 object/fetch/exact_blob.go create mode 100644 object/fetch/exact_blob_reader.go create mode 100644 object/fetch/exact_commit.go create mode 100644 object/fetch/exact_commit_reader.go create mode 100644 object/fetch/exact_object.go create mode 100644 object/fetch/exact_reader.go create mode 100644 object/fetch/exact_tag.go create mode 100644 object/fetch/exact_tag_reader.go create mode 100644 object/fetch/exact_tree.go create mode 100644 object/fetch/exact_tree_reader.go create mode 100644 object/fetch/fetcher.go create mode 100644 object/fetch/object_parse.go create mode 100644 object/fetch/path.go create mode 100644 object/fetch/peel_to_blob.go create mode 100644 object/fetch/peel_to_blob_id.go create mode 100644 object/fetch/peel_to_blob_reader.go create mode 100644 object/fetch/peel_to_commit.go create mode 100644 object/fetch/peel_to_commit_id.go create mode 100644 object/fetch/peel_to_commit_reader.go create mode 100644 object/fetch/peel_to_tag.go create mode 100644 object/fetch/peel_to_tag_id.go create mode 100644 object/fetch/peel_to_tag_reader.go create mode 100644 object/fetch/peel_to_tree.go create mode 100644 object/fetch/peel_to_tree_id.go create mode 100644 object/fetch/peel_to_tree_reader.go create mode 100644 object/fetch/treefs.go create mode 100644 object/fetch/treefs_entry.go create mode 100644 object/fetch/treefs_info.go create mode 100644 object/fetch/treefs_new.go create mode 100644 object/fetch/treefs_op.go create mode 100644 object/fetch/treefs_open.go create mode 100644 object/fetch/treefs_path.go create mode 100644 object/fetch/treefs_readdir.go create mode 100644 object/fetch/treefs_readfile.go create mode 100644 object/fetch/treefs_stat.go create mode 100644 object/fetch/treefs_sub.go create mode 100644 object/fetch/treefs_test.go delete mode 100644 object/resolve/doc.go delete mode 100644 object/resolve/exact_blob.go delete mode 100644 object/resolve/exact_blob_reader.go delete mode 100644 object/resolve/exact_commit.go delete mode 100644 object/resolve/exact_commit_reader.go delete mode 100644 object/resolve/exact_object.go delete mode 100644 object/resolve/exact_reader.go delete mode 100644 object/resolve/exact_tag.go delete mode 100644 object/resolve/exact_tag_reader.go delete mode 100644 object/resolve/exact_tree.go delete mode 100644 object/resolve/exact_tree_reader.go delete mode 100644 object/resolve/object_parse.go delete mode 100644 object/resolve/path.go delete mode 100644 object/resolve/peel_to_blob.go delete mode 100644 object/resolve/peel_to_blob_id.go delete mode 100644 object/resolve/peel_to_blob_reader.go delete mode 100644 object/resolve/peel_to_commit.go delete mode 100644 object/resolve/peel_to_commit_id.go delete mode 100644 object/resolve/peel_to_commit_reader.go delete mode 100644 object/resolve/peel_to_tag.go delete mode 100644 object/resolve/peel_to_tag_id.go delete mode 100644 object/resolve/peel_to_tag_reader.go delete mode 100644 object/resolve/peel_to_tree.go delete mode 100644 object/resolve/peel_to_tree_id.go delete mode 100644 object/resolve/peel_to_tree_reader.go delete mode 100644 object/resolve/resolver.go delete mode 100644 object/resolve/treefs.go delete mode 100644 object/resolve/treefs_entry.go delete mode 100644 object/resolve/treefs_info.go delete mode 100644 object/resolve/treefs_new.go delete mode 100644 object/resolve/treefs_op.go delete mode 100644 object/resolve/treefs_open.go delete mode 100644 object/resolve/treefs_path.go delete mode 100644 object/resolve/treefs_readdir.go delete mode 100644 object/resolve/treefs_readfile.go delete mode 100644 object/resolve/treefs_stat.go delete mode 100644 object/resolve/treefs_sub.go delete mode 100644 object/resolve/treefs_test.go diff --git a/README.md b/README.md index 151bcf01..50b26d6d 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ bare repository or a `.git` directory. Then, * A common pattern is to resolve a ref first, then pass the resulting object ID to the resolver. -* `repo.Resolver()` is the main object-facing API for most callers. +* `repo.Fetcher()` is the main object-facing API for most callers. * Use it when you want commits, trees, blobs, or tags as typed values. * It also handles peeling through annotated tags, resolving objects to the type you actually want, and walking paths inside trees. @@ -39,7 +39,7 @@ bare repository or a `.git` directory. Then, * If your goal is "show me this commit", "read this tree", "follow this tag", or "get me the file at this path", this is usually the right layer. -* `repo.Objects()` is the storage layer underneath `Resolver`. +* `repo.Objects()` is the storage layer underneath `Fetcher`. * Use it when you need to read object headers, read raw object contents, stream object data, or otherwise look up objects directly by ID. * Most callers who want to work with Git objects as commits, trees, blobs, or @@ -60,7 +60,7 @@ Note that: As a rule of thumb: * If you have a ref name, start with `repo.Refs()`. -* If you want typed objects or path-based access, use `repo.Resolver()`. +* If you want typed objects or path-based access, use `repo.Fetcher()`. * If you need raw object lookup by ID, object headers, or object streams, use `repo.Objects()`. diff --git a/object/fetch/doc.go b/object/fetch/doc.go new file mode 100644 index 00000000..e5ca33a9 --- /dev/null +++ b/object/fetch/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 Fetcher does not take ownership of the underlying object store. +package fetch diff --git a/object/fetch/exact_blob.go b/object/fetch/exact_blob.go new file mode 100644 index 00000000..96ededdc --- /dev/null +++ b/object/fetch/exact_blob.go @@ -0,0 +1,24 @@ +package fetch + +import ( + "fmt" + + "codeberg.org/lindenii/furgit/object/blob" + objectid "codeberg.org/lindenii/furgit/object/id" + "codeberg.org/lindenii/furgit/object/stored" +) + +// ExactBlob reads, parses, and wraps the blob at id. +func (r *Fetcher) ExactBlob(id objectid.ObjectID) (*stored.Stored[*blob.Blob], error) { + parsed, err := r.parseObject(id) + if err != nil { + return nil, err + } + + blob, ok := parsed.(*blob.Blob) + if !ok { + return nil, fmt.Errorf("object/fetch: expected blob object %s, got %v", id, parsed.ObjectType()) + } + + return stored.New(id, blob), nil +} diff --git a/object/fetch/exact_blob_reader.go b/object/fetch/exact_blob_reader.go new file mode 100644 index 00000000..95f5e1aa --- /dev/null +++ b/object/fetch/exact_blob_reader.go @@ -0,0 +1,14 @@ +package fetch + +import ( + "io" + + objectid "codeberg.org/lindenii/furgit/object/id" + objecttype "codeberg.org/lindenii/furgit/object/type" +) + +// ExactBlobReader returns a reader for the content of the blob at id, +// together with its content size in bytes. +func (r *Fetcher) ExactBlobReader(id objectid.ObjectID) (io.ReadCloser, int64, error) { + return r.exactReader(id, objecttype.TypeBlob, "blob") +} diff --git a/object/fetch/exact_commit.go b/object/fetch/exact_commit.go new file mode 100644 index 00000000..d3f9dee1 --- /dev/null +++ b/object/fetch/exact_commit.go @@ -0,0 +1,24 @@ +package fetch + +import ( + "fmt" + + "codeberg.org/lindenii/furgit/object/commit" + objectid "codeberg.org/lindenii/furgit/object/id" + "codeberg.org/lindenii/furgit/object/stored" +) + +// ExactCommit reads, parses, and wraps the commit at id. +func (r *Fetcher) ExactCommit(id objectid.ObjectID) (*stored.Stored[*commit.Commit], error) { + parsed, err := r.parseObject(id) + if err != nil { + return nil, err + } + + commit, ok := parsed.(*commit.Commit) + if !ok { + return nil, fmt.Errorf("object/fetch: expected commit object %s, got %v", id, parsed.ObjectType()) + } + + return stored.New(id, commit), nil +} diff --git a/object/fetch/exact_commit_reader.go b/object/fetch/exact_commit_reader.go new file mode 100644 index 00000000..5ccae4b1 --- /dev/null +++ b/object/fetch/exact_commit_reader.go @@ -0,0 +1,16 @@ +package fetch + +import ( + "io" + + objectid "codeberg.org/lindenii/furgit/object/id" + objecttype "codeberg.org/lindenii/furgit/object/type" +) + +// 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 *Fetcher) ExactCommitReader(id objectid.ObjectID) (io.ReadCloser, int64, error) { + return r.exactReader(id, objecttype.TypeCommit, "commit") +} diff --git a/object/fetch/exact_object.go b/object/fetch/exact_object.go new file mode 100644 index 00000000..869d811f --- /dev/null +++ b/object/fetch/exact_object.go @@ -0,0 +1,18 @@ +package fetch + +import ( + "codeberg.org/lindenii/furgit/object" + objectid "codeberg.org/lindenii/furgit/object/id" + "codeberg.org/lindenii/furgit/object/stored" +) + +// ExactObject reads, parses, and wraps the object at id without constraining +// its concrete object kind. +func (r *Fetcher) 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/fetch/exact_reader.go b/object/fetch/exact_reader.go new file mode 100644 index 00000000..54cf246c --- /dev/null +++ b/object/fetch/exact_reader.go @@ -0,0 +1,26 @@ +package fetch + +import ( + "fmt" + "io" + + objectid "codeberg.org/lindenii/furgit/object/id" + objecttype "codeberg.org/lindenii/furgit/object/type" +) + +// exactReader reads one object's content stream and verifies that its header +// type matches wantType. +func (r *Fetcher) 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/fetch: expected %s object %s, got %v", wantName, id, gotType) + } + + return rc, size, nil +} diff --git a/object/fetch/exact_tag.go b/object/fetch/exact_tag.go new file mode 100644 index 00000000..03c66019 --- /dev/null +++ b/object/fetch/exact_tag.go @@ -0,0 +1,24 @@ +package fetch + +import ( + "fmt" + + objectid "codeberg.org/lindenii/furgit/object/id" + "codeberg.org/lindenii/furgit/object/stored" + "codeberg.org/lindenii/furgit/object/tag" +) + +// ExactTag reads, parses, and wraps the tag at id. +func (r *Fetcher) ExactTag(id objectid.ObjectID) (*stored.Stored[*tag.Tag], error) { + parsed, err := r.parseObject(id) + if err != nil { + return nil, err + } + + tag, ok := parsed.(*tag.Tag) + if !ok { + return nil, fmt.Errorf("object/fetch: expected tag object %s, got %v", id, parsed.ObjectType()) + } + + return stored.New(id, tag), nil +} diff --git a/object/fetch/exact_tag_reader.go b/object/fetch/exact_tag_reader.go new file mode 100644 index 00000000..79a31a4c --- /dev/null +++ b/object/fetch/exact_tag_reader.go @@ -0,0 +1,16 @@ +package fetch + +import ( + "io" + + objectid "codeberg.org/lindenii/furgit/object/id" + objecttype "codeberg.org/lindenii/furgit/object/type" +) + +// 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 *Fetcher) ExactTagReader(id objectid.ObjectID) (io.ReadCloser, int64, error) { + return r.exactReader(id, objecttype.TypeTag, "tag") +} diff --git a/object/fetch/exact_tree.go b/object/fetch/exact_tree.go new file mode 100644 index 00000000..cab10ba2 --- /dev/null +++ b/object/fetch/exact_tree.go @@ -0,0 +1,24 @@ +package fetch + +import ( + "fmt" + + objectid "codeberg.org/lindenii/furgit/object/id" + "codeberg.org/lindenii/furgit/object/stored" + "codeberg.org/lindenii/furgit/object/tree" +) + +// ExactTree reads, parses, and wraps the tree at id. +func (r *Fetcher) ExactTree(id objectid.ObjectID) (*stored.Stored[*tree.Tree], error) { + parsed, err := r.parseObject(id) + if err != nil { + return nil, err + } + + tree, ok := parsed.(*tree.Tree) + if !ok { + return nil, fmt.Errorf("object/fetch: expected tree object %s, got %v", id, parsed.ObjectType()) + } + + return stored.New(id, tree), nil +} diff --git a/object/fetch/exact_tree_reader.go b/object/fetch/exact_tree_reader.go new file mode 100644 index 00000000..4660bd34 --- /dev/null +++ b/object/fetch/exact_tree_reader.go @@ -0,0 +1,16 @@ +package fetch + +import ( + "io" + + objectid "codeberg.org/lindenii/furgit/object/id" + objecttype "codeberg.org/lindenii/furgit/object/type" +) + +// 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 *Fetcher) ExactTreeReader(id objectid.ObjectID) (io.ReadCloser, int64, error) { + return r.exactReader(id, objecttype.TypeTree, "tree") +} diff --git a/object/fetch/fetcher.go b/object/fetch/fetcher.go new file mode 100644 index 00000000..ed4e74a1 --- /dev/null +++ b/object/fetch/fetcher.go @@ -0,0 +1,17 @@ +package fetch + +import objectstorer "codeberg.org/lindenii/furgit/object/storer" + +// Fetcher resolves parsed and streamed objects from an object store. +// +// A Fetcher does not take ownership of the store and does not close it. +type Fetcher struct { + store objectstorer.Store +} + +// New returns a Fetcher that reads objects from store. +// +// The returned Fetcher does not take ownership of store. +func New(store objectstorer.Store) *Fetcher { + return &Fetcher{store: store} +} diff --git a/object/fetch/object_parse.go b/object/fetch/object_parse.go new file mode 100644 index 00000000..a29e17c0 --- /dev/null +++ b/object/fetch/object_parse.go @@ -0,0 +1,28 @@ +package fetch + +import ( + "fmt" + + "codeberg.org/lindenii/furgit/object" + objectid "codeberg.org/lindenii/furgit/object/id" + objecttype "codeberg.org/lindenii/furgit/object/type" +) + +func (r *Fetcher) 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/fetch: parse object %s (%s): %w", id, tyName, err) + } + + return parsed, nil +} diff --git a/object/fetch/path.go b/object/fetch/path.go new file mode 100644 index 00000000..f26379c3 --- /dev/null +++ b/object/fetch/path.go @@ -0,0 +1,103 @@ +package fetch + +import ( + "fmt" + + objectid "codeberg.org/lindenii/furgit/object/id" + "codeberg.org/lindenii/furgit/object/tree" +) + +// PathEmptyError indicates that Path received no segments. +type PathEmptyError struct{} + +func (err *PathEmptyError) Error() string { + return "object/fetch: empty tree path" +} + +// PathSegmentEmptyError indicates that one path segment is empty. +type PathSegmentEmptyError struct { + Index int +} + +func (err *PathSegmentEmptyError) Error() string { + return fmt.Sprintf("object/fetch: empty tree path segment at index %d", err.Index) +} + +// PathNotFoundError indicates that one tree path segment was not found. +type PathNotFoundError struct { + Index int + Name []byte +} + +func (err *PathNotFoundError) Error() string { + return fmt.Sprintf("object/fetch: tree entry %q not found at index %d", err.Name, err.Index) +} + +// PathNotTreeError indicates that one intermediate path segment was not a tree. +type PathNotTreeError struct { + Index int + Name []byte +} + +func (err *PathNotTreeError) Error() string { + return fmt.Sprintf("object/fetch: path segment %q at index %d is not a tree", err.Name, err.Index) +} + +// 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. Path segments may not contain \x00. +// +// The path cannot be accurately represented as a string or a single []byte +// because Git tree entry names may include slashes. While []string is +// technically possible (since Go strings are not necessarily UTF-8), they +// do often imply UTF-8 in practice, which would be undesirable. +// +// If your entry names are valid UTF-8 and uses / solely as segment separators, +// it may be convenient to use TreeFS for an io/fs.FS-like interface. +func (r *Fetcher) Path(root objectid.ObjectID, parts [][]byte) (tree.TreeEntry, error) { + if len(parts) == 0 { + return tree.TreeEntry{}, &PathEmptyError{} + } + + current, err := r.PeelToTree(root) + if err != nil { + return tree.TreeEntry{}, err + } + + for i, part := range parts { + if len(part) == 0 { + return tree.TreeEntry{}, &PathSegmentEmptyError{Index: i} + } + + entry := current.Object().Entry(part) + if entry == nil { + return tree.TreeEntry{}, &PathNotFoundError{ + Index: i, + Name: append([]byte(nil), part...), + } + } + + if i == len(parts)-1 { + return *entry, nil + } + + if entry.Mode != tree.FileModeDir { + return tree.TreeEntry{}, &PathNotTreeError{ + Index: i, + Name: append([]byte(nil), part...), + } + } + + current, err = r.ExactTree(entry.ID) + if err != nil { + return tree.TreeEntry{}, err + } + } + + return tree.TreeEntry{}, &PathNotFoundError{Index: len(parts) - 1} +} diff --git a/object/fetch/peel_to_blob.go b/object/fetch/peel_to_blob.go new file mode 100644 index 00000000..b0a1ad1c --- /dev/null +++ b/object/fetch/peel_to_blob.go @@ -0,0 +1,29 @@ +package fetch + +import ( + "fmt" + + "codeberg.org/lindenii/furgit/object/blob" + objectid "codeberg.org/lindenii/furgit/object/id" + "codeberg.org/lindenii/furgit/object/stored" + "codeberg.org/lindenii/furgit/object/tag" +) + +// PeelToBlob peels tags until it reaches a blob. +func (r *Fetcher) PeelToBlob(id objectid.ObjectID) (*stored.Stored[*blob.Blob], error) { + for { + obj, err := r.ExactObject(id) + if err != nil { + return nil, err + } + + switch parsed := obj.Object().(type) { + case *blob.Blob: + return stored.New(id, parsed), nil + case *tag.Tag: + id = parsed.Target + default: + return nil, fmt.Errorf("object/fetch: expected blob-ish object %s, got %v", id, parsed.ObjectType()) + } + } +} diff --git a/object/fetch/peel_to_blob_id.go b/object/fetch/peel_to_blob_id.go new file mode 100644 index 00000000..9d13f640 --- /dev/null +++ b/object/fetch/peel_to_blob_id.go @@ -0,0 +1,39 @@ +package fetch + +import ( + "fmt" + + objectid "codeberg.org/lindenii/furgit/object/id" + objecttype "codeberg.org/lindenii/furgit/object/type" +) + +// PeelToBlobID peels tags until it reaches a blob object ID. +func (r *Fetcher) 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 + case objecttype.TypeInvalid, + objecttype.TypeCommit, + objecttype.TypeTree, + objecttype.TypeFuture, + objecttype.TypeOfsDelta, + objecttype.TypeRefDelta: + return objectid.ObjectID{}, fmt.Errorf("object/fetch: expected blob-ish object %s, got %v", id, ty) + default: + return objectid.ObjectID{}, fmt.Errorf("object/fetch: expected blob-ish object %s, got %v", id, ty) + } + } +} diff --git a/object/fetch/peel_to_blob_reader.go b/object/fetch/peel_to_blob_reader.go new file mode 100644 index 00000000..d7072531 --- /dev/null +++ b/object/fetch/peel_to_blob_reader.go @@ -0,0 +1,18 @@ +package fetch + +import ( + "io" + + objectid "codeberg.org/lindenii/furgit/object/id" +) + +// PeelToBlobReader returns a reader for the content of the peeled blob at id, +// together with its content size in bytes. +func (r *Fetcher) 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/fetch/peel_to_commit.go b/object/fetch/peel_to_commit.go new file mode 100644 index 00000000..5ec66411 --- /dev/null +++ b/object/fetch/peel_to_commit.go @@ -0,0 +1,29 @@ +package fetch + +import ( + "fmt" + + "codeberg.org/lindenii/furgit/object/commit" + objectid "codeberg.org/lindenii/furgit/object/id" + "codeberg.org/lindenii/furgit/object/stored" + "codeberg.org/lindenii/furgit/object/tag" +) + +// PeelToCommit peels tags until it reaches a commit. +func (r *Fetcher) PeelToCommit(id objectid.ObjectID) (*stored.Stored[*commit.Commit], error) { + for { + obj, err := r.ExactObject(id) + if err != nil { + return nil, err + } + + switch parsed := obj.Object().(type) { + case *commit.Commit: + return stored.New(id, parsed), nil + case *tag.Tag: + id = parsed.Target + default: + return nil, fmt.Errorf("object/fetch: expected commit-ish object %s, got %v", id, parsed.ObjectType()) + } + } +} diff --git a/object/fetch/peel_to_commit_id.go b/object/fetch/peel_to_commit_id.go new file mode 100644 index 00000000..b6b257cf --- /dev/null +++ b/object/fetch/peel_to_commit_id.go @@ -0,0 +1,39 @@ +package fetch + +import ( + "fmt" + + objectid "codeberg.org/lindenii/furgit/object/id" + objecttype "codeberg.org/lindenii/furgit/object/type" +) + +// PeelToCommitID peels tags until it reaches a commit object ID. +func (r *Fetcher) 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 + case objecttype.TypeInvalid, + objecttype.TypeTree, + objecttype.TypeBlob, + objecttype.TypeFuture, + objecttype.TypeOfsDelta, + objecttype.TypeRefDelta: + return objectid.ObjectID{}, fmt.Errorf("object/fetch: expected commit-ish object %s, got %v", id, ty) + default: + return objectid.ObjectID{}, fmt.Errorf("object/fetch: expected commit-ish object %s, got %v", id, ty) + } + } +} diff --git a/object/fetch/peel_to_commit_reader.go b/object/fetch/peel_to_commit_reader.go new file mode 100644 index 00000000..02bf9f6f --- /dev/null +++ b/object/fetch/peel_to_commit_reader.go @@ -0,0 +1,20 @@ +package fetch + +import ( + "io" + + objectid "codeberg.org/lindenii/furgit/object/id" +) + +// 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 *Fetcher) 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/fetch/peel_to_tag.go b/object/fetch/peel_to_tag.go new file mode 100644 index 00000000..60693069 --- /dev/null +++ b/object/fetch/peel_to_tag.go @@ -0,0 +1,12 @@ +package fetch + +import ( + objectid "codeberg.org/lindenii/furgit/object/id" + "codeberg.org/lindenii/furgit/object/stored" + "codeberg.org/lindenii/furgit/object/tag" +) + +// PeelToTag returns the tag at id without further peeling. +func (r *Fetcher) PeelToTag(id objectid.ObjectID) (*stored.Stored[*tag.Tag], error) { + return r.ExactTag(id) +} diff --git a/object/fetch/peel_to_tag_id.go b/object/fetch/peel_to_tag_id.go new file mode 100644 index 00000000..dc02c60a --- /dev/null +++ b/object/fetch/peel_to_tag_id.go @@ -0,0 +1,8 @@ +package fetch + +import objectid "codeberg.org/lindenii/furgit/object/id" + +// PeelToTagID returns id unchanged. +func (r *Fetcher) PeelToTagID(id objectid.ObjectID) (objectid.ObjectID, error) { + return id, nil +} diff --git a/object/fetch/peel_to_tag_reader.go b/object/fetch/peel_to_tag_reader.go new file mode 100644 index 00000000..562ef9e8 --- /dev/null +++ b/object/fetch/peel_to_tag_reader.go @@ -0,0 +1,20 @@ +package fetch + +import ( + "io" + + objectid "codeberg.org/lindenii/furgit/object/id" +) + +// 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 *Fetcher) 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/fetch/peel_to_tree.go b/object/fetch/peel_to_tree.go new file mode 100644 index 00000000..119e76ac --- /dev/null +++ b/object/fetch/peel_to_tree.go @@ -0,0 +1,33 @@ +package fetch + +import ( + "fmt" + + "codeberg.org/lindenii/furgit/object/commit" + objectid "codeberg.org/lindenii/furgit/object/id" + "codeberg.org/lindenii/furgit/object/stored" + "codeberg.org/lindenii/furgit/object/tag" + "codeberg.org/lindenii/furgit/object/tree" +) + +// PeelToTree peels tags until it reaches a tree or commit. If it reaches a +// commit, it returns the commit's root tree. +func (r *Fetcher) PeelToTree(id objectid.ObjectID) (*stored.Stored[*tree.Tree], error) { + for { + obj, err := r.ExactObject(id) + if err != nil { + return nil, err + } + + switch parsed := obj.Object().(type) { + case *tree.Tree: + return stored.New(id, parsed), nil + case *commit.Commit: + return r.ExactTree(parsed.Tree) + case *tag.Tag: + id = parsed.Target + default: + return nil, fmt.Errorf("object/fetch: expected tree-ish object %s, got %v", id, parsed.ObjectType()) + } + } +} diff --git a/object/fetch/peel_to_tree_id.go b/object/fetch/peel_to_tree_id.go new file mode 100644 index 00000000..21dcfde4 --- /dev/null +++ b/object/fetch/peel_to_tree_id.go @@ -0,0 +1,46 @@ +package fetch + +import ( + "fmt" + + objectid "codeberg.org/lindenii/furgit/object/id" + objecttype "codeberg.org/lindenii/furgit/object/type" +) + +// PeelToTreeID peels tags until it reaches a tree object ID, or a commit whose +// root tree object ID is then returned. +func (r *Fetcher) 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 + case objecttype.TypeInvalid, + objecttype.TypeBlob, + objecttype.TypeFuture, + objecttype.TypeOfsDelta, + objecttype.TypeRefDelta: + return objectid.ObjectID{}, fmt.Errorf("object/fetch: expected tree-ish object %s, got %v", id, ty) + default: + return objectid.ObjectID{}, fmt.Errorf("object/fetch: expected tree-ish object %s, got %v", id, ty) + } + } +} diff --git a/object/fetch/peel_to_tree_reader.go b/object/fetch/peel_to_tree_reader.go new file mode 100644 index 00000000..fe1c5b93 --- /dev/null +++ b/object/fetch/peel_to_tree_reader.go @@ -0,0 +1,20 @@ +package fetch + +import ( + "io" + + objectid "codeberg.org/lindenii/furgit/object/id" +) + +// 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 *Fetcher) 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/fetch/treefs.go b/object/fetch/treefs.go new file mode 100644 index 00000000..06b6ad25 --- /dev/null +++ b/object/fetch/treefs.go @@ -0,0 +1,30 @@ +package fetch + +import ( + "io/fs" + + objectid "codeberg.org/lindenii/furgit/object/id" + "codeberg.org/lindenii/furgit/object/tree" +) + +// TreeFS exposes one Git tree as an fs.FS. +// +// TreeFS interprets names using io/fs path rules. Those rules do not match raw +// Git tree entry naming exactly: names are UTF-8, slash-separated, and must be +// valid fs.FS paths. Tree entries that cannot be represented under those rules +// are not addressable through this API. +// +// TreeFS does not take ownership of its Fetcher. +type TreeFS struct { + resolver *Fetcher + rootTree objectid.ObjectID + rootEntry *tree.TreeEntry +} + +var ( + _ fs.FS = (*TreeFS)(nil) + _ fs.ReadFileFS = (*TreeFS)(nil) + _ fs.ReadDirFS = (*TreeFS)(nil) + _ fs.StatFS = (*TreeFS)(nil) + _ fs.SubFS = (*TreeFS)(nil) +) diff --git a/object/fetch/treefs_entry.go b/object/fetch/treefs_entry.go new file mode 100644 index 00000000..4aa7730a --- /dev/null +++ b/object/fetch/treefs_entry.go @@ -0,0 +1,90 @@ +package fetch + +import ( + "errors" + "fmt" + "io/fs" + + objectid "codeberg.org/lindenii/furgit/object/id" + "codeberg.org/lindenii/furgit/object/tree" +) + +func (treeFS *TreeFS) resolvePath(op treeFSOp, name string) (treeEntryValue, error) { + if !treeFSValidPath(name) { + return treeEntryValue{}, treeFSPathError(op, name, fs.ErrInvalid) + } + + if name == "." { + return treeEntryValue{ + name: ".", + mode: tree.FileModeDir, + treeID: treeFS.rootTree, + treeEntry: treeFS.rootEntry, + }, nil + } + + entry, err := treeFS.resolver.Path(treeFS.rootTree, treeFSSplitPath(name)) + if err != nil { + return treeEntryValue{}, treeFS.pathResolveError(op, name, err) + } + + return treeEntryValue{ + name: string(entry.Name), + mode: entry.Mode, + objectID: entry.ID, + treeEntry: &entry, + }, nil +} + +func (treeFS *TreeFS) pathResolveError(op treeFSOp, name string, err error) error { + if _, ok := errors.AsType[*PathNotFoundError](err); ok { + return treeFSPathError(op, name, fs.ErrNotExist) + } + + if _, ok := errors.AsType[*PathNotTreeError](err); ok { + return treeFSPathError(op, name, fs.ErrInvalid) + } + + if _, ok := errors.AsType[*PathEmptyError](err); ok { + return treeFSPathError(op, name, fs.ErrInvalid) + } + + if _, ok := errors.AsType[*PathSegmentEmptyError](err); ok { + return treeFSPathError(op, name, fs.ErrInvalid) + } + + return treeFSPathError(op, name, err) +} + +type treeEntryValue struct { + name string + mode tree.FileMode + objectID objectid.ObjectID + treeID objectid.ObjectID + treeEntry *tree.TreeEntry +} + +func (entry treeEntryValue) isDir() bool { + return entry.mode == tree.FileModeDir +} + +func (entry treeEntryValue) blobSize(resolve *Fetcher) (int64, error) { + _, size, err := resolve.store.ReadHeader(entry.objectID) + if err != nil { + return 0, err + } + + return size, nil +} + +func (entry treeEntryValue) subtreeID() (objectid.ObjectID, error) { + if entry.name == "." { + return entry.treeID, nil + } + + if entry.mode != tree.FileModeDir { + return objectid.ObjectID{}, fmt.Errorf("object/fetch: path %q is not a tree", entry.name) + } + + return entry.objectID, nil +} diff --git a/object/fetch/treefs_info.go b/object/fetch/treefs_info.go new file mode 100644 index 00000000..eecf183d --- /dev/null +++ b/object/fetch/treefs_info.go @@ -0,0 +1,75 @@ +package fetch + +import ( + "io/fs" + "time" + + "codeberg.org/lindenii/furgit/object/tree" +) + +type treeFSInfo struct { + name string + mode fs.FileMode + size int64 + sys any + isDir bool +} + +var ( + _ fs.FileInfo = (*treeFSInfo)(nil) + _ fs.DirEntry = (*treeFSInfo)(nil) +) + +func (info *treeFSInfo) Name() string { return info.name } +func (info *treeFSInfo) Size() int64 { return info.size } +func (info *treeFSInfo) Mode() fs.FileMode { return info.mode } +func (info *treeFSInfo) Type() fs.FileMode { return info.mode.Type() } +func (info *treeFSInfo) IsDir() bool { return info.isDir } +func (info *treeFSInfo) ModTime() time.Time { return time.Time{} } +func (info *treeFSInfo) Sys() any { return info.sys } +func (info *treeFSInfo) Info() (fs.FileInfo, error) { + return info, nil +} + +func treeFSEntryMode(mode tree.FileMode) fs.FileMode { + switch mode { + case tree.FileModeDir: + return fs.ModeDir | 0o555 + case tree.FileModeRegular: + return 0o444 + case tree.FileModeExecutable: + return 0o555 + case tree.FileModeSymlink: + return fs.ModeSymlink | 0o444 + case tree.FileModeGitlink: + return fs.ModeIrregular + default: + return fs.ModeIrregular + } +} + +func (treeFS *TreeFS) statEntry(entry treeEntryValue) (*treeFSInfo, error) { + size := int64(0) + + if entry.mode == tree.FileModeRegular || entry.mode == tree.FileModeExecutable || entry.mode == tree.FileModeSymlink { + var err error + + size, err = entry.blobSize(treeFS.resolver) + if err != nil { + return nil, err + } + } + + var sys any + if entry.treeEntry != nil { + sys = *entry.treeEntry + } + + return &treeFSInfo{ + name: entry.name, + mode: treeFSEntryMode(entry.mode), + size: size, + sys: sys, + isDir: entry.isDir(), + }, nil +} diff --git a/object/fetch/treefs_new.go b/object/fetch/treefs_new.go new file mode 100644 index 00000000..d2224fcf --- /dev/null +++ b/object/fetch/treefs_new.go @@ -0,0 +1,17 @@ +package fetch + +import objectid "codeberg.org/lindenii/furgit/object/id" + +// TreeFS returns a new filesystem view rooted at root, which may be any +// tree-ish object accepted by PeelToTreeID. +func (r *Fetcher) TreeFS(root objectid.ObjectID) (*TreeFS, error) { + rootTree, err := r.PeelToTreeID(root) + if err != nil { + return nil, err + } + + return &TreeFS{ + resolver: r, + rootTree: rootTree, + }, nil +} diff --git a/object/fetch/treefs_op.go b/object/fetch/treefs_op.go new file mode 100644 index 00000000..f0472923 --- /dev/null +++ b/object/fetch/treefs_op.go @@ -0,0 +1,28 @@ +package fetch + +type treeFSOp uint8 + +const ( + treeFSOpOpen treeFSOp = iota + treeFSOpReadFile + treeFSOpReadDir + treeFSOpStat + treeFSOpSub +) + +func (op treeFSOp) pathErrorOp() string { + switch op { + case treeFSOpOpen: + return "open" + case treeFSOpReadFile: + return "readfile" + case treeFSOpReadDir: + return "readdir" + case treeFSOpStat: + return "stat" + case treeFSOpSub: + return "sub" + default: + return "treefs" + } +} diff --git a/object/fetch/treefs_open.go b/object/fetch/treefs_open.go new file mode 100644 index 00000000..59c6ec5d --- /dev/null +++ b/object/fetch/treefs_open.go @@ -0,0 +1,122 @@ +package fetch + +import ( + "fmt" + "io" + "io/fs" + + "codeberg.org/lindenii/furgit/object/tree" +) + +// Open opens name for reading. +// +// Directories are returned as fs.ReadDirFile values. Gitlink entries are not +// readable through TreeFS. +func (treeFS *TreeFS) Open(name string) (fs.File, error) { + entry, err := treeFS.resolvePath(treeFSOpOpen, name) + if err != nil { + return nil, err + } + + info, err := treeFS.statEntry(entry) + if err != nil { + return nil, treeFSPathError(treeFSOpOpen, name, err) + } + + if entry.isDir() { + treeID, err := entry.subtreeID() + if err != nil { + return nil, treeFSPathError(treeFSOpOpen, name, err) + } + + tree, err := treeFS.resolver.ExactTree(treeID) + if err != nil { + return nil, treeFSPathError(treeFSOpOpen, name, err) + } + + entries := make([]fs.DirEntry, 0, len(tree.Object().Entries)) + for _, child := range tree.Object().Entries { + childEntry := treeEntryValue{ + name: string(child.Name), + mode: child.Mode, + objectID: child.ID, + treeEntry: &child, + } + + childInfo, err := treeFS.statEntry(childEntry) + if err != nil { + return nil, treeFSPathError(treeFSOpOpen, name, err) + } + + entries = append(entries, childInfo) + } + + return &treeFSDir{ + info: info, + entries: entries, + }, nil + } + + if entry.mode == tree.FileModeGitlink { + return nil, treeFSPathError(treeFSOpOpen, name, fmt.Errorf("object/fetch: gitlink entries are not readable as files")) + } + + reader, _, err := treeFS.resolver.ExactBlobReader(entry.objectID) + if err != nil { + return nil, treeFSPathError(treeFSOpOpen, name, err) + } + + return &treeFSBlob{ + info: info, + reader: reader, + }, nil +} + +type treeFSBlob struct { + info *treeFSInfo + reader io.ReadCloser +} + +var _ fs.File = (*treeFSBlob)(nil) + +func (file *treeFSBlob) Stat() (fs.FileInfo, error) { return file.info, nil } +func (file *treeFSBlob) Read(p []byte) (int, error) { return file.reader.Read(p) } +func (file *treeFSBlob) Close() error { return file.reader.Close() } + +type treeFSDir struct { + info *treeFSInfo + entries []fs.DirEntry + offset int +} + +var ( + _ fs.File = (*treeFSDir)(nil) + _ fs.ReadDirFile = (*treeFSDir)(nil) +) + +func (dir *treeFSDir) Stat() (fs.FileInfo, error) { return dir.info, nil } +func (dir *treeFSDir) Close() error { return nil } + +func (dir *treeFSDir) Read(_ []byte) (int, error) { + return 0, fs.ErrInvalid +} + +func (dir *treeFSDir) ReadDir(n int) ([]fs.DirEntry, error) { + if dir.offset >= len(dir.entries) && n > 0 { + return nil, io.EOF + } + + if n <= 0 { + out := append([]fs.DirEntry(nil), dir.entries[dir.offset:]...) + dir.offset = len(dir.entries) + + return out, nil + } + + end := min(dir.offset+n, len(dir.entries)) + + out := append([]fs.DirEntry(nil), dir.entries[dir.offset:end]...) + dir.offset = end + + return out, nil +} diff --git a/object/fetch/treefs_path.go b/object/fetch/treefs_path.go new file mode 100644 index 00000000..7aa31c7f --- /dev/null +++ b/object/fetch/treefs_path.go @@ -0,0 +1,29 @@ +package fetch + +import ( + "io/fs" + "strings" +) + +func treeFSValidPath(name string) bool { + return name == "." || fs.ValidPath(name) +} + +func treeFSSplitPath(name string) [][]byte { + if name == "." { + return nil + } + + parts := strings.Split(name, "/") + + out := make([][]byte, len(parts)) + for i, part := range parts { + out[i] = []byte(part) + } + + return out +} + +func treeFSPathError(op treeFSOp, path string, err error) error { + return &fs.PathError{Op: op.pathErrorOp(), Path: path, Err: err} +} diff --git a/object/fetch/treefs_readdir.go b/object/fetch/treefs_readdir.go new file mode 100644 index 00000000..7518c607 --- /dev/null +++ b/object/fetch/treefs_readdir.go @@ -0,0 +1,20 @@ +package fetch + +import "io/fs" + +// ReadDir reads and returns all directory entries for name. +func (treeFS *TreeFS) ReadDir(name string) ([]fs.DirEntry, error) { + file, err := treeFS.Open(name) + if err != nil { + return nil, err + } + + defer func() { _ = file.Close() }() + + readDirFile, ok := file.(fs.ReadDirFile) + if !ok { + return nil, treeFSPathError(treeFSOpReadDir, name, fs.ErrInvalid) + } + + return readDirFile.ReadDir(-1) +} diff --git a/object/fetch/treefs_readfile.go b/object/fetch/treefs_readfile.go new file mode 100644 index 00000000..d3671f07 --- /dev/null +++ b/object/fetch/treefs_readfile.go @@ -0,0 +1,40 @@ +package fetch + +import ( + "fmt" + "io" + + "codeberg.org/lindenii/furgit/object/tree" +) + +// ReadFile reads the blob contents at name. +// +// Directories and gitlink entries are not readable through TreeFS. +func (treeFS *TreeFS) ReadFile(name string) ([]byte, error) { + entry, err := treeFS.resolvePath(treeFSOpReadFile, name) + if err != nil { + return nil, err + } + + if entry.isDir() { + return nil, treeFSPathError(treeFSOpReadFile, name, fmt.Errorf("is a directory")) + } + + if entry.mode == tree.FileModeGitlink { + return nil, treeFSPathError(treeFSOpReadFile, name, fmt.Errorf("object/fetch: gitlink entries are not readable as files")) + } + + reader, _, err := treeFS.resolver.ExactBlobReader(entry.objectID) + if err != nil { + return nil, treeFSPathError(treeFSOpReadFile, name, err) + } + + defer func() { _ = reader.Close() }() + + data, err := io.ReadAll(reader) + if err != nil { + return nil, treeFSPathError(treeFSOpReadFile, name, err) + } + + return data, nil +} diff --git a/object/fetch/treefs_stat.go b/object/fetch/treefs_stat.go new file mode 100644 index 00000000..7d7a6418 --- /dev/null +++ b/object/fetch/treefs_stat.go @@ -0,0 +1,22 @@ +package fetch + +import "io/fs" + +// Stat returns synthetic file metadata for name. +// +// TreeFS metadata reflects Git tree entry mode and blob size where applicable. +// It does not represent filesystem stat metadata: ModTime is zero, ownership is +// unavailable, and Sys returns the underlying tree.TreeEntry when one exists. +func (treeFS *TreeFS) Stat(name string) (fs.FileInfo, error) { + entry, err := treeFS.resolvePath(treeFSOpStat, name) + if err != nil { + return nil, err + } + + info, err := treeFS.statEntry(entry) + if err != nil { + return nil, treeFSPathError(treeFSOpStat, name, err) + } + + return info, nil +} diff --git a/object/fetch/treefs_sub.go b/object/fetch/treefs_sub.go new file mode 100644 index 00000000..faf47092 --- /dev/null +++ b/object/fetch/treefs_sub.go @@ -0,0 +1,22 @@ +package fetch + +import "io/fs" + +// Sub returns a new TreeFS rooted at dir. +func (treeFS *TreeFS) Sub(dir string) (fs.FS, error) { + entry, err := treeFS.resolvePath(treeFSOpSub, dir) + if err != nil { + return nil, err + } + + treeID, err := entry.subtreeID() + if err != nil { + return nil, treeFSPathError(treeFSOpSub, dir, fs.ErrInvalid) + } + + return &TreeFS{ + resolver: treeFS.resolver, + rootTree: treeID, + rootEntry: entry.treeEntry, + }, nil +} diff --git a/object/fetch/treefs_test.go b/object/fetch/treefs_test.go new file mode 100644 index 00000000..067283f7 --- /dev/null +++ b/object/fetch/treefs_test.go @@ -0,0 +1,111 @@ +package fetch_test + +import ( + "errors" + "io/fs" + "testing" + + "codeberg.org/lindenii/furgit/internal/testgit" + objectid "codeberg.org/lindenii/furgit/object/id" + "codeberg.org/lindenii/furgit/object/fetch" + "codeberg.org/lindenii/furgit/object/tree" + "codeberg.org/lindenii/furgit/repository" +) + +func TestTreeFS(t *testing.T) { + t.Parallel() + + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper + t.Parallel() + + repoData := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo}) + repoData.WriteFile(t, "plain.txt", []byte("plain\n"), 0o644) + repoData.WriteFileAll(t, "dir/exec.sh", []byte("#!/bin/sh\nexit 0\n"), 0o755, 0o755) + repoData.SymbolicRef(t, "HEAD", "refs/heads/main") + _ = repoData.Run(t, "add", ".") + treeHex := repoData.Run(t, "write-tree") + + treeID, err := objectid.ParseHex(algo, treeHex) + if err != nil { + t.Fatalf("ParseHex(write-tree): %v", err) + } + + commitID := repoData.CommitTree(t, treeID, "treefs") + + root := repoData.OpenGitRoot(t) + + repo, err := repository.Open(root) + if err != nil { + t.Fatalf("repository.Open: %v", err) + } + + defer func() { _ = repo.Close() }() + + resolver := fetch.New(repo.Objects()) + + treeFS, err := resolver.TreeFS(commitID) + if err != nil { + t.Fatalf("resolver.TreeFS: %v", err) + } + + content, err := treeFS.ReadFile("plain.txt") + if err != nil { + t.Fatalf("ReadFile(plain.txt): %v", err) + } + + if string(content) != "plain\n" { + t.Fatalf("ReadFile(plain.txt) = %q, want %q", string(content), "plain\n") + } + + entries, err := treeFS.ReadDir(".") + if err != nil { + t.Fatalf("ReadDir(.): %v", err) + } + + if len(entries) != 2 { + t.Fatalf("len(ReadDir(.)) = %d, want 2", len(entries)) + } + + info, err := treeFS.Stat("plain.txt") + if err != nil { + t.Fatalf("Stat(plain.txt): %v", err) + } + + entry, ok := info.Sys().(tree.TreeEntry) + if !ok { + t.Fatalf("Stat(plain.txt).Sys() type = %T, want tree.TreeEntry", info.Sys()) + } + + if entry.Mode != tree.FileModeRegular { + t.Fatalf("Stat(plain.txt).Sys().Mode = %o, want %o", entry.Mode, tree.FileModeRegular) + } + + subFS, err := treeFS.Sub("dir") + if err != nil { + t.Fatalf("Sub(dir): %v", err) + } + + subReadFileFS, ok := subFS.(fs.ReadFileFS) + if !ok { + t.Fatalf("Sub(dir) type does not implement fs.ReadFileFS") + } + + subContent, err := subReadFileFS.ReadFile("exec.sh") + if err != nil { + t.Fatalf("Sub(dir).ReadFile(exec.sh): %v", err) + } + + if string(subContent) != "#!/bin/sh\nexit 0\n" { + t.Fatalf("Sub(dir).ReadFile(exec.sh) = %q", string(subContent)) + } + + _, err = treeFS.ReadFile("dir") + if err == nil { + t.Fatal("ReadFile(dir) unexpectedly succeeded") + } + + if _, ok := errors.AsType[*fs.PathError](err); !ok { + t.Fatalf("ReadFile(dir) err type = %T, want *fs.PathError", err) + } + }) +} diff --git a/object/resolve/doc.go b/object/resolve/doc.go deleted file mode 100644 index c4197087..00000000 --- a/object/resolve/doc.go +++ /dev/null @@ -1,5 +0,0 @@ -// 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 deleted file mode 100644 index 2cd8b298..00000000 --- a/object/resolve/exact_blob.go +++ /dev/null @@ -1,24 +0,0 @@ -package resolve - -import ( - "fmt" - - "codeberg.org/lindenii/furgit/object/blob" - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/stored" -) - -// ExactBlob reads, parses, and wraps the blob at id. -func (r *Resolver) ExactBlob(id objectid.ObjectID) (*stored.Stored[*blob.Blob], error) { - parsed, err := r.parseObject(id) - if err != nil { - return nil, err - } - - blob, ok := parsed.(*blob.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 deleted file mode 100644 index 5a702888..00000000 --- a/object/resolve/exact_blob_reader.go +++ /dev/null @@ -1,14 +0,0 @@ -package resolve - -import ( - "io" - - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// 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 deleted file mode 100644 index e6b379aa..00000000 --- a/object/resolve/exact_commit.go +++ /dev/null @@ -1,24 +0,0 @@ -package resolve - -import ( - "fmt" - - "codeberg.org/lindenii/furgit/object/commit" - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/stored" -) - -// ExactCommit reads, parses, and wraps the commit at id. -func (r *Resolver) ExactCommit(id objectid.ObjectID) (*stored.Stored[*commit.Commit], error) { - parsed, err := r.parseObject(id) - if err != nil { - return nil, err - } - - commit, ok := parsed.(*commit.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 deleted file mode 100644 index 6c05b016..00000000 --- a/object/resolve/exact_commit_reader.go +++ /dev/null @@ -1,16 +0,0 @@ -package resolve - -import ( - "io" - - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// 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 deleted file mode 100644 index db6165f4..00000000 --- a/object/resolve/exact_object.go +++ /dev/null @@ -1,18 +0,0 @@ -package resolve - -import ( - "codeberg.org/lindenii/furgit/object" - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/stored" -) - -// 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 deleted file mode 100644 index cf181038..00000000 --- a/object/resolve/exact_reader.go +++ /dev/null @@ -1,26 +0,0 @@ -package resolve - -import ( - "fmt" - "io" - - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// 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 deleted file mode 100644 index 8c5d22c9..00000000 --- a/object/resolve/exact_tag.go +++ /dev/null @@ -1,24 +0,0 @@ -package resolve - -import ( - "fmt" - - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/stored" - "codeberg.org/lindenii/furgit/object/tag" -) - -// ExactTag reads, parses, and wraps the tag at id. -func (r *Resolver) ExactTag(id objectid.ObjectID) (*stored.Stored[*tag.Tag], error) { - parsed, err := r.parseObject(id) - if err != nil { - return nil, err - } - - tag, ok := parsed.(*tag.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 deleted file mode 100644 index e69441d5..00000000 --- a/object/resolve/exact_tag_reader.go +++ /dev/null @@ -1,16 +0,0 @@ -package resolve - -import ( - "io" - - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// 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 deleted file mode 100644 index de58ddb1..00000000 --- a/object/resolve/exact_tree.go +++ /dev/null @@ -1,24 +0,0 @@ -package resolve - -import ( - "fmt" - - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/stored" - "codeberg.org/lindenii/furgit/object/tree" -) - -// ExactTree reads, parses, and wraps the tree at id. -func (r *Resolver) ExactTree(id objectid.ObjectID) (*stored.Stored[*tree.Tree], error) { - parsed, err := r.parseObject(id) - if err != nil { - return nil, err - } - - tree, ok := parsed.(*tree.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 deleted file mode 100644 index 8d9ec821..00000000 --- a/object/resolve/exact_tree_reader.go +++ /dev/null @@ -1,16 +0,0 @@ -package resolve - -import ( - "io" - - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// 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 deleted file mode 100644 index 8e8d8bde..00000000 --- a/object/resolve/object_parse.go +++ /dev/null @@ -1,28 +0,0 @@ -package resolve - -import ( - "fmt" - - "codeberg.org/lindenii/furgit/object" - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -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 deleted file mode 100644 index d11f3b48..00000000 --- a/object/resolve/path.go +++ /dev/null @@ -1,103 +0,0 @@ -package resolve - -import ( - "fmt" - - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/tree" -) - -// PathEmptyError indicates that Path received no segments. -type PathEmptyError struct{} - -func (err *PathEmptyError) Error() string { - return "object/resolve: empty tree path" -} - -// PathSegmentEmptyError indicates that one path segment is empty. -type PathSegmentEmptyError struct { - Index int -} - -func (err *PathSegmentEmptyError) Error() string { - return fmt.Sprintf("object/resolve: empty tree path segment at index %d", err.Index) -} - -// PathNotFoundError indicates that one tree path segment was not found. -type PathNotFoundError struct { - Index int - Name []byte -} - -func (err *PathNotFoundError) Error() string { - return fmt.Sprintf("object/resolve: tree entry %q not found at index %d", err.Name, err.Index) -} - -// PathNotTreeError indicates that one intermediate path segment was not a tree. -type PathNotTreeError struct { - Index int - Name []byte -} - -func (err *PathNotTreeError) Error() string { - return fmt.Sprintf("object/resolve: path segment %q at index %d is not a tree", err.Name, err.Index) -} - -// 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. Path segments may not contain \x00. -// -// The path cannot be accurately represented as a string or a single []byte -// because Git tree entry names may include slashes. While []string is -// technically possible (since Go strings are not necessarily UTF-8), they -// do often imply UTF-8 in practice, which would be undesirable. -// -// If your entry names are valid UTF-8 and uses / solely as segment separators, -// it may be convenient to use TreeFS for an io/fs.FS-like interface. -func (r *Resolver) Path(root objectid.ObjectID, parts [][]byte) (tree.TreeEntry, error) { - if len(parts) == 0 { - return tree.TreeEntry{}, &PathEmptyError{} - } - - current, err := r.PeelToTree(root) - if err != nil { - return tree.TreeEntry{}, err - } - - for i, part := range parts { - if len(part) == 0 { - return tree.TreeEntry{}, &PathSegmentEmptyError{Index: i} - } - - entry := current.Object().Entry(part) - if entry == nil { - return tree.TreeEntry{}, &PathNotFoundError{ - Index: i, - Name: append([]byte(nil), part...), - } - } - - if i == len(parts)-1 { - return *entry, nil - } - - if entry.Mode != tree.FileModeDir { - return tree.TreeEntry{}, &PathNotTreeError{ - Index: i, - Name: append([]byte(nil), part...), - } - } - - current, err = r.ExactTree(entry.ID) - if err != nil { - return tree.TreeEntry{}, err - } - } - - return tree.TreeEntry{}, &PathNotFoundError{Index: len(parts) - 1} -} diff --git a/object/resolve/peel_to_blob.go b/object/resolve/peel_to_blob.go deleted file mode 100644 index c8aec1ad..00000000 --- a/object/resolve/peel_to_blob.go +++ /dev/null @@ -1,29 +0,0 @@ -package resolve - -import ( - "fmt" - - "codeberg.org/lindenii/furgit/object/blob" - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/stored" - "codeberg.org/lindenii/furgit/object/tag" -) - -// PeelToBlob peels tags until it reaches a blob. -func (r *Resolver) PeelToBlob(id objectid.ObjectID) (*stored.Stored[*blob.Blob], error) { - for { - obj, err := r.ExactObject(id) - if err != nil { - return nil, err - } - - switch parsed := obj.Object().(type) { - case *blob.Blob: - return stored.New(id, parsed), nil - case *tag.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 deleted file mode 100644 index c3467cf2..00000000 --- a/object/resolve/peel_to_blob_id.go +++ /dev/null @@ -1,39 +0,0 @@ -package resolve - -import ( - "fmt" - - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// 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 - case objecttype.TypeInvalid, - objecttype.TypeCommit, - objecttype.TypeTree, - objecttype.TypeFuture, - objecttype.TypeOfsDelta, - objecttype.TypeRefDelta: - return objectid.ObjectID{}, fmt.Errorf("object/resolve: expected blob-ish object %s, got %v", id, ty) - 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 deleted file mode 100644 index d3bc7f49..00000000 --- a/object/resolve/peel_to_blob_reader.go +++ /dev/null @@ -1,18 +0,0 @@ -package resolve - -import ( - "io" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// 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 deleted file mode 100644 index 0272dd83..00000000 --- a/object/resolve/peel_to_commit.go +++ /dev/null @@ -1,29 +0,0 @@ -package resolve - -import ( - "fmt" - - "codeberg.org/lindenii/furgit/object/commit" - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/stored" - "codeberg.org/lindenii/furgit/object/tag" -) - -// PeelToCommit peels tags until it reaches a commit. -func (r *Resolver) PeelToCommit(id objectid.ObjectID) (*stored.Stored[*commit.Commit], error) { - for { - obj, err := r.ExactObject(id) - if err != nil { - return nil, err - } - - switch parsed := obj.Object().(type) { - case *commit.Commit: - return stored.New(id, parsed), nil - case *tag.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 deleted file mode 100644 index 972823ce..00000000 --- a/object/resolve/peel_to_commit_id.go +++ /dev/null @@ -1,39 +0,0 @@ -package resolve - -import ( - "fmt" - - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// 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 - case objecttype.TypeInvalid, - objecttype.TypeTree, - objecttype.TypeBlob, - objecttype.TypeFuture, - objecttype.TypeOfsDelta, - objecttype.TypeRefDelta: - return objectid.ObjectID{}, fmt.Errorf("object/resolve: expected commit-ish object %s, got %v", id, ty) - 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 deleted file mode 100644 index 6972eff3..00000000 --- a/object/resolve/peel_to_commit_reader.go +++ /dev/null @@ -1,20 +0,0 @@ -package resolve - -import ( - "io" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// 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 deleted file mode 100644 index e131f4c1..00000000 --- a/object/resolve/peel_to_tag.go +++ /dev/null @@ -1,12 +0,0 @@ -package resolve - -import ( - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/stored" - "codeberg.org/lindenii/furgit/object/tag" -) - -// PeelToTag returns the tag at id without further peeling. -func (r *Resolver) PeelToTag(id objectid.ObjectID) (*stored.Stored[*tag.Tag], error) { - return r.ExactTag(id) -} diff --git a/object/resolve/peel_to_tag_id.go b/object/resolve/peel_to_tag_id.go deleted file mode 100644 index 275ef561..00000000 --- a/object/resolve/peel_to_tag_id.go +++ /dev/null @@ -1,8 +0,0 @@ -package resolve - -import objectid "codeberg.org/lindenii/furgit/object/id" - -// 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 deleted file mode 100644 index 77279511..00000000 --- a/object/resolve/peel_to_tag_reader.go +++ /dev/null @@ -1,20 +0,0 @@ -package resolve - -import ( - "io" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// 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 deleted file mode 100644 index 2f2da4d7..00000000 --- a/object/resolve/peel_to_tree.go +++ /dev/null @@ -1,33 +0,0 @@ -package resolve - -import ( - "fmt" - - "codeberg.org/lindenii/furgit/object/commit" - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/stored" - "codeberg.org/lindenii/furgit/object/tag" - "codeberg.org/lindenii/furgit/object/tree" -) - -// 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[*tree.Tree], error) { - for { - obj, err := r.ExactObject(id) - if err != nil { - return nil, err - } - - switch parsed := obj.Object().(type) { - case *tree.Tree: - return stored.New(id, parsed), nil - case *commit.Commit: - return r.ExactTree(parsed.Tree) - case *tag.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 deleted file mode 100644 index bacee65e..00000000 --- a/object/resolve/peel_to_tree_id.go +++ /dev/null @@ -1,46 +0,0 @@ -package resolve - -import ( - "fmt" - - objectid "codeberg.org/lindenii/furgit/object/id" - objecttype "codeberg.org/lindenii/furgit/object/type" -) - -// 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 - case objecttype.TypeInvalid, - objecttype.TypeBlob, - objecttype.TypeFuture, - objecttype.TypeOfsDelta, - objecttype.TypeRefDelta: - return objectid.ObjectID{}, fmt.Errorf("object/resolve: expected tree-ish object %s, got %v", id, ty) - 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 deleted file mode 100644 index 515224ed..00000000 --- a/object/resolve/peel_to_tree_reader.go +++ /dev/null @@ -1,20 +0,0 @@ -package resolve - -import ( - "io" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// 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 deleted file mode 100644 index f5e4e8c3..00000000 --- a/object/resolve/resolver.go +++ /dev/null @@ -1,17 +0,0 @@ -package resolve - -import objectstorer "codeberg.org/lindenii/furgit/object/storer" - -// 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 objectstorer.Store -} - -// New returns a Resolver that reads objects from store. -// -// The returned Resolver does not take ownership of store. -func New(store objectstorer.Store) *Resolver { - return &Resolver{store: store} -} diff --git a/object/resolve/treefs.go b/object/resolve/treefs.go deleted file mode 100644 index a080d56d..00000000 --- a/object/resolve/treefs.go +++ /dev/null @@ -1,30 +0,0 @@ -package resolve - -import ( - "io/fs" - - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/tree" -) - -// TreeFS exposes one Git tree as an fs.FS. -// -// TreeFS interprets names using io/fs path rules. Those rules do not match raw -// Git tree entry naming exactly: names are UTF-8, slash-separated, and must be -// valid fs.FS paths. Tree entries that cannot be represented under those rules -// are not addressable through this API. -// -// TreeFS does not take ownership of its Resolver. -type TreeFS struct { - resolver *Resolver - rootTree objectid.ObjectID - rootEntry *tree.TreeEntry -} - -var ( - _ fs.FS = (*TreeFS)(nil) - _ fs.ReadFileFS = (*TreeFS)(nil) - _ fs.ReadDirFS = (*TreeFS)(nil) - _ fs.StatFS = (*TreeFS)(nil) - _ fs.SubFS = (*TreeFS)(nil) -) diff --git a/object/resolve/treefs_entry.go b/object/resolve/treefs_entry.go deleted file mode 100644 index 6d23e282..00000000 --- a/object/resolve/treefs_entry.go +++ /dev/null @@ -1,90 +0,0 @@ -package resolve - -import ( - "errors" - "fmt" - "io/fs" - - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/tree" -) - -func (treeFS *TreeFS) resolvePath(op treeFSOp, name string) (treeEntryValue, error) { - if !treeFSValidPath(name) { - return treeEntryValue{}, treeFSPathError(op, name, fs.ErrInvalid) - } - - if name == "." { - return treeEntryValue{ - name: ".", - mode: tree.FileModeDir, - treeID: treeFS.rootTree, - treeEntry: treeFS.rootEntry, - }, nil - } - - entry, err := treeFS.resolver.Path(treeFS.rootTree, treeFSSplitPath(name)) - if err != nil { - return treeEntryValue{}, treeFS.pathResolveError(op, name, err) - } - - return treeEntryValue{ - name: string(entry.Name), - mode: entry.Mode, - objectID: entry.ID, - treeEntry: &entry, - }, nil -} - -func (treeFS *TreeFS) pathResolveError(op treeFSOp, name string, err error) error { - if _, ok := errors.AsType[*PathNotFoundError](err); ok { - return treeFSPathError(op, name, fs.ErrNotExist) - } - - if _, ok := errors.AsType[*PathNotTreeError](err); ok { - return treeFSPathError(op, name, fs.ErrInvalid) - } - - if _, ok := errors.AsType[*PathEmptyError](err); ok { - return treeFSPathError(op, name, fs.ErrInvalid) - } - - if _, ok := errors.AsType[*PathSegmentEmptyError](err); ok { - return treeFSPathError(op, name, fs.ErrInvalid) - } - - return treeFSPathError(op, name, err) -} - -type treeEntryValue struct { - name string - mode tree.FileMode - objectID objectid.ObjectID - treeID objectid.ObjectID - treeEntry *tree.TreeEntry -} - -func (entry treeEntryValue) isDir() bool { - return entry.mode == tree.FileModeDir -} - -func (entry treeEntryValue) blobSize(resolve *Resolver) (int64, error) { - _, size, err := resolve.store.ReadHeader(entry.objectID) - if err != nil { - return 0, err - } - - return size, nil -} - -func (entry treeEntryValue) subtreeID() (objectid.ObjectID, error) { - if entry.name == "." { - return entry.treeID, nil - } - - if entry.mode != tree.FileModeDir { - return objectid.ObjectID{}, fmt.Errorf("object/resolve: path %q is not a tree", entry.name) - } - - return entry.objectID, nil -} diff --git a/object/resolve/treefs_info.go b/object/resolve/treefs_info.go deleted file mode 100644 index f8eb1e9e..00000000 --- a/object/resolve/treefs_info.go +++ /dev/null @@ -1,75 +0,0 @@ -package resolve - -import ( - "io/fs" - "time" - - "codeberg.org/lindenii/furgit/object/tree" -) - -type treeFSInfo struct { - name string - mode fs.FileMode - size int64 - sys any - isDir bool -} - -var ( - _ fs.FileInfo = (*treeFSInfo)(nil) - _ fs.DirEntry = (*treeFSInfo)(nil) -) - -func (info *treeFSInfo) Name() string { return info.name } -func (info *treeFSInfo) Size() int64 { return info.size } -func (info *treeFSInfo) Mode() fs.FileMode { return info.mode } -func (info *treeFSInfo) Type() fs.FileMode { return info.mode.Type() } -func (info *treeFSInfo) IsDir() bool { return info.isDir } -func (info *treeFSInfo) ModTime() time.Time { return time.Time{} } -func (info *treeFSInfo) Sys() any { return info.sys } -func (info *treeFSInfo) Info() (fs.FileInfo, error) { - return info, nil -} - -func treeFSEntryMode(mode tree.FileMode) fs.FileMode { - switch mode { - case tree.FileModeDir: - return fs.ModeDir | 0o555 - case tree.FileModeRegular: - return 0o444 - case tree.FileModeExecutable: - return 0o555 - case tree.FileModeSymlink: - return fs.ModeSymlink | 0o444 - case tree.FileModeGitlink: - return fs.ModeIrregular - default: - return fs.ModeIrregular - } -} - -func (treeFS *TreeFS) statEntry(entry treeEntryValue) (*treeFSInfo, error) { - size := int64(0) - - if entry.mode == tree.FileModeRegular || entry.mode == tree.FileModeExecutable || entry.mode == tree.FileModeSymlink { - var err error - - size, err = entry.blobSize(treeFS.resolver) - if err != nil { - return nil, err - } - } - - var sys any - if entry.treeEntry != nil { - sys = *entry.treeEntry - } - - return &treeFSInfo{ - name: entry.name, - mode: treeFSEntryMode(entry.mode), - size: size, - sys: sys, - isDir: entry.isDir(), - }, nil -} diff --git a/object/resolve/treefs_new.go b/object/resolve/treefs_new.go deleted file mode 100644 index 9f5fe77d..00000000 --- a/object/resolve/treefs_new.go +++ /dev/null @@ -1,17 +0,0 @@ -package resolve - -import objectid "codeberg.org/lindenii/furgit/object/id" - -// TreeFS returns a new filesystem view rooted at root, which may be any -// tree-ish object accepted by PeelToTreeID. -func (r *Resolver) TreeFS(root objectid.ObjectID) (*TreeFS, error) { - rootTree, err := r.PeelToTreeID(root) - if err != nil { - return nil, err - } - - return &TreeFS{ - resolver: r, - rootTree: rootTree, - }, nil -} diff --git a/object/resolve/treefs_op.go b/object/resolve/treefs_op.go deleted file mode 100644 index ed93ec85..00000000 --- a/object/resolve/treefs_op.go +++ /dev/null @@ -1,28 +0,0 @@ -package resolve - -type treeFSOp uint8 - -const ( - treeFSOpOpen treeFSOp = iota - treeFSOpReadFile - treeFSOpReadDir - treeFSOpStat - treeFSOpSub -) - -func (op treeFSOp) pathErrorOp() string { - switch op { - case treeFSOpOpen: - return "open" - case treeFSOpReadFile: - return "readfile" - case treeFSOpReadDir: - return "readdir" - case treeFSOpStat: - return "stat" - case treeFSOpSub: - return "sub" - default: - return "treefs" - } -} diff --git a/object/resolve/treefs_open.go b/object/resolve/treefs_open.go deleted file mode 100644 index 8e2b3588..00000000 --- a/object/resolve/treefs_open.go +++ /dev/null @@ -1,122 +0,0 @@ -package resolve - -import ( - "fmt" - "io" - "io/fs" - - "codeberg.org/lindenii/furgit/object/tree" -) - -// Open opens name for reading. -// -// Directories are returned as fs.ReadDirFile values. Gitlink entries are not -// readable through TreeFS. -func (treeFS *TreeFS) Open(name string) (fs.File, error) { - entry, err := treeFS.resolvePath(treeFSOpOpen, name) - if err != nil { - return nil, err - } - - info, err := treeFS.statEntry(entry) - if err != nil { - return nil, treeFSPathError(treeFSOpOpen, name, err) - } - - if entry.isDir() { - treeID, err := entry.subtreeID() - if err != nil { - return nil, treeFSPathError(treeFSOpOpen, name, err) - } - - tree, err := treeFS.resolver.ExactTree(treeID) - if err != nil { - return nil, treeFSPathError(treeFSOpOpen, name, err) - } - - entries := make([]fs.DirEntry, 0, len(tree.Object().Entries)) - for _, child := range tree.Object().Entries { - childEntry := treeEntryValue{ - name: string(child.Name), - mode: child.Mode, - objectID: child.ID, - treeEntry: &child, - } - - childInfo, err := treeFS.statEntry(childEntry) - if err != nil { - return nil, treeFSPathError(treeFSOpOpen, name, err) - } - - entries = append(entries, childInfo) - } - - return &treeFSDir{ - info: info, - entries: entries, - }, nil - } - - if entry.mode == tree.FileModeGitlink { - return nil, treeFSPathError(treeFSOpOpen, name, fmt.Errorf("object/resolve: gitlink entries are not readable as files")) - } - - reader, _, err := treeFS.resolver.ExactBlobReader(entry.objectID) - if err != nil { - return nil, treeFSPathError(treeFSOpOpen, name, err) - } - - return &treeFSBlob{ - info: info, - reader: reader, - }, nil -} - -type treeFSBlob struct { - info *treeFSInfo - reader io.ReadCloser -} - -var _ fs.File = (*treeFSBlob)(nil) - -func (file *treeFSBlob) Stat() (fs.FileInfo, error) { return file.info, nil } -func (file *treeFSBlob) Read(p []byte) (int, error) { return file.reader.Read(p) } -func (file *treeFSBlob) Close() error { return file.reader.Close() } - -type treeFSDir struct { - info *treeFSInfo - entries []fs.DirEntry - offset int -} - -var ( - _ fs.File = (*treeFSDir)(nil) - _ fs.ReadDirFile = (*treeFSDir)(nil) -) - -func (dir *treeFSDir) Stat() (fs.FileInfo, error) { return dir.info, nil } -func (dir *treeFSDir) Close() error { return nil } - -func (dir *treeFSDir) Read(_ []byte) (int, error) { - return 0, fs.ErrInvalid -} - -func (dir *treeFSDir) ReadDir(n int) ([]fs.DirEntry, error) { - if dir.offset >= len(dir.entries) && n > 0 { - return nil, io.EOF - } - - if n <= 0 { - out := append([]fs.DirEntry(nil), dir.entries[dir.offset:]...) - dir.offset = len(dir.entries) - - return out, nil - } - - end := min(dir.offset+n, len(dir.entries)) - - out := append([]fs.DirEntry(nil), dir.entries[dir.offset:end]...) - dir.offset = end - - return out, nil -} diff --git a/object/resolve/treefs_path.go b/object/resolve/treefs_path.go deleted file mode 100644 index c35791cb..00000000 --- a/object/resolve/treefs_path.go +++ /dev/null @@ -1,29 +0,0 @@ -package resolve - -import ( - "io/fs" - "strings" -) - -func treeFSValidPath(name string) bool { - return name == "." || fs.ValidPath(name) -} - -func treeFSSplitPath(name string) [][]byte { - if name == "." { - return nil - } - - parts := strings.Split(name, "/") - - out := make([][]byte, len(parts)) - for i, part := range parts { - out[i] = []byte(part) - } - - return out -} - -func treeFSPathError(op treeFSOp, path string, err error) error { - return &fs.PathError{Op: op.pathErrorOp(), Path: path, Err: err} -} diff --git a/object/resolve/treefs_readdir.go b/object/resolve/treefs_readdir.go deleted file mode 100644 index 5516de33..00000000 --- a/object/resolve/treefs_readdir.go +++ /dev/null @@ -1,20 +0,0 @@ -package resolve - -import "io/fs" - -// ReadDir reads and returns all directory entries for name. -func (treeFS *TreeFS) ReadDir(name string) ([]fs.DirEntry, error) { - file, err := treeFS.Open(name) - if err != nil { - return nil, err - } - - defer func() { _ = file.Close() }() - - readDirFile, ok := file.(fs.ReadDirFile) - if !ok { - return nil, treeFSPathError(treeFSOpReadDir, name, fs.ErrInvalid) - } - - return readDirFile.ReadDir(-1) -} diff --git a/object/resolve/treefs_readfile.go b/object/resolve/treefs_readfile.go deleted file mode 100644 index e1d514a3..00000000 --- a/object/resolve/treefs_readfile.go +++ /dev/null @@ -1,40 +0,0 @@ -package resolve - -import ( - "fmt" - "io" - - "codeberg.org/lindenii/furgit/object/tree" -) - -// ReadFile reads the blob contents at name. -// -// Directories and gitlink entries are not readable through TreeFS. -func (treeFS *TreeFS) ReadFile(name string) ([]byte, error) { - entry, err := treeFS.resolvePath(treeFSOpReadFile, name) - if err != nil { - return nil, err - } - - if entry.isDir() { - return nil, treeFSPathError(treeFSOpReadFile, name, fmt.Errorf("is a directory")) - } - - if entry.mode == tree.FileModeGitlink { - return nil, treeFSPathError(treeFSOpReadFile, name, fmt.Errorf("object/resolve: gitlink entries are not readable as files")) - } - - reader, _, err := treeFS.resolver.ExactBlobReader(entry.objectID) - if err != nil { - return nil, treeFSPathError(treeFSOpReadFile, name, err) - } - - defer func() { _ = reader.Close() }() - - data, err := io.ReadAll(reader) - if err != nil { - return nil, treeFSPathError(treeFSOpReadFile, name, err) - } - - return data, nil -} diff --git a/object/resolve/treefs_stat.go b/object/resolve/treefs_stat.go deleted file mode 100644 index 396dfbae..00000000 --- a/object/resolve/treefs_stat.go +++ /dev/null @@ -1,22 +0,0 @@ -package resolve - -import "io/fs" - -// Stat returns synthetic file metadata for name. -// -// TreeFS metadata reflects Git tree entry mode and blob size where applicable. -// It does not represent filesystem stat metadata: ModTime is zero, ownership is -// unavailable, and Sys returns the underlying tree.TreeEntry when one exists. -func (treeFS *TreeFS) Stat(name string) (fs.FileInfo, error) { - entry, err := treeFS.resolvePath(treeFSOpStat, name) - if err != nil { - return nil, err - } - - info, err := treeFS.statEntry(entry) - if err != nil { - return nil, treeFSPathError(treeFSOpStat, name, err) - } - - return info, nil -} diff --git a/object/resolve/treefs_sub.go b/object/resolve/treefs_sub.go deleted file mode 100644 index f0eefdc5..00000000 --- a/object/resolve/treefs_sub.go +++ /dev/null @@ -1,22 +0,0 @@ -package resolve - -import "io/fs" - -// Sub returns a new TreeFS rooted at dir. -func (treeFS *TreeFS) Sub(dir string) (fs.FS, error) { - entry, err := treeFS.resolvePath(treeFSOpSub, dir) - if err != nil { - return nil, err - } - - treeID, err := entry.subtreeID() - if err != nil { - return nil, treeFSPathError(treeFSOpSub, dir, fs.ErrInvalid) - } - - return &TreeFS{ - resolver: treeFS.resolver, - rootTree: treeID, - rootEntry: entry.treeEntry, - }, nil -} diff --git a/object/resolve/treefs_test.go b/object/resolve/treefs_test.go deleted file mode 100644 index 59eebd5e..00000000 --- a/object/resolve/treefs_test.go +++ /dev/null @@ -1,111 +0,0 @@ -package resolve_test - -import ( - "errors" - "io/fs" - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/resolve" - "codeberg.org/lindenii/furgit/object/tree" - "codeberg.org/lindenii/furgit/repository" -) - -func TestTreeFS(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - t.Parallel() - - repoData := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo}) - repoData.WriteFile(t, "plain.txt", []byte("plain\n"), 0o644) - repoData.WriteFileAll(t, "dir/exec.sh", []byte("#!/bin/sh\nexit 0\n"), 0o755, 0o755) - repoData.SymbolicRef(t, "HEAD", "refs/heads/main") - _ = repoData.Run(t, "add", ".") - treeHex := repoData.Run(t, "write-tree") - - treeID, err := objectid.ParseHex(algo, treeHex) - if err != nil { - t.Fatalf("ParseHex(write-tree): %v", err) - } - - commitID := repoData.CommitTree(t, treeID, "treefs") - - root := repoData.OpenGitRoot(t) - - repo, err := repository.Open(root) - if err != nil { - t.Fatalf("repository.Open: %v", err) - } - - defer func() { _ = repo.Close() }() - - resolver := resolve.New(repo.Objects()) - - treeFS, err := resolver.TreeFS(commitID) - if err != nil { - t.Fatalf("resolver.TreeFS: %v", err) - } - - content, err := treeFS.ReadFile("plain.txt") - if err != nil { - t.Fatalf("ReadFile(plain.txt): %v", err) - } - - if string(content) != "plain\n" { - t.Fatalf("ReadFile(plain.txt) = %q, want %q", string(content), "plain\n") - } - - entries, err := treeFS.ReadDir(".") - if err != nil { - t.Fatalf("ReadDir(.): %v", err) - } - - if len(entries) != 2 { - t.Fatalf("len(ReadDir(.)) = %d, want 2", len(entries)) - } - - info, err := treeFS.Stat("plain.txt") - if err != nil { - t.Fatalf("Stat(plain.txt): %v", err) - } - - entry, ok := info.Sys().(tree.TreeEntry) - if !ok { - t.Fatalf("Stat(plain.txt).Sys() type = %T, want tree.TreeEntry", info.Sys()) - } - - if entry.Mode != tree.FileModeRegular { - t.Fatalf("Stat(plain.txt).Sys().Mode = %o, want %o", entry.Mode, tree.FileModeRegular) - } - - subFS, err := treeFS.Sub("dir") - if err != nil { - t.Fatalf("Sub(dir): %v", err) - } - - subReadFileFS, ok := subFS.(fs.ReadFileFS) - if !ok { - t.Fatalf("Sub(dir) type does not implement fs.ReadFileFS") - } - - subContent, err := subReadFileFS.ReadFile("exec.sh") - if err != nil { - t.Fatalf("Sub(dir).ReadFile(exec.sh): %v", err) - } - - if string(subContent) != "#!/bin/sh\nexit 0\n" { - t.Fatalf("Sub(dir).ReadFile(exec.sh) = %q", string(subContent)) - } - - _, err = treeFS.ReadFile("dir") - if err == nil { - t.Fatal("ReadFile(dir) unexpectedly succeeded") - } - - if _, ok := errors.AsType[*fs.PathError](err); !ok { - t.Fatalf("ReadFile(dir) err type = %T, want *fs.PathError", err) - } - }) -} diff --git a/repository/resolver.go b/repository/resolver.go index ed87fa09..920045c5 100644 --- a/repository/resolver.go +++ b/repository/resolver.go @@ -1,11 +1,11 @@ package repository -import "codeberg.org/lindenii/furgit/object/resolve" +import "codeberg.org/lindenii/furgit/object/fetch" // Resolver returns an object resolver backed by the repository's object store. // // The returned resolver is ready for use, borrows the repository's object // store, does not need closing, and must not be used after Close. -func (repo *Repository) Resolver() *resolve.Resolver { - return resolve.New(repo.objects) +func (repo *Repository) Resolver() *fetch.Fetcher { + return fetch.New(repo.objects) } -- cgit v1.3.1-10-gc9f91