routes/routes.go (view raw)
1package routes
2
3import (
4 "compress/gzip"
5 "fmt"
6 "html/template"
7 "log"
8 "net/http"
9 "os"
10 "path/filepath"
11 "sort"
12 "strconv"
13 "strings"
14 "time"
15
16 "git.icyphox.sh/legit/config"
17 "git.icyphox.sh/legit/git"
18 "github.com/dustin/go-humanize"
19 "github.com/microcosm-cc/bluemonday"
20 "github.com/russross/blackfriday/v2"
21)
22
23type deps struct {
24 c *config.Config
25}
26
27func (d *deps) Index(w http.ResponseWriter, r *http.Request) {
28 dirs, err := os.ReadDir(d.c.Repo.ScanPath)
29 if err != nil {
30 d.Write500(w)
31 log.Printf("reading scan path: %s", err)
32 return
33 }
34
35 type info struct {
36 DisplayName, Name, Desc, Idle string
37 d time.Time
38 }
39
40 infos := []info{}
41
42 for _, dir := range dirs {
43 name := dir.Name()
44 if !dir.IsDir() || d.isIgnored(name) || d.isUnlisted(name) {
45 continue
46 }
47
48 path := filepath.Join(d.c.Repo.ScanPath, name)
49 gr, err := git.Open(path, "")
50 if err != nil {
51 log.Println(err)
52 continue
53 }
54
55 c, err := gr.LastCommit()
56 if err != nil {
57 d.Write500(w)
58 log.Println(err)
59 return
60 }
61
62 infos = append(infos, info{
63 DisplayName: getDisplayName(name),
64 Name: name,
65 Desc: getDescription(path),
66 Idle: humanize.Time(c.Author.When),
67 d: c.Author.When,
68 })
69 }
70
71 sort.Slice(infos, func(i, j int) bool {
72 return infos[j].d.Before(infos[i].d)
73 })
74
75 tpath := filepath.Join(d.c.Dirs.Templates, "*")
76 t := template.Must(template.ParseGlob(tpath))
77
78 data := make(map[string]interface{})
79 data["meta"] = d.c.Meta
80 data["info"] = infos
81
82 if err := t.ExecuteTemplate(w, "index", data); err != nil {
83 log.Println(err)
84 return
85 }
86}
87
88func (d *deps) RepoIndex(w http.ResponseWriter, r *http.Request) {
89 name := r.PathValue("name")
90 if d.isIgnored(name) {
91 d.Write404(w)
92 return
93 }
94 name = filepath.Clean(name)
95 path := filepath.Join(d.c.Repo.ScanPath, name)
96
97 gr, err := git.Open(path, "")
98 if err != nil {
99 d.Write404(w)
100 return
101 }
102
103 commits, err := gr.Commits()
104 if err != nil {
105 d.Write500(w)
106 log.Println(err)
107 return
108 }
109
110 var readmeContent template.HTML
111 for _, readme := range d.c.Repo.Readme {
112 ext := filepath.Ext(readme)
113 content, _ := gr.FileContent(readme)
114 if len(content) > 0 {
115 switch ext {
116 case ".md", ".mkd", ".markdown":
117 unsafe := blackfriday.Run(
118 []byte(content),
119 blackfriday.WithExtensions(blackfriday.CommonExtensions),
120 )
121 html := bluemonday.UGCPolicy().SanitizeBytes(unsafe)
122 readmeContent = template.HTML(html)
123 default:
124 safe := bluemonday.UGCPolicy().SanitizeBytes([]byte(content))
125 readmeContent = template.HTML(
126 fmt.Sprintf(`<pre>%s</pre>`, safe),
127 )
128 }
129 break
130 }
131 }
132
133 if readmeContent == "" {
134 log.Printf("no readme found for %s", name)
135 }
136
137 mainBranch, err := gr.FindMainBranch(d.c.Repo.MainBranch)
138 if err != nil {
139 d.Write500(w)
140 log.Println(err)
141 return
142 }
143
144 tpath := filepath.Join(d.c.Dirs.Templates, "*")
145 t := template.Must(template.ParseGlob(tpath))
146
147 if len(commits) >= 3 {
148 commits = commits[:3]
149 }
150
151 data := make(map[string]any)
152 data["name"] = name
153 data["displayname"] = getDisplayName(name)
154 data["ref"] = mainBranch
155 data["readme"] = readmeContent
156 data["commits"] = commits
157 data["desc"] = getDescription(path)
158 data["servername"] = d.c.Server.Name
159 data["meta"] = d.c.Meta
160 data["gomod"] = isGoModule(gr)
161
162 if err := t.ExecuteTemplate(w, "repo", data); err != nil {
163 log.Println(err)
164 return
165 }
166
167 return
168}
169
170func (d *deps) RepoTree(w http.ResponseWriter, r *http.Request) {
171 name := r.PathValue("name")
172 if d.isIgnored(name) {
173 d.Write404(w)
174 return
175 }
176 treePath := r.PathValue("rest")
177 ref := r.PathValue("ref")
178
179 name = filepath.Clean(name)
180 path := filepath.Join(d.c.Repo.ScanPath, name)
181 gr, err := git.Open(path, ref)
182 if err != nil {
183 d.Write404(w)
184 return
185 }
186
187 files, err := gr.FileTree(treePath)
188 if err != nil {
189 d.Write500(w)
190 log.Println(err)
191 return
192 }
193
194 data := make(map[string]any)
195 data["name"] = name
196 data["displayname"] = getDisplayName(name)
197 data["ref"] = ref
198 data["parent"] = treePath
199 data["desc"] = getDescription(path)
200 data["dotdot"] = filepath.Dir(treePath)
201
202 d.listFiles(files, data, w)
203 return
204}
205
206func (d *deps) FileContent(w http.ResponseWriter, r *http.Request) {
207 var raw bool
208 if rawParam, err := strconv.ParseBool(r.URL.Query().Get("raw")); err == nil {
209 raw = rawParam
210 }
211
212 name := r.PathValue("name")
213 if d.isIgnored(name) {
214 d.Write404(w)
215 return
216 }
217 treePath := r.PathValue("rest")
218 ref := r.PathValue("ref")
219
220 name = filepath.Clean(name)
221 path := filepath.Join(d.c.Repo.ScanPath, name)
222 gr, err := git.Open(path, ref)
223 if err != nil {
224 d.Write404(w)
225 return
226 }
227
228 contents, err := gr.FileContent(treePath)
229 if err != nil {
230 d.Write500(w)
231 return
232 }
233 data := make(map[string]any)
234 data["name"] = name
235 data["displayname"] = getDisplayName(name)
236 data["ref"] = ref
237 data["desc"] = getDescription(path)
238 data["path"] = treePath
239
240 if raw {
241 d.showRaw(contents, w)
242 } else {
243 if d.c.Meta.SyntaxHighlight == "" {
244 d.showFile(contents, data, w)
245 } else {
246 d.showFileWithHighlight(treePath, contents, data, w)
247 }
248 }
249}
250
251func (d *deps) Archive(w http.ResponseWriter, r *http.Request) {
252 name := r.PathValue("name")
253 if d.isIgnored(name) {
254 d.Write404(w)
255 return
256 }
257
258 file := r.PathValue("file")
259
260 // TODO: extend this to add more files compression (e.g.: xz)
261 if !strings.HasSuffix(file, ".tar.gz") {
262 d.Write404(w)
263 return
264 }
265
266 ref := strings.TrimSuffix(file, ".tar.gz")
267
268 // This allows the browser to use a proper name for the file when
269 // downloading
270 filename := fmt.Sprintf("%s-%s.tar.gz", name, ref)
271 setContentDisposition(w, filename)
272 setGZipMIME(w)
273
274 path := filepath.Join(d.c.Repo.ScanPath, name)
275 gr, err := git.Open(path, ref)
276 if err != nil {
277 d.Write404(w)
278 return
279 }
280
281 gw := gzip.NewWriter(w)
282 defer gw.Close()
283
284 prefix := fmt.Sprintf("%s-%s", name, ref)
285 err = gr.WriteTar(gw, prefix)
286 if err != nil {
287 // once we start writing to the body we can't report error anymore
288 // so we are only left with printing the error.
289 log.Println(err)
290 return
291 }
292
293 err = gw.Flush()
294 if err != nil {
295 // once we start writing to the body we can't report error anymore
296 // so we are only left with printing the error.
297 log.Println(err)
298 return
299 }
300}
301
302func (d *deps) Log(w http.ResponseWriter, r *http.Request) {
303 name := r.PathValue("name")
304 if d.isIgnored(name) {
305 d.Write404(w)
306 return
307 }
308 ref := r.PathValue("ref")
309
310 path := filepath.Join(d.c.Repo.ScanPath, name)
311 gr, err := git.Open(path, ref)
312 if err != nil {
313 d.Write404(w)
314 return
315 }
316
317 commits, err := gr.Commits()
318 if err != nil {
319 d.Write500(w)
320 log.Println(err)
321 return
322 }
323
324 tpath := filepath.Join(d.c.Dirs.Templates, "*")
325 t := template.Must(template.ParseGlob(tpath))
326
327 data := make(map[string]interface{})
328 data["commits"] = commits
329 data["meta"] = d.c.Meta
330 data["name"] = name
331 data["displayname"] = getDisplayName(name)
332 data["ref"] = ref
333 data["desc"] = getDescription(path)
334 data["log"] = true
335
336 if err := t.ExecuteTemplate(w, "log", data); err != nil {
337 log.Println(err)
338 return
339 }
340}
341
342func (d *deps) Diff(w http.ResponseWriter, r *http.Request) {
343 name := r.PathValue("name")
344 if d.isIgnored(name) {
345 d.Write404(w)
346 return
347 }
348 ref := r.PathValue("ref")
349
350 path := filepath.Join(d.c.Repo.ScanPath, name)
351 gr, err := git.Open(path, ref)
352 if err != nil {
353 d.Write404(w)
354 return
355 }
356
357 diff, err := gr.Diff()
358 if err != nil {
359 d.Write500(w)
360 log.Println(err)
361 return
362 }
363
364 tpath := filepath.Join(d.c.Dirs.Templates, "*")
365 t := template.Must(template.ParseGlob(tpath))
366
367 data := make(map[string]interface{})
368
369 data["commit"] = diff.Commit
370 data["stat"] = diff.Stat
371 data["diff"] = diff.Diff
372 data["meta"] = d.c.Meta
373 data["name"] = name
374 data["displayname"] = getDisplayName(name)
375 data["ref"] = ref
376 data["desc"] = getDescription(path)
377
378 if err := t.ExecuteTemplate(w, "commit", data); err != nil {
379 log.Println(err)
380 return
381 }
382}
383
384func (d *deps) Refs(w http.ResponseWriter, r *http.Request) {
385 name := r.PathValue("name")
386 if d.isIgnored(name) {
387 d.Write404(w)
388 return
389 }
390
391 path := filepath.Join(d.c.Repo.ScanPath, name)
392 gr, err := git.Open(path, "")
393 if err != nil {
394 d.Write404(w)
395 return
396 }
397
398 tags, err := gr.Tags()
399 if err != nil {
400 // Non-fatal, we *should* have at least one branch to show.
401 log.Println(err)
402 }
403
404 branches, err := gr.Branches()
405 if err != nil {
406 log.Println(err)
407 d.Write500(w)
408 return
409 }
410
411 tpath := filepath.Join(d.c.Dirs.Templates, "*")
412 t := template.Must(template.ParseGlob(tpath))
413
414 data := make(map[string]interface{})
415
416 data["meta"] = d.c.Meta
417 data["name"] = name
418 data["displayname"] = getDisplayName(name)
419 data["branches"] = branches
420 data["tags"] = tags
421 data["desc"] = getDescription(path)
422
423 if err := t.ExecuteTemplate(w, "refs", data); err != nil {
424 log.Println(err)
425 return
426 }
427}
428
429func (d *deps) ServeStatic(w http.ResponseWriter, r *http.Request) {
430 f := r.PathValue("file")
431 f = filepath.Clean(filepath.Join(d.c.Dirs.Static, f))
432
433 http.ServeFile(w, r, f)
434}