- commit
- 7224ece
- parent
- 8d0c699
- author
- Eric Bower
- date
- 2023-08-19 16:52:22 +0000 UTC
feat: nested tree folder structure This feature is designed to support a folder/file structure that most people are familiar with. Instead of just having every single file in a large list, we nest files inside of folders. The main benefit here is that for large repos with a lot of files we don't ask end-users to download a massive MB tree file. It's more clicking -- which I don't like -- but this seems necessary when there are 10k+ files in a project.
5 files changed,
+272,
-89
+29,
-2
1@@ -1,10 +1,37 @@
2 {{template "base" .}}
3-{{define "title"}}{{.Path}}@{{.RevData.Name}}{{end}}
4+{{define "title"}}{{.Item.Path}}@{{.RevData.Name}}{{end}}
5 {{define "meta"}}
6 <link rel="stylesheet" href="/syntax.css" />
7 {{end}}
8
9 {{define "content"}}
10- <h2 class="text-lg">{{.Path}}</h2>
11+ <div class="text-md">
12+ {{range .Item.Crumbs}}
13+ <a href="{{.URL}}">{{.Text}}</a> {{if .IsLast}}{{else}}/{{end}}
14+ {{end}}
15+ </div>
16+
17+ {{if .Repo.HideTreeLastCommit}}
18+ {{else}}
19+ <div class="box">
20+ <div class="flex items-center justify-between">
21+ <div class="flex-1">
22+ <a href="{{.Item.CommitURL}}">{{.Item.Summary}}</a>
23+ </div>
24+ <div>
25+ <a href="{{.Item.CommitURL}}">{{.Item.CommitID}}</a>
26+ </div>
27+ </div>
28+
29+ <div class="flex items-center gap-xs">
30+ <span>{{.Item.Author.Name}}</span>
31+ <span>·</span>
32+ <span>{{.Item.When}}</span>
33+ </div>
34+ </div>
35+ {{end}}
36+
37+ <h2 class="text-lg">{{.Item.Name}}</h2>
38+
39 {{.Contents}}
40 {{end }}
+5,
-5
1@@ -5,12 +5,12 @@
2
3 {{define "content"}}
4 <div>
5+ <div><span class="font-bold">({{.NumCommits}})</span> commits</div>
6 {{range .Logs}}
7 <div class="box">
8 <div class="flex justify-between items-center">
9- <div>
10- <a href="{{.URL}}">{{.SummaryStr}}</a>
11- </div>
12+ <a href="{{.URL}}" class="text-md">{{.SummaryStr}}</a>
13+
14 <div class="flex gap">
15 {{.ShortID}}
16
17@@ -24,9 +24,9 @@
18 </div>
19 </div>
20
21- <div class="flex items-center">
22+ <div class="flex items-center gap-xs">
23 <span>{{.AuthorStr}}</span>
24- <span> committed </span>
25+ <span>·</span>
26 <span>{{.WhenStr}}</span>
27 </div>
28
+28,
-15
1@@ -5,24 +5,37 @@
2
3 {{define "content"}}
4 <div>
5- {{range .Tree}}
6- <div class="flex justify-between items-center gap my-sm border-b">
7- <div class="flex-1 tree-path">
8- <a href="{{.URL}}">{{.Path}}</a>
9- </div>
10-
11- <div class="flex items-center gap">
12- {{if $.Repo.HideTreeLastCommit}}
13+ <div class="text-md mb">
14+ {{range .Tree.Crumbs}}
15+ {{if .IsLast}}
16+ <span class="font-bold">{{.Text}}</span>
17 {{else}}
18- <div class="flex-1 tree-commit">
19- <a href="{{.CommitURL}}" title="{{.Summary}}">{{.When}}</a>
20- </div>
21+ <a href="{{.URL}}">{{.Text}}</a> {{if .IsLast}}{{else}}/{{end}}
22 {{end}}
23- <div class="tree-size">
24- {{if .IsTextFile}}{{.NumLines}} L{{else}}{{.Size}}{{end}}
25+ {{end}}
26+ </div>
27+
28+ {{range .Tree.Items}}
29+ <div class="flex justify-between items-center gap p-sm border-b tree-row">
30+ <div class="flex-1 tree-path">
31+ <a href="{{.URL}}">{{.Name}}{{if .IsDir}}/{{end}}</a>
32+ </div>
33+
34+ <div class="flex items-center gap">
35+ {{if $.Repo.HideTreeLastCommit}}
36+ {{else}}
37+ <div class="flex-1 tree-commit">
38+ <a href="{{.CommitURL}}" title="{{.Summary}}">{{.When}}</a>
39+ </div>
40+ {{end}}
41+ <div class="tree-size">
42+ {{if .IsDir}}
43+ {{else}}
44+ {{if .IsTextFile}}{{.NumLines}} L{{else}}{{.Size}}{{end}}
45+ {{end}}
46+ </div>
47 </div>
48 </div>
49- </div>
50- {{end}}
51+ {{end}}
52 </div>
53 {{end}}
M
main.go
+193,
-66
1@@ -117,14 +117,19 @@ type CommitData struct {
2
3 type TreeItem struct {
4 IsTextFile bool
5+ IsDir bool
6 Size string
7 NumLines int
8+ Name string
9 Path string
10 URL template.URL
11+ CommitID string
12 CommitURL template.URL
13 Summary string
14 When string
15+ Author *git.Signature
16 Entry *git.TreeEntry
17+ Crumbs []*Breadcrumb
18 }
19
20 type DiffRender struct {
21@@ -176,18 +181,19 @@ type SummaryPageData struct {
22
23 type TreePageData struct {
24 *PageData
25- Tree []*TreeItem
26+ Tree *TreeRoot
27 }
28
29 type LogPageData struct {
30 *PageData
31- Logs []*CommitData
32+ NumCommits int
33+ Logs []*CommitData
34 }
35
36 type FilePageData struct {
37 *PageData
38 Contents template.HTML
39- Path string
40+ Item *TreeItem
41 }
42
43 type CommitPageData struct {
44@@ -347,11 +353,11 @@ func (c *Config) writeRootSummary(data *PageData, readme template.HTML) {
45 })
46 }
47
48-func (c *Config) writeTree(data *PageData, tree []*TreeItem) {
49- c.Logger.Infof("writing tree (%s)", data.RevData.Name())
50+func (c *Config) writeTree(data *PageData, tree *TreeRoot) {
51+ c.Logger.Infof("writing tree (%s)", tree.Path)
52 c.writeHtml(&WriteData{
53 Filename: "index.html",
54- Subdir: getTreeBaseDir(data.RevData),
55+ Subdir: tree.Path,
56 Template: "html/tree.page.tmpl",
57 Data: &TreePageData{
58 PageData: data,
59@@ -367,8 +373,9 @@ func (c *Config) writeLog(data *PageData, logs []*CommitData) {
60 Subdir: getLogBaseDir(data.RevData),
61 Template: "html/log.page.tmpl",
62 Data: &LogPageData{
63- PageData: data,
64- Logs: logs,
65+ PageData: data,
66+ NumCommits: len(logs),
67+ Logs: logs,
68 },
69 })
70 }
71@@ -414,7 +421,7 @@ func (c *Config) writeHTMLTreeFile(pageData *PageData, treeItem *TreeItem) strin
72 Data: &FilePageData{
73 PageData: pageData,
74 Contents: template.HTML(contents),
75- Path: treeItem.Path,
76+ Item: treeItem,
77 },
78 Subdir: getFileURL(pageData.RevData, d),
79 })
80@@ -522,8 +529,12 @@ func getLogBaseDir(info RevInfo) string {
81 return filepath.Join("/", "logs", subdir)
82 }
83
84+func getFileBaseDir(info RevInfo) string {
85+ return filepath.Join(getTreeBaseDir(info), "item")
86+}
87+
88 func getFileURL(info RevInfo, fname string) string {
89- return filepath.Join(getTreeBaseDir(info), "item", fname)
90+ return filepath.Join(getFileBaseDir(info), fname)
91 }
92
93 func getTreeURL(info RevInfo) template.URL {
94@@ -672,33 +683,164 @@ func (c *Config) writeRepo() *BranchOutput {
95 return mainOutput
96 }
97
98+type TreeRoot struct {
99+ Path string
100+ Items []*TreeItem
101+ Crumbs []*Breadcrumb
102+}
103+
104 type TreeWalker struct {
105- revData *RevData
106- treeItem chan *TreeItem
107+ treeItem chan *TreeItem
108+ tree chan *TreeRoot
109+ HideTreeLastCommit bool
110+ PageData *PageData
111+ Repo *git.Repository
112+}
113+
114+type Breadcrumb struct {
115+ Text string
116+ URL template.URL
117+ IsLast bool
118+}
119+
120+func (tw *TreeWalker) calcBreadcrumbs(curpath string) []*Breadcrumb {
121+ if curpath == "" {
122+ return []*Breadcrumb{}
123+ }
124+ parts := strings.Split(curpath, string(os.PathSeparator))
125+ rootURL := template.URL(
126+ filepath.Join(
127+ getTreeBaseDir(tw.PageData.RevData),
128+ "index.html",
129+ ),
130+ )
131+
132+ crumbs := make([]*Breadcrumb, len(parts)+1)
133+ crumbs[0] = &Breadcrumb {
134+ URL: rootURL,
135+ Text: tw.PageData.Repo.RepoName,
136+ }
137+
138+ cur := ""
139+ for idx, d := range parts {
140+ crumbs[idx+1] = &Breadcrumb{
141+ Text: d,
142+ URL: template.URL(filepath.Join(getFileBaseDir(tw.PageData.RevData), cur, d, "index.html")),
143+ }
144+ if idx == len(parts) - 1 {
145+ crumbs[idx+1].IsLast = true
146+ }
147+ cur = filepath.Join(cur, d)
148+ }
149+
150+ return crumbs
151+}
152+
153+func (tw *TreeWalker) NewTreeItem(entry *git.TreeEntry, curpath string, crumbs []*Breadcrumb) *TreeItem {
154+ typ := entry.Type()
155+ fname := filepath.Join(curpath, entry.Name())
156+ item := &TreeItem{
157+ Size: toPretty(entry.Size()),
158+ Name: entry.Name(),
159+ Path: fname,
160+ Entry: entry,
161+ URL: template.URL(getFileURL(tw.PageData.RevData, fname)),
162+ Crumbs: crumbs,
163+ }
164+
165+ // `git rev-list` is pretty expensive here, so we have a flag to disable
166+ if tw.HideTreeLastCommit {
167+ // c.Logger.Info("skipping the process of finding the last commit for each file")
168+ } else {
169+ id := tw.PageData.RevData.ID()
170+ lastCommits, err := tw.Repo.RevList([]string{id}, git.RevListOptions{
171+ Path: item.Path,
172+ CommandOptions: git.CommandOptions{Args: []string{"-1"}},
173+ })
174+ bail(err)
175+
176+ var lc *git.Commit
177+ if len(lastCommits) > 0 {
178+ lc = lastCommits[0]
179+ }
180+ item.CommitURL = getCommitURL(lc.ID.String())
181+ item.CommitID = getShortID(lc.ID.String())
182+ item.Summary = lc.Summary()
183+ item.When = lc.Author.When.Format("02 Jan 06")
184+ item.Author = lc.Author
185+ }
186+
187+ fpath := getFileURL(
188+ tw.PageData.RevData,
189+ fmt.Sprintf("%s.html", fname),
190+ )
191+ if typ == git.ObjectTree {
192+ item.IsDir = true
193+ fpath = filepath.Join(
194+ getFileBaseDir(tw.PageData.RevData),
195+ curpath,
196+ entry.Name(),
197+ "index.html",
198+ )
199+ }
200+ item.URL = template.URL(fpath)
201+
202+ return item
203 }
204
205 func (tw *TreeWalker) walk(tree *git.Tree, curpath string) {
206 entries, err := tree.Entries()
207 bail(err)
208
209+ crumbs := tw.calcBreadcrumbs(curpath)
210+ treeEntries := []*TreeItem{}
211 for _, entry := range entries {
212- fname := filepath.Join(curpath, entry.Name())
213 typ := entry.Type()
214+ item := tw.NewTreeItem(entry, curpath, crumbs)
215
216 if typ == git.ObjectTree {
217+ item.IsDir = true
218 re, _ := tree.Subtree(entry.Name())
219- tw.walk(re, fname)
220+ tw.walk(re, item.Path)
221+ treeEntries = append(treeEntries, item)
222+ tw.treeItem <- item
223 } else if typ == git.ObjectBlob {
224- tw.treeItem <- &TreeItem{
225- Size: toPretty(entry.Size()),
226- Path: fname,
227- Entry: entry,
228- URL: template.URL(getFileURL(tw.revData, fname)),
229- }
230+ treeEntries = append(treeEntries, item)
231+ tw.treeItem <- item
232 }
233 }
234
235+ sort.Slice(treeEntries, func(i, j int) bool {
236+ nameI := treeEntries[i].Name
237+ nameJ := treeEntries[j].Name
238+ if treeEntries[i].IsDir && treeEntries[j].IsDir {
239+ return nameI < nameJ
240+ }
241+
242+ if treeEntries[i].IsDir && !treeEntries[j].IsDir {
243+ return true
244+ }
245+
246+ return nameI < nameJ
247+ })
248+
249+ fpath := filepath.Join(
250+ getFileBaseDir(tw.PageData.RevData),
251+ curpath,
252+ )
253+ // root gets a special spot outside of `item` subdir
254 if curpath == "" {
255+ fpath = getTreeBaseDir(tw.PageData.RevData)
256+ }
257+
258+ tw.tree <- &TreeRoot{
259+ Path: fpath,
260+ Items: treeEntries,
261+ Crumbs: crumbs,
262+ }
263+
264+ if curpath == "" {
265+ close(tw.tree)
266 close(tw.treeItem)
267 }
268 }
269@@ -773,12 +915,14 @@ func (c *Config) writeRevision(repo *git.Repository, pageData *PageData, refs []
270 tree, err := repo.LsTree(pageData.RevData.ID())
271 bail(err)
272
273- treeEntries := []*TreeItem{}
274 readme := ""
275 entries := make(chan *TreeItem)
276+ subtrees := make(chan *TreeRoot)
277 tw := &TreeWalker{
278- revData: pageData.RevData,
279+ PageData: pageData,
280+ Repo: repo,
281 treeItem: entries,
282+ tree: subtrees,
283 }
284 wg.Add(1)
285 go func() {
286@@ -786,62 +930,45 @@ func (c *Config) writeRevision(repo *git.Repository, pageData *PageData, refs []
287 tw.walk(tree, "")
288 }()
289
290- for e := range entries {
291- wg.Add(1)
292- go func(entry *TreeItem) {
293- defer wg.Done()
294- entry.Path = strings.TrimPrefix(entry.Path, "/")
295-
296- var lastCommits []*git.Commit
297- // `git rev-list` is pretty expensive here, so we have a flag to disable
298- if pageData.Repo.HideTreeLastCommit {
299- // c.Logger.Info("skipping the process of finding the last commit for each file")
300- } else {
301- lastCommits, err = repo.RevList([]string{pageData.RevData.ID()}, git.RevListOptions{
302- Path: entry.Path,
303- CommandOptions: git.CommandOptions{Args: []string{"-1"}},
304- })
305- bail(err)
306-
307- var lc *git.Commit
308- if len(lastCommits) > 0 {
309- lc = lastCommits[0]
310+ wg.Add(1)
311+ go func() {
312+ defer wg.Done()
313+ for e := range entries {
314+ wg.Add(1)
315+ go func(entry *TreeItem) {
316+ defer wg.Done()
317+ if entry.IsDir {
318+ return
319 }
320- entry.CommitURL = getCommitURL(lc.ID.String())
321- entry.Summary = lc.Summary()
322- entry.When = lc.Author.When.Format("02 Jan 06")
323- }
324
325- fpath := getFileURL(
326- pageData.RevData,
327- fmt.Sprintf("%s.html", entry.Path),
328- )
329- entry.URL = template.URL(fpath)
330+ readmeStr := c.writeHTMLTreeFile(pageData, entry)
331+ if readmeStr != "" {
332+ readme = readmeStr
333+ }
334+ }(e)
335+ }
336+ }()
337
338- readmeStr := c.writeHTMLTreeFile(pageData, entry)
339- if readmeStr != "" {
340- readme = readmeStr
341- }
342- treeEntries = append(treeEntries, entry)
343- }(e)
344- }
345+ wg.Add(1)
346+ go func() {
347+ defer wg.Done()
348+ for t := range subtrees {
349+ wg.Add(1)
350+ go func(tree *TreeRoot) {
351+ defer wg.Done()
352+ c.writeTree(pageData, tree)
353+ }(t)
354+ }
355+ }()
356
357 wg.Wait()
358
359- sort.Slice(treeEntries, func(i, j int) bool {
360- nameI := treeEntries[i].Path
361- nameJ := treeEntries[j].Path
362- return nameI < nameJ
363- })
364-
365 c.Logger.Infof(
366 "compilation complete (%s) branch (%s)",
367 c.RepoName,
368 pageData.RevData.Name(),
369 )
370
371- c.writeTree(pageData, treeEntries)
372-
373 output.Readme = readme
374 return output
375 }
+17,
-1
1@@ -140,7 +140,7 @@ hr {
2 margin: 0;
3 height: 1px;
4 background: var(--grey);
5- margin: 2rem auto;
6+ margin: 1rem auto;
7 text-align: center;
8 }
9
10@@ -314,6 +314,10 @@ figure {
11 margin-top: 0.5rem;
12 }
13
14+.mb {
15+ margin-bottom: 0.5rem;
16+}
17+
18 .mt-lg {
19 margin-top: 1.35rem;
20 }
21@@ -343,6 +347,10 @@ figure {
22 margin-right: 1rem;
23 }
24
25+.p-sm {
26+ padding: 0.5rem;
27+}
28+
29 .justify-between {
30 justify-content: space-between;
31 }
32@@ -355,6 +363,10 @@ figure {
33 gap: 1rem;
34 }
35
36+.gap-xs {
37+ gap: 0.25rem;
38+}
39+
40 .border-b {
41 border-bottom: 1px solid #666;
42 }
43@@ -368,6 +380,10 @@ figure {
44 text-wrap: wrap;
45 }
46
47+.tree-row:hover {
48+ background-color: var(--grey);
49+}
50+
51 @media only screen and (max-width: 900px) {
52 body {
53 padding: 1rem;