4 files changed,
151 insertions(+),
37 deletions(-)
Author:
Oleksandr Smirnov
olexsmir@gmail.com
Committed at:
2026-02-15 15:44:39 +0200
Authored at:
2026-02-15 15:28:40 +0200
Change ID:
wuzskmsqmpukyyqurtmnmrnnxslruxvm
Parent:
b7d443e
A
internal/cache/cache.go
路路路 1 +package cache 2 + 3 +import ( 4 + "errors" 5 + "sync" 6 + "time" 7 +) 8 + 9 +var ErrNotFound = errors.New("not found") 10 + 11 +type Cacher[T any] interface { 12 + Set(key string, val T) 13 + Get(key string) (val T, found bool) 14 +} 15 + 16 +type item[T any] struct { 17 + v T 18 + expiry time.Time 19 +} 20 + 21 +func (i item[T]) isExpired() bool { 22 + return time.Now().After(i.expiry) 23 +} 24 + 25 +type InMemory[T any] struct { 26 + mu sync.RWMutex 27 + ttl time.Duration 28 + data map[string]item[T] 29 +} 30 + 31 +func NewInMemory[T any](ttl time.Duration) *InMemory[T] { 32 + c := &InMemory[T]{ 33 + data: make(map[string]item[T]), 34 + ttl: ttl, 35 + } 36 + 37 + go c.clean() 38 + return c 39 +} 40 + 41 +func (m *InMemory[T]) Set(key string, val T) { 42 + m.mu.Lock() 43 + defer m.mu.Unlock() 44 + m.data[key] = item[T]{ 45 + v: val, 46 + expiry: time.Now().Add(m.ttl), 47 + } 48 +} 49 + 50 +func (m *InMemory[T]) Get(key string) (T, bool) { 51 + m.mu.Lock() 52 + defer m.mu.Unlock() 53 + 54 + val, found := m.data[key] 55 + if !found { 56 + var t T 57 + return t, false 58 + } 59 + return val.v, true 60 +} 61 + 62 +func (m *InMemory[T]) clean() { 63 + for range time.Tick(5 * time.Second) { 64 + m.mu.Lock() 65 + for k, v := range m.data { 66 + if v.isExpired() { 67 + delete(m.data, k) 68 + } 69 + } 70 + 71 + m.mu.Unlock() 72 + } 73 +}
M
internal/config/config.go
路路路 46 46 Interval time.Duration `yaml:"interval"` 47 47 GithubToken string `yaml:"github_token"` 48 48 } 49 + 50 +type CacheConfig struct { 51 + HomePage time.Duration `yaml:"home_page"` 52 + Readme time.Duration `yaml:"readme"` 49 53 } 50 54 51 55 type Config struct { 路路路 54 58 Repo RepoConfig `yaml:"repo"` 55 59 SSH SSHConfig `yaml:"ssh"` 56 60 Mirror MirrorConfig `yaml:"mirror"` 61 + Cache CacheConfig `yaml:"cache"` 57 62 } 58 63 59 64 func Load(fpath string) (*Config, error) { 路路路 142 147 // mirroring 143 148 if c.Mirror.Interval == 0 { 144 149 c.Mirror.Interval = 8 * time.Hour 150 + } 151 + 152 + // cache 153 + if c.Cache.HomePage == 0 { 154 + c.Cache.HomePage = 5 * time.Minute 155 + } 156 + 157 + if c.Cache.Readme == 0 { 158 + c.Cache.Readme = 1 * time.Minute 145 159 } 146 160 } 147 161
M
internal/handlers/handlers.go
路路路 7 7 "strings" 8 8 "time" 9 9 10 + "olexsmir.xyz/mugit/internal/cache" 10 11 "olexsmir.xyz/mugit/internal/config" 11 12 "olexsmir.xyz/mugit/internal/humanize" 12 13 "olexsmir.xyz/mugit/web" 路路路 15 16 type handlers struct { 16 17 c *config.Config 17 18 t *template.Template 19 + 20 + repoListCache cache.Cacher[[]repoList] 21 + readmeCache cache.Cacher[template.HTML] 18 22 } 19 23 20 24 func InitRoutes(cfg *config.Config) http.Handler { 21 25 tmpls := template.Must(template.New(""). 22 26 Funcs(templateFuncs). 23 27 ParseFS(web.TemplatesFS, "*")) 24 - h := handlers{cfg, tmpls} 28 + h := handlers{ 29 + cfg, tmpls, 30 + cache.NewInMemory[[]repoList](cfg.Cache.HomePage), 31 + cache.NewInMemory[template.HTML](cfg.Cache.Readme), 32 + } 25 33 26 34 mux := http.NewServeMux() 27 35 mux.HandleFunc("GET /", h.indexHandler)
M
internal/handlers/repo.go
路路路 35 35 h.templ(w, "index", data) 36 36 } 37 37 38 -var markdown = goldmark.New( 39 - goldmark.WithRendererOptions(html.WithUnsafe()), 40 - goldmark.WithExtensions( 41 - extension.GFM, 42 - extension.Linkify, 43 - )) 44 - 45 38 func (h *handlers) repoIndex(w http.ResponseWriter, r *http.Request) { 46 39 repo, err := h.openPublicRepo(r.PathValue("name"), "") 47 40 if err != nil { 路路路 67 60 return 68 61 } 69 62 70 - var readmeContents template.HTML 71 - for _, readme := range h.c.Repo.Readmes { 72 - fc, ferr := repo.FileContent(readme) 73 - if ferr != nil { 74 - continue 75 - } 76 - 77 - if fc.IsBinary { 78 - continue 79 - } 80 - 81 - ext := filepath.Ext(readme) 82 - content := fc.String() 83 - if len(content) > 0 { 84 - switch ext { 85 - case ".md", ".markdown", ".mkd": 86 - var buf bytes.Buffer 87 - if cerr := markdown.Convert([]byte(content), &buf); cerr != nil { 88 - h.write500(w, cerr) 89 - return 90 - } 91 - readmeContents = template.HTML(buf.String()) 92 - default: 93 - readmeContents = template.HTML(fmt.Sprintf(`<pre>%s</pre>`, content)) 94 - } 95 - break 96 - } 63 + masterBranch, err := repo.FindMasterBranch(h.c.Repo.Masters) 64 + if err != nil { 65 + h.write500(w, err) 66 + return 97 67 } 98 68 99 - masterBranch, err := repo.FindMasterBranch(h.c.Repo.Masters) 69 + readme, err := h.renderReadme(repo) 100 70 if err != nil { 101 71 h.write500(w, err) 102 72 return 路路路 113 83 } 114 84 115 85 data["ref"] = masterBranch 116 - data["readme"] = readmeContents 86 + data["readme"] = readme 117 87 data["commits"] = commits 118 88 data["gomod"] = repo.IsGoMod() 119 89 路路路 383 353 } 384 354 385 355 func (h *handlers) listPublicRepos() ([]repoList, error) { 356 + if v, found := h.repoListCache.Get("repo_list"); found { 357 + return v, nil 358 + } 359 + 386 360 dirs, err := os.ReadDir(h.c.Repo.Dir) 387 361 if err != nil { 388 362 return nil, err 路路路 425 399 return repos[j].LastCommit.Before(repos[i].LastCommit) 426 400 }) 427 401 402 + h.repoListCache.Set("repo_list", repos) 428 403 return repos, errors.Join(errs...) 429 404 } 405 + 406 +var markdown = goldmark.New( 407 + goldmark.WithRendererOptions(html.WithUnsafe()), 408 + goldmark.WithExtensions( 409 + extension.GFM, 410 + extension.Linkify, 411 + )) 412 + 413 +func (h *handlers) renderReadme(r *git.Repo) (template.HTML, error) { 414 + if v, found := h.readmeCache.Get(r.Name()); found { 415 + return v, nil 416 + } 417 + 418 + var readmeContents template.HTML 419 + for _, readme := range h.c.Repo.Readmes { 420 + fc, ferr := r.FileContent(readme) 421 + if ferr != nil { 422 + continue 423 + } 424 + 425 + if fc.IsBinary { 426 + continue 427 + } 428 + 429 + ext := filepath.Ext(readme) 430 + content := fc.String() 431 + if len(content) > 0 { 432 + switch ext { 433 + case ".md", ".markdown", ".mkd": 434 + var buf bytes.Buffer 435 + if cerr := markdown.Convert([]byte(content), &buf); cerr != nil { 436 + return "", cerr 437 + } 438 + readmeContents = template.HTML(buf.String()) 439 + default: 440 + readmeContents = template.HTML(fmt.Sprintf(`<pre>%s</pre>`, content)) 441 + } 442 + break 443 + } 444 + } 445 + 446 + h.readmeCache.Set(r.Name(), readmeContents) 447 + return readmeContents, nil 448 +}