ref: 3c61d27c921e62ad11282c9f1283af1611bf0b82
dir: /obj_tree.go/
package furgit
import (
"bytes"
"errors"
"fmt"
"sort"
"strconv"
)
// Tree represents a Git tree object.
type Tree struct {
// Entries represents the entries in the tree.
Entries []TreeEntry
}
// StoredTree represents a tree stored in the object database.
type StoredTree struct {
Tree
hash Hash
}
// Hash returns the hash of the stored tree.
func (sTree *StoredTree) Hash() Hash {
return sTree.hash
}
// FileMode represents the mode of a file in a Git tree.
type FileMode uint32
const (
// FileModeDir represents a directory (tree) in a Git tree.
FileModeDir FileMode = 0o40000
// FileModeRegular represents a regular file (blob) in a Git tree.
FileModeRegular FileMode = 0o100644
// FileModeExecutable represents an executable file (blob) in a Git tree.
FileModeExecutable FileMode = 0o100755
// FileModeSymlink represents a symbolic link (blob) in a Git tree.
FileModeSymlink FileMode = 0o120000
// FileModeGitlink represents a Git link (submodule) in a Git tree.
FileModeGitlink FileMode = 0o160000
)
// TreeEntry represents a single entry in a Git tree.
type TreeEntry struct {
// Mode represents the file mode of the entry.
Mode FileMode
// Name represents the name of the entry.
Name []byte
// ID represents the hash of the entry. This is typically
// either a blob or a tree.
ID Hash
}
// ObjectType returns the object type of the tree.
//
// It always returns ObjectTypeTree.
func (tree *Tree) ObjectType() ObjectType {
_ = tree
return ObjectTypeTree
}
// parseTree decodes a tree body.
func parseTree(id Hash, body []byte, repo *Repository) (*StoredTree, error) {
var entries []TreeEntry
i := 0
for i < len(body) {
space := bytes.IndexByte(body[i:], ' ')
if space < 0 {
return nil, errors.New("furgit: tree: missing mode terminator")
}
modeBytes := body[i : i+space]
i += space + 1
nul := bytes.IndexByte(body[i:], 0)
if nul < 0 {
return nil, errors.New("furgit: tree: missing name terminator")
}
nameBytes := body[i : i+nul]
i += nul + 1
if i+repo.hashAlgo.Size() > len(body) {
return nil, errors.New("furgit: tree: truncated child hash")
}
var child Hash
copy(child.data[:], body[i:i+repo.hashAlgo.Size()])
child.algo = repo.hashAlgo
i += repo.hashAlgo.Size()
mode, err := strconv.ParseUint(string(modeBytes), 8, 32)
if err != nil {
return nil, fmt.Errorf("furgit: tree: parse mode: %w", err)
}
entry := TreeEntry{
Mode: FileMode(mode),
Name: append([]byte(nil), nameBytes...),
ID: child,
}
entries = append(entries, entry)
}
return &StoredTree{
hash: id,
Tree: Tree{
Entries: entries,
},
}, nil
}
// treeBody builds the entry list for a tree without the Git header.
func (tree *Tree) serialize() []byte {
var bodyLen int
for _, e := range tree.Entries {
mode := strconv.FormatUint(uint64(e.Mode), 8)
bodyLen += len(mode) + 1 + len(e.Name) + 1 + e.ID.Size()
}
body := make([]byte, bodyLen)
pos := 0
for _, e := range tree.Entries {
mode := strconv.FormatUint(uint64(e.Mode), 8)
pos += copy(body[pos:], []byte(mode))
body[pos] = ' '
pos++
pos += copy(body[pos:], e.Name)
body[pos] = 0
pos++
size := e.ID.Size()
pos += copy(body[pos:], e.ID.data[:size])
}
return body
}
// Serialize renders the tree into its raw byte representation,
// including the header (i.e., "type size\0").
func (tree *Tree) Serialize() ([]byte, error) {
body := tree.serialize()
header, err := headerForType(ObjectTypeTree, body)
if err != nil {
return nil, err
}
raw := make([]byte, len(header)+len(body))
copy(raw, header)
copy(raw[len(header):], body)
return raw, nil
}
// Entry looks up a tree entry by name.
//
// Lookups are not recursive.
// It returns nil if no such entry exists.
func (tree *Tree) Entry(name []byte) *TreeEntry {
if len(tree.Entries) == 0 {
return nil
}
if e := tree.entry(name, true); e != nil {
return e
}
return tree.entry(name, false)
}
// EntryRecursive looks up a tree entry by path.
//
// Lookups are recursive.
func (sTree *StoredTree) EntryRecursive(repo *Repository, path [][]byte) (*TreeEntry, error) {
if len(path) == 0 {
return nil, errors.New("furgit: tree: empty path")
}
currentTree := sTree
for i, part := range path {
entry := currentTree.Entry(part)
if entry == nil {
return nil, ErrNotFound
}
if i == len(path)-1 {
return entry, nil
}
obj, err := repo.ReadObject(entry.ID)
if err != nil {
return nil, err
}
nextTree, ok := obj.(*StoredTree)
if !ok {
return nil, fmt.Errorf("furgit: tree: expected tree object at %s, got %T", part, obj)
// TODO: It may be useful to check the mode instead of reporting
// an object type error.
}
currentTree = nextTree
}
return nil, ErrNotFound
}
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]
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
}
}
return nil
}
// InsertEntry inserts a tree entry while preserving Git's name ordering.
// It returns an error if an entry with the same name already exists.
func (tree *Tree) InsertEntry(newEntry TreeEntry) error {
if tree == nil {
return ErrInvalidObject
}
for _, entry := range tree.Entries {
if bytes.Equal(entry.Name, newEntry.Name) {
return fmt.Errorf("furgit: tree: entry %q already exists", 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
})
tree.Entries = append(tree.Entries, TreeEntry{})
copy(tree.Entries[insertAt+1:], tree.Entries[insertAt:])
tree.Entries[insertAt] = newEntry
return nil
}
// RemoveEntry removes a tree entry by name.
// It returns ErrNotFound if no matching entry exists.
func (tree *Tree) RemoveEntry(name []byte) error {
if tree == nil {
return ErrInvalidObject
}
if len(tree.Entries) == 0 {
return ErrNotFound
}
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
}
}
return ErrNotFound
}
// TreeEntryNameCompare compares names using Git's tree ordering rules.
func TreeEntryNameCompare(entryName []byte, entryMode FileMode, searchName []byte, searchIsTree bool) int {
isEntryTree := entryMode == FileModeDir
entryLen := len(entryName)
if isEntryTree {
entryLen++
}
searchLen := len(searchName)
if searchIsTree {
searchLen++
}
n := entryLen
if searchLen < n {
n = searchLen
}
for i := 0; i < n; i++ {
var ec, sc byte
if i < len(entryName) {
ec = entryName[i]
} else {
ec = '/'
}
if i < len(searchName) {
sc = searchName[i]
} else {
sc = '/'
}
if ec < sc {
return -1
}
if ec > sc {
return 1
}
}
if entryLen < searchLen {
return -1
}
if entryLen > searchLen {
return 1
}
return 0
}