aboutsummaryrefslogtreecommitdiff
path: root/repository
diff options
context:
space:
mode:
authorGravatar Runxi Yu2026-02-21 14:56:56 +0800
committerGravatar Runxi Yu2026-02-21 15:01:46 +0800
commit3011c5e84e9c05bfabe0a5f24b8b267b4bd23912 (patch)
tree611bd4be729be1287924d77d0ed85615114ca0c1 /repository
parentrepository: Add Repository abstraction (diff)
signatureNo signature
repository, objectstored: Add Stored interface and implementations
Diffstat (limited to 'repository')
-rw-r--r--repository/read_stored.go99
-rw-r--r--repository/stored_test.go163
-rw-r--r--repository/tree_resolve.go48
3 files changed, 310 insertions, 0 deletions
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")
+}