all repos

mugit @ 7d517fc

🐮 git server that your cow will love

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

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