Commit 8a37f7f
config.go
@@ -0,0 +1,123 @@
+package main
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/BurntSushi/toml"
+)
+
+type SingleRepoConfig struct {
+ Name string `toml:"name"`
+ Path string `toml:"path"`
+ Description string `toml:"description"`
+ DefaultBranch string `toml:"default_branch"`
+}
+
+type RepoEntry struct {
+ Name string `toml:"name"`
+ Slug string `toml:"slug"`
+ Path string `toml:"path"`
+ Description string `toml:"description"`
+ DefaultBranch string `toml:"default_branch"`
+}
+
+type Config struct {
+ SiteName string `toml:"site_name"`
+ Repo *SingleRepoConfig `toml:"repo"`
+ Repos []RepoEntry `toml:"repos"`
+}
+
+func parseConfig(path string) (*Config, error) {
+ absPath, err := filepath.Abs(path)
+ if err != nil {
+ return nil, fmt.Errorf("resolve config path: %w", err)
+ }
+
+ data, err := os.ReadFile(absPath)
+ if err != nil {
+ return nil, fmt.Errorf("read config file: %w", err)
+ }
+
+ var cfg Config
+ if err := toml.Unmarshal(data, &cfg); err != nil {
+ return nil, fmt.Errorf("parse config: %w", err)
+ }
+
+ if cfg.SiteName == "" {
+ cfg.SiteName = "Git repositories"
+ }
+
+ if hasRepo := cfg.Repo != nil; hasRepo {
+ if len(cfg.Repos) > 0 {
+ return nil, fmt.Errorf("config: cannot use both [repo] and [[repos]]")
+ }
+ if cfg.Repo.Name == "" {
+ return nil, fmt.Errorf("config: [repo].name is required")
+ }
+ if cfg.Repo.Path == "" {
+ return nil, fmt.Errorf("config: [repo].path is required")
+ }
+ if err := normalizeRepoPath(&cfg.Repo.Path); err != nil {
+ return nil, fmt.Errorf("config: [repo].path: %w", err)
+ }
+ if cfg.Repo.DefaultBranch == "" {
+ cfg.Repo.DefaultBranch = autoDefaultBranchName(cfg.Repo.Path)
+ }
+ }
+
+ if len(cfg.Repos) > 0 {
+ for i := range cfg.Repos {
+ repo := &cfg.Repos[i]
+ if repo.Name == "" {
+ return nil, fmt.Errorf("config: repos[%d].name is required", i)
+ }
+ if repo.Slug == "" {
+ return nil, fmt.Errorf("config: repos[%d].slug is required", i)
+ }
+ if repo.Path == "" {
+ return nil, fmt.Errorf("config: repos[%d].path is required", i)
+ }
+ if err := normalizeRepoPath(&repo.Path); err != nil {
+ return nil, fmt.Errorf("config: repos[%d].path: %w", i, err)
+ }
+ if repo.DefaultBranch == "" {
+ repo.DefaultBranch = autoDefaultBranchName(repo.Path)
+ }
+ }
+ }
+
+ return &cfg, nil
+}
+
+func normalizeRepoPath(path *string) error {
+ abs, err := filepath.Abs(*path)
+ if err != nil {
+ return err
+ }
+ *path = abs
+ return nil
+}
+
+func autoDefaultBranchName(repoPath string) string {
+ candidates := []string{"master", "main"}
+ for _, name := range candidates {
+ refPath := filepath.Join(repoPath, ".git", "refs", "heads", name)
+ if _, err := os.Stat(refPath); err == nil {
+ return name
+ }
+ }
+ return ""
+}
+
+func sanitizeSlug(name string) string {
+ return strings.NewReplacer(
+ " ", "-",
+ "_", "-",
+ "/", "-",
+ "\\", "-",
+ ".", "-",
+ ).Replace(strings.ToLower(name))
+}
go.mod
@@ -3,6 +3,7 @@ module github.com/antonmedv/gitmal
go 1.24.0
require (
+ github.com/BurntSushi/toml v1.6.0
github.com/alecthomas/chroma/v2 v2.20.0
github.com/spf13/pflag v1.0.10
github.com/tdewolff/minify/v2 v2.24.7
go.sum
@@ -1,3 +1,5 @@
+github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
+github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
main.go
@@ -22,6 +22,7 @@ var (
flagTheme string
flagThemeLight string
flagThemeDark string
+ flagConfig string
flagPreviewThemes bool
flagMinify bool
flagGzip bool
@@ -74,6 +75,7 @@ func main() {
flag.StringVar(&flagTheme, "theme", "github", "Style theme")
flag.StringVar(&flagThemeLight, "theme-light", "", "Light theme for code highlighting (overrides --theme)")
flag.StringVar(&flagThemeDark, "theme-dark", "", "Dark theme for code highlighting (overrides --theme)")
+ flag.StringVar(&flagConfig, "config", "", "Path to TOML config file for multi-repo support")
flag.BoolVar(&flagPreviewThemes, "preview-themes", false, "Preview available themes")
flag.BoolVar(&flagMinify, "minify", false, "Minify all generated HTML files")
flag.BoolVar(&flagGzip, "gzip", false, "Compress all generated HTML files")