all repos

mugit @ a41a1ac89942e6fe92b8ec5c4626e34e02f9d6df

🐮 git server that your cow will love

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

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