gitview/internal/routes/routes.go
internal/routes/routes.goRaw
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 securejoin "github.com/cyphar/filepath-securejoin"
17 "github.com/dustin/go-humanize"
18 "github.com/microcosm-cc/bluemonday"
19 "github.com/russross/blackfriday/v2"
20 "gitview/internal/config"
21 "gitview/internal/git"
22)
23
24type deps struct {
25 c *config.Config
26}
27
28func (d *deps) Index(w http.ResponseWriter, r *http.Request) {
29 dirs, err := os.ReadDir(d.c.Repo.ScanPath)
30 if err != nil {
31 d.Write500(w)
32 log.Printf("reading scan path: %s", err)
33 return
34 }
35
36 type info struct {
37 DisplayName, Name, Desc, Idle string
38 Languages []git.LanguageStat
39 d time.Time
40 }
41
42 infos := []info{}
43
44 for _, dir := range dirs {
45 name := dir.Name()
46 if !dir.IsDir() || d.isIgnored(name) || d.isUnlisted(name) {
47 continue
48 }
49
50 path, err := securejoin.SecureJoin(d.c.Repo.ScanPath, name)
51 if err != nil {
52 log.Printf("securejoin error: %v", err)
53 d.Write404(w)
54 return
55 }
56
57 gr, err := git.Open(path, "")
58 if err != nil {
59 log.Println(err)
60 continue
61 }
62
63 // Use the same default branch as the repository page for language statistics.
64 mainBranch, err := gr.FindMainBranch(d.c.Repo.MainBranch)
65 if err != nil {
66 log.Println(err)
67 continue
68 }
69 gr, err = git.Open(path, mainBranch)
70 if err != nil {
71 log.Println(err)
72 continue
73 }
74 languages, err := gr.Languages()
75 if err != nil {
76 log.Printf("languages for %s: %v", name, err)
77 }
78
79 c, err := gr.LastCommit()
80 if err != nil {
81 log.Println(err)
82 continue
83 }
84
85 infos = append(infos, info{
86 DisplayName: getDisplayName(name),
87 Name: name,
88 Desc: getDescription(path),
89 Languages: languages,
90 Idle: humanize.Time(c.Commit().Author.When),
91 d: c.Commit().Author.When,
92 })
93 }
94
95 // Pin configured repositories first; duplicates keep their first listed position.
96 pinned := make(map[string]int, len(d.c.Repo.Pinned))
97 for i, name := range d.c.Repo.Pinned {
98 name = getDisplayName(name)
99 if _, exists := pinned[name]; !exists {
100 pinned[name] = i
101 }
102 }
103 sort.SliceStable(infos, func(i, j int) bool {
104 iRank, iPinned := pinned[infos[i].DisplayName]
105 jRank, jPinned := pinned[infos[j].DisplayName]
106 if iPinned != jPinned {
107 return iPinned
108 }
109 if iPinned && iRank != jRank {
110 return iRank < jRank
111 }
112 return infos[j].d.Before(infos[i].d)
113 })
114
115 tpath := filepath.Join(d.c.Dirs.Templates, "*")
116 t := template.Must(template.ParseGlob(tpath))
117
118 data := make(map[string]interface{})
119 data["meta"] = d.c.Meta
120 data["info"] = infos
121 data["total"] = len(infos)
122 data["pinned"] = len(pinned) > 0
123
124 if err := t.ExecuteTemplate(w, "index", data); err != nil {
125 log.Println(err)
126 return
127 }
128}
129
130func (d *deps) RepoIndex(w http.ResponseWriter, r *http.Request) {
131 name := r.PathValue("name")
132 if d.isIgnored(name) {
133 d.Write404(w)
134 return
135 }
136 name = filepath.Clean(name)
137 path, err := securejoin.SecureJoin(d.c.Repo.ScanPath, name)
138 if err != nil {
139 log.Printf("securejoin error: %v", err)
140 d.Write404(w)
141 return
142 }
143
144 gr, err := git.Open(path, "")
145 if err != nil {
146 d.Write404(w)
147 return
148 }
149
150 mainBranch, err := gr.FindMainBranch(d.c.Repo.MainBranch)
151 if err != nil {
152 d.Write500(w)
153 log.Println(err)
154 return
155 }
156
157 // Resolve the selected default branch before reading its files and history.
158 gr, err = git.Open(path, mainBranch)
159 if err != nil {
160 d.Write404(w)
161 return
162 }
163
164 commits, err := gr.Commits()
165 if err != nil {
166 d.Write500(w)
167 log.Println(err)
168 return
169 }
170
171 var readmeContent template.HTML
172 for _, readme := range d.c.Repo.Readme {
173 ext := filepath.Ext(readme)
174 content, _ := gr.FileContent(readme)
175 if len(content) > 0 {
176 switch ext {
177 case ".md", ".mkd", ".markdown":
178 unsafe := blackfriday.Run(
179 []byte(content),
180 blackfriday.WithExtensions(blackfriday.CommonExtensions),
181 )
182 html := bluemonday.UGCPolicy().SanitizeBytes(unsafe)
183 readmeContent = template.HTML(html)
184 default:
185 safe := bluemonday.UGCPolicy().SanitizeBytes([]byte(content))
186 readmeContent = template.HTML(
187 fmt.Sprintf(`<pre>%s</pre>`, safe),
188 )
189 }
190 break
191 }
192 }
193
194 if readmeContent == "" {
195 log.Printf("no readme found for %s", name)
196 }
197
198 tpath := filepath.Join(d.c.Dirs.Templates, "*")
199 t := template.Must(template.ParseGlob(tpath))
200
201 files, err := gr.FileTree("")
202 if err != nil {
203 d.Write500(w)
204 return
205 }
206 branches, _ := gr.Branches()
207 tags, _ := gr.Tags()
208
209 data := make(map[string]any)
210 data["name"] = name
211 data["displayname"] = getDisplayName(name)
212 data["ref"] = mainBranch
213 data["readme"] = readmeContent
214 data["commits"] = commits
215 data["desc"] = getDescription(path)
216 data["servername"] = d.c.Server.Name
217 data["meta"] = d.c.Meta
218 data["gomod"] = isGoModule(gr)
219 data["files"] = files
220 data["branches"] = branches
221 data["tags"] = tags
222 data["commitcount"] = len(commits)
223 data["languages"], err = gr.Languages()
224 if err != nil {
225 log.Printf("languages for %s: %v", name, err)
226 }
227
228 if err := t.ExecuteTemplate(w, "repo", data); err != nil {
229 log.Println(err)
230 return
231 }
232}
233
234func (d *deps) RepoTree(w http.ResponseWriter, r *http.Request) {
235 name := r.PathValue("name")
236 if d.isIgnored(name) {
237 d.Write404(w)
238 return
239 }
240 treePath := r.PathValue("rest")
241 ref := r.PathValue("ref")
242
243 name = filepath.Clean(name)
244 path, err := securejoin.SecureJoin(d.c.Repo.ScanPath, name)
245 if err != nil {
246 log.Printf("securejoin error: %v", err)
247 d.Write404(w)
248 return
249 }
250 gr, err := git.Open(path, ref)
251 if err != nil {
252 d.Write404(w)
253 return
254 }
255
256 files, err := gr.FileTree(treePath)
257 if err != nil {
258 d.Write404(w)
259 return
260 }
261
262 data := make(map[string]any)
263 data["name"] = name
264 data["displayname"] = getDisplayName(name)
265 data["ref"] = ref
266 data["parent"] = treePath
267 data["desc"] = getDescription(path)
268 data["dotdot"] = filepath.Dir(treePath)
269 if data["dotdot"] == "." {
270 data["dotdot"] = ""
271 }
272 data["branches"], _ = gr.Branches()
273 data["commits"], _ = gr.Commits()
274
275 d.listFiles(files, data, w)
276}
277
278func (d *deps) FileContent(w http.ResponseWriter, r *http.Request) {
279 var raw bool
280 if rawParam, err := strconv.ParseBool(r.URL.Query().Get("raw")); err == nil {
281 raw = rawParam
282 }
283
284 name := r.PathValue("name")
285 if d.isIgnored(name) {
286 d.Write404(w)
287 return
288 }
289 treePath := r.PathValue("rest")
290 ref := r.PathValue("ref")
291
292 name = filepath.Clean(name)
293 path, err := securejoin.SecureJoin(d.c.Repo.ScanPath, name)
294 if err != nil {
295 log.Printf("securejoin error: %v", err)
296 d.Write404(w)
297 return
298 }
299
300 gr, err := git.Open(path, ref)
301 if err != nil {
302 d.Write404(w)
303 return
304 }
305
306 contents, err := gr.FileContent(treePath)
307 if err != nil {
308 d.Write404(w)
309 return
310 }
311 data := make(map[string]any)
312 data["name"] = name
313 data["displayname"] = getDisplayName(name)
314 data["ref"] = ref
315 data["desc"] = getDescription(path)
316 data["path"] = treePath
317
318 if raw {
319 d.showRaw(contents, w)
320 } else {
321 if d.c.Meta.SyntaxHighlight == "" {
322 d.showFile(contents, data, w)
323 } else {
324 d.showFileWithHighlight(treePath, contents, data, w)
325 }
326 }
327}
328
329func (d *deps) Archive(w http.ResponseWriter, r *http.Request) {
330 name := r.PathValue("name")
331 if d.isIgnored(name) {
332 d.Write404(w)
333 return
334 }
335
336 file := r.PathValue("file")
337
338 // TODO: extend this to add more files compression (e.g.: xz)
339 if !strings.HasSuffix(file, ".tar.gz") {
340 d.Write404(w)
341 return
342 }
343
344 ref := strings.TrimSuffix(file, ".tar.gz")
345
346 // This allows the browser to use a proper name for the file when
347 // downloading
348 filename := fmt.Sprintf("%s-%s.tar.gz", name, ref)
349 setContentDisposition(w, filename)
350 setGZipMIME(w)
351
352 path, err := securejoin.SecureJoin(d.c.Repo.ScanPath, name)
353 if err != nil {
354 log.Printf("securejoin error: %v", err)
355 d.Write404(w)
356 return
357 }
358
359 gr, err := git.Open(path, ref)
360 if err != nil {
361 d.Write404(w)
362 return
363 }
364
365 gw := gzip.NewWriter(w)
366 defer func() { _ = gw.Close() }()
367
368 prefix := fmt.Sprintf("%s-%s", name, ref)
369 err = gr.WriteTar(gw, prefix)
370 if err != nil {
371 // once we start writing to the body we can't report error anymore
372 // so we are only left with printing the error.
373 log.Println(err)
374 return
375 }
376
377 err = gw.Flush()
378 if err != nil {
379 // once we start writing to the body we can't report error anymore
380 // so we are only left with printing the error.
381 log.Println(err)
382 return
383 }
384}
385
386func (d *deps) Log(w http.ResponseWriter, r *http.Request) {
387 name := r.PathValue("name")
388 if d.isIgnored(name) {
389 d.Write404(w)
390 return
391 }
392 ref := r.PathValue("ref")
393
394 path, err := securejoin.SecureJoin(d.c.Repo.ScanPath, name)
395 if err != nil {
396 log.Printf("securejoin error: %v", err)
397 d.Write404(w)
398 return
399 }
400
401 gr, err := git.Open(path, ref)
402 if err != nil {
403 d.Write404(w)
404 return
405 }
406
407 commits, err := gr.Commits()
408 if err != nil {
409 d.Write500(w)
410 log.Println(err)
411 return
412 }
413
414 tpath := filepath.Join(d.c.Dirs.Templates, "*")
415 t := template.Must(template.ParseGlob(tpath))
416
417 data := make(map[string]interface{})
418 data["commits"] = commits
419 data["meta"] = d.c.Meta
420 data["name"] = name
421 data["displayname"] = getDisplayName(name)
422 data["ref"] = ref
423 data["desc"] = getDescription(path)
424 data["log"] = true
425
426 if err := t.ExecuteTemplate(w, "log", data); err != nil {
427 log.Println(err)
428 return
429 }
430}
431
432func (d *deps) Diff(w http.ResponseWriter, r *http.Request) {
433 name := r.PathValue("name")
434 if d.isIgnored(name) {
435 d.Write404(w)
436 return
437 }
438 ref := r.PathValue("ref")
439
440 path, err := securejoin.SecureJoin(d.c.Repo.ScanPath, name)
441 if err != nil {
442 log.Printf("securejoin error: %v", err)
443 d.Write404(w)
444 return
445 }
446 gr, err := git.Open(path, ref)
447 if err != nil {
448 d.Write404(w)
449 return
450 }
451
452 diff, err := gr.Diff()
453 if err != nil {
454 d.Write500(w)
455 log.Println(err)
456 return
457 }
458
459 tpath := filepath.Join(d.c.Dirs.Templates, "*")
460 t := template.Must(template.ParseGlob(tpath))
461
462 data := make(map[string]interface{})
463
464 data["commit"] = diff.Commit
465 data["stat"] = diff.Stat
466 data["diff"] = diff.Diff
467 data["meta"] = d.c.Meta
468 data["name"] = name
469 data["displayname"] = getDisplayName(name)
470 data["ref"] = ref
471 data["desc"] = getDescription(path)
472
473 if err := t.ExecuteTemplate(w, "commit", data); err != nil {
474 log.Println(err)
475 return
476 }
477}
478
479func (d *deps) Refs(w http.ResponseWriter, r *http.Request) {
480 name := r.PathValue("name")
481 if d.isIgnored(name) {
482 d.Write404(w)
483 return
484 }
485
486 path, err := securejoin.SecureJoin(d.c.Repo.ScanPath, name)
487 if err != nil {
488 log.Printf("securejoin error: %v", err)
489 d.Write404(w)
490 return
491 }
492
493 gr, err := git.Open(path, "")
494 if err != nil {
495 d.Write404(w)
496 return
497 }
498
499 tags, err := gr.Tags()
500 if err != nil {
501 // Non-fatal, we *should* have at least one branch to show.
502 log.Println(err)
503 }
504
505 branches, err := gr.Branches()
506 if err != nil {
507 log.Println(err)
508 d.Write500(w)
509 return
510 }
511
512 tpath := filepath.Join(d.c.Dirs.Templates, "*")
513 t := template.Must(template.ParseGlob(tpath))
514
515 data := make(map[string]interface{})
516
517 data["meta"] = d.c.Meta
518 data["name"] = name
519 data["displayname"] = getDisplayName(name)
520 data["branches"] = branches
521 data["tags"] = tags
522 data["isrefs"] = true
523 data["desc"] = getDescription(path)
524
525 if err := t.ExecuteTemplate(w, "refs", data); err != nil {
526 log.Println(err)
527 return
528 }
529}
530
531func (d *deps) ServeStatic(w http.ResponseWriter, r *http.Request) {
532 f := r.PathValue("file")
533 f = filepath.Clean(f)
534 f, err := securejoin.SecureJoin(d.c.Dirs.Static, f)
535 if err != nil {
536 d.Write404(w)
537 return
538 }
539
540 http.ServeFile(w, r, f)
541}