all repos

mugit @ 1db697a

🐮 git server that your cow will love

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

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