1package git
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "path"
8 "time"
9
10 "github.com/go-git/go-git/v5/plumbing/filemode"
11 "github.com/go-git/go-git/v5/plumbing/object"
12 "tangled.org/core/types"
13)
14
15func (g *GitRepo) FileTree(ctx context.Context, path string) ([]types.NiceTree, error) {
16 c, err := g.r.CommitObject(g.h)
17 if err != nil {
18 return nil, fmt.Errorf("commit object: %w", err)
19 }
20
21 files := []types.NiceTree{}
22 tree, err := c.Tree()
23 if err != nil {
24 return nil, fmt.Errorf("file tree: %w", err)
25 }
26
27 if path == "" {
28 files = g.makeNiceTree(ctx, tree, "")
29 } else {
30 o, err := tree.FindEntry(path)
31 if err != nil {
32 return nil, err
33 }
34
35 if !o.Mode.IsFile() {
36 subtree, err := tree.Tree(path)
37 if err != nil {
38 return nil, err
39 }
40
41 files = g.makeNiceTree(ctx, subtree, path)
42 }
43 }
44
45 return files, nil
46}
47
48func (g *GitRepo) makeNiceTree(ctx context.Context, subtree *object.Tree, parent string) []types.NiceTree {
49 nts := []types.NiceTree{}
50
51 entries := make([]string, len(subtree.Entries))
52 for _, e := range subtree.Entries {
53 entries = append(entries, e.Name)
54 }
55
56 lastCommitDir := lastCommitDir{
57 dir: parent,
58 entries: entries,
59 }
60
61 times, err := g.lastCommitDirIn(ctx, lastCommitDir, 2*time.Second)
62 if err != nil {
63 return nts
64 }
65
66 for _, e := range subtree.Entries {
67 sz, _ := subtree.Size(e.Name)
68 fpath := path.Join(parent, e.Name)
69
70 var lastCommit *types.LastCommitInfo
71 if t, ok := times[fpath]; ok {
72 lastCommit = &types.LastCommitInfo{
73 Hash: t.hash,
74 Message: t.message,
75 When: t.when,
76 }
77 }
78
79 nts = append(nts, types.NiceTree{
80 Name: e.Name,
81 Mode: e.Mode.String(),
82 Size: sz,
83 LastCommit: lastCommit,
84 })
85
86 }
87
88 return nts
89}
90
91var (
92 TerminateWalk error = errors.New("terminate walk")
93)
94
95type callback = func(node object.TreeEntry, parent *object.Tree, fullPath string) error
96
97func (g *GitRepo) Walk(
98 ctx context.Context,
99 root string,
100 cb callback,
101) error {
102 c, err := g.r.CommitObject(g.h)
103 if err != nil {
104 return fmt.Errorf("commit object: %w", err)
105 }
106
107 tree, err := c.Tree()
108 if err != nil {
109 return fmt.Errorf("file tree: %w", err)
110 }
111
112 subtree := tree
113 if root != "" {
114 subtree, err = tree.Tree(root)
115 if err != nil {
116 return fmt.Errorf("sub tree: %w", err)
117 }
118 }
119
120 return g.walkHelper(ctx, root, subtree, cb)
121}
122
123func (g *GitRepo) walkHelper(
124 ctx context.Context,
125 root string,
126 currentTree *object.Tree,
127 cb callback,
128) error {
129 for _, e := range currentTree.Entries {
130 // check if context hits deadline before processing
131 select {
132 case <-ctx.Done():
133 return ctx.Err()
134 default:
135 }
136
137 if e.Mode.IsFile() {
138 if err := cb(e, currentTree, root); errors.Is(err, TerminateWalk) {
139 return err
140 }
141 }
142
143 // e is a directory
144 if e.Mode == filemode.Dir {
145 subtree, err := currentTree.Tree(e.Name)
146 if err != nil {
147 return fmt.Errorf("sub tree %s: %w", e.Name, err)
148 }
149
150 fullPath := path.Join(root, e.Name)
151
152 err = g.walkHelper(ctx, fullPath, subtree, cb)
153 if err != nil {
154 return err
155 }
156 }
157 }
158
159 return nil
160}