ref: aa7e2873aa4da53fbaa4774fdd33f5af20c1afb0
parent: c874533e9b12d4959bbd438775ee07fadc770e45
author: Runxi Yu <runxiyu@umich.edu>
date: Mon Mar 30 01:40:58 EDT 2026
object/tree: Add helpers and cleanup
--- a/object/tree/entry.go
+++ b/object/tree/entry.go
@@ -2,6 +2,7 @@
import (
"bytes"
+ "slices"
objectid "codeberg.org/lindenii/furgit/object/id"
)
@@ -16,26 +17,25 @@
}
func (tree *Tree) entry(name []byte, searchIsTree bool) *TreeEntry {- low, high := 0, len(tree.Entries)-1
- for low <= high {- mid := low + (high-low)/2
- entry := &tree.Entries[mid]
+ index, ok := slices.BinarySearchFunc(tree.Entries, treeEntrySearch{+ name: name,
+ isTree: searchIsTree,
+ }, func(entry TreeEntry, search treeEntrySearch) int {+ return TreeEntryNameCompare(entry.Name, entry.Mode, search.name, search.isTree)
+ })
+ if !ok {+ return nil
+ }
- cmp := TreeEntryNameCompare(entry.Name, entry.Mode, name, searchIsTree)
- if cmp == 0 {- if bytes.Equal(entry.Name, name) {- return entry
- }
-
- return nil
- }
-
- if cmp < 0 {- low = mid + 1
- } else {- high = mid - 1
- }
+ entry := &tree.Entries[index]
+ if !bytes.Equal(entry.Name, name) {+ return nil
}
- return nil
+ return entry
+}
+
+type treeEntrySearch struct {+ name []byte
+ isTree bool
}
--- a/object/tree/insert.go
+++ b/object/tree/insert.go
@@ -2,7 +2,7 @@
import (
"fmt"
- "sort"
+ "slices"
)
// InsertEntry inserts a tree entry while preserving Git ordering.
@@ -15,13 +15,13 @@
newEntry.Name = append([]byte(nil), newEntry.Name...)
- newIsTree := newEntry.Mode == FileModeDir
- insertAt := sort.Search(len(tree.Entries), func(i int) bool {- return TreeEntryNameCompare(tree.Entries[i].Name, tree.Entries[i].Mode, newEntry.Name, newIsTree) >= 0
+ insertAt, _ := slices.BinarySearchFunc(tree.Entries, treeEntrySearch{+ name: newEntry.Name,
+ isTree: newEntry.Mode == FileModeDir,
+ }, func(entry TreeEntry, search treeEntrySearch) int {+ return TreeEntryNameCompare(entry.Name, entry.Mode, search.name, search.isTree)
})
- tree.Entries = append(tree.Entries, TreeEntry{})- copy(tree.Entries[insertAt+1:], tree.Entries[insertAt:])
- tree.Entries[insertAt] = newEntry
+ tree.Entries = slices.Insert(tree.Entries, insertAt, newEntry)
return nil
}
--- a/object/tree/mode.go
+++ b/object/tree/mode.go
@@ -10,15 +10,3 @@
FileModeSymlink FileMode = 0o120000
FileModeGitlink FileMode = 0o160000
)
-
-// IsBlobLike reports whether mode names one blob-like tree entry kind.
-//
-// Blob-like entries store blob object IDs as their targets.
-func (mode FileMode) IsBlobLike() bool {- switch mode {- case FileModeRegular, FileModeExecutable, FileModeSymlink:
- return true
- default:
- return false
- }
-}
--- /dev/null
+++ b/object/tree/mode_details.go
@@ -1,0 +1,9 @@
+package tree
+
+type fileModeDetails struct {+ isBlobLike bool
+}
+
+func (mode FileMode) details() fileModeDetails {+ return fileModeTable[mode]
+}
--- /dev/null
+++ b/object/tree/mode_is_blob_like.go
@@ -1,0 +1,8 @@
+package tree
+
+// IsBlobLike reports whether mode names one blob-like tree entry kind.
+//
+// Blob-like entries store blob object IDs as their targets.
+func (mode FileMode) IsBlobLike() bool {+ return mode.details().isBlobLike
+}
--- /dev/null
+++ b/object/tree/mode_table.go
@@ -1,0 +1,19 @@
+package tree
+
+var fileModeTable = map[FileMode]fileModeDetails{ //nolint:gochecknoglobals+ FileModeDir: {+ isBlobLike: false,
+ },
+ FileModeRegular: {+ isBlobLike: true,
+ },
+ FileModeExecutable: {+ isBlobLike: true,
+ },
+ FileModeSymlink: {+ isBlobLike: true,
+ },
+ FileModeGitlink: {+ isBlobLike: false,
+ },
+}
--- /dev/null
+++ b/object/tree/path_append.go
@@ -1,0 +1,14 @@
+package tree
+
+// AppendPath appends path to dst as one slash-separated byte path.
+func AppendPath(dst []byte, path [][]byte) []byte {+ for i := range path {+ if i > 0 {+ dst = append(dst, '/')
+ }
+
+ dst = append(dst, path[i]...)
+ }
+
+ return dst
+}
--- /dev/null
+++ b/object/tree/path_clone.go
@@ -1,0 +1,16 @@
+package tree
+
+import (
+ "bytes"
+ "slices"
+)
+
+// ClonePath returns one deep copy of path.
+func ClonePath(path [][]byte) [][]byte {+ cloned := slices.Clone(path)
+ for i := range cloned {+ cloned[i] = bytes.Clone(cloned[i])
+ }
+
+ return cloned
+}
--- /dev/null
+++ b/object/tree/path_prefix.go
@@ -1,0 +1,19 @@
+package tree
+
+import (
+ "bytes"
+ "slices"
+)
+
+// HasPathPrefix reports whether path begins with prefix as whole components.
+func HasPathPrefix(path, prefix [][]byte) bool {+ if len(prefix) == 0 {+ return true
+ }
+
+ if len(path) < len(prefix) {+ return false
+ }
+
+ return slices.EqualFunc(path[:len(prefix)], prefix, bytes.Equal)
+}
--- /dev/null
+++ b/object/tree/path_split.go
@@ -1,0 +1,19 @@
+package tree
+
+import (
+ "bytes"
+)
+
+// SplitPath splits one slash-separated tree path into components.
+func SplitPath(path []byte) [][]byte {+ if len(path) == 0 {+ return nil
+ }
+
+ parts := bytes.Split(path, []byte{'/'})+ for i := range parts {+ parts[i] = bytes.Clone(parts[i])
+ }
+
+ return parts
+}
--- a/object/tree/remove.go
+++ b/object/tree/remove.go
@@ -3,6 +3,7 @@
import (
"bytes"
"fmt"
+ "slices"
)
// RemoveEntry removes a tree entry by name.
@@ -11,13 +12,12 @@
return fmt.Errorf("object: tree: entry %q not found", name)}
- for i := range tree.Entries {- if bytes.Equal(tree.Entries[i].Name, name) {- copy(tree.Entries[i:], tree.Entries[i+1:])
- tree.Entries = tree.Entries[:len(tree.Entries)-1]
-
- return nil
- }
+ index := slices.IndexFunc(tree.Entries, func(entry TreeEntry) bool {+ return bytes.Equal(entry.Name, name)
+ })
+ if index >= 0 {+ tree.Entries = slices.Delete(tree.Entries, index, index+1)
+ return nil
}
return fmt.Errorf("object: tree: entry %q not found", name)--
⑨