scooter  ·  2026-04-15

generator.go

  1package pgit
  2
  3import (
  4	"encoding/json"
  5	"fmt"
  6	"html/template"
  7	"os"
  8	"path/filepath"
  9	"sort"
 10	"strings"
 11	"sync"
 12	"time"
 13
 14	git "github.com/gogs/git-module"
 15)
 16
 17func (c *Config) WriteRootSummary(data *PageData, readme template.HTML, lastCommit *CommitData) {
 18	c.Logger.Info("writing root html", "repoPath", c.RepoPath)
 19	c.WriteHTML(&WriteData{
 20		Filename: "index.html",
 21		Template: "html/summary.page.tmpl",
 22		Data: &SummaryPageData{
 23			PageData:   data,
 24			Readme:     readme,
 25			LastCommit: lastCommit,
 26		},
 27	})
 28}
 29
 30func (c *Config) WriteRepoMetadata(lastCommit *CommitData) {
 31	c.Logger.Info("writing repo metadata JSON", "repoPath", c.RepoPath)
 32
 33	// Keep HTML description for rendering in index
 34	desc := string(c.Desc)
 35
 36	metadata := &RepoMetadata{
 37		Name:        c.RepoName,
 38		Description: desc,
 39		LastUpdated: lastCommit.Author.When,
 40	}
 41
 42	data, err := json.MarshalIndent(metadata, "", "  ")
 43	if err != nil {
 44		c.Logger.Error("failed to marshal metadata", "error", err)
 45		return
 46	}
 47
 48	fp := filepath.Join(c.Outdir, "pgit.json")
 49	err = os.WriteFile(fp, data, 0644)
 50	if err != nil {
 51		c.Logger.Error("failed to write metadata file", "error", err)
 52		return
 53	}
 54	c.Logger.Info("wrote metadata file", "filepath", fp)
 55}
 56
 57func (c *Config) WriteTree(data *PageData, tree *TreeRoot) {
 58	c.Logger.Info("writing tree", "treePath", tree.Path)
 59	c.WriteHTML(&WriteData{
 60		Filename: "index.html",
 61		Subdir:   tree.Path,
 62		Template: "html/tree.page.tmpl",
 63		Data: &TreePageData{
 64			PageData: data,
 65			Tree:     tree,
 66		},
 67	})
 68}
 69
 70func (c *Config) WriteLog(data *PageData, logs []*CommitData) {
 71	c.Logger.Info("writing log file", "revision", (*data.RevData).Name())
 72	c.WriteHTML(&WriteData{
 73		Filename: "index.html",
 74		Subdir:   GetLogBaseDir(*data.RevData),
 75		Template: "html/log.page.tmpl",
 76		Data: &LogPageData{
 77			PageData:   data,
 78			NumCommits: len(logs),
 79			Logs:       logs,
 80		},
 81	})
 82}
 83
 84func (c *Config) WriteRefs(data *PageData, refs []*RefInfo) {
 85	c.Logger.Info("writing refs", "repoPath", c.RepoPath)
 86	c.WriteHTML(&WriteData{
 87		Filename: "refs.html",
 88		Template: "html/refs.page.tmpl",
 89		Data: &RefPageData{
 90			PageData: data,
 91			Refs:     refs,
 92		},
 93	})
 94}
 95
 96func (c *Config) WriteHTMLTreeFile(pageData *PageData, treeItem *TreeItem) string {
 97	readme := ""
 98	b, err := treeItem.Entry.Blob().Bytes()
 99	Bail(err)
100	str := string(b)
101
102	treeItem.IsTextFile = IsText(str)
103
104	contents := "binary file, cannot display"
105	if treeItem.IsTextFile {
106		treeItem.NumLines = len(strings.Split(str, "\n"))
107		if IsMarkdownFile(treeItem.Entry.Name()) {
108			contents = string(c.RenderMarkdown(str))
109		} else {
110			contents, err = c.ParseText(treeItem.Entry.Name(), string(b))
111			Bail(err)
112		}
113	}
114
115	d := filepath.Dir(treeItem.Path)
116
117	nameLower := strings.ToLower(treeItem.Entry.Name())
118	summary := ReadmeFile(pageData.Repo)
119	if d == "." && nameLower == summary {
120		readme = str
121	}
122
123	c.WriteHTML(&WriteData{
124		Filename: fmt.Sprintf("%s.html", treeItem.Entry.Name()),
125		Template: "html/file.page.tmpl",
126		Data: &FilePageData{
127			PageData: pageData,
128			Contents: template.HTML(contents),
129			Item:     treeItem,
130		},
131		Subdir: GetFileDir(*pageData.RevData, d),
132	})
133	return readme
134}
135
136func (c *Config) WriteLogDiff(repo *git.Repository, pageData *PageData, commit *CommitData) {
137	commitID := commit.ID.String()
138
139	c.Mutex.RLock()
140	hasCommit := c.Cache[commitID]
141	c.Mutex.RUnlock()
142
143	if hasCommit {
144		c.Logger.Info("commit file already generated, skipping", "commitID", GetShortID(commitID))
145		return
146	}
147	c.Mutex.Lock()
148	c.Cache[commitID] = true
149	c.Mutex.Unlock()
150
151	diff, err := repo.Diff(commitID, 0, 0, 0, git.DiffOptions{})
152	Bail(err)
153
154	rnd := &DiffRender{
155		NumFiles:       diff.NumFiles(),
156		TotalAdditions: diff.TotalAdditions(),
157		TotalDeletions: diff.TotalDeletions(),
158	}
159	fls := []*DiffRenderFile{}
160	for _, file := range diff.Files {
161		fl := &DiffRenderFile{
162			FileType:     DiffFileType(file.Type),
163			OldMode:      file.OldMode(),
164			OldName:      file.OldName(),
165			Mode:         file.Mode(),
166			Name:         file.Name,
167			NumAdditions: file.NumAdditions(),
168			NumDeletions: file.NumDeletions(),
169		}
170		content := ""
171		for _, section := range file.Sections {
172			for _, line := range section.Lines {
173				content += fmt.Sprintf("%s\n", line.Content)
174			}
175		}
176		finContent, err := c.ParseText("commit.diff", content)
177		Bail(err)
178
179		fl.Content = template.HTML(finContent)
180		fls = append(fls, fl)
181	}
182	rnd.Files = fls
183
184	parentSha, _ := commit.Commit.ParentID(0)
185	parentID := ""
186	if parentSha == nil {
187		parentID = commit.ID.String()
188	} else {
189		parentID = parentSha.String()
190	}
191
192	commitData := &CommitPageData{
193		PageData:  pageData,
194		Commit:    commit,
195		CommitID:  GetShortID(commitID),
196		Diff:      rnd,
197		Parent:    GetShortID(parentID),
198		CommitURL: c.GetCommitURL(commitID),
199		ParentURL: c.GetCommitURL(parentID),
200	}
201
202	c.WriteHTML(&WriteData{
203		Filename: fmt.Sprintf("%s.html", commitID),
204		Template: "html/commit.page.tmpl",
205		Subdir:   "commits",
206		Data:     commitData,
207	})
208}
209
210func (c *Config) WriteRepo() *BranchOutput {
211	c.Logger.Info("writing repo", "repoPath", c.RepoPath)
212	repo, err := git.Open(c.RepoPath)
213	Bail(err)
214
215	refs, err := repo.ShowRef(git.ShowRefOptions{Heads: true, Tags: true})
216	Bail(err)
217
218	var first *RevData
219	revs := []*RevData{}
220	for _, revStr := range c.Revs {
221		fullRevID, err := repo.RevParse(revStr)
222		Bail(err)
223
224		revID := GetShortID(fullRevID)
225		revName := revID
226		for _, ref := range refs {
227			if revStr == git.RefShortName(ref.Refspec) || revStr == ref.Refspec {
228				revName = revStr
229				break
230			}
231		}
232
233		data := &RevData{
234			id:     fullRevID,
235			name:   revName,
236			Config: c,
237		}
238
239		if first == nil {
240			first = data
241		}
242		revs = append(revs, data)
243	}
244
245	if first == nil {
246		Bail(fmt.Errorf("could not find a git reference that matches criteria"))
247	}
248
249	refInfoMap := map[string]*RefInfo{}
250	for _, revData := range revs {
251		refInfoMap[revData.Name()] = &RefInfo{
252			ID:      revData.ID(),
253			Refspec: revData.Name(),
254			URL:     revData.TreeURL(),
255		}
256	}
257
258	for _, ref := range refs {
259		refspec := git.RefShortName(ref.Refspec)
260		if refInfoMap[refspec] != nil {
261			continue
262		}
263
264		refInfoMap[refspec] = &RefInfo{
265			ID:      ref.ID,
266			Refspec: refspec,
267		}
268	}
269
270	refInfoList := []*RefInfo{}
271	for _, val := range refInfoMap {
272		refInfoList = append(refInfoList, val)
273	}
274	sort.Slice(refInfoList, func(i, j int) bool {
275		urlI := refInfoList[i].URL
276		urlJ := refInfoList[j].URL
277		refI := refInfoList[i].Refspec
278		refJ := refInfoList[j].Refspec
279		if urlI == urlJ {
280			return refI < refJ
281		}
282		return urlI > urlJ
283	})
284
285	mainOutput := &BranchOutput{}
286	var wg sync.WaitGroup
287	for i, revData := range revs {
288		c.Logger.Info("writing revision", "revision", revData.Name())
289		revInfo := RevInfo(revData)
290		data := &PageData{
291			Repo:     c,
292			RevData:  &revInfo,
293			SiteURLs: c.GetURLs(),
294			Refs:     refInfoList,
295		}
296
297		if i == 0 {
298			branchOutput := c.WriteRevision(repo, data, refInfoList)
299			mainOutput = branchOutput
300		} else {
301			wg.Add(1)
302			go func() {
303				defer wg.Done()
304				c.WriteRevision(repo, data, refInfoList)
305			}()
306		}
307	}
308	wg.Wait()
309
310	revData := &RevData{
311		id:     first.ID(),
312		name:   first.Name(),
313		Config: c,
314	}
315
316	revInfo := RevInfo(revData)
317	data := &PageData{
318		RevData:  &revInfo,
319		Repo:     c,
320		SiteURLs: c.GetURLs(),
321		Refs:     refInfoList,
322	}
323
324	if c.Issues {
325		err := c.WriteIssues(data)
326		if err != nil {
327			c.Logger.Warn("failed to write issues", "error", err)
328		}
329	}
330
331	c.WriteRefs(data, refInfoList)
332	var readmeHTML template.HTML
333	if IsMarkdownFile(ReadmeFile(c)) {
334		readmeHTML = c.RenderMarkdown(mainOutput.Readme)
335	} else {
336		readmeHTML = template.HTML(mainOutput.Readme)
337	}
338	readmeHTML = template.HTML(`<div class="readme">` + string(readmeHTML) + `</div>`)
339	c.WriteRootSummary(data, readmeHTML, mainOutput.LastCommit)
340	c.WriteRepoMetadata(mainOutput.LastCommit)
341	return mainOutput
342}
343
344func (c *Config) WriteRevision(repo *git.Repository, pageData *PageData, refs []*RefInfo) *BranchOutput {
345	c.Logger.Info(
346		"compiling revision",
347		"repoName", c.RepoName,
348		"revision", (*pageData.RevData).Name(),
349	)
350
351	output := &BranchOutput{}
352	var wg sync.WaitGroup
353
354	wg.Add(1)
355	go func() {
356		defer wg.Done()
357
358		pageSize := pageData.Repo.MaxCommits
359		if pageSize == 0 {
360			pageSize = 5000
361		}
362		commits, err := repo.CommitsByPage((*pageData.RevData).ID(), 0, pageSize)
363		Bail(err)
364
365		logs := []*CommitData{}
366		for i, commit := range commits {
367			tags := []*RefInfo{}
368			for _, ref := range refs {
369				if commit.ID.String() == ref.ID {
370					tags = append(tags, ref)
371				}
372			}
373
374			parentSha, _ := commit.ParentID(0)
375			parentID := ""
376			if parentSha == nil {
377				parentID = commit.ID.String()
378			} else {
379				parentID = parentSha.String()
380			}
381			trailers, messageBodyOnly := ParseCommitMessage(commit.Message)
382			cd := &CommitData{
383				ParentID:        parentID,
384				URL:             c.GetCommitURL(commit.ID.String()),
385				ShortID:         GetShortID(commit.ID.String()),
386				SummaryStr:      commit.Summary(),
387				AuthorStr:       commit.Author.Name,
388				WhenStr:         commit.Author.When.Format(time.DateOnly),
389				WhenISO:         commit.Author.When.UTC().Format(time.RFC3339),
390				WhenDisplay:     FormatDateForDisplay(commit.Author.When),
391				Commit:          commit,
392				Refs:            tags,
393				Trailers:        trailers,
394				MessageBodyOnly: messageBodyOnly,
395			}
396			logs = append(logs, cd)
397			if i == 0 {
398				output.LastCommit = cd
399			}
400		}
401
402		c.WriteLog(pageData, logs)
403
404		for _, cm := range logs {
405			wg.Add(1)
406			go func(commit *CommitData) {
407				defer wg.Done()
408				c.WriteLogDiff(repo, pageData, commit)
409			}(cm)
410		}
411	}()
412
413	tree, err := repo.LsTree((*pageData.RevData).ID())
414	Bail(err)
415
416	readme := ""
417	entries := make(chan *TreeItem)
418	subtrees := make(chan *TreeRoot)
419	tw := &TreeWalker{
420		Config:   c,
421		PageData: pageData,
422		Repo:     repo,
423		treeItem: entries,
424		tree:     subtrees,
425	}
426	wg.Add(1)
427	go func() {
428		defer wg.Done()
429		tw.Walk(tree, "")
430	}()
431
432	wg.Add(1)
433	go func() {
434		defer wg.Done()
435		for e := range entries {
436			wg.Add(1)
437			go func(entry *TreeItem) {
438				defer wg.Done()
439				if entry.IsDir {
440					return
441				}
442
443				readmeStr := c.WriteHTMLTreeFile(pageData, entry)
444				if readmeStr != "" {
445					readme = readmeStr
446				}
447			}(e)
448		}
449	}()
450
451	wg.Add(1)
452	go func() {
453		defer wg.Done()
454		for t := range subtrees {
455			wg.Add(1)
456			go func(tree *TreeRoot) {
457				defer wg.Done()
458				c.WriteTree(pageData, tree)
459			}(t)
460		}
461	}()
462
463	wg.Wait()
464
465	c.Logger.Info(
466		"compilation complete",
467		"repoName", c.RepoName,
468		"revision", (*pageData.RevData).Name(),
469	)
470
471	output.Readme = readme
472	return output
473}