1diff --git a/pkg/ext/router.go b/pkg/ext/router.go
2index ce4c126ea26f2c5b65529daf028e8931f250e863..434972b1a4cd545a5b275ae43581678032625c83 100644
3--- a/pkg/ext/router.go
4+++ b/pkg/ext/router.go
5@@ -69,6 +69,13 @@ Message: "Not Found",
6 }, r.Context())
7 }
8
9+func BadRequest(w http.ResponseWriter, r *http.Request, msg string) {
10+ w.WriteHeader(http.StatusBadRequest)
11+ templates.WritePageTemplate(w, &templates.ErrorPage{
12+ Message: msg,
13+ }, r.Context())
14+}
15+
16 func Redirect(w http.ResponseWriter, location string) {
17 w.Header().Add("location", location)
18 w.WriteHeader(http.StatusTemporaryRedirect)
19diff --git a/pkg/git/git.go b/pkg/git/git.go
20index 64c721a49be34538ddf32be507231e2eec6b38a5..95355f39daf9f06f2be40b80e9c63682d5336b14 100644
21--- a/pkg/git/git.go
22+++ b/pkg/git/git.go
23@@ -3,12 +3,17 @@
24 import (
25 "archive/tar"
26 "bytes"
27+ "context"
28 "errors"
29 "fmt"
30 "io"
31 "io/fs"
32+ "log"
33+ "log/slog"
34+ "os/exec"
35 "path"
36 "sort"
37+ "syscall"
38 "time"
39
40 "github.com/go-git/go-git/v5"
41@@ -432,6 +437,66 @@
42 return buf.Bytes(), nil
43 }
44
45+func (g *GitRepository) WriteInfoRefs(ctx context.Context, w io.Writer) error {
46+ cmd := exec.CommandContext(
47+ ctx,
48+ "git-upload-pack",
49+ "--stateless-rpc",
50+ "--advertise-refs",
51+ ".",
52+ )
53+
54+ cmd.Dir = g.path
55+ cmd.Stdout = w
56+
57+ var buff bytes.Buffer
58+ cmd.Stderr = &buff
59+
60+ err := packLine(w, "# service=git-upload-pack\n")
61+ if err != nil {
62+ return err
63+ }
64+
65+ err = packFlush(w)
66+ if err != nil {
67+ return err
68+ }
69+
70+ err = cmd.Run()
71+ if err != nil {
72+ slog.Error("Error upload pack refs", "message", buff.String())
73+ return err
74+ }
75+ return nil
76+}
77+
78+func (g *GitRepository) WriteUploadPack(ctx context.Context, r io.Reader, w io.Writer) error {
79+ cmd := exec.CommandContext(
80+ ctx,
81+ "git-upload-pack",
82+ "--stateless-rpc",
83+ ".",
84+ )
85+ cmd.Dir = g.Path()
86+ cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
87+ var buff bytes.Buffer
88+ cmd.Stderr = &buff
89+ cmd.Stdin = r
90+ cmd.Stdout = w
91+
92+ if err := cmd.Start(); err != nil {
93+ log.Printf("git: failed to start git-upload-pack: %s", err)
94+ return err
95+ }
96+
97+ if err := cmd.Wait(); err != nil {
98+ log.Printf("git: failed to wait for git-upload-pack: %s", buff.String())
99+ return err
100+ }
101+
102+ return nil
103+}
104+
105 func (g *GitRepository) WriteTar(w io.Writer, prefix string) error {
106 tw := tar.NewWriter(w)
107 defer tw.Close()
108@@ -613,3 +678,31 @@ }
109
110 return dateI.After(dateJ)
111 }
112+
113+func packLine(w io.Writer, s string) error {
114+ _, err := fmt.Fprintf(w, "%04x%s", len(s)+4, s)
115+ return err
116+}
117+
118+func packFlush(w io.Writer) error {
119+ _, err := fmt.Fprint(w, "0000")
120+ return err
121+}
122+
123+type debugReader struct {
124+ r io.Reader
125+}
126+
127+func (d *debugReader) Read(p []byte) (n int, err error) {
128+ fmt.Printf("READ: %x\n", p)
129+ return d.r.Read(p)
130+}
131+
132+type debugWriter struct {
133+ w io.Writer
134+}
135+
136+func (d *debugWriter) Write(p []byte) (n int, err error) {
137+ fmt.Printf("WRITE: %x\n", p)
138+ return d.w.Write(p)
139+}
140diff --git a/pkg/handler/git/handler.go b/pkg/handler/git/handler.go
141index a9be54cb3938c347e46d52a37a73f0732cfe9a12..61765bba9771841dbf6d4987c3d75c8e84801522 100644
142--- a/pkg/handler/git/handler.go
143+++ b/pkg/handler/git/handler.go
144@@ -2,6 +2,7 @@ package git
145
146 import (
147 "bytes"
148+ "compress/gzip"
149 "errors"
150 "fmt"
151 "io"
152@@ -110,6 +111,49 @@ if err != nil {
153 // once we start writing to the body we can't report error anymore
154 // so we are only left with printing the error.
155 slog.Error("Error generating tar gzip file", "error", err)
156+ }
157+
158+ return nil
159+}
160+
161+func (g *GitHandler) Multiplex(w http.ResponseWriter, r *http.Request) error {
162+ path := r.PathValue("rest")
163+ name := r.PathValue("name")
164+
165+ if r.URL.RawQuery == "service=git-receive-pack" {
166+ ext.BadRequest(w, r, "no pushing allowed")
167+ return nil
168+ }
169+
170+ if path == "info/refs" && r.URL.RawQuery == "service=git-upload-pack" && r.Method == "GET" {
171+ w.Header().Set("content-type", "application/x-git-upload-pack-advertisement")
172+
173+ err := g.gitService.WriteInfoRefs(r.Context(), name, w)
174+ if err != nil {
175+ slog.Error("Error WriteInfoRefs", "error", err)
176+ }
177+ } else if path == "git-upload-pack" && r.Method == "POST" {
178+ w.Header().Set("content-type", "application/x-git-upload-pack-result")
179+ w.Header().Set("Connection", "Keep-Alive")
180+ w.Header().Set("Transfer-Encoding", "chunked")
181+ w.WriteHeader(http.StatusOK)
182+
183+ reader := r.Body
184+
185+ if r.Header.Get("Content-Encoding") == "gzip" {
186+ reader, err := gzip.NewReader(r.Body)
187+ if err != nil {
188+ return err
189+ }
190+ defer reader.Close()
191+ }
192+
193+ err := g.gitService.WriteUploadPack(r.Context(), name, reader, w)
194+ if err != nil {
195+ slog.Error("Error WriteUploadPack", "error", err)
196+ }
197+ } else if r.Method == "GET" {
198+ return g.Summary(w, r)
199 }
200
201 return nil
202diff --git a/pkg/handler/router.go b/pkg/handler/router.go
203index e461922aa785bf61778e46a13b5f32479b312a26..fea882737aac94a9f22681315783e1831bfe9663 100644
204--- a/pkg/handler/router.go
205+++ b/pkg/handler/router.go
206@@ -46,7 +46,8 @@ }
207
208 mux.HandleFunc("/static/{file}", staticHandler)
209 mux.HandleFunc("/{name}/about/{$}", gitHandler.About)
210- mux.HandleFunc("/{name}/", gitHandler.Summary)
211+ mux.HandleFunc("/{name}", gitHandler.Multiplex)
212+ mux.HandleFunc("/{name}/{rest...}", gitHandler.Multiplex)
213 mux.HandleFunc("/{name}/refs/{$}", gitHandler.Refs)
214 mux.HandleFunc("/{name}/tree/{ref}/{rest...}", gitHandler.Tree)
215 mux.HandleFunc("/{name}/blob/{ref}/{rest...}", gitHandler.Blob)
216diff --git a/pkg/service/git.go b/pkg/service/git.go
217index 5410d7a7c3b20f084208b754d767b3e10b9e97f0..6aa5cd673dba18a5c65c35108378e6229275e366 100644
218--- a/pkg/service/git.go
219+++ b/pkg/service/git.go
220@@ -2,6 +2,7 @@ package service
221
222 import (
223 "compress/gzip"
224+ "context"
225 "errors"
226 "io"
227 "log/slog"
228@@ -299,3 +300,31 @@ }
229
230 return repo.Head()
231 }
232+
233+func (g *GitService) WriteInfoRefs(ctx context.Context, name string, w io.Writer) error {
234+ r := g.configRepo.GetByName(name)
235+ if r == nil {
236+ return ErrRepositoryNotFound
237+ }
238+
239+ repo, err := git.OpenRepository(r.Path)
240+ if err != nil {
241+ return err
242+ }
243+
244+ return repo.WriteInfoRefs(ctx, w)
245+}
246+
247+func (g *GitService) WriteUploadPack(ctx context.Context, name string, re io.Reader, w io.Writer) error {
248+ r := g.configRepo.GetByName(name)
249+ if r == nil {
250+ return ErrRepositoryNotFound
251+ }
252+
253+ repo, err := git.OpenRepository(r.Path)
254+ if err != nil {
255+ return err
256+ }
257+
258+ return repo.WriteUploadPack(ctx, re, w)
259+}