Commit 7224ece

Eric Bower  ·  2023-08-19 12:52:22 -0400 EDT
parent 8d0c699
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,10 +1,37 @@
11 {{template "base" .}}
2-{{define "title"}}{{.Path}}@{{.RevData.Name}}{{end}}
2+{{define "title"}}{{.Item.Path}}@{{.RevData.Name}}{{end}}
33 {{define "meta"}}
44 <link rel="stylesheet" href="/syntax.css" />
55 {{end}}
66
77 {{define "content"}}
8- <h2 class="text-lg">{{.Path}}</h2>
8+ <div class="text-md">
9+ {{range .Item.Crumbs}}
10+ <a href="{{.URL}}">{{.Text}}</a> {{if .IsLast}}{{else}}/{{end}}
11+ {{end}}
12+ </div>
13+
14+ {{if .Repo.HideTreeLastCommit}}
15+ {{else}}
16+ <div class="box">
17+ <div class="flex items-center justify-between">
18+ <div class="flex-1">
19+ <a href="{{.Item.CommitURL}}">{{.Item.Summary}}</a>
20+ </div>
21+ <div>
22+ <a href="{{.Item.CommitURL}}">{{.Item.CommitID}}</a>
23+ </div>
24+ </div>
25+
26+ <div class="flex items-center gap-xs">
27+ <span>{{.Item.Author.Name}}</span>
28+ <span>&centerdot;</span>
29+ <span>{{.Item.When}}</span>
30+ </div>
31+ </div>
32+ {{end}}
33+
34+ <h2 class="text-lg">{{.Item.Name}}</h2>
35+
936 {{.Contents}}
1037 {{end }}
+5, -5
......@@ -5,12 +5,12 @@
55
66 {{define "content"}}
77 <div>
8+ <div><span class="font-bold">({{.NumCommits}})</span> commits</div>
89 {{range .Logs}}
910 <div class="box">
1011 <div class="flex justify-between items-center">
11- <div>
12- <a href="{{.URL}}">{{.SummaryStr}}</a>
13- </div>
12+ <a href="{{.URL}}" class="text-md">{{.SummaryStr}}</a>
13+
1414 <div class="flex gap">
1515 {{.ShortID}}
1616
......@@ -24,9 +24,9 @@
2424 </div>
2525 </div>
2626
27- <div class="flex items-center">
27+ <div class="flex items-center gap-xs">
2828 <span>{{.AuthorStr}}</span>
29- <span>&nbsp;committed&nbsp;</span>
29+ <span>&centerdot;</span>
3030 <span>{{.WhenStr}}</span>
3131 </div>
3232
+28, -15
......@@ -5,24 +5,37 @@
55
66 {{define "content"}}
77 <div>
8- {{range .Tree}}
9- <div class="flex justify-between items-center gap my-sm border-b">
10- <div class="flex-1 tree-path">
11- <a href="{{.URL}}">{{.Path}}</a>
12- </div>
13-
14- <div class="flex items-center gap">
15- {{if $.Repo.HideTreeLastCommit}}
8+ <div class="text-md mb">
9+ {{range .Tree.Crumbs}}
10+ {{if .IsLast}}
11+ <span class="font-bold">{{.Text}}</span>
1612 {{else}}
17- <div class="flex-1 tree-commit">
18- <a href="{{.CommitURL}}" title="{{.Summary}}">{{.When}}</a>
19- </div>
13+ <a href="{{.URL}}">{{.Text}}</a> {{if .IsLast}}{{else}}/{{end}}
2014 {{end}}
21- <div class="tree-size">
22- {{if .IsTextFile}}{{.NumLines}} L{{else}}{{.Size}}{{end}}
15+ {{end}}
16+ </div>
17+
18+ {{range .Tree.Items}}
19+ <div class="flex justify-between items-center gap p-sm border-b tree-row">
20+ <div class="flex-1 tree-path">
21+ <a href="{{.URL}}">{{.Name}}{{if .IsDir}}/{{end}}</a>
22+ </div>
23+
24+ <div class="flex items-center gap">
25+ {{if $.Repo.HideTreeLastCommit}}
26+ {{else}}
27+ <div class="flex-1 tree-commit">
28+ <a href="{{.CommitURL}}" title="{{.Summary}}">{{.When}}</a>
29+ </div>
30+ {{end}}
31+ <div class="tree-size">
32+ {{if .IsDir}}
33+ {{else}}
34+ {{if .IsTextFile}}{{.NumLines}} L{{else}}{{.Size}}{{end}}
35+ {{end}}
36+ </div>
2337 </div>
2438 </div>
25- </div>
26- {{end}}
39+ {{end}}
2740 </div>
2841 {{end}}
+193, -66
......@@ -117,14 +117,19 @@ type CommitData struct {
117117
118118 type TreeItem struct {
119119 IsTextFile bool
120+ IsDir bool
120121 Size string
121122 NumLines int
123+ Name string
122124 Path string
123125 URL template.URL
126+ CommitID string
124127 CommitURL template.URL
125128 Summary string
126129 When string
130+ Author *git.Signature
127131 Entry *git.TreeEntry
132+ Crumbs []*Breadcrumb
128133 }
129134
130135 type DiffRender struct {
......@@ -176,18 +181,19 @@ type SummaryPageData struct {
176181
177182 type TreePageData struct {
178183 *PageData
179- Tree []*TreeItem
184+ Tree *TreeRoot
180185 }
181186
182187 type LogPageData struct {
183188 *PageData
184- Logs []*CommitData
189+ NumCommits int
190+ Logs []*CommitData
185191 }
186192
187193 type FilePageData struct {
188194 *PageData
189195 Contents template.HTML
190- Path string
196+ Item *TreeItem
191197 }
192198
193199 type CommitPageData struct {
......@@ -347,11 +353,11 @@ func (c *Config) writeRootSummary(data *PageData, readme template.HTML) {
347353 })
348354 }
349355
350-func (c *Config) writeTree(data *PageData, tree []*TreeItem) {
351- c.Logger.Infof("writing tree (%s)", data.RevData.Name())
356+func (c *Config) writeTree(data *PageData, tree *TreeRoot) {
357+ c.Logger.Infof("writing tree (%s)", tree.Path)
352358 c.writeHtml(&WriteData{
353359 Filename: "index.html",
354- Subdir: getTreeBaseDir(data.RevData),
360+ Subdir: tree.Path,
355361 Template: "html/tree.page.tmpl",
356362 Data: &TreePageData{
357363 PageData: data,
......@@ -367,8 +373,9 @@ func (c *Config) writeLog(data *PageData, logs []*CommitData) {
367373 Subdir: getLogBaseDir(data.RevData),
368374 Template: "html/log.page.tmpl",
369375 Data: &LogPageData{
370- PageData: data,
371- Logs: logs,
376+ PageData: data,
377+ NumCommits: len(logs),
378+ Logs: logs,
372379 },
373380 })
374381 }
......@@ -414,7 +421,7 @@ func (c *Config) writeHTMLTreeFile(pageData *PageData, treeItem *TreeItem) strin
414421 Data: &FilePageData{
415422 PageData: pageData,
416423 Contents: template.HTML(contents),
417- Path: treeItem.Path,
424+ Item: treeItem,
418425 },
419426 Subdir: getFileURL(pageData.RevData, d),
420427 })
......@@ -522,8 +529,12 @@ func getLogBaseDir(info RevInfo) string {
522529 return filepath.Join("/", "logs", subdir)
523530 }
524531
532+func getFileBaseDir(info RevInfo) string {
533+ return filepath.Join(getTreeBaseDir(info), "item")
534+}
535+
525536 func getFileURL(info RevInfo, fname string) string {
526- return filepath.Join(getTreeBaseDir(info), "item", fname)
537+ return filepath.Join(getFileBaseDir(info), fname)
527538 }
528539
529540 func getTreeURL(info RevInfo) template.URL {
......@@ -672,33 +683,164 @@ func (c *Config) writeRepo() *BranchOutput {
672683 return mainOutput
673684 }
674685
686+type TreeRoot struct {
687+ Path string
688+ Items []*TreeItem
689+ Crumbs []*Breadcrumb
690+}
691+
675692 type TreeWalker struct {
676- revData *RevData
677- treeItem chan *TreeItem
693+ treeItem chan *TreeItem
694+ tree chan *TreeRoot
695+ HideTreeLastCommit bool
696+ PageData *PageData
697+ Repo *git.Repository
698+}
699+
700+type Breadcrumb struct {
701+ Text string
702+ URL template.URL
703+ IsLast bool
704+}
705+
706+func (tw *TreeWalker) calcBreadcrumbs(curpath string) []*Breadcrumb {
707+ if curpath == "" {
708+ return []*Breadcrumb{}
709+ }
710+ parts := strings.Split(curpath, string(os.PathSeparator))
711+ rootURL := template.URL(
712+ filepath.Join(
713+ getTreeBaseDir(tw.PageData.RevData),
714+ "index.html",
715+ ),
716+ )
717+
718+ crumbs := make([]*Breadcrumb, len(parts)+1)
719+ crumbs[0] = &Breadcrumb {
720+ URL: rootURL,
721+ Text: tw.PageData.Repo.RepoName,
722+ }
723+
724+ cur := ""
725+ for idx, d := range parts {
726+ crumbs[idx+1] = &Breadcrumb{
727+ Text: d,
728+ URL: template.URL(filepath.Join(getFileBaseDir(tw.PageData.RevData), cur, d, "index.html")),
729+ }
730+ if idx == len(parts) - 1 {
731+ crumbs[idx+1].IsLast = true
732+ }
733+ cur = filepath.Join(cur, d)
734+ }
735+
736+ return crumbs
737+}
738+
739+func (tw *TreeWalker) NewTreeItem(entry *git.TreeEntry, curpath string, crumbs []*Breadcrumb) *TreeItem {
740+ typ := entry.Type()
741+ fname := filepath.Join(curpath, entry.Name())
742+ item := &TreeItem{
743+ Size: toPretty(entry.Size()),
744+ Name: entry.Name(),
745+ Path: fname,
746+ Entry: entry,
747+ URL: template.URL(getFileURL(tw.PageData.RevData, fname)),
748+ Crumbs: crumbs,
749+ }
750+
751+ // `git rev-list` is pretty expensive here, so we have a flag to disable
752+ if tw.HideTreeLastCommit {
753+ // c.Logger.Info("skipping the process of finding the last commit for each file")
754+ } else {
755+ id := tw.PageData.RevData.ID()
756+ lastCommits, err := tw.Repo.RevList([]string{id}, git.RevListOptions{
757+ Path: item.Path,
758+ CommandOptions: git.CommandOptions{Args: []string{"-1"}},
759+ })
760+ bail(err)
761+
762+ var lc *git.Commit
763+ if len(lastCommits) > 0 {
764+ lc = lastCommits[0]
765+ }
766+ item.CommitURL = getCommitURL(lc.ID.String())
767+ item.CommitID = getShortID(lc.ID.String())
768+ item.Summary = lc.Summary()
769+ item.When = lc.Author.When.Format("02 Jan 06")
770+ item.Author = lc.Author
771+ }
772+
773+ fpath := getFileURL(
774+ tw.PageData.RevData,
775+ fmt.Sprintf("%s.html", fname),
776+ )
777+ if typ == git.ObjectTree {
778+ item.IsDir = true
779+ fpath = filepath.Join(
780+ getFileBaseDir(tw.PageData.RevData),
781+ curpath,
782+ entry.Name(),
783+ "index.html",
784+ )
785+ }
786+ item.URL = template.URL(fpath)
787+
788+ return item
678789 }
679790
680791 func (tw *TreeWalker) walk(tree *git.Tree, curpath string) {
681792 entries, err := tree.Entries()
682793 bail(err)
683794
795+ crumbs := tw.calcBreadcrumbs(curpath)
796+ treeEntries := []*TreeItem{}
684797 for _, entry := range entries {
685- fname := filepath.Join(curpath, entry.Name())
686798 typ := entry.Type()
799+ item := tw.NewTreeItem(entry, curpath, crumbs)
687800
688801 if typ == git.ObjectTree {
802+ item.IsDir = true
689803 re, _ := tree.Subtree(entry.Name())
690- tw.walk(re, fname)
804+ tw.walk(re, item.Path)
805+ treeEntries = append(treeEntries, item)
806+ tw.treeItem <- item
691807 } else if typ == git.ObjectBlob {
692- tw.treeItem <- &TreeItem{
693- Size: toPretty(entry.Size()),
694- Path: fname,
695- Entry: entry,
696- URL: template.URL(getFileURL(tw.revData, fname)),
697- }
808+ treeEntries = append(treeEntries, item)
809+ tw.treeItem <- item
698810 }
699811 }
700812
813+ sort.Slice(treeEntries, func(i, j int) bool {
814+ nameI := treeEntries[i].Name
815+ nameJ := treeEntries[j].Name
816+ if treeEntries[i].IsDir && treeEntries[j].IsDir {
817+ return nameI < nameJ
818+ }
819+
820+ if treeEntries[i].IsDir && !treeEntries[j].IsDir {
821+ return true
822+ }
823+
824+ return nameI < nameJ
825+ })
826+
827+ fpath := filepath.Join(
828+ getFileBaseDir(tw.PageData.RevData),
829+ curpath,
830+ )
831+ // root gets a special spot outside of `item` subdir
701832 if curpath == "" {
833+ fpath = getTreeBaseDir(tw.PageData.RevData)
834+ }
835+
836+ tw.tree <- &TreeRoot{
837+ Path: fpath,
838+ Items: treeEntries,
839+ Crumbs: crumbs,
840+ }
841+
842+ if curpath == "" {
843+ close(tw.tree)
702844 close(tw.treeItem)
703845 }
704846 }
......@@ -773,12 +915,14 @@ func (c *Config) writeRevision(repo *git.Repository, pageData *PageData, refs []
773915 tree, err := repo.LsTree(pageData.RevData.ID())
774916 bail(err)
775917
776- treeEntries := []*TreeItem{}
777918 readme := ""
778919 entries := make(chan *TreeItem)
920+ subtrees := make(chan *TreeRoot)
779921 tw := &TreeWalker{
780- revData: pageData.RevData,
922+ PageData: pageData,
923+ Repo: repo,
781924 treeItem: entries,
925+ tree: subtrees,
782926 }
783927 wg.Add(1)
784928 go func() {
......@@ -786,62 +930,45 @@ func (c *Config) writeRevision(repo *git.Repository, pageData *PageData, refs []
786930 tw.walk(tree, "")
787931 }()
788932
789- for e := range entries {
790- wg.Add(1)
791- go func(entry *TreeItem) {
792- defer wg.Done()
793- entry.Path = strings.TrimPrefix(entry.Path, "/")
794-
795- var lastCommits []*git.Commit
796- // `git rev-list` is pretty expensive here, so we have a flag to disable
797- if pageData.Repo.HideTreeLastCommit {
798- // c.Logger.Info("skipping the process of finding the last commit for each file")
799- } else {
800- lastCommits, err = repo.RevList([]string{pageData.RevData.ID()}, git.RevListOptions{
801- Path: entry.Path,
802- CommandOptions: git.CommandOptions{Args: []string{"-1"}},
803- })
804- bail(err)
805-
806- var lc *git.Commit
807- if len(lastCommits) > 0 {
808- lc = lastCommits[0]
933+ wg.Add(1)
934+ go func() {
935+ defer wg.Done()
936+ for e := range entries {
937+ wg.Add(1)
938+ go func(entry *TreeItem) {
939+ defer wg.Done()
940+ if entry.IsDir {
941+ return
809942 }
810- entry.CommitURL = getCommitURL(lc.ID.String())
811- entry.Summary = lc.Summary()
812- entry.When = lc.Author.When.Format("02 Jan 06")
813- }
814943
815- fpath := getFileURL(
816- pageData.RevData,
817- fmt.Sprintf("%s.html", entry.Path),
818- )
819- entry.URL = template.URL(fpath)
944+ readmeStr := c.writeHTMLTreeFile(pageData, entry)
945+ if readmeStr != "" {
946+ readme = readmeStr
947+ }
948+ }(e)
949+ }
950+ }()
820951
821- readmeStr := c.writeHTMLTreeFile(pageData, entry)
822- if readmeStr != "" {
823- readme = readmeStr
824- }
825- treeEntries = append(treeEntries, entry)
826- }(e)
827- }
952+ wg.Add(1)
953+ go func() {
954+ defer wg.Done()
955+ for t := range subtrees {
956+ wg.Add(1)
957+ go func(tree *TreeRoot) {
958+ defer wg.Done()
959+ c.writeTree(pageData, tree)
960+ }(t)
961+ }
962+ }()
828963
829964 wg.Wait()
830965
831- sort.Slice(treeEntries, func(i, j int) bool {
832- nameI := treeEntries[i].Path
833- nameJ := treeEntries[j].Path
834- return nameI < nameJ
835- })
836-
837966 c.Logger.Infof(
838967 "compilation complete (%s) branch (%s)",
839968 c.RepoName,
840969 pageData.RevData.Name(),
841970 )
842971
843- c.writeTree(pageData, treeEntries)
844-
845972 output.Readme = readme
846973 return output
847974 }
+17, -1
......@@ -140,7 +140,7 @@ hr {
140140 margin: 0;
141141 height: 1px;
142142 background: var(--grey);
143- margin: 2rem auto;
143+ margin: 1rem auto;
144144 text-align: center;
145145 }
146146
......@@ -314,6 +314,10 @@ figure {
314314 margin-top: 0.5rem;
315315 }
316316
317+.mb {
318+ margin-bottom: 0.5rem;
319+}
320+
317321 .mt-lg {
318322 margin-top: 1.35rem;
319323 }
......@@ -343,6 +347,10 @@ figure {
343347 margin-right: 1rem;
344348 }
345349
350+.p-sm {
351+ padding: 0.5rem;
352+}
353+
346354 .justify-between {
347355 justify-content: space-between;
348356 }
......@@ -355,6 +363,10 @@ figure {
355363 gap: 1rem;
356364 }
357365
366+.gap-xs {
367+ gap: 0.25rem;
368+}
369+
358370 .border-b {
359371 border-bottom: 1px solid #666;
360372 }
......@@ -368,6 +380,10 @@ figure {
368380 text-wrap: wrap;
369381 }
370382
383+.tree-row:hover {
384+ background-color: var(--grey);
385+}
386+
371387 @media only screen and (max-width: 900px) {
372388 body {
373389 padding: 1rem;