all repos

mugit @ 96b3828

🐮 git server that your cow will love

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

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
show binary files, 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
	"github.com/yuin/goldmark/extension"
20
	"github.com/yuin/goldmark/renderer/html"
21
	"olexsmir.xyz/mugit/internal/git"
22
)
23
24
func (h *handlers) indexHandler(w http.ResponseWriter, r *http.Request) {
25
	repos, err := h.listPublicRepos()
26
	if err != nil {
27
		h.write500(w, err)
28
		return
29
	}
30
31
	data := make(map[string]any)
32
	data["meta"] = h.c.Meta
33
	data["repos"] = repos
34
	data["servername"] = h.c.Meta.Host
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
	repo, err := h.openPublicRepo(r.PathValue("name"), "")
47
	if err != nil {
48
		h.write404(w, err)
49
		return
50
	}
51
52
	desc, err := repo.Description()
53
	if err != nil {
54
		h.write500(w, err)
55
		return
56
	}
57
58
	data := make(map[string]any)
59
	data["name"] = repo.Name()
60
	data["desc"] = desc
61
	data["servername"] = h.c.Meta.Host
62
	data["meta"] = h.c.Meta
63
64
	if repo.IsEmpty() {
65
		data["empty"] = true
66
		h.templ(w, "repo_index", data)
67
		return
68
	}
69
70
	var readmeContents template.HTML
71
	for _, readme := range h.c.Repo.Readmes {
72
		fc, ferr := repo.FileContent(readme)
73
		if ferr != nil {
74
			continue
75
		}
76
77
		if fc.IsBinary {
78
			continue
79
		}
80
81
		ext := filepath.Ext(readme)
82
		content := fc.String()
83
		if len(content) > 0 {
84
			switch ext {
85
			case ".md", ".markdown", ".mkd":
86
				var buf bytes.Buffer
87
				if cerr := markdown.Convert([]byte(content), &buf); cerr != nil {
88
					h.write500(w, cerr)
89
					return
90
				}
91
				readmeContents = template.HTML(buf.String())
92
			default:
93
				readmeContents = template.HTML(fmt.Sprintf(`<pre>%s</pre>`, content))
94
			}
95
			break
96
		}
97
	}
98
99
	masterBranch, err := repo.FindMasterBranch(h.c.Repo.Masters)
100
	if err != nil {
101
		h.write500(w, err)
102
		return
103
	}
104
105
	commits, err := repo.Commits()
106
	if err != nil {
107
		h.write500(w, err)
108
		return
109
	}
110
111
	if len(commits) >= 4 {
112
		commits = commits[:3]
113
	}
114
115
	data["ref"] = masterBranch
116
	data["readme"] = readmeContents
117
	data["commits"] = commits
118
	data["gomod"] = repo.IsGoMod()
119
120
	if isMirror, err := repo.IsMirror(); err == nil && isMirror {
121
		lastSync, _ := repo.LastSync()
122
		remoteURL, _ := repo.RemoteURL()
123
		data["mirrorinfo"] = map[string]any{
124
			"isMirror": true,
125
			"url":      remoteURL,
126
			"lastSync": lastSync,
127
		}
128
	}
129
130
	h.templ(w, "repo_index", data)
131
}
132
133
func (h *handlers) repoTreeHandler(w http.ResponseWriter, r *http.Request) {
134
	name := r.PathValue("name")
135
	ref := r.PathValue("ref")
136
	treePath := r.PathValue("rest")
137
138
	repo, err := h.openPublicRepo(name, ref)
139
	if err != nil {
140
		h.write404(w, err)
141
		return
142
	}
143
144
	desc, err := repo.Description()
145
	if err != nil {
146
		h.write500(w, err)
147
		return
148
	}
149
150
	files, err := repo.FileTree(treePath)
151
	if err != nil {
152
		h.write500(w, err)
153
		return
154
	}
155
156
	data := make(map[string]any)
157
	data["name"] = name
158
	data["ref"] = ref
159
	data["parent"] = treePath
160
	data["dotdot"] = filepath.Dir(treePath)
161
	data["desc"] = desc
162
	data["meta"] = h.c.Meta
163
	data["files"] = files
164
165
	h.templ(w, "repo_tree", data)
166
}
167
168
func (h *handlers) fileContentsHandler(w http.ResponseWriter, r *http.Request) {
169
	name := r.PathValue("name")
170
	ref := r.PathValue("ref")
171
	treePath := r.PathValue("rest")
172
173
	var raw bool
174
	if rawParam, err := strconv.ParseBool(r.URL.Query().Get("raw")); err == nil {
175
		raw = rawParam
176
	}
177
178
	repo, err := h.openPublicRepo(name, ref)
179
	if err != nil {
180
		h.write404(w, err)
181
		return
182
	}
183
184
	desc, err := repo.Description()
185
	if err != nil {
186
		h.write500(w, err)
187
		return
188
	}
189
190
	fc, err := repo.FileContent(treePath)
191
	if err != nil {
192
		if errors.Is(err, git.ErrFileNotFound) {
193
			h.write404(w, err)
194
			return
195
		}
196
		h.write500(w, err)
197
		return
198
	}
199
200
	if raw {
201
		w.WriteHeader(http.StatusOK)
202
		w.Header().Set("Content-Type", fc.Mime)
203
		w.Write(fc.Content)
204
		return
205
	}
206
207
	if fc.IsImage() || fc.IsBinary {
208
		data := make(map[string]any)
209
		data["name"] = name
210
		data["ref"] = ref
211
		data["desc"] = desc
212
		data["path"] = treePath
213
		data["is_image"] = fc.IsImage()
214
		data["is_binary"] = fc.IsBinary
215
		data["mime_type"] = fc.Mime
216
		data["size"] = fc.Size
217
		data["meta"] = h.c.Meta
218
		h.templ(w, "repo_file", data)
219
		return
220
	}
221
222
	data := make(map[string]any)
223
	data["name"] = name
224
	data["ref"] = ref
225
	data["desc"] = desc
226
	data["path"] = treePath
227
228
	contentStr := fc.String()
229
	lc, err := countLines(strings.NewReader(contentStr))
230
	if err != nil {
231
		slog.Error("failed to count line numbers", "err", err)
232
	}
233
234
	lines := make([]int, lc)
235
	if lc > 0 {
236
		for i := range lines {
237
			lines[i] = i + 1
238
		}
239
	}
240
241
	data["linecount"] = lines
242
	data["content"] = contentStr
243
	data["meta"] = h.c.Meta
244
245
	h.templ(w, "repo_file", data)
246
}
247
248
func (h *handlers) logHandler(w http.ResponseWriter, r *http.Request) {
249
	name := r.PathValue("name")
250
	ref := r.PathValue("ref")
251
252
	repo, err := h.openPublicRepo(name, ref)
253
	if err != nil {
254
		h.write404(w, err)
255
		return
256
	}
257
258
	commits, err := repo.Commits()
259
	if err != nil {
260
		h.write500(w, err)
261
		return
262
	}
263
264
	desc, err := repo.Description()
265
	if err != nil {
266
		h.write500(w, err)
267
		return
268
	}
269
270
	data := make(map[string]any)
271
	data["name"] = name
272
	data["ref"] = ref
273
	data["desc"] = desc
274
	data["meta"] = h.c.Meta
275
	data["log"] = true
276
	data["commits"] = commits
277
	h.templ(w, "repo_log", data)
278
}
279
280
func (h *handlers) commitHandler(w http.ResponseWriter, r *http.Request) {
281
	name := r.PathValue("name")
282
	ref := r.PathValue("ref")
283
	repo, err := h.openPublicRepo(name, ref)
284
	if err != nil {
285
		h.write404(w, err)
286
		return
287
	}
288
289
	diff, err := repo.Diff()
290
	if err != nil {
291
		h.write500(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
	data := make(map[string]any)
302
	data["diff"] = diff.Diff
303
	data["commit"] = diff.Commit
304
	data["parents"] = diff.Parents
305
	data["stat"] = diff.Stat
306
	data["name"] = name
307
	data["ref"] = ref
308
	data["desc"] = desc
309
	h.templ(w, "repo_commit", data)
310
}
311
312
func (h *handlers) refsHandler(w http.ResponseWriter, r *http.Request) {
313
	repo, err := h.openPublicRepo(r.PathValue("name"), "")
314
	if err != nil {
315
		h.write404(w, err)
316
		return
317
	}
318
319
	desc, err := repo.Description()
320
	if err != nil {
321
		h.write500(w, err)
322
		return
323
	}
324
325
	masterBranch, err := repo.FindMasterBranch(h.c.Repo.Masters)
326
	if err != nil {
327
		h.write500(w, err)
328
		return
329
	}
330
331
	branches, err := repo.Branches()
332
	if err != nil {
333
		h.write500(w, err)
334
		return
335
	}
336
337
	tags, err := repo.Tags()
338
	if err != nil {
339
		// repo should have at least one branch, tags are *optional*
340
		slog.Error("couldn't fetch repo tags", "err", err)
341
	}
342
343
	data := make(map[string]any)
344
	data["meta"] = h.c.Meta
345
	data["name"] = repo.Name()
346
	data["desc"] = desc
347
	data["ref"] = masterBranch
348
	data["branches"] = branches
349
	data["tags"] = tags
350
	h.templ(w, "repo_refs", data)
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
	dirs, err := os.ReadDir(h.c.Repo.Dir)
387
	if err != nil {
388
		return nil, err
389
	}
390
391
	var repos []repoList
392
	var errs []error
393
	for _, dir := range dirs {
394
		if !dir.IsDir() {
395
			continue
396
		}
397
398
		name := dir.Name()
399
		repo, err := h.openPublicRepo(name, "")
400
		if err != nil {
401
			// if it's not git repo, just ignore it
402
			continue
403
		}
404
405
		desc, err := repo.Description()
406
		if err != nil {
407
			errs = append(errs, err)
408
			continue
409
		}
410
411
		lastCommit, err := repo.LastCommit()
412
		if err != nil {
413
			errs = append(errs, err)
414
			continue
415
		}
416
417
		repos = append(repos, repoList{
418
			Name:       repo.Name(),
419
			Desc:       desc,
420
			LastCommit: lastCommit.Committed,
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
}