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 readmeContent = template.HTML(
125 fmt.Sprintf(`<pre>%s</pre>`, content),
126 )
127 }
128 break
129 }
130 }
131
132 if readmeContent == "" {
133 log.Printf("no readme found for %s", name)
134 }
135
136 mainBranch, err := gr.FindMainBranch(d.c.Repo.MainBranch)
137 if err != nil {
138 d.Write500(w)
139 log.Println(err)
140 return
141 }
142
143 tpath := filepath.Join(d.c.Dirs.Templates, "*")
144 t := template.Must(template.ParseGlob(tpath))
145
146 if len(commits) >= 3 {
147 commits = commits[:3]
148 }
149
150 data := make(map[string]any)
151 data["name"] = name
152 data["displayname"] = getDisplayName(name)
153 data["ref"] = mainBranch
154 data["readme"] = readmeContent
155 data["commits"] = commits
156 data["desc"] = getDescription(path)
157 data["servername"] = d.c.Server.Name
158 data["meta"] = d.c.Meta
159 data["gomod"] = isGoModule(gr)
160
161 if err := t.ExecuteTemplate(w, "repo", data); err != nil {
162 log.Println(err)
163 return
164 }
165
166 return
167}
168
169func (d *deps) RepoTree(w http.ResponseWriter, r *http.Request) {
170 name := r.PathValue("name")
171 if d.isIgnored(name) {
172 d.Write404(w)
173 return
174 }
175 treePath := r.PathValue("rest")
176 ref := r.PathValue("ref")
177
178 name = filepath.Clean(name)
179 path := filepath.Join(d.c.Repo.ScanPath, name)
180 gr, err := git.Open(path, ref)
181 if err != nil {
182 d.Write404(w)
183 return
184 }
185
186 files, err := gr.FileTree(treePath)
187 if err != nil {
188 d.Write500(w)
189 log.Println(err)
190 return
191 }
192
193 data := make(map[string]any)
194 data["name"] = name
195 data["displayname"] = getDisplayName(name)
196 data["ref"] = ref
197 data["parent"] = treePath
198 data["desc"] = getDescription(path)
199 data["dotdot"] = filepath.Dir(treePath)
200
201 d.listFiles(files, data, w)
202 return
203}
204
205func (d *deps) FileContent(w http.ResponseWriter, r *http.Request) {
206 var raw bool
207 if rawParam, err := strconv.ParseBool(r.URL.Query().Get("raw")); err == nil {
208 raw = rawParam
209 }
210
211 name := r.PathValue("name")
212 if d.isIgnored(name) {
213 d.Write404(w)
214 return
215 }
216 treePath := r.PathValue("rest")
217 ref := r.PathValue("ref")
218
219 name = filepath.Clean(name)
220 path := filepath.Join(d.c.Repo.ScanPath, name)
221 gr, err := git.Open(path, ref)
222 if err != nil {
223 d.Write404(w)
224 return
225 }
226
227 contents, err := gr.FileContent(treePath)
228 if err != nil {
229 d.Write500(w)
230 return
231 }
232 data := make(map[string]any)
233 data["name"] = name
234 data["displayname"] = getDisplayName(name)
235 data["ref"] = ref
236 data["desc"] = getDescription(path)
237 data["path"] = treePath
238
239 if raw {
240 d.showRaw(contents, w)
241 } else {
242 if d.c.Meta.SyntaxHighlight == "" {
243 d.showFile(contents, data, w)
244 } else {
245 d.showFileWithHighlight(treePath, contents, data, w)
246 }
247 }
248}
249
250func (d *deps) Archive(w http.ResponseWriter, r *http.Request) {
251 name := r.PathValue("name")
252 if d.isIgnored(name) {
253 d.Write404(w)
254 return
255 }
256
257 file := r.PathValue("file")
258
259 // TODO: extend this to add more files compression (e.g.: xz)
260 if !strings.HasSuffix(file, ".tar.gz") {
261 d.Write404(w)
262 return
263 }
264
265 ref := strings.TrimSuffix(file, ".tar.gz")
266
267 // This allows the browser to use a proper name for the file when
268 // downloading
269 filename := fmt.Sprintf("%s-%s.tar.gz", name, ref)
270 setContentDisposition(w, filename)
271 setGZipMIME(w)
272
273 path := filepath.Join(d.c.Repo.ScanPath, name)
274 gr, err := git.Open(path, ref)
275 if err != nil {
276 d.Write404(w)
277 return
278 }
279
280 gw := gzip.NewWriter(w)
281 defer gw.Close()
282
283 prefix := fmt.Sprintf("%s-%s", name, ref)
284 err = gr.WriteTar(gw, prefix)
285 if err != nil {
286 // once we start writing to the body we can't report error anymore
287 // so we are only left with printing the error.
288 log.Println(err)
289 return
290 }
291
292 err = gw.Flush()
293 if err != nil {
294 // once we start writing to the body we can't report error anymore
295 // so we are only left with printing the error.
296 log.Println(err)
297 return
298 }
299}
300
301func (d *deps) Log(w http.ResponseWriter, r *http.Request) {
302 name := r.PathValue("name")
303 if d.isIgnored(name) {
304 d.Write404(w)
305 return
306 }
307 ref := r.PathValue("ref")
308
309 path := filepath.Join(d.c.Repo.ScanPath, name)
310 gr, err := git.Open(path, ref)
311 if err != nil {
312 d.Write404(w)
313 return
314 }
315
316 commits, err := gr.Commits()
317 if err != nil {
318 d.Write500(w)
319 log.Println(err)
320 return
321 }
322
323 tpath := filepath.Join(d.c.Dirs.Templates, "*")
324 t := template.Must(template.ParseGlob(tpath))
325
326 data := make(map[string]interface{})
327 data["commits"] = commits
328 data["meta"] = d.c.Meta
329 data["name"] = name
330 data["displayname"] = getDisplayName(name)
331 data["ref"] = ref
332 data["desc"] = getDescription(path)
333 data["log"] = true
334
335 if err := t.ExecuteTemplate(w, "log", data); err != nil {
336 log.Println(err)
337 return
338 }
339}
340
341func (d *deps) Diff(w http.ResponseWriter, r *http.Request) {
342 name := r.PathValue("name")
343 if d.isIgnored(name) {
344 d.Write404(w)
345 return
346 }
347 ref := r.PathValue("ref")
348
349 path := filepath.Join(d.c.Repo.ScanPath, name)
350 gr, err := git.Open(path, ref)
351 if err != nil {
352 d.Write404(w)
353 return
354 }
355
356 diff, err := gr.Diff()
357 if err != nil {
358 d.Write500(w)
359 log.Println(err)
360 return
361 }
362
363 tpath := filepath.Join(d.c.Dirs.Templates, "*")
364 t := template.Must(template.ParseGlob(tpath))
365
366 data := make(map[string]interface{})
367
368 data["commit"] = diff.Commit
369 data["stat"] = diff.Stat
370 data["diff"] = diff.Diff
371 data["meta"] = d.c.Meta
372 data["name"] = name
373 data["displayname"] = getDisplayName(name)
374 data["ref"] = ref
375 data["desc"] = getDescription(path)
376
377 if err := t.ExecuteTemplate(w, "commit", data); err != nil {
378 log.Println(err)
379 return
380 }
381}
382
383func (d *deps) Refs(w http.ResponseWriter, r *http.Request) {
384 name := r.PathValue("name")
385 if d.isIgnored(name) {
386 d.Write404(w)
387 return
388 }
389
390 path := filepath.Join(d.c.Repo.ScanPath, name)
391 gr, err := git.Open(path, "")
392 if err != nil {
393 d.Write404(w)
394 return
395 }
396
397 tags, err := gr.Tags()
398 if err != nil {
399 // Non-fatal, we *should* have at least one branch to show.
400 log.Println(err)
401 }
402
403 branches, err := gr.Branches()
404 if err != nil {
405 log.Println(err)
406 d.Write500(w)
407 return
408 }
409
410 tpath := filepath.Join(d.c.Dirs.Templates, "*")
411 t := template.Must(template.ParseGlob(tpath))
412
413 data := make(map[string]interface{})
414
415 data["meta"] = d.c.Meta
416 data["name"] = name
417 data["displayname"] = getDisplayName(name)
418 data["branches"] = branches
419 data["tags"] = tags
420 data["desc"] = getDescription(path)
421
422 if err := t.ExecuteTemplate(w, "refs", data); err != nil {
423 log.Println(err)
424 return
425 }
426}
427
428func (d *deps) ServeStatic(w http.ResponseWriter, r *http.Request) {
429 f := r.PathValue("file")
430 f = filepath.Clean(filepath.Join(d.c.Dirs.Static, f))
431
432 http.ServeFile(w, r, f)
433}