depp

No frills static page generator for Git repositories

git clone https://git.8pit.net/depp.git

  1package main
  2
  3import (
  4	"embed"
  5	"errors"
  6	"flag"
  7	"fmt"
  8	"html/template"
  9	"io/fs"
 10	"log"
 11	"net/url"
 12	"os"
 13	"path/filepath"
 14
 15	"github.com/nmeum/depp/css"
 16	"github.com/nmeum/depp/gitweb"
 17)
 18
 19//go:embed tmpl
 20var templates embed.FS
 21
 22var (
 23	commits     = flag.Uint("c", 5, "amount of recent commits to include")
 24	force       = flag.Bool("f", false, "force rebuilding of all HTML files")
 25	gitURL      = flag.String("u", "", "clone URL for the Git repository")
 26	destination = flag.String("d", "./www", "output directory for HTML files")
 27	verbose     = flag.Bool("v", false, "print the name of each changed file")
 28)
 29
 30var tmpl *template.Template
 31
 32const (
 33	// Name of file used to record the hash of the generated tree object.
 34	stateFile = ".tree"
 35)
 36
 37func usage() {
 38	fmt.Fprintf(flag.CommandLine.Output(),
 39		"USAGE: %s [FLAGS] REPOSITORY\n\n"+
 40			"The following flags are supported:\n\n", os.Args[0])
 41
 42	flag.PrintDefaults()
 43	os.Exit(2)
 44}
 45
 46func walkPages(name string, page *gitweb.RepoPage) error {
 47	if *verbose {
 48		fmt.Println(name)
 49	}
 50
 51	dest := filepath.Join(*destination, name+".html")
 52	if page == nil { // file was removed
 53		err := os.Remove(dest)
 54		if err != nil && !errors.Is(err, fs.ErrNotExist) {
 55			return err
 56		}
 57
 58		// In case name refers to a (now empty) directory:
 59		os.Remove(filepath.Join(*destination, name))
 60
 61		return nil
 62	} else if isIndexPage(page) {
 63		dest = filepath.Join(*destination, "index.html")
 64	}
 65
 66	err := os.MkdirAll(filepath.Dir(dest), 0755)
 67	if err != nil {
 68		return err
 69	}
 70	file, err := os.Create(dest)
 71	if err != nil {
 72		return err
 73	}
 74	defer file.Close()
 75
 76	err = tmpl.Execute(file, page)
 77	if err != nil {
 78		return err
 79	}
 80
 81	return nil
 82}
 83
 84func buildHTML() (*template.Template, error) {
 85	var err error
 86
 87	const name = "base.tmpl"
 88	tmpl := template.New(name)
 89
 90	funcMap := template.FuncMap{
 91		"summarize":    summarize,
 92		"getRelPath":   getRelPath,
 93		"increment":    increment,
 94		"decrement":    decrement,
 95		"getLines":     getLines,
 96		"padNumber":    padNumber,
 97		"relIndex":     relIndex,
 98		"isIndexPage":  isIndexPage,
 99		"renderReadme": renderReadme,
100	}
101	tmpl = tmpl.Funcs(funcMap)
102
103	tmpl, err = tmpl.ParseFS(templates, "tmpl/*.tmpl")
104	if err != nil {
105		return nil, err
106	}
107
108	return tmpl, nil
109}
110
111func generate(repo *gitweb.Repo) error {
112	var err error
113	tmpl, err = buildHTML()
114	if err != nil {
115		return err
116	}
117	err = repo.Walk(walkPages)
118	if err != nil {
119		return err
120	}
121
122	cssPath := filepath.Join(*destination, "style.css")
123	_, err = os.Stat(cssPath)
124	if *force || errors.Is(err, os.ErrNotExist) {
125		err = css.Create(cssPath)
126		if err != nil {
127			return err
128		}
129	}
130
131	return nil
132}
133
134func main() {
135	flag.Usage = usage
136	flag.Parse()
137
138	log.SetFlags(log.Lshortfile)
139	if flag.NArg() != 1 {
140		usage()
141	}
142
143	gitURL, err := url.Parse(*gitURL)
144	if err != nil {
145		log.Fatal(err)
146	}
147
148	path := flag.Arg(0)
149	statePath := filepath.Join(*destination, stateFile)
150
151	repo, err := gitweb.NewRepo(path, gitURL, *commits)
152	if err != nil {
153		log.Fatal(err)
154	}
155	if !*force {
156		err = repo.ReadState(statePath)
157		if err != nil && !errors.Is(err, os.ErrNotExist) {
158			log.Fatal(err)
159		}
160	}
161
162	err = generate(repo)
163	if err != nil {
164		log.Fatal(err)
165	}
166	err = repo.WriteState(statePath)
167	if err != nil {
168		log.Fatal(err)
169	}
170}