123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172 | // Command repo-render turns bare git repositories into a static website.
//
// repo-render --config FILE <command> [args]
//
// $REPO_RENDER_CONFIG names the config when --config does not; there is
// no default.
//
// Commands:
//
// kick drain the job queue (what the post-receive hook runs)
// render <repo>... render repositories at their branch tip (--full to redo everything)
// render --all render every repository under git_dir, removing any other
// index rebuild the repository index and static assets only
// enqueue R REF OLD NEW append a job (what the post-receive hook runs)
// check validate config, lexers and templates; exit non-zero on problems
package main
import (
"errors"
"fmt"
"io"
"log"
"log/syslog"
"os"
"github.com/alecthomas/kong"
"code.bruce-hill.com/repo-renderer/internal/config"
"code.bruce-hill.com/repo-renderer/internal/gitx"
"code.bruce-hill.com/repo-renderer/internal/queue"
"code.bruce-hill.com/repo-renderer/internal/render"
)
// cli is the command line. Each command's Run takes what it needs: the
// *config.Config alone, or the *render.Site, which also opens the log.
// enqueue runs on every push, so it must be fast, and takes only the
// config.
type cli struct {
Config string `default:"${default_config}" placeholder:"FILE" help:"Config file ($$REPO_RENDER_CONFIG sets it)."`
Kick kickCmd `cmd:"" help:"Drain the job queue under the render lock."`
Render renderCmd `cmd:"" help:"Render repositories at their branch tip."`
Index indexCmd `cmd:"" help:"Rebuild the index page and static assets."`
Enqueue enqueueCmd `cmd:"" help:"Append a job to the queue."`
Check checkCmd `cmd:"" help:"Validate configuration, lexers and templates."`
}
func main() {
// Not kong's env tag: that takes the variable even when it is empty,
// and an empty one has always meant none.
var c cli
ctx := kong.Parse(&c,
kong.Name("repo-render"),
kong.Description("Turn bare git repositories into a static website."),
kong.Vars{"default_config": os.Getenv("REPO_RENDER_CONFIG")},
kong.ConfigureHelp(kong.HelpOptions{Compact: true}),
kong.UsageOnError(),
// Usage errors exit 2, as they always have; kong's own is 80.
kong.Exit(func(code int) {
if code == 80 {
code = 2
}
os.Exit(code)
}),
)
if c.Config == "" {
// A usage error, which exits 2 like kong's own.
fmt.Fprintln(os.Stderr, "repo-render: no config: give --config FILE or set REPO_RENDER_CONFIG")
os.Exit(2)
}
cfg, err := config.Load(c.Config)
if err != nil {
fatal(err)
}
if cfg.Git != "" {
gitx.Program = cfg.Git
}
ctx.Bind(cfg)
var logFile io.Closer
if err := ctx.BindSingletonProvider(func() (*render.Site, error) {
site, lf, err := newSite(cfg)
logFile = lf
return site, err
}); err != nil {
fatal(err)
}
err = ctx.Run()
if logFile != nil {
// Nothing is left to report a failed close of the log to.
_ = logFile.Close()
}
if err != nil {
fatal(err)
}
}
// newSite loads the site, logging to stderr and to the configured log
// (syslog or a file), if that opens. What is returned to close may be nil.
func newSite(cfg *config.Config) (*render.Site, io.Closer, error) {
logger := log.New(os.Stderr, "", log.LstdFlags)
site, err := render.New(cfg, logger)
if err != nil {
return nil, nil, err
}
var lw io.WriteCloser
switch cfg.Log {
case "":
return site, nil, nil
case config.LogSyslog:
lw, err = syslog.New(syslog.LOG_DAEMON|syslog.LOG_INFO, "repo-render")
default:
lw, err = os.OpenFile(cfg.Log, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0o644)
}
if err != nil {
logger.Printf("logging to stderr only: %v", err)
return site, nil, nil
}
logger.SetOutput(io.MultiWriter(os.Stderr, lw))
return site, lw, nil
}
func fatal(err error) {
fmt.Fprintln(os.Stderr, "repo-render:", err)
os.Exit(1)
}
type kickCmd struct{}
func (kickCmd) Run(site *render.Site) error { return site.Kick() }
type renderCmd struct {
Full bool `help:"Re-render everything, ignoring saved state."`
All bool `help:"Render every repository under git_dir, and remove the output of any other."`
Repos []string `arg:"" optional:"" name:"repo" help:"Repositories to render."`
}
func (c *renderCmd) Validate() error {
if c.All == (len(c.Repos) > 0) {
return errors.New("give either --all or repositories")
}
return nil
}
func (c *renderCmd) Run(site *render.Site) error {
if c.All {
return site.RenderAll(c.Full)
}
return site.Render(c.Repos, c.Full)
}
type indexCmd struct{}
func (indexCmd) Run(site *render.Site) error { return site.RenderIndex() }
type enqueueCmd struct {
Repo string `arg:"" help:"Repository name."`
Ref string `arg:"" help:"Ref that was pushed."`
Old string `arg:"" help:"Object id before the push."`
New string `arg:"" help:"Object id after the push."`
}
func (c *enqueueCmd) Run(cfg *config.Config) error {
if err := render.Enqueue(cfg, queue.Job{Repo: c.Repo, Ref: c.Ref, Old: c.Old, New: c.New}); err != nil {
return fmt.Errorf("enqueue: %w", err)
}
return nil
}
type checkCmd struct{}
func (checkCmd) Run(site *render.Site, root *cli) error { return site.Check(os.Stdout, root.Config) }
|