arnaucube
arnaucube/gitviewPublic Self-hosted repository
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}