all repos

mugit @ 850c26a5c0990b179be8a94ab0337c2c9636584e

🐮 git server that your cow will love
4 files changed, 151 insertions(+), 37 deletions(-)
http: add cache for index page contents, and repos readmes
Author: Oleksandr Smirnov olexsmir@gmail.com
Committed at: 2026-02-15 15:44:39 +0200
Change ID: wuzskmsqmpukyyqurtmnmrnnxslruxvm
Parent: b7d443e
A internal/cache/cache.go

@@ -0,0 +1,73 @@

+package cache + +import ( + "errors" + "sync" + "time" +) + +var ErrNotFound = errors.New("not found") + +type Cacher[T any] interface { + Set(key string, val T) + Get(key string) (val T, found bool) +} + +type item[T any] struct { + v T + expiry time.Time +} + +func (i item[T]) isExpired() bool { + return time.Now().After(i.expiry) +} + +type InMemory[T any] struct { + mu sync.RWMutex + ttl time.Duration + data map[string]item[T] +} + +func NewInMemory[T any](ttl time.Duration) *InMemory[T] { + c := &InMemory[T]{ + data: make(map[string]item[T]), + ttl: ttl, + } + + go c.clean() + return c +} + +func (m *InMemory[T]) Set(key string, val T) { + m.mu.Lock() + defer m.mu.Unlock() + m.data[key] = item[T]{ + v: val, + expiry: time.Now().Add(m.ttl), + } +} + +func (m *InMemory[T]) Get(key string) (T, bool) { + m.mu.Lock() + defer m.mu.Unlock() + + val, found := m.data[key] + if !found { + var t T + return t, false + } + return val.v, true +} + +func (m *InMemory[T]) clean() { + for range time.Tick(5 * time.Second) { + m.mu.Lock() + for k, v := range m.data { + if v.isExpired() { + delete(m.data, k) + } + } + + m.mu.Unlock() + } +}
M internal/config/config.go

@@ -46,6 +46,10 @@ Enable bool `yaml:"enable"`

Interval time.Duration `yaml:"interval"` GithubToken string `yaml:"github_token"` } + +type CacheConfig struct { + HomePage time.Duration `yaml:"home_page"` + Readme time.Duration `yaml:"readme"` } type Config struct {

@@ -54,6 +58,7 @@ Meta MetaConfig `yaml:"meta"`

Repo RepoConfig `yaml:"repo"` SSH SSHConfig `yaml:"ssh"` Mirror MirrorConfig `yaml:"mirror"` + Cache CacheConfig `yaml:"cache"` } func Load(fpath string) (*Config, error) {

@@ -142,6 +147,15 @@

// mirroring if c.Mirror.Interval == 0 { c.Mirror.Interval = 8 * time.Hour + } + + // cache + if c.Cache.HomePage == 0 { + c.Cache.HomePage = 5 * time.Minute + } + + if c.Cache.Readme == 0 { + c.Cache.Readme = 1 * time.Minute } }
M internal/handlers/handlers.go

@@ -7,6 +7,7 @@ "path/filepath"

"strings" "time" + "olexsmir.xyz/mugit/internal/cache" "olexsmir.xyz/mugit/internal/config" "olexsmir.xyz/mugit/internal/humanize" "olexsmir.xyz/mugit/web"

@@ -15,13 +16,20 @@

type handlers struct { c *config.Config t *template.Template + + repoListCache cache.Cacher[[]repoList] + readmeCache cache.Cacher[template.HTML] } func InitRoutes(cfg *config.Config) http.Handler { tmpls := template.Must(template.New(""). Funcs(templateFuncs). ParseFS(web.TemplatesFS, "*")) - h := handlers{cfg, tmpls} + h := handlers{ + cfg, tmpls, + cache.NewInMemory[[]repoList](cfg.Cache.HomePage), + cache.NewInMemory[template.HTML](cfg.Cache.Readme), + } mux := http.NewServeMux() mux.HandleFunc("GET /", h.indexHandler)
M internal/handlers/repo.go

@@ -35,13 +35,6 @@ data["servername"] = h.c.Meta.Host

h.templ(w, "index", data) } -var markdown = goldmark.New( - goldmark.WithRendererOptions(html.WithUnsafe()), - goldmark.WithExtensions( - extension.GFM, - extension.Linkify, - )) - func (h *handlers) repoIndex(w http.ResponseWriter, r *http.Request) { repo, err := h.openPublicRepo(r.PathValue("name"), "") if err != nil {

@@ -67,36 +60,13 @@ h.templ(w, "repo_index", data)

return } - var readmeContents template.HTML - for _, readme := range h.c.Repo.Readmes { - fc, ferr := repo.FileContent(readme) - if ferr != nil { - continue - } - - if fc.IsBinary { - continue - } - - ext := filepath.Ext(readme) - content := fc.String() - if len(content) > 0 { - switch ext { - case ".md", ".markdown", ".mkd": - var buf bytes.Buffer - if cerr := markdown.Convert([]byte(content), &buf); cerr != nil { - h.write500(w, cerr) - return - } - readmeContents = template.HTML(buf.String()) - default: - readmeContents = template.HTML(fmt.Sprintf(`<pre>%s</pre>`, content)) - } - break - } + masterBranch, err := repo.FindMasterBranch(h.c.Repo.Masters) + if err != nil { + h.write500(w, err) + return } - masterBranch, err := repo.FindMasterBranch(h.c.Repo.Masters) + readme, err := h.renderReadme(repo) if err != nil { h.write500(w, err) return

@@ -113,7 +83,7 @@ commits = commits[:3]

} data["ref"] = masterBranch - data["readme"] = readmeContents + data["readme"] = readme data["commits"] = commits data["gomod"] = repo.IsGoMod()

@@ -383,6 +353,10 @@ LastCommit time.Time

} func (h *handlers) listPublicRepos() ([]repoList, error) { + if v, found := h.repoListCache.Get("repo_list"); found { + return v, nil + } + dirs, err := os.ReadDir(h.c.Repo.Dir) if err != nil { return nil, err

@@ -425,5 +399,50 @@ sort.Slice(repos, func(i, j int) bool {

return repos[j].LastCommit.Before(repos[i].LastCommit) }) + h.repoListCache.Set("repo_list", repos) return repos, errors.Join(errs...) } + +var markdown = goldmark.New( + goldmark.WithRendererOptions(html.WithUnsafe()), + goldmark.WithExtensions( + extension.GFM, + extension.Linkify, + )) + +func (h *handlers) renderReadme(r *git.Repo) (template.HTML, error) { + if v, found := h.readmeCache.Get(r.Name()); found { + return v, nil + } + + var readmeContents template.HTML + for _, readme := range h.c.Repo.Readmes { + fc, ferr := r.FileContent(readme) + if ferr != nil { + continue + } + + if fc.IsBinary { + continue + } + + ext := filepath.Ext(readme) + content := fc.String() + if len(content) > 0 { + switch ext { + case ".md", ".markdown", ".mkd": + var buf bytes.Buffer + if cerr := markdown.Convert([]byte(content), &buf); cerr != nil { + return "", cerr + } + readmeContents = template.HTML(buf.String()) + default: + readmeContents = template.HTML(fmt.Sprintf(`<pre>%s</pre>`, content)) + } + break + } + } + + h.readmeCache.Set(r.Name(), readmeContents) + return readmeContents, nil +}