all repos

mugit @ 850c26a

馃惍 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
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
        +}