gshr
git static host repo -- generates static html for repos
git clone https://git.vogt.world/gshr.git
Log | Files | README.md | LICENSE
← All files
name: main.go
-rw-r--r--
7174
  1package main
  2
  3import (
  4	"bytes"
  5	"embed"
  6	_ "embed"
  7	"flag"
  8	"fmt"
  9	"log"
 10	"os"
 11	"os/exec"
 12	"path"
 13	"strings"
 14	"time"
 15
 16	"github.com/alecthomas/chroma/formatters/html"
 17	"github.com/alecthomas/chroma/lexers"
 18	"github.com/alecthomas/chroma/styles"
 19	"github.com/go-git/go-git/v5"
 20)
 21
 22//go:embed template.*.html
 23var htmlTemplates embed.FS
 24
 25//go:embed gshr.css
 26var css []byte
 27
 28//go:embed favicon.ico
 29var favicon []byte
 30
 31var args cmdArgs
 32
 33var conf config
 34
 35var stt settings
 36
 37func main() {
 38	var r *git.Repository = &git.Repository{}
 39	initialize()
 40	allRepoData := []repoData{}
 41	for _, repo := range conf.Repos {
 42		data := cloneAndGetData(repo, r)
 43		allRepoData = append(allRepoData, data)
 44		renderLogPage(data, r)
 45		renderAllCommitPages(data, r)
 46		renderAllFilesPage(data)
 47		renderIndividualFilePages(data)
 48	}
 49	renderIndexPage(allRepoData)
 50	renderAssets()
 51	for _, repo := range conf.Repos {
 52		hostRepo(repo)
 53	}
 54}
 55
 56func initialize() {
 57	log.SetFlags(0)
 58	log.SetOutput(new(logger))
 59	args = defaultCmdArgs()
 60	stt = defaultSettings()
 61	pwd, err := os.Getwd()
 62	checkErr(err)
 63	args.Wd = pwd
 64	flag.StringVar(&args.ConfigPath, "c", "", "Config file.")
 65	flag.StringVar(&args.OutputDir, "o", "", "Dir of output.")
 66	flag.BoolVar(&args.Silent, "s", false, "Run in silent mode.")
 67	flag.Parse()
 68	debug("working dir '%v'", args.Wd)
 69
 70	if !strings.HasPrefix(args.ConfigPath, "/") {
 71		args.ConfigPath = path.Join(args.Wd, args.ConfigPath)
 72		checkFile(args.ConfigPath)
 73	}
 74
 75	if !strings.HasPrefix(args.OutputDir, "/") {
 76		args.OutputDir = path.Join(args.Wd, args.OutputDir)
 77		checkDir(args.OutputDir)
 78	}
 79
 80	debug("config '%v'", args.ConfigPath)
 81	debug("output '%v'", args.OutputDir)
 82	configFileBytes, err := os.ReadFile(args.ConfigPath)
 83	configString := string(configFileBytes)
 84	checkErr(err)
 85	conf = parseConfig(configString)
 86	debug("base_url '%v'", conf.Site.BaseURL)
 87	debug("site_name '%v'", conf.Site.Name)
 88	conf.validate()
 89}
 90
 91func cloneAndGetData(repo repoConfig, r *git.Repository) repoData {
 92	err := os.MkdirAll(repo.cloneDir(), 0755)
 93	checkErr(err)
 94	err = os.MkdirAll(path.Join(args.OutputDir, repo.Name), 0755)
 95	checkErr(err)
 96	debug("cloning '%v'", repo.Name)
 97	repoRef, err := git.PlainClone(repo.cloneDir(), false, &git.CloneOptions{
 98		URL: repo.URL,
 99	})
100	checkErr(err)
101	data := repoData{
102		repoConfig: repo,
103		AltLink:    repo.AltLink,
104		BaseURL:    conf.Site.BaseURL,
105		HeadData: HeadData{
106			BaseURL:  conf.Site.BaseURL,
107			SiteName: conf.Site.Name,
108			GenTime:  args.GenTime,
109		},
110		ReadMePath:      repo.findFileInRoot(stt.AllowedReadMeFiles),
111		LicenseFilePath: repo.findFileInRoot(stt.AllowedLicenseFiles),
112	}
113	*r = *repoRef
114	return data
115}
116
117func renderAssets() {
118	debug("rendering gshr.css")
119	debug("rendering favicon.ico")
120	checkErr(os.WriteFile(path.Join(args.OutputDir, "gshr.css"), css, 0666))
121	checkErr(os.WriteFile(path.Join(args.OutputDir, "favicon.ico"), favicon, 0666))
122}
123
124func hostRepo(data repoConfig) {
125	debug("hosting '%v'", data.Name)
126	old := path.Join(data.cloneDir(), ".git")
127	renamed := path.Join(args.OutputDir, fmt.Sprintf("%v.git", data.Name))
128	repoFiles := path.Join(args.OutputDir, data.Name, "git")
129	final := path.Join(args.OutputDir, fmt.Sprintf("%v.git", data.Name))
130	debug("renaming '%v' to %v", data.Name, renamed)
131	checkErr(os.Rename(old, renamed))
132	debug("running 'git update-server-info' in %v", renamed)
133	cmd := exec.Command("git", "update-server-info")
134	cmd.Dir = renamed
135	checkErr(cmd.Run())
136	os.RemoveAll(repoFiles)
137	debug("hosting '%v' at %v", data.Name, final)
138}
139
140type logger struct{}
141
142func (writer logger) Write(bytes []byte) (int, error) {
143	return fmt.Print(string(bytes))
144}
145
146func checkErr(err error) {
147	if err != nil {
148		log.Printf("ERROR: %v", err)
149		os.Exit(1)
150	}
151}
152
153func checkFile(filename string) {
154	_, err := os.Stat(filename)
155	checkErr(err)
156}
157
158func checkDir(dir string) {
159	_, err := os.ReadDir(dir)
160	checkErr(err)
161}
162
163func debug(format string, a ...any) {
164	if !args.Silent {
165		log.Printf("DEBUG: "+format, a...)
166	}
167}
168
169func highlight(pathOrExtension string, data *string) string {
170	lexer := lexers.Match(pathOrExtension)
171	if lexer == nil {
172		lexer = lexers.Fallback
173	}
174	style := styles.Get("borland")
175	if style == nil {
176		style = styles.Fallback
177	}
178	formatter := html.New(
179		html.WithClasses(true),
180		html.WithLineNumbers(true),
181		html.LinkableLineNumbers(true, ""),
182	)
183	iterator, err := lexer.Tokenise(nil, *data)
184	buf := bytes.NewBufferString("")
185	err = formatter.Format(buf, style, iterator)
186	checkErr(err)
187	return buf.String()
188}
189
190type cmdArgs struct {
191	Silent     bool
192	Wd         string
193	ConfigPath string
194	OutputDir  string
195	GenTime    string
196}
197
198func defaultCmdArgs() cmdArgs {
199	return cmdArgs{
200		Silent:     true,
201		ConfigPath: "",
202		OutputDir:  "",
203		GenTime:    time.Now().Format("Mon Jan 2 15:04:05 MST 2006"),
204	}
205}
206
207type settings struct {
208	TextExtensions      map[string]bool
209	PlainFiles          map[string]bool
210	AllowedLicenseFiles map[string]bool
211	AllowedReadMeFiles  map[string]bool
212}
213
214func defaultSettings() settings {
215	return settings{
216		TextExtensions: map[string]bool{
217			".c":              true,
218			".cc":             true,
219			".conf":           true,
220			".config":         true,
221			".cpp":            true,
222			".cs":             true,
223			".css":            true,
224			".csv":            true,
225			".Dockerfile":     true,
226			".dot":            true,
227			".eslintignore":   true,
228			".eslintrc":       true,
229			".bashrc":         true,
230			".zshrc":          true,
231			".zshprofile":     true,
232			".g4":             true,
233			".gitignore":      true,
234			".gitmodules":     true,
235			".go":             true,
236			".h":              true,
237			".htm":            true,
238			".html":           true,
239			".iml":            true,
240			".interp":         true,
241			".java":           true,
242			".js":             true,
243			".json":           true,
244			".jsx":            true,
245			".less":           true,
246			".lock":           true,
247			".log":            true,
248			".Makefile":       true,
249			".md":             true,
250			".mod":            true,
251			".php":            true,
252			".prettierignore": true,
253			".py":             true,
254			".rb":             true,
255			".rs":             true,
256			".scss":           true,
257			".sql":            true,
258			".sum":            true,
259			".svg":            true,
260			".tokens":         true,
261			".toml":           true,
262			".ts":             true,
263			".tsv":            true,
264			".tsx":            true,
265			".txt":            true,
266			".xml":            true,
267			".yaml":           true,
268			".yml":            true,
269		},
270		PlainFiles: map[string]bool{
271			"Dockerfile":  true,
272			"license-mit": true,
273			"LICENSE-MIT": true,
274			"license":     true,
275			"LICENSE":     true,
276			"Makefile":    true,
277			"readme":      true,
278			"Readme":      true,
279			"ReadMe":      true,
280			"README":      true,
281		},
282		AllowedLicenseFiles: map[string]bool{
283			"license-mit": true,
284			"LICENSE-MIT": true,
285			"license.md":  true,
286			"LICENSE.md":  true,
287			"license.txt": true,
288			"LICENSE.txt": true,
289			"LICENSE":     true,
290		},
291		AllowedReadMeFiles: map[string]bool{
292			"readme.md":  true,
293			"Readme.md":  true,
294			"ReadMe.md":  true,
295			"README.md":  true,
296			"readme.txt": true,
297			"README.txt": true,
298			"README":     true,
299		},
300	}
301}