all repos

mugit @ 2d69e9b

🐮 git server that your cow will love

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

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
git: move gitx into git, 3 months 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
			h.gitError(w, http.StatusInternalServerError, 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 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
		h.gitError(w, http.StatusInternalServerError, err.Error())
78
		slog.Error("git: upload-pack", "err", err)
79
		return
80
81
	}
82
}
83
84
func (h *handlers) receivePackHandler(w http.ResponseWriter, _ *http.Request) {
85
	h.gitError(w, http.StatusForbidden, "pushes are only supported over ssh")
86
}
87
88
func (h *handlers) archiveHandler(w http.ResponseWriter, r *http.Request) {
89
	name := r.PathValue("name")
90
	ref := h.parseRef(r.PathValue("ref"))
91
92
	repo, err := h.openPublicRepo(name, ref)
93
	if err != nil {
94
		h.write404(w, err)
95
		return
96
	}
97
98
	filename := fmt.Sprintf("%s-%s.tar.gz", name, ref)
99
	w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filename))
100
	w.Header().Set("Content-Type", "application/gzip")
101
	w.WriteHeader(http.StatusOK)
102
103
	if err := repo.ArchiveTar(r.Context(), ref, w); err != nil {
104
		slog.Error("git: archive", "ref", ref, "err", err)
105
		return
106
	}
107
}
108
109
func (h *handlers) gitError(w http.ResponseWriter, code int, msg string) {
110
	w.Header().Set("content-type", "text/plain; charset=UTF-8")
111
	w.WriteHeader(code)
112
	fmt.Fprintf(w, "%s\n", msg)
113
}
114
115
func (h *handlers) openPublicRepo(name, ref string) (*git.Repo, error) {
116
	name = git.ResolveName(name)
117
	path, err := git.ResolvePath(h.c.Repo.Dir, name)
118
	if err != nil {
119
		return nil, err
120
	}
121
	return git.OpenPublic(path, ref)
122
}
123
124
type flushWriter struct {
125
	w io.Writer
126
	f http.Flusher
127
}
128
129
func newFlushWriter(w http.ResponseWriter) io.Writer {
130
	f, _ := w.(http.Flusher)
131
	return &flushWriter{w: w, f: f}
132
}
133
134
func (fw *flushWriter) Write(p []byte) (int, error) {
135
	n, err := fw.w.Write(p)
136
	if fw.f != nil {
137
		fw.f.Flush()
138
	}
139
	return n, err
140
}