shithub: furgit

Download patch

ref: 79c40dcb08f0d512bd6d75d5e2acd3ddceec4530
parent: 8d555a5aae15017c3c3332605bdf4fd33e20aaa0
author: Runxi Yu <runxiyu@umich.edu>
date: Mon Mar 23 02:12:26 EDT 2026

object/resolve: Fix error handling; don't substring-match errors

--- a/object/resolve/path.go
+++ b/object/resolve/path.go
@@ -7,6 +7,42 @@
 	"codeberg.org/lindenii/furgit/objectid"
 )
 
+// PathEmptyError indicates that Path received no segments.
+type PathEmptyError struct{}
+
+func (err *PathEmptyError) Error() string {
+	return "object/resolve: empty tree path"
+}
+
+// PathSegmentEmptyError indicates that one path segment is empty.
+type PathSegmentEmptyError struct {
+	Index int
+}
+
+func (err *PathSegmentEmptyError) Error() string {
+	return fmt.Sprintf("object/resolve: empty tree path segment at index %d", err.Index)
+}
+
+// PathNotFoundError indicates that one tree path segment was not found.
+type PathNotFoundError struct {
+	Index int
+	Name  []byte
+}
+
+func (err *PathNotFoundError) Error() string {
+	return fmt.Sprintf("object/resolve: tree entry %q not found at index %d", err.Name, err.Index)
+}
+
+// PathNotTreeError indicates that one intermediate path segment was not a tree.
+type PathNotTreeError struct {
+	Index int
+	Name  []byte
+}
+
+func (err *PathNotTreeError) Error() string {
+	return fmt.Sprintf("object/resolve: path segment %q at index %d is not a tree", err.Name, err.Index)
+}
+
 // Path resolves parts within the tree identified by root and returns the final
 // tree entry.
 //
@@ -17,7 +53,7 @@
 // its object.
 func (r *Resolver) Path(root objectid.ObjectID, parts [][]byte) (object.TreeEntry, error) {
 	if len(parts) == 0 {
-		return object.TreeEntry{}, fmt.Errorf("object/resolve: empty tree path")
+		return object.TreeEntry{}, &PathEmptyError{}
 	}
 
 	current, err := r.PeelToTree(root)
@@ -27,12 +63,15 @@
 
 	for i, part := range parts {
 		if len(part) == 0 {
-			return object.TreeEntry{}, fmt.Errorf("object/resolve: empty tree path segment")
+			return object.TreeEntry{}, &PathSegmentEmptyError{Index: i}
 		}
 
 		entry := current.Object().Entry(part)
 		if entry == nil {
-			return object.TreeEntry{}, fmt.Errorf("object/resolve: tree entry %q not found", part)
+			return object.TreeEntry{}, &PathNotFoundError{
+				Index: i,
+				Name:  append([]byte(nil), part...),
+			}
 		}
 
 		if i == len(parts)-1 {
@@ -40,7 +79,10 @@
 		}
 
 		if entry.Mode != object.FileModeDir {
-			return object.TreeEntry{}, fmt.Errorf("object/resolve: path segment %q is not a tree", part)
+			return object.TreeEntry{}, &PathNotTreeError{
+				Index: i,
+				Name:  append([]byte(nil), part...),
+			}
 		}
 
 		current, err = r.ExactTree(entry.ID)
@@ -49,5 +91,5 @@
 		}
 	}
 
-	return object.TreeEntry{}, fmt.Errorf("object/resolve: tree entry not found")
+	return object.TreeEntry{}, &PathNotFoundError{Index: len(parts) - 1}
 }
--- a/object/resolve/treefs_entry.go
+++ b/object/resolve/treefs_entry.go
@@ -1,9 +1,9 @@
 package resolve
 
 import (
+	"errors"
 	"fmt"
 	"io/fs"
-	"strings"
 
 	"codeberg.org/lindenii/furgit/object"
 	"codeberg.org/lindenii/furgit/objectid"
@@ -37,11 +37,19 @@
 }
 
 func (treeFS *TreeFS) pathResolveError(op treeFSOp, name string, err error) error {
-	if err != nil && strings.Contains(err.Error(), "not found") {
+	if _, ok := errors.AsType[*PathNotFoundError](err); ok {
 		return treeFSPathError(op, name, fs.ErrNotExist)
 	}
 
-	if err != nil && strings.Contains(err.Error(), "is not a tree") {
+	if _, ok := errors.AsType[*PathNotTreeError](err); ok {
+		return treeFSPathError(op, name, fs.ErrInvalid)
+	}
+
+	if _, ok := errors.AsType[*PathEmptyError](err); ok {
+		return treeFSPathError(op, name, fs.ErrInvalid)
+	}
+
+	if _, ok := errors.AsType[*PathSegmentEmptyError](err); ok {
 		return treeFSPathError(op, name, fs.ErrInvalid)
 	}
 
--