From 21aedc6a1455771edeb1190f15101e40b51b53f6 Mon Sep 17 00:00:00 2001 From: Runxi Yu Date: Mon, 8 Jun 2026 07:47:11 +0000 Subject: object/fetch: Add simple treefs test and fix some lints --- object/fetch/object.go | 2 +- object/fetch/treefs.go | 146 ++++++++++++++++++------------------- object/fetch/treefs_test.go | 172 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 246 insertions(+), 74 deletions(-) create mode 100644 object/fetch/treefs_test.go (limited to 'object/fetch') diff --git a/object/fetch/object.go b/object/fetch/object.go index 5dbb84ab..fdb46634 100644 --- a/object/fetch/object.go +++ b/object/fetch/object.go @@ -21,7 +21,7 @@ func (fetcher *Fetcher) ExactObject(id oid.ObjectID) (*stored.Stored[object.Obje return stored.New(id, parsed), nil } -func (fetcher *Fetcher) parseObject(id oid.ObjectID) (object.Object, error) { +func (fetcher *Fetcher) parseObject(id oid.ObjectID) (object.Object, error) { //nolint:ireturn ty, content, err := fetcher.store.ReadBytesContent(id) if err != nil { return nil, wrapObjectReadError(id, err) diff --git a/object/fetch/treefs.go b/object/fetch/treefs.go index f5060a30..6a70f98f 100644 --- a/object/fetch/treefs.go +++ b/object/fetch/treefs.go @@ -37,33 +37,6 @@ var ( _ fs.SubFS = (*TreeFS)(nil) ) -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: mode.Directory, - treeID: treeFS.rootTree, - treeEntry: treeFS.rootEntry, - }, nil - } - - entry, err := treeFS.fetcher.Path(treeFS.rootTree, splitPath(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 splitPath(path string) []string { if len(path) == 0 { return nil @@ -72,22 +45,6 @@ func splitPath(path string) []string { return strings.Split(path, "/") } -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 errors.Is(err, ErrPathInvalid) { - return treeFSPathError(op, name, fs.ErrInvalid) - } - - return treeFSPathError(op, name, err) -} - type treeEntryValue struct { name string mode mode.Mode @@ -157,35 +114,6 @@ func treeFSEntryMode(mod mode.Mode) fs.FileMode { } } -func (treeFS *TreeFS) statEntry(entry treeEntryValue) (*treeFSInfo, error) { - size := int64(0) - - if entry.mode == mode.Regular || entry.mode == mode.Executable || entry.mode == mode.Symlink { - sz, err := entry.blobSize(treeFS.fetcher) - if err != nil { - return nil, err - } - - size, err = intconv.Uint64ToInt64(sz) - 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 -} - // TreeFS returns a new filesystem view rooted at root, which may be any // tree-ish object accepted by PeelToTreeID. // @@ -258,7 +186,7 @@ func (treeFS *TreeFS) Open(name string) (fs.File, error) { entries := make([]fs.DirEntry, 0, len(tree.Object().Entries())) for _, child := range tree.Object().Entries() { childEntry := treeEntryValue{ - name: string(child.Name), + name: child.Name, mode: child.Mode, objectID: child.ID, treeEntry: &child, @@ -436,3 +364,75 @@ func (treeFS *TreeFS) Sub(dir string) (fs.FS, error) { rootEntry: entry.treeEntry, }, nil } + +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: mode.Directory, + treeID: treeFS.rootTree, + treeEntry: treeFS.rootEntry, + }, nil + } + + entry, err := treeFS.fetcher.Path(treeFS.rootTree, splitPath(name)) + if err != nil { + return treeEntryValue{}, treeFS.pathResolveError(op, name, err) + } + + return treeEntryValue{ + name: 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 errors.Is(err, ErrPathInvalid) { + return treeFSPathError(op, name, fs.ErrInvalid) + } + + return treeFSPathError(op, name, err) +} + +func (treeFS *TreeFS) statEntry(entry treeEntryValue) (*treeFSInfo, error) { + size := int64(0) + + if entry.mode == mode.Regular || entry.mode == mode.Executable || entry.mode == mode.Symlink { + sz, err := entry.blobSize(treeFS.fetcher) + if err != nil { + return nil, err + } + + size, err = intconv.Uint64ToInt64(sz) + 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_test.go b/object/fetch/treefs_test.go new file mode 100644 index 00000000..73f1c591 --- /dev/null +++ b/object/fetch/treefs_test.go @@ -0,0 +1,172 @@ +package fetch_test + +import ( + "errors" + "io/fs" + "testing" + + "lindenii.org/go/furgit/object/commit" + "lindenii.org/go/furgit/object/fetch" + "lindenii.org/go/furgit/object/id" + "lindenii.org/go/furgit/object/signature" + "lindenii.org/go/furgit/object/store/memory" + "lindenii.org/go/furgit/object/tree" + "lindenii.org/go/furgit/object/tree/mode" + "lindenii.org/go/furgit/object/typ" +) + +func TestTreeFS(t *testing.T) { + t.Parallel() + + for _, objectFormat := range id.SupportedObjectFormats() { + t.Run(objectFormat.String(), func(t *testing.T) { + t.Parallel() + + store := memory.New(objectFormat) + + plainID, err := store.WriteBytesContent(typ.TypeBlob, []byte("plain\n")) + if err != nil { + t.Fatalf("WriteBytesContent(plain.txt): %v", err) + } + + execID, err := store.WriteBytesContent(typ.TypeBlob, []byte("#!/bin/sh\nexit 0\n")) + if err != nil { + t.Fatalf("WriteBytesContent(exec.sh): %v", err) + } + + subTreeID := writeTree(t, store, []tree.Entry{ + {Mode: mode.Executable, Name: "exec.sh", ID: execID}, + }) + + rootTreeID := writeTree(t, store, []tree.Entry{ + {Mode: mode.Regular, Name: "plain.txt", ID: plainID}, + {Mode: mode.Directory, Name: "dir", ID: subTreeID}, + }) + + commitID := writeCommit(t, store, rootTreeID) + + fetcher := fetch.New(store) + + treeFS, err := fetcher.TreeFS(commitID) + if err != nil { + t.Fatalf("fetcher.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.Entry) + if !ok { + t.Fatalf("Stat(plain.txt).Sys() type = %T, want tree.Entry", info.Sys()) + } + + if entry.Mode != mode.Regular { + t.Fatalf("Stat(plain.txt).Sys().Mode = %o, want %o", entry.Mode, mode.Regular) + } + + 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) + } + }) + } +} + +// writeTree builds a tree object from entries, writes it to store, +// and returns its object ID. +func writeTree(t *testing.T, store *memory.Memory, entries []tree.Entry) id.ObjectID { + t.Helper() + + tr := new(tree.Tree) + for _, entry := range entries { + err := tr.Insert(entry) + if err != nil { + t.Fatalf("tree.Insert(%q): %v", entry.Name, err) + } + } + + body, err := tr.AppendWithoutHeader(nil) + if err != nil { + t.Fatalf("tree.AppendWithoutHeader: %v", err) + } + + treeID, err := store.WriteBytesContent(typ.TypeTree, body) + if err != nil { + t.Fatalf("WriteBytesContent(tree): %v", err) + } + + return treeID +} + +// writeCommit builds a commit object pointing at tree, writes it to store, +// and returns its object ID. +func writeCommit(t *testing.T, store *memory.Memory, tree id.ObjectID) id.ObjectID { + t.Helper() + + who := signature.Signature{ + Name: []byte("Test Author"), + Email: []byte("author@example.org"), + WhenUnix: 1234567890, + OffsetMinutes: 0, + } + + body, err := (&commit.Commit{ + Tree: tree, + Author: who, + Committer: who, + Message: []byte("treefs\n"), + }).AppendWithoutHeader(nil) + if err != nil { + t.Fatalf("commit.AppendWithoutHeader: %v", err) + } + + commitID, err := store.WriteBytesContent(typ.TypeCommit, body) + if err != nil { + t.Fatalf("WriteBytesContent(commit): %v", err) + } + + return commitID +} -- cgit v1.3.1-10-gc9f91