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}