Eric Bower
·
25 Apr 24
main.go
1package main
2
3import (
4 "bytes"
5 "embed"
6 _ "embed"
7 "flag"
8 "fmt"
9 "html/template"
10 "log/slog"
11 "math"
12 "os"
13 "path/filepath"
14 "sort"
15 "strings"
16 "sync"
17 "unicode/utf8"
18
19 "github.com/alecthomas/chroma/v2"
20 formatterHtml "github.com/alecthomas/chroma/v2/formatters/html"
21 "github.com/alecthomas/chroma/v2/lexers"
22 "github.com/alecthomas/chroma/v2/styles"
23 "github.com/dustin/go-humanize"
24 git "github.com/gogs/git-module"
25)
26
27//go:embed html/*.tmpl static/*
28var efs embed.FS
29
30type Config struct {
31 // required params
32 Outdir string
33 // abs path to git repo
34 RepoPath string
35
36 // optional params
37 // generate logs anad tree based on the git revisions provided
38 Revs []string
39 // description of repo used in the header of site
40 Desc string
41 // maximum number of commits that we will process in descending order
42 MaxCommits int
43 // name of the readme file
44 Readme string
45 // In order to get the latest commit per file we do a `git rev-list {ref} {file}`
46 // which is n+1 where n is a file in the tree.
47 // We offer a way to disable showing the latest commit in the output
48 // for those who want a faster build time
49 HideTreeLastCommit bool
50
51 // user-defined urls
52 HomeURL template.URL
53 CloneURL template.URL
54
55 // computed
56 // cache for skipping commits, trees, etc.
57 Cache map[string]bool
58 // mutex for Cache
59 Mutex sync.RWMutex
60 // pretty name for the repo
61 RepoName string
62 // logger
63 Logger *slog.Logger
64 // chroma style
65 Theme *chroma.Style
66 Formatter *formatterHtml.Formatter
67}
68
69type RevInfo interface {
70 ID() string
71 Name() string
72}
73
74// revision data
75type RevData struct {
76 id string
77 name string
78}
79
80func (r *RevData) ID() string {
81 return r.id
82}
83
84func (r *RevData) Name() string {
85 return r.name
86}
87
88func (r *RevData) TreeURL() template.URL {
89 return getTreeURL(r)
90}
91
92func (r *RevData) LogURL() template.URL {
93 return getLogsURL(r)
94}
95
96type TagData struct {
97 Name string
98 URL template.URL
99}
100
101type CommitData struct {
102 SummaryStr string
103 URL template.URL
104 WhenStr string
105 AuthorStr string
106 ShortID string
107 ParentID string
108 Refs []*RefInfo
109 *git.Commit
110}
111
112type TreeItem struct {
113 IsTextFile bool
114 IsDir bool
115 Size string
116 NumLines int
117 Name string
118 Icon string
119 Path string
120 URL template.URL
121 CommitID string
122 CommitURL template.URL
123 Summary string
124 When string
125 Author *git.Signature
126 Entry *git.TreeEntry
127 Crumbs []*Breadcrumb
128}
129
130type DiffRender struct {
131 NumFiles int
132 TotalAdditions int
133 TotalDeletions int
134 Files []*DiffRenderFile
135}
136
137type DiffRenderFile struct {
138 FileType string
139 OldMode git.EntryMode
140 OldName string
141 Mode git.EntryMode
142 Name string
143 Content template.HTML
144 NumAdditions int
145 NumDeletions int
146}
147
148type RefInfo struct {
149 ID string
150 Refspec string
151 URL template.URL
152}
153
154type BranchOutput struct {
155 Readme string
156 LastCommit *git.Commit
157}
158
159type SiteURLs struct {
160 HomeURL template.URL
161 CloneURL template.URL
162 SummaryURL template.URL
163 RefsURL template.URL
164}
165
166type PageData struct {
167 Repo *Config
168 SiteURLs *SiteURLs
169 RevData *RevData
170}
171
172type SummaryPageData struct {
173 *PageData
174 Readme template.HTML
175}
176
177type TreePageData struct {
178 *PageData
179 Tree *TreeRoot
180}
181
182type LogPageData struct {
183 *PageData
184 NumCommits int
185 Logs []*CommitData
186}
187
188type FilePageData struct {
189 *PageData
190 Contents template.HTML
191 Item *TreeItem
192}
193
194type CommitPageData struct {
195 *PageData
196 CommitMsg template.HTML
197 CommitID string
198 Commit *CommitData
199 Diff *DiffRender
200 Parent string
201 ParentURL template.URL
202 CommitURL template.URL
203}
204
205type RefPageData struct {
206 *PageData
207 Refs []*RefInfo
208}
209
210type WriteData struct {
211 Template string
212 Filename string
213 Subdir string
214 Data interface{}
215}
216
217func bail(err error) {
218 if err != nil {
219 panic(err)
220 }
221}
222
223func diffFileType(_type git.DiffFileType) string {
224 if _type == git.DiffFileAdd {
225 return "A"
226 } else if _type == git.DiffFileChange {
227 return "M"
228 } else if _type == git.DiffFileDelete {
229 return "D"
230 } else if _type == git.DiffFileRename {
231 return "R"
232 }
233
234 return ""
235}
236
237// converts contents of files in git tree to pretty formatted code
238func (c *Config) parseText(filename string, text string) (string, error) {
239 lexer := lexers.Match(filename)
240 if lexer == nil {
241 lexer = lexers.Analyse(text)
242 }
243 if lexer == nil {
244 lexer = lexers.Get("plaintext")
245 }
246 iterator, err := lexer.Tokenise(nil, text)
247 if err != nil {
248 return text, err
249 }
250 var buf bytes.Buffer
251 err = c.Formatter.Format(&buf, c.Theme, iterator)
252 if err != nil {
253 return text, err
254 }
255 return buf.String(), nil
256}
257
258// isText reports whether a significant prefix of s looks like correct UTF-8;
259// that is, if it is likely that s is human-readable text.
260func isText(s string) bool {
261 const max = 1024 // at least utf8.UTFMax
262 if len(s) > max {
263 s = s[0:max]
264 }
265 for i, c := range s {
266 if i+utf8.UTFMax > len(s) {
267 // last char may be incomplete - ignore
268 break
269 }
270 if c == 0xFFFD || c < ' ' && c != '\n' && c != '\t' && c != '\f' && c != '\r' {
271 // decoding error or control character - not a text file
272 return false
273 }
274 }
275 return true
276}
277
278// isTextFile reports whether the file has a known extension indicating
279// a text file, or if a significant chunk of the specified file looks like
280// correct UTF-8; that is, if it is likely that the file contains human-
281// readable text.
282func isTextFile(text string) bool {
283 num := math.Min(float64(len(text)), 1024)
284 return isText(text[0:int(num)])
285}
286
287func toPretty(b int64) string {
288 return humanize.Bytes(uint64(b))
289}
290
291func repoName(root string) string {
292 _, file := filepath.Split(root)
293 return file
294}
295
296func readmeFile(repo *Config) string {
297 if repo.Readme == "" {
298 return "readme.md"
299 }
300
301 return strings.ToLower(repo.Readme)
302}
303
304func (c *Config) writeHtml(writeData *WriteData) {
305 ts, err := template.ParseFS(
306 efs,
307 writeData.Template,
308 "html/header.partial.tmpl",
309 "html/footer.partial.tmpl",
310 "html/base.layout.tmpl",
311 )
312 bail(err)
313
314 dir := filepath.Join(c.Outdir, writeData.Subdir)
315 err = os.MkdirAll(dir, os.ModePerm)
316 bail(err)
317
318 fp := filepath.Join(dir, writeData.Filename)
319 c.Logger.Info("writing", "filepath", fp)
320
321 w, err := os.OpenFile(fp, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
322 bail(err)
323
324 err = ts.Execute(w, writeData.Data)
325 bail(err)
326}
327
328func (c *Config) copyStatic(dir string) error {
329 entries, err := efs.ReadDir(dir)
330 bail(err)
331
332 for _, e := range entries {
333 infp := filepath.Join(dir, e.Name())
334 if e.IsDir() {
335 continue
336 }
337
338 w, err := efs.ReadFile(infp)
339 bail(err)
340 fp := filepath.Join(c.Outdir, e.Name())
341 c.Logger.Info("writing", "filepath", fp)
342 os.WriteFile(fp, w, 0644)
343 }
344
345 return nil
346}
347
348func (c *Config) writeRootSummary(data *PageData, readme template.HTML) {
349 c.Logger.Info("writing root html", "repoPath", c.RepoPath)
350 c.writeHtml(&WriteData{
351 Filename: "index.html",
352 Template: "html/summary.page.tmpl",
353 Data: &SummaryPageData{
354 PageData: data,
355 Readme: readme,
356 },
357 })
358}
359
360func (c *Config) writeTree(data *PageData, tree *TreeRoot) {
361 c.Logger.Info("writing tree", "treePath", tree.Path)
362 c.writeHtml(&WriteData{
363 Filename: "index.html",
364 Subdir: tree.Path,
365 Template: "html/tree.page.tmpl",
366 Data: &TreePageData{
367 PageData: data,
368 Tree: tree,
369 },
370 })
371}
372
373func (c *Config) writeLog(data *PageData, logs []*CommitData) {
374 c.Logger.Info("writing log file", "revision", data.RevData.Name())
375 c.writeHtml(&WriteData{
376 Filename: "index.html",
377 Subdir: getLogBaseDir(data.RevData),
378 Template: "html/log.page.tmpl",
379 Data: &LogPageData{
380 PageData: data,
381 NumCommits: len(logs),
382 Logs: logs,
383 },
384 })
385}
386
387func (c *Config) writeRefs(data *PageData, refs []*RefInfo) {
388 c.Logger.Info("writing refs", "repoPath", c.RepoPath)
389 c.writeHtml(&WriteData{
390 Filename: "refs.html",
391 Template: "html/refs.page.tmpl",
392 Data: &RefPageData{
393 PageData: data,
394 Refs: refs,
395 },
396 })
397}
398
399func (c *Config) writeHTMLTreeFile(pageData *PageData, treeItem *TreeItem) string {
400 readme := ""
401 b, err := treeItem.Entry.Blob().Bytes()
402 bail(err)
403 str := string(b)
404
405 treeItem.IsTextFile = isTextFile(str)
406
407 contents := "binary file, cannot display"
408 if treeItem.IsTextFile {
409 treeItem.NumLines = len(strings.Split(str, "\n"))
410 contents, err = c.parseText(treeItem.Entry.Name(), string(b))
411 bail(err)
412 }
413
414 d := filepath.Dir(treeItem.Path)
415
416 nameLower := strings.ToLower(treeItem.Entry.Name())
417 summary := readmeFile(pageData.Repo)
418 if nameLower == summary {
419 readme = contents
420 }
421
422 c.writeHtml(&WriteData{
423 Filename: fmt.Sprintf("%s.html", treeItem.Entry.Name()),
424 Template: "html/file.page.tmpl",
425 Data: &FilePageData{
426 PageData: pageData,
427 Contents: template.HTML(contents),
428 Item: treeItem,
429 },
430 Subdir: getFileURL(pageData.RevData, d),
431 })
432 return readme
433}
434
435func (c *Config) writeLogDiff(repo *git.Repository, pageData *PageData, commit *CommitData) {
436 commitID := commit.ID.String()
437
438 c.Mutex.RLock()
439 hasCommit := c.Cache[commitID]
440 c.Mutex.RUnlock()
441
442 if hasCommit {
443 c.Logger.Info("commit file already generated, skipping", "commitID", getShortID(commitID))
444 return
445 } else {
446 c.Mutex.Lock()
447 c.Cache[commitID] = true
448 c.Mutex.Unlock()
449 }
450
451 diff, err := repo.Diff(
452 commitID,
453 0,
454 0,
455 0,
456 git.DiffOptions{},
457 )
458 bail(err)
459
460 rnd := &DiffRender{
461 NumFiles: diff.NumFiles(),
462 TotalAdditions: diff.TotalAdditions(),
463 TotalDeletions: diff.TotalDeletions(),
464 }
465 fls := []*DiffRenderFile{}
466 for _, file := range diff.Files {
467 fl := &DiffRenderFile{
468 FileType: diffFileType(file.Type),
469 OldMode: file.OldMode(),
470 OldName: file.OldName(),
471 Mode: file.Mode(),
472 Name: file.Name,
473 NumAdditions: file.NumAdditions(),
474 NumDeletions: file.NumDeletions(),
475 }
476 content := ""
477 for _, section := range file.Sections {
478 for _, line := range section.Lines {
479 content += fmt.Sprintf("%s\n", line.Content)
480 }
481 }
482 // set filename to something our `ParseText` recognizes (e.g. `.diff`)
483 finContent, err := c.parseText("commit.diff", content)
484 bail(err)
485
486 fl.Content = template.HTML(finContent)
487 fls = append(fls, fl)
488 }
489 rnd.Files = fls
490
491 commitData := &CommitPageData{
492 PageData: pageData,
493 Commit: commit,
494 CommitID: getShortID(commitID),
495 Diff: rnd,
496 Parent: getShortID(commit.ParentID),
497 CommitURL: getCommitURL(commitID),
498 ParentURL: getCommitURL(commit.ParentID),
499 }
500
501 c.writeHtml(&WriteData{
502 Filename: fmt.Sprintf("%s.html", commitID),
503 Template: "html/commit.page.tmpl",
504 Subdir: "commits",
505 Data: commitData,
506 })
507}
508
509func getSummaryURL() template.URL {
510 url := "/index.html"
511 return template.URL(url)
512}
513
514func getRefsURL() template.URL {
515 url := "/refs.html"
516 return template.URL(url)
517}
518
519// controls the url for trees and logs
520// /logs/getRevIDForURL()/index.html
521// /tree/getRevIDForURL()/item/file.x.html
522func getRevIDForURL(info RevInfo) string {
523 return info.Name()
524}
525
526func getTreeBaseDir(info RevInfo) string {
527 subdir := getRevIDForURL(info)
528 return filepath.Join("/", "tree", subdir)
529}
530
531func getLogBaseDir(info RevInfo) string {
532 subdir := getRevIDForURL(info)
533 return filepath.Join("/", "logs", subdir)
534}
535
536func getFileBaseDir(info RevInfo) string {
537 return filepath.Join(getTreeBaseDir(info), "item")
538}
539
540func getFileURL(info RevInfo, fname string) string {
541 return filepath.Join(getFileBaseDir(info), fname)
542}
543
544func getTreeURL(info RevInfo) template.URL {
545 dir := getTreeBaseDir(info)
546 url := filepath.Join(dir, "index.html")
547 return template.URL(url)
548}
549
550func getLogsURL(info RevInfo) template.URL {
551 dir := getLogBaseDir(info)
552 url := filepath.Join(dir, "index.html")
553 return template.URL(url)
554}
555
556func getCommitURL(commitID string) template.URL {
557 url := fmt.Sprintf("/commits/%s.html", commitID)
558 return template.URL(url)
559}
560
561func (c *Config) getURLs() *SiteURLs {
562 return &SiteURLs{
563 HomeURL: c.HomeURL,
564 CloneURL: c.CloneURL,
565 RefsURL: getRefsURL(),
566 SummaryURL: getSummaryURL(),
567 }
568}
569
570func getShortID(id string) string {
571 return id[:7]
572}
573
574func (c *Config) writeRepo() *BranchOutput {
575 c.Logger.Info("writing repo", "repoPath", c.RepoPath)
576 repo, err := git.Open(c.RepoPath)
577 bail(err)
578
579 refs, err := repo.ShowRef(git.ShowRefOptions{Heads: true, Tags: true})
580 bail(err)
581
582 var first *RevData
583 revs := []*RevData{}
584 for _, revStr := range c.Revs {
585 fullRevID, err := repo.RevParse(revStr)
586 bail(err)
587
588 revID := getShortID(fullRevID)
589 revName := revID
590 // if it's a reference then label it as such
591 for _, ref := range refs {
592 if revStr == git.RefShortName(ref.Refspec) || revStr == ref.Refspec {
593 revName = revStr
594 break
595 }
596 }
597
598 data := &RevData{
599 id: fullRevID,
600 name: revName,
601 }
602
603 if first == nil {
604 first = data
605 }
606 revs = append(revs, data)
607 }
608
609 if first == nil {
610 bail(fmt.Errorf("could find find a git reference that matches criteria"))
611 }
612
613 refInfoMap := map[string]*RefInfo{}
614 mainOutput := &BranchOutput{}
615 claimed := false
616 for _, revData := range revs {
617 refInfoMap[revData.Name()] = &RefInfo{
618 ID: revData.ID(),
619 Refspec: revData.Name(),
620 URL: revData.TreeURL(),
621 }
622 }
623
624 // loop through ALL refs that don't have URLs
625 // and add them to the map
626 for _, ref := range refs {
627 refspec := git.RefShortName(ref.Refspec)
628 if refInfoMap[refspec] != nil {
629 continue
630 }
631
632 refInfoMap[refspec] = &RefInfo{
633 ID: ref.ID,
634 Refspec: refspec,
635 }
636 }
637
638 // gather lists of refs to display on refs.html page
639 refInfoList := []*RefInfo{}
640 for _, val := range refInfoMap {
641 refInfoList = append(refInfoList, val)
642 }
643 sort.Slice(refInfoList, func(i, j int) bool {
644 urlI := refInfoList[i].URL
645 urlJ := refInfoList[j].URL
646 refI := refInfoList[i].Refspec
647 refJ := refInfoList[j].Refspec
648 if urlI == urlJ {
649 return refI < refJ
650 }
651 return urlI > urlJ
652 })
653
654 for _, revData := range revs {
655 c.Logger.Info("writing revision", "revision", revData.Name())
656 data := &PageData{
657 Repo: c,
658 RevData: revData,
659 SiteURLs: c.getURLs(),
660 }
661
662 if claimed {
663 go func() {
664 c.writeRevision(repo, data, refInfoList)
665 }()
666 } else {
667 branchOutput := c.writeRevision(repo, data, refInfoList)
668 mainOutput = branchOutput
669 claimed = true
670 }
671 }
672
673 // use the first revision in our list to generate
674 // the root summary, logs, and tree the user can click
675 revData := &RevData{
676 id: first.ID(),
677 name: first.Name(),
678 }
679
680 data := &PageData{
681 RevData: revData,
682 Repo: c,
683 SiteURLs: c.getURLs(),
684 }
685 c.writeRefs(data, refInfoList)
686 c.writeRootSummary(data, template.HTML(mainOutput.Readme))
687 return mainOutput
688}
689
690type TreeRoot struct {
691 Path string
692 Items []*TreeItem
693 Crumbs []*Breadcrumb
694}
695
696type TreeWalker struct {
697 treeItem chan *TreeItem
698 tree chan *TreeRoot
699 HideTreeLastCommit bool
700 PageData *PageData
701 Repo *git.Repository
702}
703
704type Breadcrumb struct {
705 Text string
706 URL template.URL
707 IsLast bool
708}
709
710func (tw *TreeWalker) calcBreadcrumbs(curpath string) []*Breadcrumb {
711 if curpath == "" {
712 return []*Breadcrumb{}
713 }
714 parts := strings.Split(curpath, string(os.PathSeparator))
715 rootURL := template.URL(
716 filepath.Join(
717 getTreeBaseDir(tw.PageData.RevData),
718 "index.html",
719 ),
720 )
721
722 crumbs := make([]*Breadcrumb, len(parts)+1)
723 crumbs[0] = &Breadcrumb{
724 URL: rootURL,
725 Text: tw.PageData.Repo.RepoName,
726 }
727
728 cur := ""
729 for idx, d := range parts {
730 crumbs[idx+1] = &Breadcrumb{
731 Text: d,
732 URL: template.URL(filepath.Join(getFileBaseDir(tw.PageData.RevData), cur, d, "index.html")),
733 }
734 if idx == len(parts)-1 {
735 crumbs[idx+1].IsLast = true
736 }
737 cur = filepath.Join(cur, d)
738 }
739
740 return crumbs
741}
742
743func FilenameToDevIcon(filename string) string {
744 ext := filepath.Ext(filename)
745 extMappr := map[string]string{
746 ".html": "html5",
747 ".go": "go",
748 ".py": "python",
749 ".css": "css3",
750 ".js": "javascript",
751 ".md": "markdown",
752 ".ts": "typescript",
753 ".tsx": "react",
754 ".jsx": "react",
755 }
756
757 nameMappr := map[string]string{
758 "Makefile": "cmake",
759 "Dockerfile": "docker",
760 }
761
762 icon := extMappr[ext]
763 if icon == "" {
764 icon = nameMappr[filename]
765 }
766
767 return fmt.Sprintf("devicon-%s-original", icon)
768}
769
770func (tw *TreeWalker) NewTreeItem(entry *git.TreeEntry, curpath string, crumbs []*Breadcrumb) *TreeItem {
771 typ := entry.Type()
772 fname := filepath.Join(curpath, entry.Name())
773 item := &TreeItem{
774 Size: toPretty(entry.Size()),
775 Name: entry.Name(),
776 Path: fname,
777 Entry: entry,
778 URL: template.URL(getFileURL(tw.PageData.RevData, fname)),
779 Crumbs: crumbs,
780 }
781
782 // `git rev-list` is pretty expensive here, so we have a flag to disable
783 if tw.HideTreeLastCommit {
784 // c.Logger.Info("skipping the process of finding the last commit for each file")
785 } else {
786 id := tw.PageData.RevData.ID()
787 lastCommits, err := tw.Repo.RevList([]string{id}, git.RevListOptions{
788 Path: item.Path,
789 CommandOptions: git.CommandOptions{Args: []string{"-1"}},
790 })
791 bail(err)
792
793 var lc *git.Commit
794 if len(lastCommits) > 0 {
795 lc = lastCommits[0]
796 }
797 item.CommitURL = getCommitURL(lc.ID.String())
798 item.CommitID = getShortID(lc.ID.String())
799 item.Summary = lc.Summary()
800 item.When = lc.Author.When.Format("02 Jan 06")
801 item.Author = lc.Author
802 }
803
804 fpath := getFileURL(
805 tw.PageData.RevData,
806 fmt.Sprintf("%s.html", fname),
807 )
808 if typ == git.ObjectTree {
809 item.IsDir = true
810 fpath = filepath.Join(
811 getFileBaseDir(tw.PageData.RevData),
812 curpath,
813 entry.Name(),
814 "index.html",
815 )
816 } else if typ == git.ObjectBlob {
817 item.Icon = FilenameToDevIcon(item.Name)
818 }
819 item.URL = template.URL(fpath)
820
821 return item
822}
823
824func (tw *TreeWalker) walk(tree *git.Tree, curpath string) {
825 entries, err := tree.Entries()
826 bail(err)
827
828 crumbs := tw.calcBreadcrumbs(curpath)
829 treeEntries := []*TreeItem{}
830 for _, entry := range entries {
831 typ := entry.Type()
832 item := tw.NewTreeItem(entry, curpath, crumbs)
833
834 if typ == git.ObjectTree {
835 item.IsDir = true
836 re, _ := tree.Subtree(entry.Name())
837 tw.walk(re, item.Path)
838 treeEntries = append(treeEntries, item)
839 tw.treeItem <- item
840 } else if typ == git.ObjectBlob {
841 treeEntries = append(treeEntries, item)
842 tw.treeItem <- item
843 }
844 }
845
846 sort.Slice(treeEntries, func(i, j int) bool {
847 nameI := treeEntries[i].Name
848 nameJ := treeEntries[j].Name
849 if treeEntries[i].IsDir && treeEntries[j].IsDir {
850 return nameI < nameJ
851 }
852
853 if treeEntries[i].IsDir && !treeEntries[j].IsDir {
854 return true
855 }
856
857 if !treeEntries[i].IsDir && treeEntries[j].IsDir {
858 return false
859 }
860
861 return nameI < nameJ
862 })
863
864 fpath := filepath.Join(
865 getFileBaseDir(tw.PageData.RevData),
866 curpath,
867 )
868 // root gets a special spot outside of `item` subdir
869 if curpath == "" {
870 fpath = getTreeBaseDir(tw.PageData.RevData)
871 }
872
873 tw.tree <- &TreeRoot{
874 Path: fpath,
875 Items: treeEntries,
876 Crumbs: crumbs,
877 }
878
879 if curpath == "" {
880 close(tw.tree)
881 close(tw.treeItem)
882 }
883}
884
885func (c *Config) writeRevision(repo *git.Repository, pageData *PageData, refs []*RefInfo) *BranchOutput {
886 c.Logger.Info(
887 "compiling revision",
888 "repoName", c.RepoName,
889 "revision", pageData.RevData.Name(),
890 )
891
892 output := &BranchOutput{}
893
894 var wg sync.WaitGroup
895
896 wg.Add(1)
897 go func() {
898 defer wg.Done()
899
900 pageSize := pageData.Repo.MaxCommits
901 if pageSize == 0 {
902 pageSize = 5000
903 }
904 commits, err := repo.CommitsByPage(pageData.RevData.ID(), 0, pageSize)
905 bail(err)
906
907 logs := []*CommitData{}
908 for i, commit := range commits {
909 if i == 0 {
910 output.LastCommit = commit
911 }
912
913 tags := []*RefInfo{}
914 for _, ref := range refs {
915 if commit.ID.String() == ref.ID {
916 tags = append(tags, ref)
917 }
918 }
919
920 parentSha, _ := commit.ParentID(0)
921 parentID := ""
922 if parentSha == nil {
923 parentID = commit.ID.String()
924 } else {
925 parentID = parentSha.String()
926 }
927 logs = append(logs, &CommitData{
928 ParentID: parentID,
929 URL: getCommitURL(commit.ID.String()),
930 ShortID: getShortID(commit.ID.String()),
931 SummaryStr: commit.Summary(),
932 AuthorStr: commit.Author.Name,
933 WhenStr: commit.Author.When.Format("02 Jan 06"),
934 Commit: commit,
935 Refs: tags,
936 })
937 }
938
939 c.writeLog(pageData, logs)
940
941 for _, cm := range logs {
942 wg.Add(1)
943 go func(commit *CommitData) {
944 defer wg.Done()
945 c.writeLogDiff(repo, pageData, commit)
946 }(cm)
947 }
948 }()
949
950 tree, err := repo.LsTree(pageData.RevData.ID())
951 bail(err)
952
953 readme := ""
954 entries := make(chan *TreeItem)
955 subtrees := make(chan *TreeRoot)
956 tw := &TreeWalker{
957 PageData: pageData,
958 Repo: repo,
959 treeItem: entries,
960 tree: subtrees,
961 }
962 wg.Add(1)
963 go func() {
964 defer wg.Done()
965 tw.walk(tree, "")
966 }()
967
968 wg.Add(1)
969 go func() {
970 defer wg.Done()
971 for e := range entries {
972 wg.Add(1)
973 go func(entry *TreeItem) {
974 defer wg.Done()
975 if entry.IsDir {
976 return
977 }
978
979 readmeStr := c.writeHTMLTreeFile(pageData, entry)
980 if readmeStr != "" {
981 readme = readmeStr
982 }
983 }(e)
984 }
985 }()
986
987 wg.Add(1)
988 go func() {
989 defer wg.Done()
990 for t := range subtrees {
991 wg.Add(1)
992 go func(tree *TreeRoot) {
993 defer wg.Done()
994 c.writeTree(pageData, tree)
995 }(t)
996 }
997 }()
998
999 wg.Wait()
1000
1001 c.Logger.Info(
1002 "compilation complete branch",
1003 "repoName", c.RepoName,
1004 "revision", pageData.RevData.Name(),
1005 )
1006
1007 output.Readme = readme
1008 return output
1009}
1010
1011func style(theme chroma.Style) string {
1012 bg := theme.Get(chroma.Background)
1013 txt := theme.Get(chroma.Text)
1014 kw := theme.Get(chroma.Keyword)
1015 nv := theme.Get(chroma.NameVariable)
1016 cm := theme.Get(chroma.Comment)
1017 ln := theme.Get(chroma.LiteralNumber)
1018 return fmt.Sprintf(`:root {
1019 --bg-color: %s;
1020 --text-color: %s;
1021 --border: %s;
1022 --link-color: %s;
1023 --hover: %s;
1024 --visited: %s;
1025}`,
1026 bg.Background.String(),
1027 txt.Colour.String(),
1028 cm.Colour.String(),
1029 nv.Colour.String(),
1030 kw.Colour.String(),
1031 ln.Colour.String(),
1032 )
1033}
1034
1035func main() {
1036 var outdir = flag.String("out", "./public", "output directory")
1037 var rpath = flag.String("repo", ".", "path to git repo")
1038 var revsFlag = flag.String("revs", "HEAD", "list of revs to generate logs and tree (e.g. main,v1,c69f86f,HEAD")
1039 var themeFlag = flag.String("theme", "dracula", "theme to use for site")
1040 var labelFlag = flag.String("label", "", "pretty name for the subdir where we create the repo, default is last folder in --repo")
1041 var cloneFlag = flag.String("clone-url", "", "git clone URL")
1042 var homeFlag = flag.String("home-url", "", "URL for breadcumbs to get to list of repositories")
1043 var descFlag = flag.String("desc", "", "description for repo")
1044 var maxCommitsFlag = flag.Int("max-commits", 0, "maximum number of commits to generate")
1045 var hideTreeLastCommitFlag = flag.Bool("hide-tree-last-commit", false, "dont calculate last commit for each file in the tree")
1046
1047 flag.Parse()
1048
1049 out, err := filepath.Abs(*outdir)
1050 bail(err)
1051 repoPath, err := filepath.Abs(*rpath)
1052 bail(err)
1053
1054 theme := styles.Get(*themeFlag)
1055
1056 logger := slog.Default()
1057
1058 label := repoName(repoPath)
1059 if *labelFlag != "" {
1060 label = *labelFlag
1061 }
1062
1063 revs := strings.Split(*revsFlag, ",")
1064 if len(revs) == 1 && revs[0] == "" {
1065 revs = []string{}
1066 }
1067
1068 formatter := formatterHtml.New(
1069 formatterHtml.WithLineNumbers(true),
1070 formatterHtml.WithLinkableLineNumbers(true, ""),
1071 formatterHtml.WithClasses(true),
1072 )
1073
1074 config := &Config{
1075 Outdir: out,
1076 RepoPath: repoPath,
1077 RepoName: label,
1078 Cache: make(map[string]bool),
1079 Revs: revs,
1080 Theme: theme,
1081 Logger: logger,
1082 CloneURL: template.URL(*cloneFlag),
1083 HomeURL: template.URL(*homeFlag),
1084 Desc: *descFlag,
1085 MaxCommits: *maxCommitsFlag,
1086 HideTreeLastCommit: *hideTreeLastCommitFlag,
1087 Formatter: formatter,
1088 }
1089 config.Logger.Info("config", "config", config)
1090
1091 if len(revs) == 0 {
1092 bail(fmt.Errorf("you must provide --revs"))
1093 }
1094
1095 config.writeRepo()
1096 config.copyStatic("static")
1097
1098 styles := style(*theme)
1099 fmt.Println(styles)
1100 err = os.WriteFile(filepath.Join(out, "vars.css"), []byte(styles), 0644)
1101 if err != nil {
1102 panic(err)
1103 }
1104
1105 fp := filepath.Join(out, "syntax.css")
1106 w, err := os.OpenFile(fp, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
1107 if err != nil {
1108 bail(err)
1109 }
1110 err = formatter.WriteCSS(w, theme)
1111 if err != nil {
1112 bail(err)
1113 }
1114
1115 url := filepath.Join("/", "index.html")
1116 config.Logger.Info("root url", "url", url)
1117}