all repos

mugit @ 5957dc0

🐮 git server that your cow will love

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

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