penguin/flatgit

flatgit showcase

internal/config/config.go

raw ยท 3941 bytes

package config

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/url"
	"os"
	"path/filepath"
	"strings"
	"time"
)

type Config struct {
	Addr      string        `json:"addr"`
	DataDir   string        `json:"data_dir"`
	PublicURL string        `json:"public_url"`
	Webhook   WebhookConfig `json:"webhook"`
	Git       GitConfig     `json:"git"`
	Render    RenderConfig  `json:"render"`
	Repos     []Repo        `json:"repos"`
}

type WebhookConfig struct {
	Secret string `json:"secret"`
}

type GitConfig struct {
	Command      string `json:"command"`
	CloneTimeout string `json:"clone_timeout"`
	FetchTimeout string `json:"fetch_timeout"`
}

type RenderConfig struct {
	Workers    int `json:"workers"`
	MaxCommits int `json:"max_commits"`
}

type Repo struct {
	Name          string `json:"name"`
	Owner         string `json:"owner"`
	URL           string `json:"url"`
	DefaultBranch string `json:"default_branch"`
	MirrorDir     string `json:"mirror_dir"`
	OutputDir     string `json:"output_dir"`
	Description   string `json:"description"`

	fullName string
	repoBase string
}

func Load(path string) (*Config, error) {
	b, err := os.ReadFile(path)
	if err != nil {
		return nil, err
	}

	var cfg Config
	if err := json.Unmarshal(b, &cfg); err != nil {
		return nil, fmt.Errorf("parse %s: %w", path, err)
	}
	if err := cfg.ApplyDefaults(); err != nil {
		return nil, err
	}
	return &cfg, nil
}

func (c *Config) ApplyDefaults() error {
	if c.Addr == "" {
		c.Addr = ":8080"
	}
	if c.DataDir == "" {
		c.DataDir = "/var/lib/flatgit"
	}
	if c.Git.Command == "" {
		c.Git.Command = "git"
	}
	if c.Git.CloneTimeout == "" {
		c.Git.CloneTimeout = "2m"
	}
	if c.Git.FetchTimeout == "" {
		c.Git.FetchTimeout = "2m"
	}
	if c.Render.Workers <= 0 {
		c.Render.Workers = 1
	}
	if c.Render.MaxCommits <= 0 {
		c.Render.MaxCommits = 500
	}

	seen := make(map[string]bool, len(c.Repos))
	for i := range c.Repos {
		r := &c.Repos[i]
		if r.Name == "" {
			return errors.New("repo missing name")
		}
		if seen[r.Name] {
			return fmt.Errorf("duplicate repo name %q", r.Name)
		}
		seen[r.Name] = true

		r.Name = cleanName(r.Name)
		if r.Owner != "" {
			r.Owner = cleanName(r.Owner)
		}
		if r.DefaultBranch == "" {
			r.DefaultBranch = "main"
		}
		if r.MirrorDir == "" {
			r.MirrorDir = filepath.Join(c.DataDir, "repos", r.RepoBase()+".git")
		}
		if r.OutputDir == "" {
			r.OutputDir = filepath.Join(c.DataDir, "www", r.RepoBase())
		}
	}
	return nil
}

func (c *Config) CloneTimeout() time.Duration {
	return parseDuration(c.Git.CloneTimeout, 2*time.Minute)
}

func (c *Config) FetchTimeout() time.Duration {
	return parseDuration(c.Git.FetchTimeout, 2*time.Minute)
}

func (c *Config) WebRoot() string {
	return filepath.Join(c.DataDir, "www")
}

func (c Config) RepoByName(name string) (*Repo, bool) {
	for _, repo := range c.Repos {
		if repo.FullName() == name || repo.Name == name {
			return &repo, true
		}
	}
	return &Repo{}, false
}

func (c Config) RepoByWebhookName(name string) (Repo, bool) {
	for _, repo := range c.Repos {
		if repo.FullName() == name {
			return repo, true
		}

		// Fallback for single-owner or old/simple configs.
		if repo.Name == name {
			return repo, true
		}
	}

	return Repo{}, false
}

func (r Repo) FullName() string {
	if r.fullName != "" {
		return r.fullName
	}

	if r.Owner == "" {
		return r.Name
	}

	return r.Owner + "/" + r.Name
}

func (r Repo) RepoBase() string {
	if r.repoBase != "" {
		return r.repoBase
	}

	if r.Owner == "" {
		return "/" + url.PathEscape(r.Name) + "/"
	}

	return "/" + url.PathEscape(r.Owner) + "/" + url.PathEscape(r.Name) + "/"
}

func parseDuration(s string, fallback time.Duration) time.Duration {
	d, err := time.ParseDuration(s)
	if err != nil {
		return fallback
	}
	return d
}

func cleanName(s string) string {
	s = strings.TrimSpace(s)
	s = strings.ReplaceAll(s, "\\", "_")
	s = strings.ReplaceAll(s, "/", "_")
	s = strings.ReplaceAll(s, "..", "_")
	return s
}