shithub: furgit

ref: 34d47f9e0cf66904432a3c774707b4773273ef4f
dir: /object/tree.go/

View raw version
package object

import (
	"bytes"
	"fmt"
	"sort"

	"codeberg.org/lindenii/furgit/objectid"
	"codeberg.org/lindenii/furgit/objecttype"
)

// FileMode represents the mode of a file in a Git tree.
type FileMode uint32

const (
	FileModeDir        FileMode = 0o40000
	FileModeRegular    FileMode = 0o100644
	FileModeExecutable FileMode = 0o100755
	FileModeSymlink    FileMode = 0o120000
	FileModeGitlink    FileMode = 0o160000
)

// TreeEntry represents a single entry in a tree.
type TreeEntry struct {
	Mode FileMode
	Name []byte
	ID   objectid.ObjectID
}

// Tree represents a Git tree object.
type Tree struct {
	Entries []TreeEntry
}

// ObjectType returns TypeTree.
func (tree *Tree) ObjectType() objecttype.Type {
	_ = tree
	return objecttype.TypeTree
}

// Entry looks up a tree entry by name.
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)
}

// InsertEntry inserts a tree entry while preserving Git ordering.
func (tree *Tree) InsertEntry(newEntry TreeEntry) error {
	if tree.entry(newEntry.Name, true) != nil || tree.entry(newEntry.Name, false) != nil {
		return fmt.Errorf("object: 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.
func (tree *Tree) RemoveEntry(name []byte) error {
	if len(tree.Entries) == 0 {
		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
		}
	}
	return fmt.Errorf("object: tree: entry %q not found", name)
}

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
}

// TreeEntryNameCompare compares names using Git 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 := min(searchLen, entryLen)

	for i := range n {
		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
}