all repos

mugit @ e1d1a9eda5d5e5aacfe4a2f899f9f65ba5b772d5

🐮 git server that your cow will love

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

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
refactor: if repo is empty so last commit is empty too, 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
	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["diff"] = diff.Diff
279
	data["commit"] = diff.Commit
280
	data["parents"] = diff.Parents
281
	data["stat"] = diff.Stat
282
	data["name"] = name
283
	data["ref"] = ref
284
	data["desc"] = desc
285
	h.templ(w, "repo_commit", data)
286
}
287
288
func (h *handlers) refsHandler(w http.ResponseWriter, r *http.Request) {
289
	name := getNormalizedName(r.PathValue("name"))
290
	repo, err := h.openPublicRepo(name, "")
291
	if err != nil {
292
		h.write404(w, err)
293
		return
294
	}
295
296
	desc, err := repo.Description()
297
	if err != nil {
298
		h.write500(w, err)
299
		return
300
	}
301
302
	masterBranch, err := repo.FindMasterBranch(h.c.Repo.Masters)
303
	if err != nil {
304
		h.write500(w, err)
305
		return
306
	}
307
308
	branches, err := repo.Branches()
309
	if err != nil {
310
		h.write500(w, err)
311
		return
312
	}
313
314
	tags, err := repo.Tags()
315
	if err != nil {
316
		// repo should have at least one branch, tags are *optional*
317
		slog.Error("couldn't fetch repo tags", "err", err)
318
	}
319
320
	data := make(map[string]any)
321
	data["meta"] = h.c.Meta
322
	data["name"] = name
323
	data["desc"] = desc
324
	data["ref"] = masterBranch
325
	data["branches"] = branches
326
	data["tags"] = tags
327
	h.templ(w, "repo_refs", data)
328
}
329
330
func countLines(r io.Reader) (int, error) {
331
	buf := make([]byte, 32*1024)
332
	bufLen := 0
333
	count := 0
334
	nl := []byte{'\n'}
335
336
	for {
337
		c, err := r.Read(buf)
338
		if c > 0 {
339
			bufLen += c
340
		}
341
		count += bytes.Count(buf[:c], nl)
342
343
		switch {
344
		case err == io.EOF:
345
			// handle last line not having a newline at the end
346
			if bufLen >= 1 && buf[(bufLen-1)%(32*1024)] != '\n' {
347
				count++
348
			}
349
			return count, nil
350
		case err != nil:
351
			return 0, err
352
		}
353
	}
354
}
355
356
var errPrivateRepo = errors.New("private err")
357
358
func (h *handlers) openPublicRepo(name, ref string) (*git.Repo, error) {
359
	// Convert normalized name back to filesystem path with .git suffix
360
	name = repoNameToPath(name)
361
362
	path, err := securejoin.SecureJoin(h.c.Repo.Dir, name)
363
	if err != nil {
364
		return nil, err
365
	}
366
367
	repo, err := git.Open(path, ref)
368
	if err != nil {
369
		return nil, err
370
	}
371
372
	isPrivate, err := repo.IsPrivate()
373
	if err != nil {
374
		return nil, err
375
	}
376
	if isPrivate {
377
		return nil, errPrivateRepo
378
	}
379
380
	return repo, nil
381
}
382
383
type repoList struct {
384
	Name       string
385
	Desc       string
386
	LastCommit time.Time
387
}
388
389
func (h *handlers) listPublicRepos() ([]repoList, error) {
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
		normalizedName := getNormalizedName(name)
404
		repo, err := h.openPublicRepo(normalizedName, "")
405
		if err != nil {
406
			// if it's not git repo, just ignore it
407
			continue
408
		}
409
410
		desc, err := repo.Description()
411
		if err != nil {
412
			errs = append(errs, err)
413
			continue
414
		}
415
416
		lastCommit, err := repo.LastCommit()
417
		if err != nil {
418
			errs = append(errs, err)
419
			continue
420
		}
421
422
		repos = append(repos, repoList{
423
			Name:       normalizedName,
424
			Desc:       desc,
425
			LastCommit: lastCommit.Committed,
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
}