all repos

mugit @ 334ab23

🐮 git server that your cow will love

mugit/internal/handlers/repo.go (view raw)

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
ui: HTML-escape plain text readmes, 2 months ago
1
package handlers
2
3
import (
4
	"errors"
5
	"fmt"
6
	"html"
7
	"html/template"
8
	"log/slog"
9
	"net/http"
10
	"os"
11
	"path/filepath"
12
	"sort"
13
	"strings"
14
	"time"
15
16
	"olexsmir.xyz/mugit/internal/git"
17
	"olexsmir.xyz/mugit/internal/markdown"
18
)
19
20
type Meta struct {
21
	Title       string
22
	Description string
23
	Host        string
24
	IsEmpty     bool
25
	GoMod       bool
26
	SSHEnabled  bool
27
}
28
29
type RepoBase struct {
30
	Ref  string
31
	Desc string
32
}
33
34
type PageData[T any] struct {
35
	Meta     Meta
36
	RepoName string // empty for non-repo pages, needed for _head.html to  compile
37
	P        T
38
}
39
40
func (h *handlers) indexHandler(w http.ResponseWriter, r *http.Request) {
41
	repos, err := h.listPublicRepos()
42
	if err != nil {
43
		h.write500(w, err)
44
		return
45
	}
46
	h.templ(w, "index", h.pageData(nil, repos))
47
}
48
49
type RepoIndex struct {
50
	Desc           string
51
	SSHUser        string
52
	IsEmpty        bool
53
	Readme         template.HTML
54
	Ref            string
55
	Commits        []*git.Commit
56
	IsMirror       bool
57
	MirrorURL      string
58
	MirrorLastSync time.Time
59
}
60
61
func (h *handlers) repoIndexHandler(w http.ResponseWriter, r *http.Request) {
62
	repo, err := h.openPublicRepo(r.PathValue("name"), "")
63
	if err != nil {
64
		h.write404(w, err)
65
		return
66
	}
67
68
	desc, err := repo.Description()
69
	if err != nil {
70
		h.write500(w, err)
71
		return
72
	}
73
74
	p := RepoIndex{
75
		Desc:    desc,
76
		IsEmpty: repo.IsEmpty(),
77
		SSHUser: h.c.SSH.User,
78
	}
79
80
	if isMirror, merr := repo.IsMirror(); isMirror && merr == nil {
81
		p.IsMirror = true
82
		p.MirrorURL, _ = repo.RemoteURL()
83
		p.MirrorLastSync, _ = repo.LastSync()
84
	}
85
86
	if p.IsEmpty {
87
		h.templ(w, "repo_index", h.pageData(repo, p))
88
		return
89
	}
90
91
	p.Ref, err = repo.FindMasterBranch(h.c.Repo.Masters)
92
	if err != nil {
93
		h.write500(w, err)
94
		return
95
	}
96
97
	p.Readme, err = h.renderReadme(repo, p.Ref, "")
98
	if err != nil {
99
		h.write500(w, err)
100
		return
101
	}
102
103
	p.Commits, err = repo.Commits("")
104
	if err != nil {
105
		h.write500(w, err)
106
		return
107
	}
108
109
	if len(p.Commits) >= 3 {
110
		p.Commits = p.Commits[:3:3]
111
	}
112
113
	h.templ(w, "repo_index", h.pageData(repo, p))
114
}
115
116
type RepoTree struct {
117
	Desc        string
118
	Ref         string
119
	Tree        []git.NiceTree
120
	Breadcrumbs []Breadcrumb
121
	ParentPath  string
122
	DotDot      string
123
	Readme      template.HTML
124
}
125
126
func (h *handlers) repoTreeHandler(w http.ResponseWriter, r *http.Request) {
127
	name := r.PathValue("name")
128
	ref := h.parseRef(r.PathValue("ref"))
129
	treePath := r.PathValue("rest")
130
131
	repo, err := h.openPublicRepo(name, ref)
132
	if err != nil {
133
		h.write404(w, err)
134
		return
135
	}
136
137
	desc, err := repo.Description()
138
	if err != nil {
139
		h.write500(w, err)
140
		return
141
	}
142
143
	tree, err := repo.FileTree(r.Context(), treePath)
144
	if err != nil {
145
		h.write500(w, err)
146
		return
147
	}
148
149
	readme, err := h.renderReadme(repo, ref, treePath)
150
	if err != nil {
151
		h.write500(w, err)
152
		return
153
	}
154
155
	h.templ(w, "repo_tree", h.pageData(repo, RepoTree{
156
		Desc:        desc,
157
		Ref:         ref,
158
		Tree:        tree,
159
		ParentPath:  treePath,
160
		DotDot:      filepath.Dir(treePath),
161
		Breadcrumbs: Breadcrumbs(treePath),
162
		Readme:      readme,
163
	}))
164
}
165
166
type RepoFile struct {
167
	Ref         string
168
	Desc        string
169
	Lines       []string
170
	Breadcrumbs []Breadcrumb
171
	Path        string
172
	IsImage     bool
173
	IsBinary    bool
174
	Mime        string
175
	Size        int64
176
}
177
178
func (h *handlers) fileContentsHandler(w http.ResponseWriter, r *http.Request) {
179
	name := r.PathValue("name")
180
	ref := h.parseRef(r.PathValue("ref"))
181
	treePath := r.PathValue("rest")
182
183
	repo, err := h.openPublicRepo(name, ref)
184
	if err != nil {
185
		h.write404(w, err)
186
		return
187
	}
188
189
	fc, err := repo.FileContent(treePath)
190
	if err != nil {
191
		if errors.Is(err, git.ErrFileNotFound) {
192
			h.write404(w, err)
193
			return
194
		}
195
		h.write500(w, err)
196
		return
197
	}
198
199
	p := RepoFile{
200
		Ref:      ref,
201
		Path:     treePath,
202
		IsImage:  fc.IsImage,
203
		IsBinary: fc.IsBinary,
204
		Mime:     fc.Mime,
205
		Size:     fc.Size,
206
	}
207
208
	p.Desc, err = repo.Description()
209
	if err != nil {
210
		h.write500(w, err)
211
		return
212
	}
213
214
	p.Breadcrumbs = Breadcrumbs(treePath)
215
	if !fc.IsImage && !fc.IsBinary {
216
		p.Lines = strings.Split(strings.TrimRight(fc.String(), "\n"), "\n")
217
	}
218
219
	h.templ(w, "repo_file", h.pageData(repo, p))
220
}
221
222
func (h *handlers) rawFileContentsHandler(w http.ResponseWriter, r *http.Request) {
223
	name := r.PathValue("name")
224
	ref := h.parseRef(r.PathValue("ref"))
225
	treePath := r.PathValue("rest")
226
227
	repo, err := h.openPublicRepo(name, ref)
228
	if err != nil {
229
		w.WriteHeader(http.StatusNotFound)
230
		slog.Info("404", "err", err)
231
		return
232
	}
233
234
	fc, err := repo.FileContent(treePath)
235
	if err != nil {
236
		if errors.Is(err, git.ErrFileNotFound) {
237
			w.WriteHeader(http.StatusNotFound)
238
			slog.Info("404", "err", err)
239
			return
240
		}
241
242
		w.WriteHeader(http.StatusInternalServerError)
243
		slog.Info("500", "err", err)
244
		return
245
	}
246
247
	w.Header().Set("Content-Type", fc.Mime)
248
	w.WriteHeader(http.StatusOK)
249
	w.Write(fc.Content)
250
}
251
252
type RepoLog struct {
253
	Desc      string
254
	Commits   []*git.Commit
255
	Ref       string
256
	NextAfter string
257
}
258
259
func (h *handlers) logHandler(w http.ResponseWriter, r *http.Request) {
260
	name := r.PathValue("name")
261
	ref := h.parseRef(r.PathValue("ref"))
262
	after := r.URL.Query().Get("after")
263
264
	repo, err := h.openPublicRepo(name, ref)
265
	if err != nil {
266
		h.write404(w, err)
267
		return
268
	}
269
270
	desc, err := repo.Description()
271
	if err != nil {
272
		h.write500(w, err)
273
		return
274
	}
275
276
	commits, err := repo.Commits(after)
277
	if err != nil {
278
		h.write500(w, err)
279
		return
280
	}
281
282
	// if we got full page of commits, we probably have more.
283
	// NOTE: this has an edge case, when last page is len(git.CommitsPage), "load more" would be shown
284
	nextAfter := ""
285
	if len(commits) == git.CommitsPage && len(commits) > 0 {
286
		nextAfter = commits[len(commits)-1].HashShort
287
	}
288
289
	h.templ(w, "repo_log", h.pageData(repo, RepoLog{
290
		Desc:      desc,
291
		Ref:       ref,
292
		Commits:   commits,
293
		NextAfter: nextAfter,
294
	}))
295
}
296
297
type RepoCommit struct {
298
	Diff *git.NiceDiff
299
	Ref  string
300
	Desc string
301
}
302
303
func (h *handlers) commitHandler(w http.ResponseWriter, r *http.Request) {
304
	name := r.PathValue("name")
305
	ref := h.parseRef(r.PathValue("ref"))
306
307
	repo, err := h.openPublicRepo(name, ref)
308
	if err != nil {
309
		h.write404(w, err)
310
		return
311
	}
312
313
	diff, err := h.getDiff(repo, ref)
314
	if err != nil {
315
		h.write500(w, err)
316
		return
317
	}
318
319
	desc, err := repo.Description()
320
	if err != nil {
321
		h.write500(w, err)
322
		return
323
	}
324
325
	h.templ(w, "repo_commit", h.pageData(repo, RepoCommit{
326
		Desc: desc,
327
		Ref:  ref,
328
		Diff: diff,
329
	}))
330
}
331
332
type RepoRefs struct {
333
	Desc     string
334
	Ref      string
335
	Branches []*git.Branch
336
	Tags     []*git.TagReference
337
}
338
339
func (h *handlers) refsHandler(w http.ResponseWriter, r *http.Request) {
340
	repo, err := h.openPublicRepo(r.PathValue("name"), "")
341
	if err != nil {
342
		h.write404(w, err)
343
		return
344
	}
345
346
	desc, err := repo.Description()
347
	if err != nil {
348
		h.write500(w, err)
349
		return
350
	}
351
352
	master, err := repo.FindMasterBranch(h.c.Repo.Masters)
353
	if err != nil {
354
		h.write500(w, err)
355
		return
356
	}
357
358
	branches, err := repo.Branches()
359
	if err != nil {
360
		h.write500(w, err)
361
		return
362
	}
363
364
	// repo should have at least one branch, tags are *optional*
365
	tags, _ := repo.Tags()
366
367
	h.templ(w, "repo_refs", h.pageData(repo, RepoRefs{
368
		Desc:     desc,
369
		Ref:      master,
370
		Tags:     tags,
371
		Branches: branches,
372
	}))
373
}
374
375
type repoList struct {
376
	Name       string
377
	Desc       string
378
	LastCommit time.Time
379
}
380
381
func (h *handlers) listPublicRepos() ([]repoList, error) {
382
	if v, found := h.repoListCache.Get("repo_list"); found {
383
		return v, nil
384
	}
385
386
	dirs, err := os.ReadDir(h.c.Repo.Dir)
387
	if err != nil {
388
		return nil, err
389
	}
390
391
	var repos []repoList
392
	var errs []error
393
	for _, dir := range dirs {
394
		if !dir.IsDir() {
395
			continue
396
		}
397
398
		name := dir.Name()
399
		repo, err := h.openPublicRepo(name, "")
400
		if err != nil {
401
			// if it's not git repo, just ignore it
402
			continue
403
		}
404
405
		desc, err := repo.Description()
406
		if err != nil {
407
			errs = append(errs, err)
408
			continue
409
		}
410
411
		lastCommit, err := repo.LastCommit()
412
		if err != nil {
413
			errs = append(errs, err)
414
			continue
415
		}
416
417
		repos = append(repos, repoList{
418
			Name:       repo.Name(),
419
			Desc:       desc,
420
			LastCommit: lastCommit.Committed,
421
		})
422
	}
423
424
	sort.Slice(repos, func(i, j int) bool {
425
		return repos[j].LastCommit.Before(repos[i].LastCommit)
426
	})
427
428
	h.repoListCache.Set("repo_list", repos)
429
	return repos, errors.Join(errs...)
430
}
431
432
func (h handlers) getDiff(r *git.Repo, ref string) (*git.NiceDiff, error) {
433
	cacheKey := fmt.Sprintf("%s:%s", r.Name(), ref)
434
	if v, found := h.diffCache.Get(cacheKey); found {
435
		return v, nil
436
	}
437
438
	diff, err := r.Diff()
439
	if err != nil {
440
		return nil, err
441
	}
442
443
	h.diffCache.Set(cacheKey, diff)
444
	return diff, nil
445
}
446
447
func (h *handlers) renderReadme(r *git.Repo, ref, treePath string) (template.HTML, error) {
448
	name := r.Name()
449
	cacheKey := fmt.Sprintf("%s:%s:%s", name, ref, treePath)
450
	if v, found := h.readmeCache.Get(cacheKey); found {
451
		return v, nil
452
	}
453
454
	var readmeContents template.HTML
455
	for _, readme := range h.c.Repo.Readmes {
456
		fullPath := filepath.Join(treePath, readme)
457
		fc, ferr := r.FileContent(fullPath)
458
		if ferr != nil {
459
			continue
460
		}
461
462
		if fc.IsBinary && fc.IsImage {
463
			continue
464
		}
465
466
		ext := filepath.Ext(readme)
467
		content := fc.String()
468
		if len(content) > 0 {
469
			switch ext {
470
			case ".md", ".markdown", ".mkd":
471
				readme, err := markdown.Render(name, ref, fullPath, content)
472
				if err != nil {
473
					return "", err
474
				}
475
				return template.HTML(readme), nil
476
477
			default:
478
				readmeContents = template.HTML(fmt.Sprintf(
479
					`<pre class="raw">%s</pre>`, html.EscapeString(content)))
480
			}
481
			break
482
		}
483
	}
484
485
	h.readmeCache.Set(cacheKey, readmeContents)
486
	return readmeContents, nil
487
}
488
489
func (h handlers) pageData(repo *git.Repo, p any) PageData[any] {
490
	var name string
491
	var gomod, empty bool
492
	if repo != nil {
493
		gomod = repo.IsGoMod()
494
		empty = repo.IsEmpty()
495
		name = repo.Name()
496
	}
497
498
	return PageData[any]{
499
		P:        p,
500
		RepoName: name,
501
		Meta: Meta{
502
			Title:       h.c.Meta.Title,
503
			Description: h.c.Meta.Description,
504
			Host:        h.c.Meta.Host,
505
			GoMod:       gomod,
506
			SSHEnabled:  h.c.SSH.Enable,
507
			IsEmpty:     empty,
508
		},
509
	}
510
}
511
512
type Breadcrumb struct {
513
	Name   string
514
	Path   string
515
	IsLast bool
516
}
517
518
func Breadcrumbs(path string) []Breadcrumb {
519
	if path == "" {
520
		return nil
521
	}
522
	parts := strings.Split(path, "/")
523
	crumbs := make([]Breadcrumb, len(parts))
524
	for i, part := range parts {
525
		crumbs[i] = Breadcrumb{
526
			Name:   part,
527
			Path:   strings.Join(parts[:i+1], "/"),
528
			IsLast: i == len(parts)-1,
529
		}
530
	}
531
	return crumbs
532
}