all repos

mugit @ 8baa851

🐮 git server that your cow will love

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

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
handle empty repo correctly, 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 mirrorInfo, err := repo.MirrorInfo(); err == nil && mirrorInfo.IsMirror {
113
		lastSync, _ := repo.ReadLastSync()
114
		data["mirrorinfo"] = map[string]any{
115
			"isMirror": true,
116
			"url":      mirrorInfo.RemoteURL,
117
			"lastSync": lastSync,
118
		}
119
	}
120
121
	h.templ(w, "repo_index", data)
122
}
123
124
func (h *handlers) repoTreeHandler(w http.ResponseWriter, r *http.Request) {
125
	name := getNormalizedName(r.PathValue("name"))
126
	ref := r.PathValue("ref")
127
	treePath := r.PathValue("rest")
128
129
	repo, err := h.openPublicRepo(name, ref)
130
	if err != nil {
131
		h.write404(w, err)
132
		return
133
	}
134
135
	desc, err := repo.Description()
136
	if err != nil {
137
		h.write500(w, err)
138
		return
139
	}
140
141
	files, err := repo.FileTree(treePath)
142
	if err != nil {
143
		h.write500(w, err)
144
		return
145
	}
146
147
	data := make(map[string]any)
148
	data["name"] = name
149
	data["ref"] = ref
150
	data["parent"] = treePath
151
	data["dotdot"] = filepath.Dir(treePath)
152
	data["desc"] = desc
153
	data["meta"] = h.c.Meta
154
	data["files"] = files
155
156
	h.templ(w, "repo_tree", data)
157
}
158
159
func (h *handlers) fileContentsHandler(w http.ResponseWriter, r *http.Request) {
160
	name := getNormalizedName(r.PathValue("name"))
161
	ref := r.PathValue("ref")
162
	treePath := r.PathValue("rest")
163
164
	var raw bool
165
	if rawParam, err := strconv.ParseBool(r.URL.Query().Get("raw")); err == nil {
166
		raw = rawParam
167
	}
168
169
	repo, err := h.openPublicRepo(name, ref)
170
	if err != nil {
171
		h.write404(w, err)
172
		return
173
	}
174
175
	desc, err := repo.Description()
176
	if err != nil {
177
		h.write500(w, err)
178
		return
179
	}
180
181
	contents, err := repo.FileContent(treePath)
182
	if err != nil {
183
		h.write500(w, err)
184
		return
185
	}
186
187
	if raw {
188
		w.WriteHeader(http.StatusOK)
189
		w.Header().Set("Content-Type", "text/plain")
190
		w.Write([]byte(contents))
191
		return
192
	}
193
194
	data := make(map[string]any)
195
	data["name"] = name
196
	data["ref"] = ref
197
	data["desc"] = desc
198
	data["path"] = treePath
199
200
	lc, err := countLines(strings.NewReader(contents))
201
	if err != nil {
202
		slog.Error("failed to count line numbers", "err", err)
203
	}
204
205
	lines := make([]int, lc)
206
	if lc > 0 {
207
		for i := range lines {
208
			lines[i] = i + 1
209
		}
210
	}
211
212
	data["linecount"] = lines
213
	data["content"] = contents
214
	data["meta"] = h.c.Meta
215
216
	h.templ(w, "repo_file", data)
217
}
218
219
func (h *handlers) logHandler(w http.ResponseWriter, r *http.Request) {
220
	name := getNormalizedName(r.PathValue("name"))
221
	ref := r.PathValue("ref")
222
223
	repo, err := h.openPublicRepo(name, ref)
224
	if err != nil {
225
		h.write404(w, err)
226
		return
227
	}
228
229
	commits, err := repo.Commits()
230
	if err != nil {
231
		h.write500(w, err)
232
		return
233
	}
234
235
	desc, err := repo.Description()
236
	if err != nil {
237
		h.write500(w, err)
238
		return
239
	}
240
241
	data := make(map[string]any)
242
	data["name"] = name
243
	data["ref"] = ref
244
	data["desc"] = desc
245
	data["meta"] = h.c.Meta
246
	data["log"] = true
247
	data["commits"] = commits
248
	h.templ(w, "repo_log", data)
249
}
250
251
func (h *handlers) commitHandler(w http.ResponseWriter, r *http.Request) {
252
	name := getNormalizedName(r.PathValue("name"))
253
	ref := r.PathValue("ref")
254
	repo, err := h.openPublicRepo(name, ref)
255
	if err != nil {
256
		h.write404(w, err)
257
		return
258
	}
259
260
	diff, err := repo.Diff()
261
	if err != nil {
262
		h.write500(w, err)
263
		return
264
	}
265
266
	desc, err := repo.Description()
267
	if err != nil {
268
		h.write500(w, err)
269
		return
270
	}
271
272
	data := make(map[string]any)
273
	data["stat"] = diff.Stat
274
	data["diff"] = diff.Diff
275
	data["commit"] = diff.Commit
276
	data["name"] = name
277
	data["ref"] = ref
278
	data["desc"] = desc
279
	h.templ(w, "commit", data)
280
}
281
282
func (h *handlers) refsHandler(w http.ResponseWriter, r *http.Request) {
283
	name := getNormalizedName(r.PathValue("name"))
284
	repo, err := h.openPublicRepo(name, "")
285
	if err != nil {
286
		h.write404(w, err)
287
		return
288
	}
289
290
	desc, err := repo.Description()
291
	if err != nil {
292
		h.write500(w, err)
293
		return
294
	}
295
296
	branches, err := repo.Branches()
297
	if err != nil {
298
		h.write500(w, err)
299
		return
300
	}
301
302
	tags, err := repo.Tags()
303
	if err != nil {
304
		// repo should have at least one branch, tags are *optional*
305
		slog.Error("couldn't fetch repo tags", "err", err)
306
	}
307
308
	data := make(map[string]any)
309
	data["meta"] = h.c.Meta
310
	data["name"] = name
311
	data["desc"] = desc
312
	data["branches"] = branches
313
	data["tags"] = tags
314
	h.templ(w, "repo_refs", data)
315
}
316
317
func countLines(r io.Reader) (int, error) {
318
	buf := make([]byte, 32*1024)
319
	bufLen := 0
320
	count := 0
321
	nl := []byte{'\n'}
322
323
	for {
324
		c, err := r.Read(buf)
325
		if c > 0 {
326
			bufLen += c
327
		}
328
		count += bytes.Count(buf[:c], nl)
329
330
		switch {
331
		case err == io.EOF:
332
			// handle last line not having a newline at the end
333
			if bufLen >= 1 && buf[(bufLen-1)%(32*1024)] != '\n' {
334
				count++
335
			}
336
			return count, nil
337
		case err != nil:
338
			return 0, err
339
		}
340
	}
341
}
342
343
var errPrivateRepo = errors.New("private err")
344
345
func (h *handlers) openPublicRepo(name, ref string) (*git.Repo, error) {
346
	// Convert normalized name back to filesystem path with .git suffix
347
	name = repoNameToPath(name)
348
349
	path, err := securejoin.SecureJoin(h.c.Repo.Dir, name)
350
	if err != nil {
351
		return nil, err
352
	}
353
354
	repo, err := git.Open(path, ref)
355
	if err != nil {
356
		return nil, err
357
	}
358
359
	isPrivate, err := repo.IsPrivate()
360
	if err != nil {
361
		return nil, err
362
	}
363
	if isPrivate {
364
		return nil, errPrivateRepo
365
	}
366
367
	return repo, nil
368
}
369
370
type repoList struct {
371
	Name       string
372
	Desc       string
373
	LastCommit time.Time
374
}
375
376
func (h *handlers) listPublicRepos() ([]repoList, error) {
377
	dirs, err := os.ReadDir(h.c.Repo.Dir)
378
	if err != nil {
379
		return nil, err
380
	}
381
382
	var repos []repoList
383
	var errs []error
384
	for _, dir := range dirs {
385
		if !dir.IsDir() {
386
			continue
387
		}
388
389
		name := dir.Name()
390
		normalizedName := getNormalizedName(name)
391
		repo, err := h.openPublicRepo(normalizedName, "")
392
		if err != nil {
393
			if errors.Is(err, errPrivateRepo) {
394
				continue
395
			}
396
			errs = append(errs, err)
397
			continue
398
		}
399
400
		desc, err := repo.Description()
401
		if err != nil {
402
			errs = append(errs, err)
403
			continue
404
		}
405
406
		var lastCommitTime time.Time
407
		lastCommit, err := repo.LastCommit()
408
		if err != nil {
409
			if !errors.Is(err, git.ErrEmptyRepo) {
410
				errs = append(errs, err)
411
				continue
412
			}
413
		} else {
414
			lastCommitTime = lastCommit.Author.When
415
		}
416
417
		repos = append(repos, repoList{
418
			Name:       normalizedName,
419
			Desc:       desc,
420
			LastCommit: lastCommitTime,
421
		})
422
	}
423
424
	sort.Slice(repos, func(i, j int) bool {
425
		return repos[j].LastCommit.Before(repos[i].LastCommit)
426
	})
427
428
	return repos, errors.Join(errs...)
429
}