1// SPDX-License-Identifier: MIT
2
3package git
4
5import (
6 "bytes"
7 "fmt"
8)
9
10// Entry is an entry of a tree.
11type Entry struct {
12 // Name is the file name. It points into the tree buffer and is only
13 // valid during the callback that receives the entry; copy it to keep it.
14 Name []byte
15 Mode Mode
16 ID ObjectID
17}
18
19// ParseTree calls fn for each entry of a tree object in order, until fn
20// returns false. The entries are not copied, so iterating allocates nothing.
21// It returns ErrCorrupt on a malformed tree.
22func ParseTree(data []byte, fn func(Entry) bool) error {
23 var e Entry
24 for len(data) > 0 {
25 sp := bytes.IndexByte(data, ' ')
26 if sp <= 0 || sp > 7 {
27 return corrupt("tree: bad mode")
28 }
29 var mode uint32
30 for _, c := range data[:sp] {
31 if c < '0' || c > '7' {
32 return corrupt("tree: bad mode")
33 }
34 mode = mode<<3 | uint32(c-'0')
35 }
36 data = data[sp+1:]
37 nul := bytes.IndexByte(data, 0)
38 if nul <= 0 || len(data)-nul-1 < 20 {
39 return corrupt("tree: truncated entry")
40 }
41 e.Name = data[:nul]
42 e.Mode = Mode(mode)
43 copy(e.ID[:], data[nul+1:nul+21])
44 data = data[nul+21:]
45 if !fn(e) {
46 return nil
47 }
48 }
49 return nil
50}
51
52// ReadTree reads the tree id and calls fn for each entry, in tree order,
53// until fn returns false. Entry.Name is only valid during the call. The tree
54// is read into a pooled buffer, so listing allocates nothing per entry.
55func (r *Repo) ReadTree(id ObjectID, fn func(Entry) bool) error {
56 b, err := r.readTyped(id, TypeTree)
57 if err != nil {
58 return err
59 }
60 defer putBuf(b)
61 if err := ParseTree(b.b, fn); err != nil {
62 return wrapObj(id, err)
63 }
64 return nil
65}
66
67// LookupPath finds the entry at path (slash separated, no leading slash)
68// below the tree root. It returns ErrNotFound when a component does not
69// exist or is not a directory. An empty path designates root itself, as a
70// directory.
71func (r *Repo) LookupPath(root ObjectID, path string) (Entry, error) {
72 cur := Entry{Mode: ModeTree, ID: root}
73 for path != "" {
74 var name string
75 name, path, _ = cutByte(path, '/')
76 if name == "" {
77 return Entry{}, notFound("empty path component")
78 }
79 if !cur.Mode.IsTree() {
80 return Entry{}, notFound("%q is not a directory", name)
81 }
82 found := false
83 var next Entry
84 err := r.ReadTree(cur.ID, func(e Entry) bool {
85 if string(e.Name) == name {
86 next = Entry{Mode: e.Mode, ID: e.ID}
87 found = true
88 return false
89 }
90 return true
91 })
92 if err != nil {
93 return Entry{}, err
94 }
95 if !found {
96 return Entry{}, notFound("path component %q", name)
97 }
98 cur = next
99 }
100 return cur, nil
101}
102
103func cutByte(s string, c byte) (before, after string, found bool) {
104 for i := 0; i < len(s); i++ {
105 if s[i] == c {
106 return s[:i], s[i+1:], true
107 }
108 }
109 return s, "", false
110}
111
112func wrongType(id ObjectID, got, want Type) error {
113 return fmt.Errorf("%w: %s is a %s, not a %s", ErrWrongType, id.String(), got, want)
114}
115
116func wrapObj(id ObjectID, err error) error {
117 return fmt.Errorf("object %s: %w", id.String(), err)
118}