gshr
git static host repo -- generates static html for repos
git clone https://git.vogt.world/gshr.git
Log | Files | README.md | LICENSE
← Commit log
commit
message
Cleaner configuration, hosting all repos.
author
Ben Vogt <[email protected]>
date
2023-04-10 18:47:15
stats
13 file(s) changed, 133 insertions(+), 109 deletions(-)
files
README.md
commit.go
config.go
dev-config-gshr.toml
example-config-ghsr-simple.toml
example-config-go-git.toml
file.go
files.go
index.go
log.go
main.go
template.index.html
template.partials.html
  1diff --git a/README.md b/README.md
  2index 545e355..d3ee0ca 100644
  3--- a/README.md
  4+++ b/README.md
  5@@ -10,7 +10,7 @@ directory for multiple repos, with...
  6 * Individual commit page summarizing commit including diff.
  7 * File list page for each repo for the current HEAD ref.
  8 * File detail/preview page for each file in current HEAD ref.
  9-* Statically clone-able git folder for each repo.
 10+* Statically clone-able git dir for each repo.
 11 
 12 ---
 13 
 14@@ -45,27 +45,27 @@ Usage of gshr:
 15 
 16 The toml file needs to be in the format:
 17 
 18-* `base_url`: Base url for the site. Eg: `"/"` or `"https://mysite.com/git/"`.  
 19-* `site_name`: Site name displayed on the main index.html page that lists all repos.
 20+* `site`: Site-level configuration.
 21+  * `base_url`: Base url for the site. Eg: `"/"` or `"https://mysite.com/code/"`.
 22+  * `name`: Site name displayed on the main index.html page that lists all repos.
 23 * `repos` List of data for each repo.
 24   * `name`: Name of repo to be used in display, paths.
 25   * `description`: Description of repo used in html pages.
 26   * `url`: Absolute, relative, or remote. eg: `/home/repo`, `./repo`, `git://`, `http://`.
 27-  * `published_git_url`: Optional Link to where the repo lives. Eg: `github.com/vogtb/gshr`.
 28-  * `host_git`: Bool of whether we should package the repo up into `{name}.git` and host it.
 29+  * `alt_link`: Optional Link to where the repo lives. Eg: `github.com/vogtb/gshr`.
 30 
 31-For example:
 32+Example:
 33 
 34 ```toml
 35+[site]
 36 base_url = "http://localhost/"
 37-site_name = "public, self hosted git repositories"
 38+name = "development site - should run on port 80"
 39 
 40 [[repos]]
 41 name = "gshr"
 42 description = "git static host repo -- generates static html for repos"
 43 url = "https://github.com/vogtb/gshr"
 44-published_git_url = "https://github.com/vogtb/gshr"
 45-host_git = true
 46+alt_link = "https://github.com/vogtb/gshr"
 47 ```
 48 
 49 ---
 50diff --git a/commit.go b/commit.go
 51index 0c203a9..8251934 100644
 52--- a/commit.go
 53+++ b/commit.go
 54@@ -13,7 +13,7 @@ import (
 55 )
 56 
 57 type CommitPage struct {
 58-	RepoData        RepoData
 59+	RepoData        repoData
 60 	Author          string
 61 	AuthorEmail     string
 62 	Date            string
 63@@ -36,7 +36,7 @@ func (c *CommitPage) RenderPage(t *template.Template) {
 64 	checkErr(err)
 65 }
 66 
 67-func RenderAllCommitPages(data RepoData, r *git.Repository) {
 68+func RenderAllCommitPages(data repoData, r *git.Repository) {
 69 	t, err := template.ParseFS(htmlTemplates, "template.commit.html", "template.partials.html")
 70 	checkErr(err)
 71 	ref, err := r.Head()
 72diff --git a/config.go b/config.go
 73index 096d8c5..819d20a 100644
 74--- a/config.go
 75+++ b/config.go
 76@@ -1,34 +1,62 @@
 77 package main
 78 
 79 import (
 80+	"errors"
 81+	"fmt"
 82 	"os"
 83 	"path"
 84+	"regexp"
 85 
 86 	"github.com/BurntSushi/toml"
 87 )
 88 
 89-type Config struct {
 90-	BaseURL  string `toml:"base_url"`
 91-	SiteName string `toml:"site_name"`
 92-	Repos    []Repo `toml:"repos"`
 93+type config struct {
 94+	Site  siteConfig   `toml:"site"`
 95+	Repos []repoConfig `toml:"repos"`
 96 }
 97 
 98-type Repo struct {
 99-	Name            string `toml:"name"`
100-	Description     string `toml:"description"`
101-	URL             string `toml:"url"`
102-	HostGit         bool   `toml:"host_git"`
103-	PublishedGitURL string `toml:"published_git_url"`
104+func (c *config) validate() {
105+	names := map[string]bool{}
106+	for _, r := range c.Repos {
107+		_, duplicate := names[r.Name]
108+		if duplicate {
109+			checkErr(errors.New(fmt.Sprintf("duplicate repo name: '%s'", r.Name)))
110+		}
111+		names[r.Name] = true
112+	}
113+}
114+
115+type repoConfig struct {
116+	Name        string `toml:"name"`
117+	Description string `toml:"description"`
118+	URL         string `toml:"url"`
119+	AltLink     string `toml:"alt_link"`
120+}
121+
122+func (r *repoConfig) validate() {
123+	ok, err := regexp.MatchString(`[A-Za-z0-9_.-]`, r.Name)
124+	checkErr(err)
125+	if !ok {
126+		checkErr(errors.New("repo names only allow [A-Za-z0-9_.-]"))
127+	}
128+	if r.Name == "git" {
129+		checkErr(errors.New("repo in config cannot have the name 'git'"))
130+	}
131+}
132+
133+type siteConfig struct {
134+	BaseURL string `toml:"base_url"`
135+	Name    string `toml:"name"`
136 }
137 
138-// / CloneDir gets the directory that this repo was cloned into using the output directory
139-// / from the program arguments, and this repo's name.
140-func (r *Repo) CloneDir() string {
141+// cloneDir gets the directory that this repo was cloned into using the output directory
142+// from the program arguments, and this repo's name.
143+func (r *repoConfig) cloneDir() string {
144 	return path.Join(args.OutputDir, r.Name, "git")
145 }
146 
147-func (r *Repo) FindFileInRoot(oneOfThese map[string]bool) string {
148-	dir, err := os.ReadDir(r.CloneDir())
149+func (r *repoConfig) findFileInRoot(oneOfThese map[string]bool) string {
150+	dir, err := os.ReadDir(r.cloneDir())
151 	checkErr(err)
152 	for _, e := range dir {
153 		name := e.Name()
154@@ -39,16 +67,16 @@ func (r *Repo) FindFileInRoot(oneOfThese map[string]bool) string {
155 	return ""
156 }
157 
158-func ParseConfiguration(data string) Config {
159-	conf := Config{}
160+func parseConfig(data string) config {
161+	conf := config{}
162 	_, err := toml.Decode(data, &conf)
163 	checkErr(err)
164 	return conf
165 }
166 
167-type RepoData struct {
168-	Repo
169-	PublishedGitURL string
170+type repoData struct {
171+	repoConfig
172+	AltLink         string
173 	BaseURL         string
174 	HeadData        HeadData
175 	ReadMePath      string
176diff --git a/dev-config-gshr.toml b/dev-config-gshr.toml
177index f5ac421..52835e5 100644
178--- a/dev-config-gshr.toml
179+++ b/dev-config-gshr.toml
180@@ -1,9 +1,9 @@
181+[site]
182 base_url = "http://localhost/"
183-site_name = "development site - should run on port 80"
184+name = "development site - should run on port 80"
185 
186 [[repos]]
187 name = "gshr"
188 description = "git static host repo -- generates static html for repos"
189 url = "https://github.com/vogtb/gshr"
190-published_git_url = "https://github.com/vogtb/gshr"
191-host_git = true
192+alt_link = "https://github.com/vogtb/gshr"
193diff --git a/example-config-ghsr-simple.toml b/example-config-ghsr-simple.toml
194index c52f5cf..41ee112 100644
195--- a/example-config-ghsr-simple.toml
196+++ b/example-config-ghsr-simple.toml
197@@ -1,9 +1,9 @@
198+[site]
199 base_url = "http://localhost/"
200-site_name = "public, self hosted git repositories"
201+name = "public, self hosted git repositories"
202 
203 [[repos]]
204 name = "gshr"
205 description = "git static host repo -- generates static html for repos"
206 url = "https://github.com/vogtb/gshr"
207-published_git_url = "https://github.com/vogtb/gshr"
208-host_git = true
209+alt_link = "https://github.com/vogtb/gshr"
210diff --git a/example-config-go-git.toml b/example-config-go-git.toml
211index d626e9a..7389c68 100644
212--- a/example-config-go-git.toml
213+++ b/example-config-go-git.toml
214@@ -1,9 +1,9 @@
215+[site]
216 base_url = "http://localhost/"
217-site_name = "example of a couple go repos on github"
218+name = "example of a couple go repos on github"
219 
220 [[repos]]
221 name = "go-git"
222 description = "A highly extensible Git implementation in pure Go."
223 url = "https://github.com/go-git/go-git"
224-published_git_url = "https://github.com/go-git/go-git"
225-host_git = true
226+alt_link = "https://github.com/go-git/go-git"
227diff --git a/file.go b/file.go
228index d14471c..c275856 100644
229--- a/file.go
230+++ b/file.go
231@@ -11,7 +11,7 @@ import (
232 )
233 
234 type FilePage struct {
235-	RepoData       RepoData
236+	RepoData       repoData
237 	Mode           string
238 	Name           string
239 	Size           string
240@@ -35,20 +35,20 @@ func (f *FilePage) RenderPage(t *template.Template) {
241 	checkErr(err)
242 }
243 
244-func RenderSingleFilePages(data RepoData) {
245+func RenderSingleFilePages(data repoData) {
246 	t, err := template.ParseFS(htmlTemplates, "template.file.html", "template.partials.html")
247 	checkErr(err)
248-	err = filepath.Walk(data.CloneDir(), func(filename string, info fs.FileInfo, err error) error {
249+	err = filepath.Walk(data.cloneDir(), func(filename string, info fs.FileInfo, err error) error {
250 		if info.IsDir() && info.Name() == ".git" {
251 			return filepath.SkipDir
252 		}
253 
254 		if !info.IsDir() {
255 			ext := filepath.Ext(filename)
256-			_, canRenderExtension := settings.TextExtensions[ext]
257-			_, canRenderByFullName := settings.PlainFiles[filepath.Base(filename)]
258+			_, canRenderExtension := stt.TextExtensions[ext]
259+			_, canRenderByFullName := stt.PlainFiles[filepath.Base(filename)]
260 			canRender := canRenderExtension || canRenderByFullName
261-			partialPath, _ := strings.CutPrefix(filename, data.CloneDir())
262+			partialPath, _ := strings.CutPrefix(filename, data.cloneDir())
263 			destDir := path.Join(args.OutputDir, data.Name, "files", partialPath)
264 			outputName := path.Join(args.OutputDir, data.Name, "files", partialPath, "index.html")
265 			var content template.HTML
266diff --git a/files.go b/files.go
267index d8bb711..5254499 100644
268--- a/files.go
269+++ b/files.go
270@@ -18,7 +18,7 @@ type FileOverview struct {
271 }
272 
273 type FilesPage struct {
274-	RepoData RepoData
275+	RepoData repoData
276 	Files    []FileOverview
277 }
278 
279@@ -30,11 +30,11 @@ func (f *FilesPage) RenderPage(t *template.Template) {
280 	checkErr(err)
281 }
282 
283-func RenderAllFilesPage(data RepoData) {
284+func RenderAllFilesPage(data repoData) {
285 	t, err := template.ParseFS(htmlTemplates, "template.files.html", "template.partials.html")
286 	checkErr(err)
287 	files := make([]FileOverview, 0)
288-	err = filepath.Walk(data.CloneDir(), func(filename string, info fs.FileInfo, err error) error {
289+	err = filepath.Walk(data.cloneDir(), func(filename string, info fs.FileInfo, err error) error {
290 		if info.IsDir() && info.Name() == ".git" {
291 			return filepath.SkipDir
292 		}
293@@ -42,7 +42,7 @@ func RenderAllFilesPage(data RepoData) {
294 		if !info.IsDir() {
295 			info, err := os.Stat(filename)
296 			checkErr(err)
297-			Name, _ := strings.CutPrefix(filename, data.CloneDir())
298+			Name, _ := strings.CutPrefix(filename, data.cloneDir())
299 			Name, _ = strings.CutPrefix(Name, "/")
300 			tf := FileOverview{
301 				Origin: filename,
302diff --git a/index.go b/index.go
303index f1fc94a..e27b16a 100644
304--- a/index.go
305+++ b/index.go
306@@ -8,7 +8,7 @@ import (
307 
308 type IndexPage struct {
309 	HeadData HeadData
310-	Repos    []RepoData
311+	Repos    []repoData
312 }
313 
314 func (l *IndexPage) RenderPage(t *template.Template) {
315@@ -19,13 +19,13 @@ func (l *IndexPage) RenderPage(t *template.Template) {
316 	checkErr(err)
317 }
318 
319-func RenderIndexPage(repos []RepoData) {
320+func RenderIndexPage(repos []repoData) {
321 	t, err := template.ParseFS(htmlTemplates, "template.index.html", "template.partials.html")
322 	checkErr(err)
323 	(&IndexPage{
324 		HeadData: HeadData{
325-			BaseURL:  config.BaseURL,
326-			SiteName: config.SiteName,
327+			BaseURL:  conf.Site.BaseURL,
328+			SiteName: conf.Site.Name,
329 		},
330 		Repos: repos,
331 	}).RenderPage(t)
332diff --git a/log.go b/log.go
333index 20dc28b..870c35b 100644
334--- a/log.go
335+++ b/log.go
336@@ -20,7 +20,7 @@ type LogPageCommit struct {
337 }
338 
339 type LogPage struct {
340-	RepoData   RepoData
341+	RepoData   repoData
342 	HasReadMe  bool
343 	ReadMePath string
344 	Commits    []LogPageCommit
345@@ -34,7 +34,7 @@ func (l *LogPage) RenderPage(t *template.Template) {
346 	checkErr(err)
347 }
348 
349-func RenderLogPage(data RepoData, r *git.Repository) {
350+func RenderLogPage(data repoData, r *git.Repository) {
351 	t, err := template.ParseFS(htmlTemplates, "template.log.html", "template.partials.html")
352 	checkErr(err)
353 	commits := make([]LogPageCommit, 0)
354diff --git a/main.go b/main.go
355index 3a333be..1c48629 100644
356--- a/main.go
357+++ b/main.go
358@@ -27,17 +27,17 @@ var css []byte
359 //go:embed favicon.ico
360 var favicon []byte
361 
362-var args CmdArgs
363+var args cmArgs
364 
365-var config Config
366+var conf config
367 
368-var settings Settings
369+var stt settings
370 
371 func main() {
372 	var r *git.Repository = &git.Repository{}
373 	Init()
374-	allRepoData := []RepoData{}
375-	for _, repo := range config.Repos {
376+	allRepoData := []repoData{}
377+	for _, repo := range conf.Repos {
378 		data := CloneAndGetData(repo, r)
379 		allRepoData = append(allRepoData, data)
380 		RenderLogPage(data, r)
381@@ -47,7 +47,7 @@ func main() {
382 	}
383 	RenderIndexPage(allRepoData)
384 	renderAssets()
385-	for _, repo := range config.Repos {
386+	for _, repo := range conf.Repos {
387 		hostRepo(repo)
388 	}
389 }
390@@ -56,7 +56,7 @@ func Init() {
391 	log.SetFlags(0)
392 	log.SetOutput(new(logger))
393 	args = DefaultCmdArgs()
394-	settings = DefaultSettings()
395+	stt = DefaultSettings()
396 	pwd, err := os.Getwd()
397 	checkErr(err)
398 	args.Wd = pwd
399@@ -81,31 +81,31 @@ func Init() {
400 	configFileBytes, err := os.ReadFile(args.ConfigPath)
401 	configString := string(configFileBytes)
402 	checkErr(err)
403-	config = ParseConfiguration(configString)
404-	debug("base_url '%v'", config.BaseURL)
405-	debug("site_name '%v'", config.SiteName)
406+	conf = parseConfig(configString)
407+	debug("base_url '%v'", conf.Site.BaseURL)
408+	debug("site_name '%v'", conf.Site.Name)
409 }
410 
411-func CloneAndGetData(repo Repo, r *git.Repository) RepoData {
412-	err := os.MkdirAll(repo.CloneDir(), 0755)
413+func CloneAndGetData(repo repoConfig, r *git.Repository) repoData {
414+	err := os.MkdirAll(repo.cloneDir(), 0755)
415 	checkErr(err)
416 	err = os.MkdirAll(path.Join(args.OutputDir, repo.Name), 0755)
417 	checkErr(err)
418 	debug("cloning '%v'", repo.Name)
419-	repoRef, err := git.PlainClone(repo.CloneDir(), false, &git.CloneOptions{
420+	repoRef, err := git.PlainClone(repo.cloneDir(), false, &git.CloneOptions{
421 		URL: repo.URL,
422 	})
423 	checkErr(err)
424-	data := RepoData{
425-		Repo:            repo,
426-		PublishedGitURL: repo.PublishedGitURL,
427-		BaseURL:         config.BaseURL,
428+	data := repoData{
429+		repoConfig: repo,
430+		AltLink:    repo.AltLink,
431+		BaseURL:    conf.Site.BaseURL,
432 		HeadData: HeadData{
433-			BaseURL:  config.BaseURL,
434-			SiteName: config.SiteName,
435+			BaseURL:  conf.Site.BaseURL,
436+			SiteName: conf.Site.Name,
437 		},
438-		ReadMePath:      repo.FindFileInRoot(settings.AllowedReadMeFiles),
439-		LicenseFilePath: repo.FindFileInRoot(settings.AllowedLicenseFiles),
440+		ReadMePath:      repo.findFileInRoot(stt.AllowedReadMeFiles),
441+		LicenseFilePath: repo.findFileInRoot(stt.AllowedLicenseFiles),
442 	}
443 	*r = *repoRef
444 	return data
445@@ -118,26 +118,20 @@ func renderAssets() {
446 	checkErr(os.WriteFile(path.Join(args.OutputDir, "favicon.ico"), favicon, 0666))
447 }
448 
449-func hostRepo(data Repo) {
450-	if data.HostGit {
451-		debug("hosting of '%v' is ON", data.Name)
452-		old := path.Join(data.CloneDir(), ".git")
453-		renamed := path.Join(args.OutputDir, fmt.Sprintf("%v.git", data.Name))
454-		repoFiles := path.Join(args.OutputDir, data.Name, "git")
455-		final := path.Join(args.OutputDir, "git", data.Name)
456-		debug("renaming '%v', new %v", data.Name, renamed)
457-		os.MkdirAll(path.Join(args.OutputDir, "git"), 0777)
458-		checkErr(os.Rename(old, renamed))
459-		debug("running 'git update-server-info' in %v", renamed)
460-		cmd := exec.Command("git", "update-server-info")
461-		cmd.Dir = renamed
462-		checkErr(cmd.Run())
463-		os.RemoveAll(repoFiles)
464-		checkErr(os.Rename(renamed, final))
465-		debug("hosting '%v' at %v", data.Name, final)
466-	} else {
467-		debug("hosting of '%v' is OFF", data.Name)
468-	}
469+func hostRepo(data repoConfig) {
470+	debug("hosting '%v'", data.Name)
471+	old := path.Join(data.cloneDir(), ".git")
472+	renamed := path.Join(args.OutputDir, fmt.Sprintf("%v.git", data.Name))
473+	repoFiles := path.Join(args.OutputDir, data.Name, "git")
474+	final := path.Join(args.OutputDir, fmt.Sprintf("%v.git", data.Name))
475+	debug("renaming '%v', new %v", data.Name, renamed)
476+	checkErr(os.Rename(old, renamed))
477+	debug("running 'git update-server-info' in %v", renamed)
478+	cmd := exec.Command("git", "update-server-info")
479+	cmd.Dir = renamed
480+	checkErr(cmd.Run())
481+	os.RemoveAll(repoFiles)
482+	debug("hosting '%v' at %v", data.Name, final)
483 }
484 
485 type logger struct{}
486@@ -190,30 +184,30 @@ func highlight(pathOrExtension string, data *string) string {
487 	return buf.String()
488 }
489 
490-type CmdArgs struct {
491+type cmArgs struct {
492 	Silent     bool
493 	Wd         string
494 	ConfigPath string
495 	OutputDir  string
496 }
497 
498-func DefaultCmdArgs() CmdArgs {
499-	return CmdArgs{
500+func DefaultCmdArgs() cmArgs {
501+	return cmArgs{
502 		Silent:     true,
503 		ConfigPath: "",
504 		OutputDir:  "",
505 	}
506 }
507 
508-type Settings struct {
509+type settings struct {
510 	TextExtensions      map[string]bool
511 	PlainFiles          map[string]bool
512 	AllowedLicenseFiles map[string]bool
513 	AllowedReadMeFiles  map[string]bool
514 }
515 
516-func DefaultSettings() Settings {
517-	return Settings{
518+func DefaultSettings() settings {
519+	return settings{
520 		TextExtensions: map[string]bool{
521 			".c":              true,
522 			".cc":             true,
523diff --git a/template.index.html b/template.index.html
524index 2cc747a..d2c80d9 100644
525--- a/template.index.html
526+++ b/template.index.html
527@@ -36,17 +36,9 @@
528           </r-cell>
529           <r-cell span="7">
530             <div class="ellipsis">
531-              {{ if .HostGit }}
532-              <a class="mono" href="{{ .BaseURL }}git/{{ .Name }}">
533-                {{ .BaseURL }}git/{{ .Name }}
534+              <a class="mono" href="{{ .BaseURL }}git/{{ .Name }}.git">
535+                {{ .BaseURL }}/{{ .Name }}.git
536               </a>
537-              {{ else if not (eq .PublishedGitURL "") }}
538-              <a class="mono" href="{{ .PublishedGitURL }}">
539-                {{ .PublishedGitURL }}
540-              </a>
541-              {{ else }}
542-              <em>none</em>
543-              {{ end }}
544             </div>
545           </r-cell>
546           {{ end }}
547diff --git a/template.partials.html b/template.partials.html
548index 3204601..4cca5ba 100644
549--- a/template.partials.html
550+++ b/template.partials.html
551@@ -32,10 +32,10 @@
552         git clone {{ .HeadData.BaseURL }}{{ .Name }}.git
553       </div>
554     </r-cell>
555-    {{ if not (eq .PublishedGitURL "") }}
556+    {{ if not (eq .AltLink "") }}
557     <r-cell span="12">
558       <div class="ellipsis">
559-        alt url: <a href="{{ .PublishedGitURL }}">{{ .PublishedGitURL }}</a>
560+        alt url: <a href="{{ .AltLink }}">{{ .AltLink }}</a>
561       </div>
562     </r-cell>
563     {{ end }}