From 2c0347566f99afec2e3963d74f4fc970e6187217 Mon Sep 17 00:00:00 2001 From: "Gabriel A. Giovanini" Date: Sun, 30 Mar 2025 17:34:11 +0200 Subject: feat: Add initial support for http git clone --- pkg/ext/router.go | 7 ++++ pkg/git/git.go | 93 ++++++++++++++++++++++++++++++++++++++++++++++ pkg/handler/git/handler.go | 44 ++++++++++++++++++++++ pkg/handler/router.go | 3 +- pkg/service/git.go | 29 +++++++++++++++ 5 files changed, 175 insertions(+), 1 deletion(-) (limited to 'pkg') diff --git a/pkg/ext/router.go b/pkg/ext/router.go index ce4c126..434972b 100644 --- a/pkg/ext/router.go +++ b/pkg/ext/router.go @@ -69,6 +69,13 @@ func NotFound(w http.ResponseWriter, r *http.Request) { }, r.Context()) } +func BadRequest(w http.ResponseWriter, r *http.Request, msg string) { + w.WriteHeader(http.StatusBadRequest) + templates.WritePageTemplate(w, &templates.ErrorPage{ + Message: msg, + }, r.Context()) +} + func Redirect(w http.ResponseWriter, location string) { w.Header().Add("location", location) w.WriteHeader(http.StatusTemporaryRedirect) diff --git a/pkg/git/git.go b/pkg/git/git.go index 64c721a..95355f3 100644 --- a/pkg/git/git.go +++ b/pkg/git/git.go @@ -3,12 +3,17 @@ package git import ( "archive/tar" "bytes" + "context" "errors" "fmt" "io" "io/fs" + "log" + "log/slog" + "os/exec" "path" "sort" + "syscall" "time" "github.com/go-git/go-git/v5" @@ -432,6 +437,66 @@ func (g *GitRepository) FileContent(path string) ([]byte, error) { return buf.Bytes(), nil } +func (g *GitRepository) WriteInfoRefs(ctx context.Context, w io.Writer) error { + cmd := exec.CommandContext( + ctx, + "git-upload-pack", + "--stateless-rpc", + "--advertise-refs", + ".", + ) + + cmd.Dir = g.path + cmd.Stdout = w + + var buff bytes.Buffer + cmd.Stderr = &buff + + err := packLine(w, "# service=git-upload-pack\n") + if err != nil { + return err + } + + err = packFlush(w) + if err != nil { + return err + } + + err = cmd.Run() + if err != nil { + slog.Error("Error upload pack refs", "message", buff.String()) + return err + } + return nil +} + +func (g *GitRepository) WriteUploadPack(ctx context.Context, r io.Reader, w io.Writer) error { + cmd := exec.CommandContext( + ctx, + "git-upload-pack", + "--stateless-rpc", + ".", + ) + cmd.Dir = g.Path() + cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + var buff bytes.Buffer + cmd.Stderr = &buff + cmd.Stdin = r + cmd.Stdout = w + + if err := cmd.Start(); err != nil { + log.Printf("git: failed to start git-upload-pack: %s", err) + return err + } + + if err := cmd.Wait(); err != nil { + log.Printf("git: failed to wait for git-upload-pack: %s", buff.String()) + return err + } + + return nil +} + func (g *GitRepository) WriteTar(w io.Writer, prefix string) error { tw := tar.NewWriter(w) defer tw.Close() @@ -613,3 +678,31 @@ func (self *tagList) Less(i, j int) bool { return dateI.After(dateJ) } + +func packLine(w io.Writer, s string) error { + _, err := fmt.Fprintf(w, "%04x%s", len(s)+4, s) + return err +} + +func packFlush(w io.Writer) error { + _, err := fmt.Fprint(w, "0000") + return err +} + +type debugReader struct { + r io.Reader +} + +func (d *debugReader) Read(p []byte) (n int, err error) { + fmt.Printf("READ: %x\n", p) + return d.r.Read(p) +} + +type debugWriter struct { + w io.Writer +} + +func (d *debugWriter) Write(p []byte) (n int, err error) { + fmt.Printf("WRITE: %x\n", p) + return d.w.Write(p) +} diff --git a/pkg/handler/git/handler.go b/pkg/handler/git/handler.go index a9be54c..61765bb 100644 --- a/pkg/handler/git/handler.go +++ b/pkg/handler/git/handler.go @@ -2,6 +2,7 @@ package git import ( "bytes" + "compress/gzip" "errors" "fmt" "io" @@ -115,6 +116,49 @@ func (g *GitHandler) Archive(w http.ResponseWriter, r *http.Request) error { return nil } +func (g *GitHandler) Multiplex(w http.ResponseWriter, r *http.Request) error { + path := r.PathValue("rest") + name := r.PathValue("name") + + if r.URL.RawQuery == "service=git-receive-pack" { + ext.BadRequest(w, r, "no pushing allowed") + return nil + } + + if path == "info/refs" && r.URL.RawQuery == "service=git-upload-pack" && r.Method == "GET" { + w.Header().Set("content-type", "application/x-git-upload-pack-advertisement") + + err := g.gitService.WriteInfoRefs(r.Context(), name, w) + if err != nil { + slog.Error("Error WriteInfoRefs", "error", err) + } + } else if path == "git-upload-pack" && r.Method == "POST" { + w.Header().Set("content-type", "application/x-git-upload-pack-result") + w.Header().Set("Connection", "Keep-Alive") + w.Header().Set("Transfer-Encoding", "chunked") + w.WriteHeader(http.StatusOK) + + reader := r.Body + + if r.Header.Get("Content-Encoding") == "gzip" { + reader, err := gzip.NewReader(r.Body) + if err != nil { + return err + } + defer reader.Close() + } + + err := g.gitService.WriteUploadPack(r.Context(), name, reader, w) + if err != nil { + slog.Error("Error WriteUploadPack", "error", err) + } + } else if r.Method == "GET" { + return g.Summary(w, r) + } + + return nil +} + func (g *GitHandler) Summary(w http.ResponseWriter, r *http.Request) error { ext.SetHTML(w) name := r.PathValue("name") diff --git a/pkg/handler/router.go b/pkg/handler/router.go index e461922..fea8827 100644 --- a/pkg/handler/router.go +++ b/pkg/handler/router.go @@ -46,7 +46,8 @@ func MountHandler( mux.HandleFunc("/static/{file}", staticHandler) mux.HandleFunc("/{name}/about/{$}", gitHandler.About) - mux.HandleFunc("/{name}/", gitHandler.Summary) + mux.HandleFunc("/{name}", gitHandler.Multiplex) + mux.HandleFunc("/{name}/{rest...}", gitHandler.Multiplex) mux.HandleFunc("/{name}/refs/{$}", gitHandler.Refs) mux.HandleFunc("/{name}/tree/{ref}/{rest...}", gitHandler.Tree) mux.HandleFunc("/{name}/blob/{ref}/{rest...}", gitHandler.Blob) diff --git a/pkg/service/git.go b/pkg/service/git.go index 5410d7a..6aa5cd6 100644 --- a/pkg/service/git.go +++ b/pkg/service/git.go @@ -2,6 +2,7 @@ package service import ( "compress/gzip" + "context" "errors" "io" "log/slog" @@ -299,3 +300,31 @@ func (g *GitService) GetHead(name string) (*plumbing.Reference, error) { return repo.Head() } + +func (g *GitService) WriteInfoRefs(ctx context.Context, name string, w io.Writer) error { + r := g.configRepo.GetByName(name) + if r == nil { + return ErrRepositoryNotFound + } + + repo, err := git.OpenRepository(r.Path) + if err != nil { + return err + } + + return repo.WriteInfoRefs(ctx, w) +} + +func (g *GitService) WriteUploadPack(ctx context.Context, name string, re io.Reader, w io.Writer) error { + r := g.configRepo.GetByName(name) + if r == nil { + return ErrRepositoryNotFound + } + + repo, err := git.OpenRepository(r.Path) + if err != nil { + return err + } + + return repo.WriteUploadPack(ctx, re, w) +} -- cgit v1.2.3