123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224 | // 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, and
// without either it is /etc/repo-render/repo-render.yaml.
//
// 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
// add REPO --branch B create a repository, or bring it up to date (what git-selfhost runs, as root)
// info REPO say what there is of a repository (what git-selfhost reads)
// mirror REPO push a repository to its mirrors now (--force over commits made there)
// list show the index's layout: each section's repositories, their tags and mirrors
package main
import (
"cmp"
"errors"
"fmt"
"io"
"log"
"log/syslog"
"os"
"github.com/alecthomas/kong"
"code.bruce-hill.com/repo-renderer/internal/admin"
"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."`
Add addCmd `cmd:"" help:"Create a repository, or bring it up to date: its section, tags, mirrors and key."`
Info infoCmd `cmd:"" help:"Say what there is of a repository, for git-selfhost."`
Mirror mirrorCmd `cmd:"" help:"Push a repository to its mirrors now."`
List listCmd `cmd:"" help:"Show the index's layout: each section's repositories, their tags and mirrors."`
}
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": cmp.Or(os.Getenv("REPO_RENDER_CONFIG"), config.DefaultPath)},
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)
}),
)
cfg, err := config.Load(c.Config)
if err != nil {
fatal(err)
}
gitx.Program = gitx.Find()
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
}
if cfg.Log != config.LogSyslog {
return site, nil, nil
}
lw, err := syslog.New(syslog.LOG_DAEMON|syslog.LOG_INFO, "repo-render")
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) }
type addCmd struct {
Repo string `arg:"" help:"Repository name."`
Branch string `required:"" help:"Its default branch."`
Section string `xor:"section" help:"The name of the section of the index to put it in (default: where it is)."`
NoSection bool `xor:"section" help:"Put it in no section."`
Tag []string `sep:"none" help:"Its tags, replacing those it has; repeatable."`
Description string `short:"d" help:"Its one-line description."`
Mirror []string `sep:"none" help:"A git URL to mirror it to; repeatable."`
Keys string `placeholder:"DIR" help:"Where the mirror keys are (default: keys/ beside the config)."`
KnownHosts string `default:"/etc/ssh/ssh_known_hosts" placeholder:"FILE" help:"Where to add mirror hosts' host keys."`
NewRepo string `hidden:"" help:"The new-repo program."`
DryRun bool `help:"Say what it would do, and do nothing."`
}
func (c *addCmd) Run(cfg *config.Config) error {
o := admin.Options{
Repo: c.Repo, Branch: c.Branch, Tags: c.Tag, Description: c.Description, Mirrors: c.Mirror,
Keys: c.Keys, KnownHosts: c.KnownHosts, NewRepo: c.NewRepo, DryRun: c.DryRun,
}
if o.Keys == "" {
o.Keys = admin.KeyDir(cfg)
}
if c.Section != "" || c.NoSection {
o.Section = &c.Section
}
return admin.Add(os.Stdout, cfg, o)
}
type infoCmd struct {
Repo string `arg:"" help:"Repository name."`
Keys string `placeholder:"DIR" help:"Where the mirror keys are (default: keys/ beside the config)."`
}
func (c *infoCmd) Run(cfg *config.Config) error {
keys := c.Keys
if keys == "" {
keys = admin.KeyDir(cfg)
}
return admin.Info(os.Stdout, cfg, c.Repo, keys)
}
type mirrorCmd struct {
Repo string `arg:"" help:"Repository name."`
Force bool `help:"Push over commits made on the mirrors, which are lost."`
}
func (c *mirrorCmd) Run(cfg *config.Config) error {
return admin.Mirror(os.Stdout, cfg, c.Repo, c.Force)
}
type listCmd struct{}
func (listCmd) Run(cfg *config.Config) error { return admin.List(os.Stdout, cfg) }
|