scooter  ·  2026-04-15

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}