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}