shithub: furgit

Download patch

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)
--