all repos

mugit @ 99ee247

🐮 git server that your cow will love

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

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
fix: 404 on file not found, 4 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
	securejoin "github.com/cyphar/filepath-securejoin"
19
	"github.com/yuin/goldmark"
20
	"github.com/yuin/goldmark/extension"
21
	"github.com/yuin/goldmark/renderer/html"
22
	"olexsmir.xyz/mugit/internal/git"
23
)
24
25
func (h *handlers) indexHandler(w http.ResponseWriter, r *http.Request) {
26
	repos, err := h.listPublicRepos()
27
	if err != nil {
28
		h.write500(w, err)
29
		return
30
	}
31
32
	data := make(map[string]any)
33
	data["meta"] = h.c.Meta
34
	data["repos"] = repos
35
	h.templ(w, "index", data)
36
}
37
38
var markdown = goldmark.New(
39
	goldmark.WithRendererOptions(html.WithUnsafe()),
40
	goldmark.WithExtensions(
41
		extension.GFM,
42
		extension.Linkify,
43
	))
44
45
func (h *handlers) repoIndex(w http.ResponseWriter, r *http.Request) {
46
	name := getNormalizedName(r.PathValue("name"))
47
	repo, err := h.openPublicRepo(name, "")
48
	if err != nil {
49
		h.write404(w, err)
50
		return
51
	}
52
53
	desc, err := repo.Description()
54
	if err != nil {
55
		h.write500(w, err)
56
		return
57
	}
58
59
	data := make(map[string]any)
60
	data["name"] = name
61
	data["desc"] = desc
62
	data["servername"] = h.c.Meta.Host
63
	data["meta"] = h.c.Meta
64
65
	if repo.IsEmpty() {
66
		data["empty"] = true
67
		h.templ(w, "repo_index", data)
68
		return
69
	}
70
71
	var readmeContents template.HTML
72
	for _, readme := range h.c.Repo.Readmes {
73
		ext := filepath.Ext(readme)
74
		content, _ := repo.FileContent(readme)
75
		if len(content) > 0 {
76
			switch ext {
77
			case ".md", ".markdown", ".mkd":
78
				var buf bytes.Buffer
79
				if cerr := markdown.Convert([]byte(content), &buf); cerr != nil {
80
					h.write500(w, cerr)
81
					return
82
				}
83
				readmeContents = template.HTML(buf.String())
84
			default:
85
				readmeContents = template.HTML(fmt.Sprintf(`<pre>%s</pre>`, content))
86
			}
87
			break
88
		}
89
	}
90
91
	masterBranch, err := repo.FindMasterBranch(h.c.Repo.Masters)
92
	if err != nil {
93
		h.write500(w, err)
94
		return
95
	}
96
97
	commits, err := repo.Commits()
98
	if err != nil {
99
		h.write500(w, err)
100
		return
101
	}
102
103
	if len(commits) >= 4 {
104
		commits = commits[:3]
105
	}
106
107
	data["ref"] = masterBranch
108
	data["readme"] = readmeContents
109
	data["commits"] = commits
110
	data["gomod"] = repo.IsGoMod()
111
112
	if isMirror, err := repo.IsMirror(); err == nil && isMirror {
113
		lastSync, _ := repo.LastSync()
114
		remoteURL, _ := repo.RemoteURL()
115
		data["mirrorinfo"] = map[string]any{
116
			"isMirror": true,
117
			"url":      remoteURL,
118
			"lastSync": lastSync,
119
		}
120
	}
121
122
	h.templ(w, "repo_index", data)
123
}
124
125
func (h *handlers) repoTreeHandler(w http.ResponseWriter, r *http.Request) {
126
	name := getNormalizedName(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
	files, err := repo.FileTree(treePath)
143
	if err != nil {
144
		h.write500(w, err)
145
		return
146
	}
147
148
	data := make(map[string]any)
149
	data["name"] = name
150
	data["ref"] = ref
151
	data["parent"] = treePath
152
	data["dotdot"] = filepath.Dir(treePath)
153
	data["desc"] = desc
154
	data["meta"] = h.c.Meta
155
	data["files"] = files
156
157
	h.templ(w, "repo_tree", data)
158
}
159
160
func (h *handlers) fileContentsHandler(w http.ResponseWriter, r *http.Request) {
161
	name := getNormalizedName(r.PathValue("name"))
162
	ref := r.PathValue("ref")
163
	treePath := r.PathValue("rest")
164
165
	var raw bool
166
	if rawParam, err := strconv.ParseBool(r.URL.Query().Get("raw")); err == nil {
167
		raw = rawParam
168
	}
169
170
	repo, err := h.openPublicRepo(name, ref)
171
	if err != nil {
172
		h.write404(w, err)
173
		return
174
	}
175
176
	desc, err := repo.Description()
177
	if err != nil {
178
		h.write500(w, err)
179
		return
180
	}
181
182
	contents, err := repo.FileContent(treePath)
183
	if err != nil {
184
		if errors.Is(err, git.ErrFileNotFound) {
185
			h.write404(w, err)
186
			return
187
		}
188
		h.write500(w, err)
189
		return
190
	}
191
192
	if raw {
193
		w.WriteHeader(http.StatusOK)
194
		w.Header().Set("Content-Type", "text/plain")
195
		w.Write([]byte(contents))
196
		return
197
	}
198
199
	data := make(map[string]any)
200
	data["name"] = name
201
	data["ref"] = ref
202
	data["desc"] = desc
203
	data["path"] = treePath
204
205
	lc, err := countLines(strings.NewReader(contents))
206
	if err != nil {
207
		slog.Error("failed to count line numbers", "err", err)
208
	}
209
210
	lines := make([]int, lc)
211
	if lc > 0 {
212
		for i := range lines {
213
			lines[i] = i + 1
214
		}
215
	}
216
217
	data["linecount"] = lines
218
	data["content"] = contents
219
	data["meta"] = h.c.Meta
220
221
	h.templ(w, "repo_file", data)
222
}
223
224
func (h *handlers) logHandler(w http.ResponseWriter, r *http.Request) {
225
	name := getNormalizedName(r.PathValue("name"))
226
	ref := r.PathValue("ref")
227
228
	repo, err := h.openPublicRepo(name, ref)
229
	if err != nil {
230
		h.write404(w, err)
231
		return
232
	}
233
234
	commits, err := repo.Commits()
235
	if err != nil {
236
		h.write500(w, err)
237
		return
238
	}
239
240
	desc, err := repo.Description()
241
	if err != nil {
242
		h.write500(w, err)
243
		return
244
	}
245
246
	data := make(map[string]any)
247
	data["name"] = name
248
	data["ref"] = ref
249
	data["desc"] = desc
250
	data["meta"] = h.c.Meta
251
	data["log"] = true
252
	data["commits"] = commits
253
	h.templ(w, "repo_log", data)
254
}
255
256
func (h *handlers) commitHandler(w http.ResponseWriter, r *http.Request) {
257
	name := getNormalizedName(r.PathValue("name"))
258
	ref := r.PathValue("ref")
259
	repo, err := h.openPublicRepo(name, ref)
260
	if err != nil {
261
		h.write404(w, err)
262
		return
263
	}
264
265
	diff, err := repo.Diff()
266
	if err != nil {
267
		h.write500(w, err)
268
		return
269
	}
270
271
	desc, err := repo.Description()
272
	if err != nil {
273
		h.write500(w, err)
274
		return
275
	}
276
277
	data := make(map[string]any)
278
	data["stat"] = diff.Stat
279
	data["diff"] = diff.Diff
280
	data["commit"] = diff.Commit
281
	data["name"] = name
282
	data["ref"] = ref
283
	data["desc"] = desc
284
	h.templ(w, "repo_commit", data)
285
}
286
287
func (h *handlers) refsHandler(w http.ResponseWriter, r *http.Request) {
288
	name := getNormalizedName(r.PathValue("name"))
289
	repo, err := h.openPublicRepo(name, "")
290
	if err != nil {
291
		h.write404(w, err)
292
		return
293
	}
294
295
	desc, err := repo.Description()
296
	if err != nil {
297
		h.write500(w, err)
298
		return
299
	}
300
301
	branches, err := repo.Branches()
302
	if err != nil {
303
		h.write500(w, err)
304
		return
305
	}
306
307
	tags, err := repo.Tags()
308
	if err != nil {
309
		// repo should have at least one branch, tags are *optional*
310
		slog.Error("couldn't fetch repo tags", "err", err)
311
	}
312
313
	data := make(map[string]any)
314
	data["meta"] = h.c.Meta
315
	data["name"] = name
316
	data["desc"] = desc
317
	data["branches"] = branches
318
	data["tags"] = tags
319
	h.templ(w, "repo_refs", data)
320
}
321
322
func countLines(r io.Reader) (int, error) {
323
	buf := make([]byte, 32*1024)
324
	bufLen := 0
325
	count := 0
326
	nl := []byte{'\n'}
327
328
	for {
329
		c, err := r.Read(buf)
330
		if c > 0 {
331
			bufLen += c
332
		}
333
		count += bytes.Count(buf[:c], nl)
334
335
		switch {
336
		case err == io.EOF:
337
			// handle last line not having a newline at the end
338
			if bufLen >= 1 && buf[(bufLen-1)%(32*1024)] != '\n' {
339
				count++
340
			}
341
			return count, nil
342
		case err != nil:
343
			return 0, err
344
		}
345
	}
346
}
347
348
var errPrivateRepo = errors.New("private err")
349
350
func (h *handlers) openPublicRepo(name, ref string) (*git.Repo, error) {
351
	// Convert normalized name back to filesystem path with .git suffix
352
	name = repoNameToPath(name)
353
354
	path, err := securejoin.SecureJoin(h.c.Repo.Dir, name)
355
	if err != nil {
356
		return nil, err
357
	}
358
359
	repo, err := git.Open(path, ref)
360
	if err != nil {
361
		return nil, err
362
	}
363
364
	isPrivate, err := repo.IsPrivate()
365
	if err != nil {
366
		return nil, err
367
	}
368
	if isPrivate {
369
		return nil, errPrivateRepo
370
	}
371
372
	return repo, nil
373
}
374
375
type repoList struct {
376
	Name       string
377
	Desc       string
378
	LastCommit time.Time
379
}
380
381
func (h *handlers) listPublicRepos() ([]repoList, error) {
382
	dirs, err := os.ReadDir(h.c.Repo.Dir)
383
	if err != nil {
384
		return nil, err
385
	}
386
387
	var repos []repoList
388
	var errs []error
389
	for _, dir := range dirs {
390
		if !dir.IsDir() {
391
			continue
392
		}
393
394
		name := dir.Name()
395
		normalizedName := getNormalizedName(name)
396
		repo, err := h.openPublicRepo(normalizedName, "")
397
		if err != nil {
398
			if errors.Is(err, errPrivateRepo) {
399
				continue
400
			}
401
			errs = append(errs, err)
402
			continue
403
		}
404
405
		desc, err := repo.Description()
406
		if err != nil {
407
			errs = append(errs, err)
408
			continue
409
		}
410
411
		var lastCommitTime time.Time
412
		lastCommit, err := repo.LastCommit()
413
		if err != nil {
414
			if !errors.Is(err, git.ErrEmptyRepo) {
415
				errs = append(errs, err)
416
				continue
417
			}
418
		} else {
419
			lastCommitTime = lastCommit.Committed
420
		}
421
422
		repos = append(repos, repoList{
423
			Name:       normalizedName,
424
			Desc:       desc,
425
			LastCommit: lastCommitTime,
426
		})
427
	}
428
429
	sort.Slice(repos, func(i, j int) bool {
430
		return repos[j].LastCommit.Before(repos[i].LastCommit)
431
	})
432
433
	return repos, errors.Join(errs...)
434
}