all repos

mugit @ d68c296

🐮 git server that your cow will love

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

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