socle

main.go

// socle is a read-only web host for local git repositories:
// browsing of committed state at refs, and anonymous fetch over smart HTTP.
// Nothing in it can write to a repo.
package main

import (
	"flag"
	"log"
	"net/http"
	"time"
)

func main() {
	configPath := flag.String("config", "", "path to the socle config file (required; there is no default location)")
	listen := flag.String("listen", ":8080", "listen address")
	themeSeed := flag.String("theme-seed", "", "theme seed override (default: the sitename; see buildFace for how that resolves)")
	flag.Parse()
	if *configPath == "" {
		log.Fatal("socle: the -config flag is required")
	}
	cfg, err := loadConfig(*configPath)
	if err != nil {
		log.Fatal("socle: ", err)
	}
	fc, err := buildFace(cfg, *themeSeed)
	if err != nil {
		log.Fatal("socle: ", err)
	}
	repos, warnings := openAll(cfg)
	for _, w := range warnings {
		log.Print("socle: ", w)
	}
	srv := newServer(repos, cfg.BaseURL, fc)

	// Config changes apply live: reloads are fail-stable
	// (any problem logs loudly and the old repo set keeps serving),
	// while startup above stays fail-closed as ever.
	watchConfig(*configPath, 5*time.Second, func() {
		reloadConfig(srv, *configPath, *themeSeed)
	})

	log.Printf("socle: serving %d repos on %s", len(repos), *listen)
	log.Fatal(http.ListenAndServe(*listen, srv))
}

// reloadConfig loads the config file and swaps the server's snapshot to it,
// identity included (a sitename edit lands live, like everything else in the config).
// Fail-stable, unlike startup: a config that cannot be loaded (parse or validation
// failure) or that yields not one openable repo is rejected loudly and the previous
// set keeps serving -- a showcase must not go dark from one typo mid-edit.
// Per-repo open failures are the same warning-and-skip as at startup.
func reloadConfig(srv *server, path, themeSeed string) {
	cfg, err := loadConfig(path)
	if err != nil {
		log.Printf("socle: config reload REJECTED, keeping previous repos: %v", err)
		return
	}
	fc, err := buildFace(cfg, themeSeed)
	if err != nil {
		log.Printf("socle: config reload REJECTED, keeping previous repos: %v", err)
		return
	}
	repos, warnings := openAll(cfg)
	for _, w := range warnings {
		log.Print("socle: ", w)
	}
	if len(repos) == 0 {
		log.Printf("socle: config reload REJECTED, keeping previous repos: no configured repo could be opened")
		return
	}
	srv.publish(repos, cfg.BaseURL, fc)
	log.Printf("socle: config reloaded: %d repos", len(repos))
}

main.go · 2.4 KiB · 245d322a2e