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}