aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--objectstored/stored.go119
-rw-r--r--repository/read_stored.go99
-rw-r--r--repository/stored_test.go163
-rw-r--r--repository/tree_resolve.go48
4 files changed, 429 insertions, 0 deletions
diff --git a/objectstored/stored.go b/objectstored/stored.go
new file mode 100644
index 00000000..8fe8800c
--- /dev/null
+++ b/objectstored/stored.go
@@ -0,0 +1,119 @@
+// Package objectstored wraps parsed objects with their storage object IDs.
+package objectstored
+
+import (
+ "codeberg.org/lindenii/furgit/object"
+ "codeberg.org/lindenii/furgit/objectid"
+)
+
+// StoredObject is a parsed object paired with its storage ID.
+type StoredObject interface {
+ // ID returns the object ID the object was loaded from.
+ ID() objectid.ObjectID
+ // Object returns the parsed object value.
+ Object() object.Object
+}
+
+// StoredBlob is a parsed blob paired with its storage ID.
+type StoredBlob struct {
+ id objectid.ObjectID
+ blob *object.Blob
+}
+
+// NewStoredBlob creates one stored blob wrapper.
+func NewStoredBlob(id objectid.ObjectID, blob *object.Blob) *StoredBlob {
+ return &StoredBlob{id: id, blob: blob}
+}
+
+// ID returns the object ID this blob was loaded from.
+func (stored *StoredBlob) ID() objectid.ObjectID {
+ return stored.id
+}
+
+// Object returns the parsed blob as the generic object interface.
+func (stored *StoredBlob) Object() object.Object {
+ return stored.blob
+}
+
+// Blob returns the parsed blob value.
+func (stored *StoredBlob) Blob() *object.Blob {
+ return stored.blob
+}
+
+// StoredTree is a parsed tree paired with its storage ID.
+type StoredTree struct {
+ id objectid.ObjectID
+ tree *object.Tree
+}
+
+// NewStoredTree creates one stored tree wrapper.
+func NewStoredTree(id objectid.ObjectID, tree *object.Tree) *StoredTree {
+ return &StoredTree{id: id, tree: tree}
+}
+
+// ID returns the object ID this tree was loaded from.
+func (stored *StoredTree) ID() objectid.ObjectID {
+ return stored.id
+}
+
+// Object returns the parsed tree as the generic object interface.
+func (stored *StoredTree) Object() object.Object {
+ return stored.tree
+}
+
+// Tree returns the parsed tree value.
+func (stored *StoredTree) Tree() *object.Tree {
+ return stored.tree
+}
+
+// StoredCommit is a parsed commit paired with its storage ID.
+type StoredCommit struct {
+ id objectid.ObjectID
+ commit *object.Commit
+}
+
+// NewStoredCommit creates one stored commit wrapper.
+func NewStoredCommit(id objectid.ObjectID, commit *object.Commit) *StoredCommit {
+ return &StoredCommit{id: id, commit: commit}
+}
+
+// ID returns the object ID this commit was loaded from.
+func (stored *StoredCommit) ID() objectid.ObjectID {
+ return stored.id
+}
+
+// Object returns the parsed commit as the generic object interface.
+func (stored *StoredCommit) Object() object.Object {
+ return stored.commit
+}
+
+// Commit returns the parsed commit value.
+func (stored *StoredCommit) Commit() *object.Commit {
+ return stored.commit
+}
+
+// StoredTag is a parsed tag paired with its storage ID.
+type StoredTag struct {
+ id objectid.ObjectID
+ tag *object.Tag
+}
+
+// NewStoredTag creates one stored tag wrapper.
+func NewStoredTag(id objectid.ObjectID, tag *object.Tag) *StoredTag {
+ return &StoredTag{id: id, tag: tag}
+}
+
+// ID returns the object ID this tag was loaded from.
+func (stored *StoredTag) ID() objectid.ObjectID {
+ return stored.id
+}
+
+// Object returns the parsed tag as the generic object interface.
+func (stored *StoredTag) Object() object.Object {
+ return stored.tag
+}
+
+// Tag returns the parsed tag value.
+func (stored *StoredTag) Tag() *object.Tag {
+ return stored.tag
+}
diff --git a/repository/read_stored.go b/repository/read_stored.go
new file mode 100644
index 00000000..b26421e8
--- /dev/null
+++ b/repository/read_stored.go
@@ -0,0 +1,99 @@
+package repository
+
+import (
+ "fmt"
+
+ "codeberg.org/lindenii/furgit/object"
+ "codeberg.org/lindenii/furgit/objectid"
+ "codeberg.org/lindenii/furgit/objectstored"
+ "codeberg.org/lindenii/furgit/objecttype"
+)
+
+// ReadStored reads, parses, and wraps one object by ID.
+func (repo *Repository) ReadStored(id objectid.ObjectID) (objectstored.StoredObject, error) {
+ parsed, err := repo.readParsedObject(id)
+ if err != nil {
+ return nil, err
+ }
+ switch parsed := parsed.(type) {
+ case *object.Blob:
+ return objectstored.NewStoredBlob(id, parsed), nil
+ case *object.Tree:
+ return objectstored.NewStoredTree(id, parsed), nil
+ case *object.Commit:
+ return objectstored.NewStoredCommit(id, parsed), nil
+ case *object.Tag:
+ return objectstored.NewStoredTag(id, parsed), nil
+ default:
+ return nil, fmt.Errorf("repository: unsupported parsed object type %T", parsed)
+ }
+}
+
+// ReadStoredBlob reads and parses a blob object by ID.
+func (repo *Repository) ReadStoredBlob(id objectid.ObjectID) (*objectstored.StoredBlob, error) {
+ stored, err := repo.ReadStored(id)
+ if err != nil {
+ return nil, err
+ }
+ blob, ok := stored.(*objectstored.StoredBlob)
+ if !ok {
+ return nil, fmt.Errorf("repository: expected blob object %s, got %v", id, stored.Object().ObjectType())
+ }
+ return blob, nil
+}
+
+// ReadStoredTree reads and parses a tree object by ID.
+func (repo *Repository) ReadStoredTree(id objectid.ObjectID) (*objectstored.StoredTree, error) {
+ stored, err := repo.ReadStored(id)
+ if err != nil {
+ return nil, err
+ }
+ tree, ok := stored.(*objectstored.StoredTree)
+ if !ok {
+ return nil, fmt.Errorf("repository: expected tree object %s, got %v", id, stored.Object().ObjectType())
+ }
+ return tree, nil
+}
+
+// ReadStoredCommit reads and parses a commit object by ID.
+func (repo *Repository) ReadStoredCommit(id objectid.ObjectID) (*objectstored.StoredCommit, error) {
+ stored, err := repo.ReadStored(id)
+ if err != nil {
+ return nil, err
+ }
+ commit, ok := stored.(*objectstored.StoredCommit)
+ if !ok {
+ return nil, fmt.Errorf("repository: expected commit object %s, got %v", id, stored.Object().ObjectType())
+ }
+ return commit, nil
+}
+
+// ReadStoredTag reads and parses a tag object by ID.
+func (repo *Repository) ReadStoredTag(id objectid.ObjectID) (*objectstored.StoredTag, error) {
+ stored, err := repo.ReadStored(id)
+ if err != nil {
+ return nil, err
+ }
+ tag, ok := stored.(*objectstored.StoredTag)
+ if !ok {
+ return nil, fmt.Errorf("repository: expected tag object %s, got %v", id, stored.Object().ObjectType())
+ }
+ return tag, nil
+}
+
+// readParsedObject reads bytes content from storage and parses one object.
+func (repo *Repository) readParsedObject(id objectid.ObjectID) (object.Object, error) {
+ ty, content, err := repo.objects.ReadBytesContent(id)
+ if err != nil {
+ return nil, err
+ }
+ parsed, err := object.ParseObjectWithoutHeader(ty, content, repo.algo)
+ if err != nil {
+ tyName, ok := objecttype.Name(ty)
+ if !ok {
+ tyName = fmt.Sprintf("type %d", ty)
+ }
+ return nil, fmt.Errorf("repository: parse object %s (%s): %w", id, tyName, err)
+ }
+ return parsed, nil
+}
diff --git a/repository/stored_test.go b/repository/stored_test.go
new file mode 100644
index 00000000..da1d1392
--- /dev/null
+++ b/repository/stored_test.go
@@ -0,0 +1,163 @@
+package repository_test
+
+import (
+ "fmt"
+ "strings"
+ "testing"
+
+ "codeberg.org/lindenii/furgit/internal/testgit"
+ "codeberg.org/lindenii/furgit/object"
+ "codeberg.org/lindenii/furgit/objectid"
+ "codeberg.org/lindenii/furgit/repository"
+)
+
+func TestReadStoredTyped(t *testing.T) {
+ t.Parallel()
+
+ testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
+ repoHarness := testgit.NewRepo(t, testgit.RepoOptions{
+ ObjectFormat: algo,
+ Bare: true,
+ RefFormat: "files",
+ })
+
+ blobID, treeID, commitID := repoHarness.MakeCommit(t, "stored types")
+
+ repo, err := repository.Open(repoHarness.Dir())
+ if err != nil {
+ t.Fatalf("repository.Open: %v", err)
+ }
+ defer func() { _ = repo.Close() }()
+
+ blob, err := repo.ReadStoredBlob(blobID)
+ if err != nil {
+ t.Fatalf("ReadStoredBlob: %v", err)
+ }
+ if blob.ID() != blobID {
+ t.Fatalf("blob ID = %s, want %s", blob.ID(), blobID)
+ }
+ if string(blob.Blob().Data) != "commit-body\n" {
+ t.Fatalf("blob body = %q, want %q", blob.Blob().Data, "commit-body\n")
+ }
+
+ tree, err := repo.ReadStoredTree(treeID)
+ if err != nil {
+ t.Fatalf("ReadStoredTree: %v", err)
+ }
+ if tree.ID() != treeID {
+ t.Fatalf("tree ID = %s, want %s", tree.ID(), treeID)
+ }
+ if len(tree.Tree().Entries) != 1 {
+ t.Fatalf("tree entries = %d, want 1", len(tree.Tree().Entries))
+ }
+
+ commit, err := repo.ReadStoredCommit(commitID)
+ if err != nil {
+ t.Fatalf("ReadStoredCommit: %v", err)
+ }
+ if commit.ID() != commitID {
+ t.Fatalf("commit ID = %s, want %s", commit.ID(), commitID)
+ }
+ if commit.Commit().Tree != treeID {
+ t.Fatalf("commit tree = %s, want %s", commit.Commit().Tree, treeID)
+ }
+ })
+}
+
+func TestResolveTreeEntry(t *testing.T) {
+ t.Parallel()
+
+ testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
+ repoHarness := testgit.NewRepo(t, testgit.RepoOptions{
+ ObjectFormat: algo,
+ Bare: true,
+ RefFormat: "files",
+ })
+
+ blobID := repoHarness.HashObject(t, "blob", []byte("nested-file\n"))
+ childTreeID := repoHarness.Mktree(t, fmt.Sprintf("100644 blob %s\tleaf.txt\n", blobID))
+ rootTreeID := repoHarness.Mktree(t, fmt.Sprintf("040000 tree %s\tdir\n", childTreeID))
+
+ repo, err := repository.Open(repoHarness.Dir())
+ if err != nil {
+ t.Fatalf("repository.Open: %v", err)
+ }
+ defer func() { _ = repo.Close() }()
+
+ rootTree, err := repo.ReadStoredTree(rootTreeID)
+ if err != nil {
+ t.Fatalf("ReadStoredTree(root): %v", err)
+ }
+
+ entry, err := repo.ResolveTreeEntry(rootTree, [][]byte{[]byte("dir"), []byte("leaf.txt")})
+ if err != nil {
+ t.Fatalf("ResolveTreeEntry: %v", err)
+ }
+ if entry.Mode != object.FileModeRegular {
+ t.Fatalf("ResolveTreeEntry mode = %o, want %o", entry.Mode, object.FileModeRegular)
+ }
+ if entry.ID != blobID {
+ t.Fatalf("ResolveTreeEntry id = %s, want %s", entry.ID, blobID)
+ }
+ })
+}
+
+func TestResolveTreeEntryErrors(t *testing.T) {
+ t.Parallel()
+
+ testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
+ t.Run("missing path component", func(t *testing.T) {
+ t.Parallel()
+ repoHarness := testgit.NewRepo(t, testgit.RepoOptions{
+ ObjectFormat: algo,
+ Bare: true,
+ RefFormat: "files",
+ })
+ blobID := repoHarness.HashObject(t, "blob", []byte("body\n"))
+ rootTreeID := repoHarness.Mktree(t, fmt.Sprintf("100644 blob %s\tfile.txt\n", blobID))
+
+ repo, err := repository.Open(repoHarness.Dir())
+ if err != nil {
+ t.Fatalf("repository.Open: %v", err)
+ }
+ defer func() { _ = repo.Close() }()
+
+ rootTree, err := repo.ReadStoredTree(rootTreeID)
+ if err != nil {
+ t.Fatalf("ReadStoredTree(root): %v", err)
+ }
+
+ _, err = repo.ResolveTreeEntry(rootTree, [][]byte{[]byte("missing")})
+ if err == nil || !strings.Contains(err.Error(), "not found") {
+ t.Fatalf("ResolveTreeEntry missing: err = %v, want not found error", err)
+ }
+ })
+
+ t.Run("non-tree intermediate", func(t *testing.T) {
+ t.Parallel()
+ repoHarness := testgit.NewRepo(t, testgit.RepoOptions{
+ ObjectFormat: algo,
+ Bare: true,
+ RefFormat: "files",
+ })
+ blobID := repoHarness.HashObject(t, "blob", []byte("body\n"))
+ rootTreeID := repoHarness.Mktree(t, fmt.Sprintf("100644 blob %s\tdir\n", blobID))
+
+ repo, err := repository.Open(repoHarness.Dir())
+ if err != nil {
+ t.Fatalf("repository.Open: %v", err)
+ }
+ defer func() { _ = repo.Close() }()
+
+ rootTree, err := repo.ReadStoredTree(rootTreeID)
+ if err != nil {
+ t.Fatalf("ReadStoredTree(root): %v", err)
+ }
+
+ _, err = repo.ResolveTreeEntry(rootTree, [][]byte{[]byte("dir"), []byte("leaf")})
+ if err == nil || !strings.Contains(err.Error(), "is not a tree") {
+ t.Fatalf("ResolveTreeEntry non-tree: err = %v, want non-tree error", err)
+ }
+ })
+ })
+}
diff --git a/repository/tree_resolve.go b/repository/tree_resolve.go
new file mode 100644
index 00000000..6b7023ba
--- /dev/null
+++ b/repository/tree_resolve.go
@@ -0,0 +1,48 @@
+package repository
+
+import (
+ "errors"
+ "fmt"
+
+ "codeberg.org/lindenii/furgit/object"
+ "codeberg.org/lindenii/furgit/objectstored"
+)
+
+// ResolveTreeEntry resolves one path within a stored root tree.
+//
+// parts must contain at least one path segment. Intermediate segments must be
+// tree entries.
+func (repo *Repository) ResolveTreeEntry(tree *objectstored.StoredTree, parts [][]byte) (object.TreeEntry, error) {
+ if tree == nil {
+ return object.TreeEntry{}, errors.New("repository: nil root tree")
+ }
+ if len(parts) == 0 {
+ return object.TreeEntry{}, errors.New("repository: empty tree path")
+ }
+
+ current := tree
+ for i, part := range parts {
+ if len(part) == 0 {
+ return object.TreeEntry{}, errors.New("repository: empty tree path segment")
+ }
+
+ entry := current.Tree().Entry(part)
+ if entry == nil {
+ return object.TreeEntry{}, fmt.Errorf("repository: tree entry %q not found", part)
+ }
+ if i == len(parts)-1 {
+ return *entry, nil
+ }
+ if entry.Mode != object.FileModeDir {
+ return object.TreeEntry{}, fmt.Errorf("repository: path segment %q is not a tree", part)
+ }
+
+ next, err := repo.ReadStoredTree(entry.ID)
+ if err != nil {
+ return object.TreeEntry{}, err
+ }
+ current = next
+ }
+
+ return object.TreeEntry{}, fmt.Errorf("repository: tree entry not found")
+}