all repos

mugit @ 3a70e61bffa70e0ac46252c75c1269031389a3c3

🐮 git server that your cow will love

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

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
run errcheck, 28 days ago
1
package handlers
2
3
import (
4
	"compress/gzip"
5
	"fmt"
6
	"io"
7
	"log/slog"
8
	"net/http"
9
10
	"olexsmir.xyz/mugit/internal/git"
11
)
12
13
func (h *handlers) infoRefsHandler(w http.ResponseWriter, r *http.Request) {
14
	repo, err := h.openPublicRepo(r.PathValue("name"), "")
15
	if err != nil {
16
		h.gitError(w, http.StatusNotFound, "repository not found")
17
		return
18
	}
19
20
	service := r.URL.Query().Get("service")
21
	gitProtocol := r.Header.Get("Git-Protocol")
22
	switch service {
23
	case "git-upload-pack":
24
		w.Header().Set("Content-Type", "application/x-git-upload-pack-advertisement")
25
		w.Header().Set("Connection", "Keep-Alive")
26
		w.Header().Set("Cache-Control", "no-cache, max-age=0, must-revalidate")
27
28
		w.WriteHeader(http.StatusOK)
29
		if err := repo.InfoRefs(r.Context(), gitProtocol, w); err != nil {
30
			_ = git.PackError(w, err.Error())
31
			slog.Error("git: info/refs", "err", err)
32
			return
33
		}
34
35
	case "git-receive-pack":
36
		h.receivePackHandler(w, r)
37
38
	default:
39
		h.gitError(w, http.StatusBadRequest, "service unsupported")
40
	}
41
}
42
43
const uploadPackExpectedContentType = "application/x-git-upload-pack-request"
44
45
func (h *handlers) uploadPackHandler(w http.ResponseWriter, r *http.Request) {
46
	gitProtocol := r.Header.Get("Git-Protocol")
47
	repo, err := h.openPublicRepo(r.PathValue("name"), "")
48
	if err != nil {
49
		h.gitError(w, http.StatusNotFound, "repository not found")
50
		return
51
	}
52
53
	contentType := r.Header.Get("Content-Type")
54
	if contentType != uploadPackExpectedContentType {
55
		h.gitError(w, http.StatusUnsupportedMediaType, "provided content type is not supported")
56
		return
57
	}
58
59
	bodyReader := r.Body
60
	if r.Header.Get("Content-Encoding") == "gzip" {
61
		gzipReader, err := gzip.NewReader(r.Body)
62
		if err != nil {
63
			h.gitError(w, http.StatusInternalServerError, err.Error())
64
			slog.Error("git: failed to create gzip reader", "err", err)
65
			return
66
		}
67
		defer func() { _ = gzipReader.Close() }()
68
		bodyReader = gzipReader
69
	}
70
71
	w.Header().Set("Content-Type", "application/x-git-upload-pack-result")
72
	w.Header().Set("Connection", "Keep-Alive")
73
	w.Header().Set("Cache-Control", "no-cache, max-age=0, must-revalidate")
74
75
	w.WriteHeader(http.StatusOK)
76
	if err := repo.UploadPack(r.Context(), true, gitProtocol, bodyReader, newFlushWriter(w)); err != nil {
77
		_ = git.PackError(w, err.Error())
78
		slog.Error("git: upload-pack", "err", err)
79
		return
80
	}
81
}
82
83
func (h *handlers) receivePackHandler(w http.ResponseWriter, _ *http.Request) {
84
	h.gitError(w, http.StatusForbidden, "pushes are only supported over ssh")
85
}
86
87
func (h *handlers) archiveHandler(w http.ResponseWriter, r *http.Request) {
88
	name := r.PathValue("name")
89
	ref := h.parseRef(r.PathValue("ref"))
90
91
	repo, err := h.openPublicRepo(name, ref)
92
	if err != nil {
93
		h.write404(w, r.URL.Path, err)
94
		return
95
	}
96
97
	filename := fmt.Sprintf("%s-%s.tar.gz", name, ref)
98
	w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filename))
99
	w.Header().Set("Content-Type", "application/gzip")
100
	w.WriteHeader(http.StatusOK)
101
102
	if err := repo.ArchiveTar(r.Context(), ref, w); err != nil {
103
		slog.Error("git: archive", "ref", ref, "err", err)
104
		return
105
	}
106
}
107
108
func (h *handlers) gitError(w http.ResponseWriter, code int, msg string) {
109
	w.Header().Set("content-type", "text/plain; charset=UTF-8")
110
	w.WriteHeader(code)
111
	_, _ = fmt.Fprintf(w, "%s\n", msg)
112
}
113
114
func (h *handlers) openPublicRepo(name, ref string) (*git.Repo, error) {
115
	name = git.ResolveName(name)
116
	path, err := git.ResolvePath(h.c.Repo.Dir, name)
117
	if err != nil {
118
		return nil, err
119
	}
120
	return git.OpenPublic(path, ref)
121
}
122
123
type flushWriter struct {
124
	w io.Writer
125
	f http.Flusher
126
}
127
128
func newFlushWriter(w http.ResponseWriter) io.Writer {
129
	f, _ := w.(http.Flusher)
130
	return &flushWriter{w: w, f: f}
131
}
132
133
func (fw *flushWriter) Write(p []byte) (int, error) {
134
	n, err := fw.w.Write(p)
135
	if fw.f != nil {
136
		fw.f.Flush()
137
	}
138
	return n, err
139
}