all repos

mugit @ 88da23a7359ef5095d24302ed438c95c78ddba73

🐮 git server that your cow will love

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

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
fix: url encode ref names, so '/' and other symbols are supported, 3 months ago
1
package handlers
2
3
import (
4
	"bytes"
5
	"errors"
6
	"fmt"
7
	"html/template"
8
	"io"
9
	"log/slog"
10
	"net/http"
11
	"os"
12
	"path/filepath"
13
	"sort"
14
	"strconv"
15
	"strings"
16
	"time"
17
18
	"github.com/yuin/goldmark"
19
	emoji "github.com/yuin/goldmark-emoji"
20
	"github.com/yuin/goldmark/extension"
21
	"github.com/yuin/goldmark/renderer/html"
22
	"olexsmir.xyz/mugit/internal/git"
23
	"olexsmir.xyz/mugit/internal/mdx"
24
)
25
26
type Meta struct {
27
	Title       string
28
	Description string
29
	Host        string
30
	IsEmpty     bool
31
	GoMod       bool
32
	SSHEnabled  bool
33
}
34
35
type RepoBase struct {
36
	Ref  string
37
	Desc string
38
}
39
40
type PageData[T any] struct {
41
	Meta     Meta
42
	RepoName string // empty for non-repo pages, needed for _head.html to  compile
43
	P        T
44
}
45
46
func (h *handlers) indexHandler(w http.ResponseWriter, r *http.Request) {
47
	repos, err := h.listPublicRepos()
48
	if err != nil {
49
		h.write500(w, err)
50
		return
51
	}
52
	h.templ(w, "index", h.pageData(nil, repos))
53
}
54
55
type RepoIndex struct {
56
	Desc           string
57
	IsEmpty        bool
58
	Readme         template.HTML
59
	Ref            string
60
	Commits        []*git.Commit
61
	IsMirror       bool
62
	MirrorURL      string
63
	MirrorLastSync time.Time
64
}
65
66
func (h *handlers) repoIndex(w http.ResponseWriter, r *http.Request) {
67
	repo, err := h.openPublicRepo(r.PathValue("name"), "")
68
	if err != nil {
69
		h.write404(w, err)
70
		return
71
	}
72
73
	desc, err := repo.Description()
74
	if err != nil {
75
		h.write500(w, err)
76
		return
77
	}
78
79
	p := RepoIndex{Desc: desc, IsEmpty: repo.IsEmpty()}
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
	if isMirror, err := repo.IsMirror(); isMirror && err == nil {
108
		p.IsMirror = true
109
		p.MirrorURL, _ = repo.RemoteURL()
110
		p.MirrorLastSync, _ = repo.LastSync()
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
	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(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
		Readme:     readme,
161
	}))
162
}
163
164
type RepoFile struct {
165
	Ref       string
166
	Desc      string
167
	LineCount []int
168
	Path      string
169
	IsImage   bool
170
	IsBinary  bool
171
	Content   string
172
	Mime      string
173
	Size      int64
174
}
175
176
func (h *handlers) fileContentsHandler(w http.ResponseWriter, r *http.Request) {
177
	name := r.PathValue("name")
178
	ref := h.parseRef(r.PathValue("ref"))
179
	treePath := r.PathValue("rest")
180
181
	var raw bool
182
	if rawParam, err := strconv.ParseBool(r.URL.Query().Get("raw")); err == nil {
183
		raw = rawParam
184
	}
185
186
	repo, err := h.openPublicRepo(name, ref)
187
	if err != nil {
188
		h.write404(w, 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, err)
196
			return
197
		}
198
		h.write500(w, err)
199
		return
200
	}
201
202
	if raw {
203
		w.Header().Set("Content-Type", fc.Mime)
204
		w.WriteHeader(http.StatusOK)
205
		w.Write(fc.Content)
206
		return
207
	}
208
209
	p := RepoFile{
210
		Ref:      ref,
211
		Path:     treePath,
212
		IsImage:  fc.IsImage(),
213
		IsBinary: fc.IsBinary,
214
		Mime:     fc.Mime,
215
		Size:     fc.Size,
216
	}
217
218
	p.Desc, err = repo.Description()
219
	if err != nil {
220
		h.write500(w, err)
221
		return
222
	}
223
224
	if !fc.IsImage() && !fc.IsBinary {
225
		contentStr := fc.String()
226
		lc, err := countLines(strings.NewReader(contentStr))
227
		if err != nil {
228
			slog.Error("failed to count line numbers", "err", err)
229
		}
230
		lines := make([]int, lc)
231
		for i := range lines {
232
			lines[i] = i + 1
233
		}
234
		p.Content = contentStr
235
		p.LineCount = lines
236
	}
237
238
	h.templ(w, "repo_file", h.pageData(repo, p))
239
}
240
241
type RepoLog struct {
242
	Desc    string
243
	Commits []*git.Commit
244
	Ref     string
245
}
246
247
func (h *handlers) logHandler(w http.ResponseWriter, r *http.Request) {
248
	name := r.PathValue("name")
249
	ref := h.parseRef(r.PathValue("ref"))
250
251
	repo, err := h.openPublicRepo(name, ref)
252
	if err != nil {
253
		h.write404(w, err)
254
		return
255
	}
256
257
	commits, err := repo.Commits()
258
	if err != nil {
259
		h.write500(w, err)
260
		return
261
	}
262
263
	desc, err := repo.Description()
264
	if err != nil {
265
		h.write500(w, err)
266
		return
267
	}
268
269
	h.templ(w, "repo_log", h.pageData(repo, RepoLog{
270
		Desc:    desc,
271
		Commits: commits,
272
		Ref:     ref,
273
	}))
274
}
275
276
type RepoCommit struct {
277
	Diff *git.NiceDiff
278
	Ref  string
279
	Desc string
280
}
281
282
func (h *handlers) commitHandler(w http.ResponseWriter, r *http.Request) {
283
	name := r.PathValue("name")
284
	ref := h.parseRef(r.PathValue("ref"))
285
286
	repo, err := h.openPublicRepo(name, ref)
287
	if err != nil {
288
		h.write404(w, err)
289
		return
290
	}
291
292
	diff, err := h.getDiff(repo, ref)
293
	if err != nil {
294
		h.write500(w, err)
295
		return
296
	}
297
298
	desc, err := repo.Description()
299
	if err != nil {
300
		h.write500(w, err)
301
		return
302
	}
303
304
	h.templ(w, "repo_commit", h.pageData(repo, RepoCommit{
305
		Desc: desc,
306
		Ref:  ref,
307
		Diff: diff,
308
	}))
309
}
310
311
type RepoRefs struct {
312
	Desc     string
313
	Ref      string
314
	Branches []*git.Branch
315
	Tags     []*git.TagReference
316
}
317
318
func (h *handlers) refsHandler(w http.ResponseWriter, r *http.Request) {
319
	repo, err := h.openPublicRepo(r.PathValue("name"), "")
320
	if err != nil {
321
		h.write404(w, err)
322
		return
323
	}
324
325
	desc, err := repo.Description()
326
	if err != nil {
327
		h.write500(w, err)
328
		return
329
	}
330
331
	master, err := repo.FindMasterBranch(h.c.Repo.Masters)
332
	if err != nil {
333
		h.write500(w, err)
334
		return
335
	}
336
337
	branches, err := repo.Branches()
338
	if err != nil {
339
		h.write500(w, err)
340
		return
341
	}
342
343
	// repo should have at least one branch, tags are *optional*
344
	tags, _ := repo.Tags()
345
346
	h.templ(w, "repo_refs", h.pageData(repo, RepoRefs{
347
		Desc:     desc,
348
		Ref:      master,
349
		Tags:     tags,
350
		Branches: branches,
351
	}))
352
}
353
354
func countLines(r io.Reader) (int, error) {
355
	buf := make([]byte, 32*1024)
356
	bufLen := 0
357
	count := 0
358
	nl := []byte{'\n'}
359
360
	for {
361
		c, err := r.Read(buf)
362
		if c > 0 {
363
			bufLen += c
364
		}
365
		count += bytes.Count(buf[:c], nl)
366
367
		switch {
368
		case err == io.EOF:
369
			// handle last line not having a newline at the end
370
			if bufLen >= 1 && buf[(bufLen-1)%(32*1024)] != '\n' {
371
				count++
372
			}
373
			return count, nil
374
		case err != nil:
375
			return 0, err
376
		}
377
	}
378
}
379
380
type repoList struct {
381
	Name       string
382
	Desc       string
383
	LastCommit time.Time
384
}
385
386
func (h *handlers) listPublicRepos() ([]repoList, error) {
387
	if v, found := h.repoListCache.Get("repo_list"); found {
388
		return v, nil
389
	}
390
391
	dirs, err := os.ReadDir(h.c.Repo.Dir)
392
	if err != nil {
393
		return nil, err
394
	}
395
396
	var repos []repoList
397
	var errs []error
398
	for _, dir := range dirs {
399
		if !dir.IsDir() {
400
			continue
401
		}
402
403
		name := dir.Name()
404
		repo, err := h.openPublicRepo(name, "")
405
		if err != nil {
406
			// if it's not git repo, just ignore it
407
			continue
408
		}
409
410
		desc, err := repo.Description()
411
		if err != nil {
412
			errs = append(errs, err)
413
			continue
414
		}
415
416
		lastCommit, err := repo.LastCommit()
417
		if err != nil {
418
			errs = append(errs, err)
419
			continue
420
		}
421
422
		repos = append(repos, repoList{
423
			Name:       repo.Name(),
424
			Desc:       desc,
425
			LastCommit: lastCommit.Committed,
426
		})
427
	}
428
429
	sort.Slice(repos, func(i, j int) bool {
430
		return repos[j].LastCommit.Before(repos[i].LastCommit)
431
	})
432
433
	h.repoListCache.Set("repo_list", repos)
434
	return repos, errors.Join(errs...)
435
}
436
437
func (h handlers) getDiff(r *git.Repo, ref string) (*git.NiceDiff, error) {
438
	cacheKey := fmt.Sprintf("%s:%s", r.Name(), ref)
439
	if v, found := h.diffCache.Get(cacheKey); found {
440
		return v, nil
441
	}
442
443
	diff, err := r.Diff()
444
	if err != nil {
445
		return nil, err
446
	}
447
448
	h.diffCache.Set(cacheKey, diff)
449
	return diff, nil
450
}
451
452
var markdown = goldmark.New(
453
	goldmark.WithRendererOptions(html.WithUnsafe()),
454
	goldmark.WithExtensions(
455
		extension.GFM,
456
		extension.Linkify,
457
		emoji.Emoji,
458
		mdx.RelativeLink,
459
	))
460
461
func (h *handlers) renderReadme(r *git.Repo, ref, treePath string) (template.HTML, error) {
462
	name := r.Name()
463
	cacheKey := fmt.Sprintf("%s:%s:%s", name, ref, treePath)
464
	if v, found := h.readmeCache.Get(cacheKey); found {
465
		return v, nil
466
	}
467
468
	var readmeContents template.HTML
469
	for _, readme := range h.c.Repo.Readmes {
470
		fullPath := filepath.Join(treePath, readme)
471
		fc, ferr := r.FileContent(fullPath)
472
		if ferr != nil {
473
			continue
474
		}
475
476
		if fc.IsBinary {
477
			continue
478
		}
479
480
		ext := filepath.Ext(readme)
481
		content := fc.String()
482
		if len(content) > 0 {
483
			switch ext {
484
			case ".md", ".markdown", ".mkd":
485
				var buf bytes.Buffer
486
				if cerr := markdown.Convert([]byte(content), &buf,
487
					mdx.NewRelativeLinkCtx(name, fullPath)); cerr != nil {
488
					return "", cerr
489
				}
490
				readmeContents = template.HTML(buf.String())
491
			default:
492
				readmeContents = template.HTML(fmt.Sprintf(`<pre>%s</pre>`, content))
493
			}
494
			break
495
		}
496
	}
497
498
	h.readmeCache.Set(cacheKey, readmeContents)
499
	return readmeContents, nil
500
}
501
502
func (h handlers) pageData(repo *git.Repo, p any) PageData[any] {
503
	var name string
504
	var gomod, empty bool
505
	if repo != nil {
506
		gomod = repo.IsGoMod()
507
		empty = repo.IsEmpty()
508
		name = repo.Name()
509
	}
510
511
	return PageData[any]{
512
		P:        p,
513
		RepoName: name,
514
		Meta: Meta{
515
			Title:       h.c.Meta.Title,
516
			Description: h.c.Meta.Description,
517
			Host:        h.c.Meta.Host,
518
			GoMod:       gomod,
519
			SSHEnabled:  h.c.SSH.Enable,
520
			IsEmpty:     empty,
521
		},
522
	}
523
}