Parent directory

repository.go

3076 bytes
  1package pages
  2
  3import (
  4	"errors"
  5	"fmt"
  6	"path"
  7	"strings"
  8
  9	"github.com/charmbracelet/soft-serve/git"
 10)
 11
 12const maxTreeEntries = 1000
 13
 14var (
 15	// ErrInvalidRef is returned for a requested ref that is not an actual ref.
 16	ErrInvalidRef = errors.New("invalid reference")
 17	// ErrInvalidCommitHash is returned before looking up a malformed object ID.
 18	ErrInvalidCommitHash = errors.New("invalid commit hash")
 19	// ErrInvalidTreePath is returned when a path is not contained in a Git tree.
 20	ErrInvalidTreePath = errors.New("invalid tree path")
 21)
 22
 23// ResolveRef resolves an empty ref to HEAD or matches a full ref name exactly.
 24func ResolveRef(repo *git.Repository, requested string) (*git.Reference, error) {
 25	if requested == "" {
 26		return repo.HEAD()
 27	}
 28
 29	refs, err := repo.References()
 30	if err != nil {
 31		return nil, err
 32	}
 33	for _, ref := range refs {
 34		if ref.Name().String() == requested {
 35			return ref, nil
 36		}
 37	}
 38	return nil, ErrInvalidRef
 39}
 40
 41// ValidCommitHash accepts only complete SHA-1 or SHA-256 hexadecimal object IDs.
 42func ValidCommitHash(hash string) bool {
 43	if len(hash) != 40 && len(hash) != 64 {
 44		return false
 45	}
 46	for _, char := range hash {
 47		if !(char >= '0' && char <= '9') && !(char >= 'a' && char <= 'f') && !(char >= 'A' && char <= 'F') {
 48			return false
 49		}
 50	}
 51	return true
 52}
 53
 54// LookupCommit validates hash before asking Git to load the commit object.
 55func LookupCommit(repo *git.Repository, hash string) (*git.Commit, error) {
 56	if !ValidCommitHash(hash) {
 57		return nil, ErrInvalidCommitHash
 58	}
 59	return repo.CatFileCommit(hash)
 60}
 61
 62// ValidateTreePath converts a POSIX tree path to its canonical relative form.
 63func ValidateTreePath(treePath string) (string, error) {
 64	if treePath == "" || treePath == "." {
 65		return "", nil
 66	}
 67	if strings.ContainsRune(treePath, 0) || path.IsAbs(treePath) {
 68		return "", ErrInvalidTreePath
 69	}
 70	cleaned := path.Clean(treePath)
 71	if cleaned == ".." || strings.HasPrefix(cleaned, "../") {
 72		return "", ErrInvalidTreePath
 73	}
 74	if cleaned == "." {
 75		return "", nil
 76	}
 77	return cleaned, nil
 78}
 79
 80// TreeEntries returns at most 1,000 entries from a validated tree path.
 81func TreeEntries(repo *git.Repository, ref *git.Reference, treePath string) (git.Entries, bool, error) {
 82	cleaned, err := ValidateTreePath(treePath)
 83	if err != nil {
 84		return nil, false, err
 85	}
 86	tree, err := repo.TreePath(ref, cleaned)
 87	if err != nil {
 88		return nil, false, err
 89	}
 90	entries, err := tree.Entries()
 91	if err != nil {
 92		return nil, false, err
 93	}
 94	entries.Sort()
 95	if len(entries) > maxTreeEntries {
 96		return entries[:maxTreeEntries], true, nil
 97	}
 98	return entries, false, nil
 99}
100
101// TreeEntry returns a validated entry without allowing Git revision expressions.
102func TreeEntry(repo *git.Repository, ref *git.Reference, treePath string) (*git.TreeEntry, error) {
103	cleaned, err := ValidateTreePath(treePath)
104	if err != nil || cleaned == "" {
105		if err != nil {
106			return nil, err
107		}
108		return nil, fmt.Errorf("%w: root is not a file", ErrInvalidTreePath)
109	}
110	tree, err := repo.Tree(ref)
111	if err != nil {
112		return nil, err
113	}
114	return tree.TreeEntry(cleaned)
115}