Download
115 lines 3.7 KiBGoModified 2026-10-27 raw
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
package render

import (
	"errors"
	"fmt"
	"io"
	"os"
	"strings"

	"code.bruce-hill.com/repo-renderer/internal/config"
	"code.bruce-hill.com/repo-renderer/internal/gitx"
	"code.bruce-hill.com/repo-renderer/internal/httpd"
)

// Check validates the configuration (loaded from cfgPath), lexers,
// templates and the directories and tools a render needs, reporting on w.
// Problems that would break a render are returned; the rest are warnings.
func (s *Site) Check(w io.Writer, cfgPath string) error {
	var problems []error
	// Run git under the same sandbox the renderer uses, so a missing
	// sandbox entry shows up here and not in the first post-receive.
	if err := s.restrict(); err != nil {
		problems = append(problems, fmt.Errorf("sandbox: %w", err))
	}
	v, err := gitx.Version()
	if err != nil {
		problems = append(problems, fmt.Errorf("git does not run (missing from the sandbox?): %w", err))
	} else {
		fmt.Fprintln(w, "git:", v)
	}
	probe := gitx.Repo{Dir: s.Lay.GitBase}
	if _, err := probe.Run("rev-parse", "--is-bare-repository"); err != nil && !strings.Contains(err.Error(), "not a git repository") {
		problems = append(problems, fmt.Errorf("git cannot run inside %s: %w", s.Lay.GitBase, err))
	}
	fmt.Fprintln(w, "config:", cfgPath)
	lay := s.Lay
	logPath := s.Cfg.Log
	if logPath == "" {
		logPath = "(stderr)"
	}
	for _, kv := range [][2]string{
		{"repositories", lay.GitBase}, {"site", lay.Htdocs}, {"spool", lay.QueueDir},
		{"state", lay.State}, {"log", logPath}, {"lexers", strings.Join(s.Cfg.LexerDirs, " ")},
	} {
		fmt.Fprintf(w, "%s: %s\n", kv[0], kv[1])
	}
	fmt.Fprintln(w, "host:", s.Cfg.Host)
	fmt.Fprintln(w, "style:", s.HL.StyleName())
	fmt.Fprintln(w, "template hash:", s.TmplHash)
	for _, name := range s.Cfg.LexerNames() {
		lx := s.HL.ByName(name)
		if s.HL.IsPlain(lx) {
			problems = append(problems, fmt.Errorf("lexer %q resolves to plain text", name))
		} else {
			fmt.Fprintf(w, "lexer %s: ok (%v)\n", name, s.Cfg.Lexers[name].Globs)
		}
	}
	for _, d := range []string{lay.GitBase, lay.Htdocs, lay.QueueDir, lay.State} {
		if st, err := os.Stat(d); err != nil || !st.IsDir() {
			fmt.Fprintf(w, "warning: %s does not exist yet\n", d)
		}
	}
	if l := s.Cfg.Log; l != "" && l != config.LogSyslog {
		if f, err := os.OpenFile(l, os.O_WRONLY|os.O_APPEND, 0); err != nil {
			fmt.Fprintf(w, "warning: cannot write the log %s: %v\n", l, err)
		} else {
			_ = f.Close() // opened only to see that it could be
		}
	}
	repos, err := s.Repos()
	if err != nil {
		problems = append(problems, err)
	}
	have := map[string]bool{}
	for _, r := range repos {
		have[r] = true
	}
	for _, sec := range s.Cfg.Sections {
		for _, r := range sec.Repos {
			if !have[r] {
				fmt.Fprintf(w, "warning: section %q lists %q, which is not under %s\n", sec.Title, r, s.Lay.GitBase)
			}
		}
	}
	var private []string
	for _, r := range repos {
		if s.Cfg.Private(r) {
			private = append(private, r)
		}
	}
	if len(private) > 0 {
		where := "not on the site: set private_htdocs to serve them behind a password"
		if s.priv != nil {
			where = "on the private site, " + s.Cfg.BaseURL() + httpd.PrivatePrefix + "/"
		}
		fmt.Fprintf(w, "private (in no section, so %s): %s\n", where, strings.Join(private, ", "))
	}
	mirrors := false
	for r, rc := range s.Cfg.Repos {
		if !have[r] {
			fmt.Fprintf(w, "warning: repos.%s configured but not under %s\n", r, s.Lay.GitBase)
		}
		mirrors = mirrors || len(rc.Mirrors) > 0
	}
	if mirrors {
		if _, err := os.Stat("/etc/ssh/ssh_known_hosts"); err != nil {
			fmt.Fprintf(w, "warning: mirroring is configured but ssh knows no host keys; as root, run this for each mirror's host\n  ssh-keyscan HOST >> /etc/ssh/ssh_known_hosts\n")
		}
	}
	if len(problems) > 0 {
		return errors.Join(problems...)
	}
	fmt.Fprintln(w, "ok")
	return nil
}