cerrado @ 2c0347566f99afec2e3963d74f4fc970e6187217

feat: Add initial support for http git clone
  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+}