all repos

mugit @ 01d1381

🐮 git server that your cow will love

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

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
ui: show both author and committer, 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 := 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 := 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 := 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 := r.PathValue("ref")
285
	repo, err := h.openPublicRepo(name, ref)
286
	if err != nil {
287
		h.write404(w, err)
288
		return
289
	}
290
291
	diff, err := h.getDiff(repo, ref)
292
	if err != nil {
293
		h.write500(w, err)
294
		return
295
	}
296
297
	desc, err := repo.Description()
298
	if err != nil {
299
		h.write500(w, err)
300
		return
301
	}
302
303
	h.templ(w, "repo_commit", h.pageData(repo, RepoCommit{
304
		Desc: desc,
305
		Ref:  ref,
306
		Diff: diff,
307
	}))
308
}
309
310
type RepoRefs struct {
311
	Desc     string
312
	Ref      string
313
	Branches []*git.Branch
314
	Tags     []*git.TagReference
315
}
316
317
func (h *handlers) refsHandler(w http.ResponseWriter, r *http.Request) {
318
	repo, err := h.openPublicRepo(r.PathValue("name"), "")
319
	if err != nil {
320
		h.write404(w, err)
321
		return
322
	}
323
324
	desc, err := repo.Description()
325
	if err != nil {
326
		h.write500(w, err)
327
		return
328
	}
329
330
	master, err := repo.FindMasterBranch(h.c.Repo.Masters)
331
	if err != nil {
332
		h.write500(w, err)
333
		return
334
	}
335
336
	branches, err := repo.Branches()
337
	if err != nil {
338
		h.write500(w, err)
339
		return
340
	}
341
342
	// repo should have at least one branch, tags are *optional*
343
	tags, _ := repo.Tags()
344
345
	h.templ(w, "repo_refs", h.pageData(repo, RepoRefs{
346
		Desc:     desc,
347
		Ref:      master,
348
		Tags:     tags,
349
		Branches: branches,
350
	}))
351
}
352
353
func countLines(r io.Reader) (int, error) {
354
	buf := make([]byte, 32*1024)
355
	bufLen := 0
356
	count := 0
357
	nl := []byte{'\n'}
358
359
	for {
360
		c, err := r.Read(buf)
361
		if c > 0 {
362
			bufLen += c
363
		}
364
		count += bytes.Count(buf[:c], nl)
365
366
		switch {
367
		case err == io.EOF:
368
			// handle last line not having a newline at the end
369
			if bufLen >= 1 && buf[(bufLen-1)%(32*1024)] != '\n' {
370
				count++
371
			}
372
			return count, nil
373
		case err != nil:
374
			return 0, err
375
		}
376
	}
377
}
378
379
type repoList struct {
380
	Name       string
381
	Desc       string
382
	LastCommit time.Time
383
}
384
385
func (h *handlers) listPublicRepos() ([]repoList, error) {
386
	if v, found := h.repoListCache.Get("repo_list"); found {
387
		return v, nil
388
	}
389
390
	dirs, err := os.ReadDir(h.c.Repo.Dir)
391
	if err != nil {
392
		return nil, err
393
	}
394
395
	var repos []repoList
396
	var errs []error
397
	for _, dir := range dirs {
398
		if !dir.IsDir() {
399
			continue
400
		}
401
402
		name := dir.Name()
403
		repo, err := h.openPublicRepo(name, "")
404
		if err != nil {
405
			// if it's not git repo, just ignore it
406
			continue
407
		}
408
409
		desc, err := repo.Description()
410
		if err != nil {
411
			errs = append(errs, err)
412
			continue
413
		}
414
415
		lastCommit, err := repo.LastCommit()
416
		if err != nil {
417
			errs = append(errs, err)
418
			continue
419
		}
420
421
		repos = append(repos, repoList{
422
			Name:       repo.Name(),
423
			Desc:       desc,
424
			LastCommit: lastCommit.Committed,
425
		})
426
	}
427
428
	sort.Slice(repos, func(i, j int) bool {
429
		return repos[j].LastCommit.Before(repos[i].LastCommit)
430
	})
431
432
	h.repoListCache.Set("repo_list", repos)
433
	return repos, errors.Join(errs...)
434
}
435
436
func (h handlers) getDiff(r *git.Repo, ref string) (*git.NiceDiff, error) {
437
	cacheKey := fmt.Sprintf("%s:%s", r.Name(), ref)
438
	if v, found := h.diffCache.Get(cacheKey); found {
439
		return v, nil
440
	}
441
442
	diff, err := r.Diff()
443
	if err != nil {
444
		return nil, err
445
	}
446
447
	h.diffCache.Set(cacheKey, diff)
448
	return diff, nil
449
}
450
451
var markdown = goldmark.New(
452
	goldmark.WithRendererOptions(html.WithUnsafe()),
453
	goldmark.WithExtensions(
454
		extension.GFM,
455
		extension.Linkify,
456
		emoji.Emoji,
457
		mdx.RelativeLink,
458
	))
459
460
func (h *handlers) renderReadme(r *git.Repo, ref, treePath string) (template.HTML, error) {
461
	name := r.Name()
462
	cacheKey := fmt.Sprintf("%s:%s:%s", name, ref, treePath)
463
	if v, found := h.readmeCache.Get(cacheKey); found {
464
		return v, nil
465
	}
466
467
	var readmeContents template.HTML
468
	for _, readme := range h.c.Repo.Readmes {
469
		fullPath := filepath.Join(treePath, readme)
470
		fc, ferr := r.FileContent(fullPath)
471
		if ferr != nil {
472
			continue
473
		}
474
475
		if fc.IsBinary {
476
			continue
477
		}
478
479
		ext := filepath.Ext(readme)
480
		content := fc.String()
481
		if len(content) > 0 {
482
			switch ext {
483
			case ".md", ".markdown", ".mkd":
484
				var buf bytes.Buffer
485
				if cerr := markdown.Convert([]byte(content), &buf,
486
					mdx.NewRelativeLinkCtx(name, fullPath)); cerr != nil {
487
					return "", cerr
488
				}
489
				readmeContents = template.HTML(buf.String())
490
			default:
491
				readmeContents = template.HTML(fmt.Sprintf(`<pre>%s</pre>`, content))
492
			}
493
			break
494
		}
495
	}
496
497
	h.readmeCache.Set(cacheKey, readmeContents)
498
	return readmeContents, nil
499
}
500
501
func (h handlers) pageData(repo *git.Repo, p any) PageData[any] {
502
	var name string
503
	var gomod, empty bool
504
	if repo != nil {
505
		gomod = repo.IsGoMod()
506
		empty = repo.IsEmpty()
507
		name = repo.Name()
508
	}
509
510
	return PageData[any]{
511
		P:        p,
512
		RepoName: name,
513
		Meta: Meta{
514
			Title:       h.c.Meta.Title,
515
			Description: h.c.Meta.Description,
516
			Host:        h.c.Meta.Host,
517
			GoMod:       gomod,
518
			SSHEnabled:  h.c.SSH.Enable,
519
			IsEmpty:     empty,
520
		},
521
	}
522
}