config.go
1package pgit
2
3import (
4 "embed"
5 "html/template"
6 "log"
7 "log/slog"
8 "path/filepath"
9 "regexp"
10 "strings"
11 "sync"
12 "time"
13 "unicode/utf8"
14
15 "github.com/alecthomas/chroma/v2"
16 formatterHtml "github.com/alecthomas/chroma/v2/formatters/html"
17 "github.com/dustin/go-humanize"
18 git "github.com/gogs/git-module"
19)
20
21//go:embed html/*.tmpl
22//go:embed static/*
23//go:embed hooks/*.bash
24var EmbedFS embed.FS
25
26//go:embed static/*
27var StaticFS embed.FS
28
29type Config struct {
30 Outdir string
31 RepoPath string
32 Revs []string
33 Desc template.HTML
34
35 MaxCommits int
36 Readme string
37 HideTreeLastCommit bool
38 Issues bool
39
40 HomeURL template.URL
41 CloneURL template.URL
42
43 RootRelative string
44
45 Cache map[string]bool
46 Mutex sync.RWMutex
47 RepoName string
48 Logger *slog.Logger
49
50 Theme *chroma.Style
51 Formatter *formatterHtml.Formatter
52 CSSFile string
53}
54
55type RepoMetadata struct {
56 Name string `json:"name"`
57 Description string `json:"description"`
58 LastUpdated time.Time `json:"last_updated"`
59}
60
61type RevInfo interface {
62 ID() string
63 Name() string
64}
65
66type RevData struct {
67 id string
68 name string
69 Config *Config
70}
71
72func (r *RevData) ID() string {
73 return r.id
74}
75
76func (r *RevData) Name() string {
77 return r.name
78}
79
80func (r *RevData) TreeURL() template.URL {
81 return r.Config.GetTreeURL(r)
82}
83
84func (r *RevData) LogURL() template.URL {
85 return r.Config.GetLogsURL(r)
86}
87
88type TagData struct {
89 Name string
90 URL template.URL
91}
92
93type Trailer struct {
94 Key string
95 Value string
96}
97
98type CommitData struct {
99 SummaryStr string
100 URL template.URL
101 WhenStr string
102 WhenISO string
103 WhenDisplay string
104 AuthorStr string
105 ShortID string
106 ParentID string
107 Refs []*RefInfo
108 Trailers []Trailer
109 MessageBodyOnly string
110 *git.Commit
111}
112
113type TreeItem struct {
114 IsTextFile bool
115 IsDir bool
116 Size string
117 NumLines int
118 Name string
119 Icon string
120 Path string
121 URL template.URL
122 CommitID string
123 CommitURL template.URL
124 Summary string
125 When string
126 WhenISO string
127 WhenDisplay string
128 Author *git.Signature
129 Entry *git.TreeEntry
130 Crumbs []*Breadcrumb
131}
132
133type DiffRender struct {
134 NumFiles int
135 TotalAdditions int
136 TotalDeletions int
137 Files []*DiffRenderFile
138}
139
140type DiffRenderFile struct {
141 FileType string
142 OldMode git.EntryMode
143 OldName string
144 Mode git.EntryMode
145 Name string
146 Content template.HTML
147 NumAdditions int
148 NumDeletions int
149}
150
151type RefInfo struct {
152 ID string
153 Refspec string
154 URL template.URL
155}
156
157type BranchOutput struct {
158 Readme string
159 LastCommit *CommitData
160}
161
162type SiteURLs struct {
163 HomeURL template.URL
164 CloneURL template.URL
165 SummaryURL template.URL
166 RefsURL template.URL
167 IssuesURL template.URL
168}
169
170type PageData struct {
171 Repo *Config
172 SiteURLs *SiteURLs
173 RevData *RevInfo
174 Refs []*RefInfo
175}
176
177type SummaryPageData struct {
178 *PageData
179 Readme template.HTML
180 LastCommit *CommitData
181}
182
183type TreePageData struct {
184 *PageData
185 Tree *TreeRoot
186}
187
188type LogPageData struct {
189 *PageData
190 NumCommits int
191 Logs []*CommitData
192}
193
194type FilePageData struct {
195 *PageData
196 Contents template.HTML
197 Item *TreeItem
198}
199
200type CommitPageData struct {
201 *PageData
202 CommitMsg template.HTML
203 CommitID string
204 Commit *CommitData
205 Diff *DiffRender
206 Parent string
207 ParentURL template.URL
208 CommitURL template.URL
209}
210
211type RefPageData struct {
212 *PageData
213 Refs []*RefInfo
214}
215
216type WriteData struct {
217 Template string
218 Filename string
219 Subdir string
220 Data any
221}
222
223func (s *SummaryPageData) Active() string { return "summary" }
224func (s *TreePageData) Active() string { return "code" }
225func (s *FilePageData) Active() string { return "code" }
226func (s *LogPageData) Active() string { return "commits" }
227func (s *CommitPageData) Active() string { return "commit" }
228func (s *RefPageData) Active() string { return "refs" }
229
230func Bail(err error) {
231 if err != nil {
232 log.Fatalf("Error: %v", err)
233 }
234}
235
236func DiffFileType(_type git.DiffFileType) string {
237 switch _type {
238 case git.DiffFileAdd:
239 return "A"
240 case git.DiffFileChange:
241 return "M"
242 case git.DiffFileDelete:
243 return "D"
244 case git.DiffFileRename:
245 return "R"
246 default:
247 return ""
248 }
249}
250
251func ToPretty(b int64) string {
252 return humanize.Bytes(uint64(b))
253}
254
255func FormatDateForDisplay(t time.Time) string {
256 if time.Since(t).Hours() > 365*24 {
257 return t.Format("Jan 2006")
258 }
259 return t.Format("Jan 2")
260}
261
262func RepoName(root string) string {
263 _, file := filepath.Split(root)
264 return file
265}
266
267func ReadmeFile(repo *Config) string {
268 if repo.Readme == "" {
269 return "readme.md"
270 }
271 return strings.ToLower(repo.Readme)
272}
273
274func ReadDescription(repoPath string) string {
275 descPath := filepath.Join(repoPath, ".git", "description")
276 data, err := EmbedFS.ReadFile(descPath)
277 if err != nil {
278 descPath = filepath.Join(repoPath, "description")
279 data, err = EmbedFS.ReadFile(descPath)
280 if err != nil {
281 return ""
282 }
283 }
284
285 desc := strings.TrimSpace(string(data))
286
287 if desc == "Unnamed repository; edit this file 'description' to name the repository." {
288 return ""
289 }
290
291 return desc
292}
293
294func GetShortID(id string) string {
295 return id[:7]
296}
297
298func ParseCommitMessage(message string) ([]Trailer, string) {
299 var trailers []Trailer
300
301 trailerRe := regexp.MustCompile(`^([A-Za-z0-9-]+): (.+)$`)
302
303 message = strings.TrimRight(message, "\n")
304 lines := strings.Split(message, "\n")
305
306 trailerStartIdx := -1
307
308 for i := len(lines) - 1; i >= 0; i-- {
309 line := strings.TrimSpace(lines[i])
310
311 if line == "" {
312 trailerStartIdx = i + 1
313 break
314 }
315
316 matches := trailerRe.FindStringSubmatch(line)
317 if matches != nil {
318 trailers = append([]Trailer{
319 {Key: matches[1], Value: matches[2]},
320 }, trailers...)
321 } else {
322 trailerStartIdx = i + 1
323 break
324 }
325 }
326
327 bodyLines := lines
328 if len(trailers) > 0 && trailerStartIdx > 0 {
329 bodyLines = lines[:trailerStartIdx]
330 }
331
332 bodyEndIdx := len(bodyLines)
333 for i := len(bodyLines) - 1; i >= 0; i-- {
334 if strings.TrimSpace(bodyLines[i]) != "" {
335 bodyEndIdx = i + 1
336 break
337 }
338 }
339 bodyLines = bodyLines[:bodyEndIdx]
340
341 messageBodyOnly := ""
342 if len(bodyLines) > 1 {
343 bodyOnlyLines := bodyLines[1:]
344 startIdx := 0
345 for i, line := range bodyOnlyLines {
346 if strings.TrimSpace(line) != "" {
347 startIdx = i
348 break
349 }
350 }
351 if startIdx < len(bodyOnlyLines) {
352 messageBodyOnly = strings.Join(bodyOnlyLines[startIdx:], "\n")
353 }
354 }
355
356 return trailers, messageBodyOnly
357}
358
359func IsText(s string) bool {
360 const max = 1024
361 if len(s) > max {
362 s = s[0:max]
363 }
364 for i, c := range s {
365 if i+utf8.UTFMax > len(s) {
366 break
367 }
368 if c == 0xFFFD || c < ' ' && c != '\n' && c != '\t' && c != '\f' && c != '\r' {
369 return false
370 }
371 }
372 return true
373}