// Command gsg serves a modular git browser. // // gsg -root /srv/git -addr :8080 -name "rschanz.org" // gsg -root /srv/git -templates ./templates # hot-reload the theme // // Per-repository settings live in each repo's own git config, cgit-style: // // git --git-dir=/srv/git/foo.git config gsg.title "foo" // git --git-dir=/srv/git/foo.git config gsg.description "a thing" // git --git-dir=/srv/git/foo.git config gsg.hide true package main import ( "embed" "flag" "fmt" "io/fs" "log" "log/slog" "net/http" "os" "path/filepath" "sort" "time" "gsg/internal/core" "gsg/internal/gitx" _ "gsg/internal/modules" // registers the built-in modules "gsg/internal/site" ) //go:embed templates var templateFS embed.FS //go:embed static var staticFS embed.FS func main() { var ( root = flag.String("root", "/srv/git", "directory containing bare repositories") addr = flag.String("addr", ":8080", "listen address") name = flag.String("name", "git", "site name") base = flag.String("base", "", "base URL") tmplDir = flag.String("templates", "", "serve templates from this directory, re-parsed per request") cache = flag.Int("cache", 4096, "module cache entries") timeout = flag.Duration("timeout", 10*time.Second, "per-request git budget") ) flag.Parse() if err := run(*root, *addr, *name, *base, *tmplDir, *cache, *timeout); err != nil { log.Fatalf("gsg: %v", err) } } func run(root, addr, name, base, tmplDir string, cacheSize int, timeout time.Duration) error { siteCfg := &core.Site{Name: name, BaseURL: base, Root: root} handles, err := discover(root) if err != nil { return err } if len(handles) == 0 { return fmt.Errorf("no repositories found under %s", root) } siteCfg.Repos = handles var tfs fs.FS if tmplDir != "" { tfs = os.DirFS(tmplDir) } else if tfs, err = fs.Sub(templateFS, "templates"); err != nil { return err } srv, err := core.NewServer(siteCfg, site.Routes(), tfs, cacheSize) if err != nil { return err } srv.TemplateDir = tmplDir srv.Timeout = timeout static, err := fs.Sub(staticFS, "static") if err != nil { return err } mux := http.NewServeMux() mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(static)))) mux.Handle("/", srv.Handler()) slog.Info("gsg listening", "addr", addr, "repos", len(handles), "routes", len(site.Routes()), "modules", len(core.Names())) hs := &http.Server{ Addr: addr, Handler: mux, ReadHeaderTimeout: 5 * time.Second, WriteTimeout: timeout + 5*time.Second, } return hs.ListenAndServe() } // discover finds bare repositories one level below root. // Extend to walk recursively if you nest by section. func discover(root string) ([]core.Handle, error) { entries, err := os.ReadDir(root) if err != nil { return nil, err } var handles []core.Handle for _, e := range entries { if !e.IsDir() { continue } r, err := gitx.Open(filepath.Join(root, e.Name())) if err != nil { continue // not a repository } handles = append(handles, core.Handle{Repo: r, Info: core.LoadRepoInfo(r)}) } sort.Slice(handles, func(i, j int) bool { return handles[i].Info.Slug < handles[j].Info.Slug }) return handles, nil }