all repos — legit @ fba146ac6867b13c40802c4d7a21a8a32571473c

web frontend for git, written in go

git: use system's git upload-pack

This is an intermediate workaround for
https://github.com/go-git/go-git/issues/1062. This should also fix #33.
Anirudh Oppiliappan x@icyphox.sh
Sat, 13 Jul 2024 20:06:37 +0300
commit

fba146ac6867b13c40802c4d7a21a8a32571473c

parent

67e355d5a4f7042d6c16a32afc9119b844d0b467

5 files changed, 178 insertions(+), 70 deletions(-)

jump to
M config.yamlconfig.yaml

@@ -12,9 +12,9 @@ dirs:

templates: ./templates static: ./static meta: - title: git good - description: i think it's a skill issue + title: icy does git + description: come get your free software server: name: git.icyphox.sh - host: 127.0.0.1 + host: 0.0.0.0 port: 5555
M flake.nixflake.nix

@@ -38,7 +38,7 @@ };

docker = pkgs.dockerTools.buildLayeredImage { name = "sini:5000/legit"; tag = "latest"; - contents = [ files legit ]; + contents = [ files legit pkgs.git ]; config = { Entrypoint = [ "${legit}/bin/legit" ]; ExposedPorts = { "5555/tcp" = { }; };
A git/service/service.go

@@ -0,0 +1,121 @@

+package service + +import ( + "bytes" + "fmt" + "io" + "log" + "net/http" + "os/exec" + "strings" + "syscall" +) + +// Mostly from charmbracelet/soft-serve and sosedoff/gitkit. + +type ServiceCommand struct { + Dir string + Stdin io.Reader + Stdout http.ResponseWriter +} + +func (c *ServiceCommand) InfoRefs() error { + cmd := exec.Command("git", []string{ + "upload-pack", + "--stateless-rpc", + "--advertise-refs", + ".", + }...) + + cmd.Dir = c.Dir + cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + stdoutPipe, _ := cmd.StdoutPipe() + cmd.Stderr = cmd.Stdout + + if err := cmd.Start(); err != nil { + log.Printf("git: failed to start git-upload-pack (info/refs): %s", err) + return err + } + + if err := packLine(c.Stdout, "# service=git-upload-pack\n"); err != nil { + log.Printf("git: failed to write pack line: %s", err) + return err + } + + if err := packFlush(c.Stdout); err != nil { + log.Printf("git: failed to flush pack: %s", err) + return err + } + + buf := bytes.Buffer{} + if _, err := io.Copy(&buf, stdoutPipe); err != nil { + log.Printf("git: failed to copy stdout to tmp buffer: %s", err) + return err + } + + if err := cmd.Wait(); err != nil { + out := strings.Builder{} + _, _ = io.Copy(&out, &buf) + log.Printf("git: failed to run git-upload-pack; err: %s; output: %s", err, out.String()) + return err + } + + if _, err := io.Copy(c.Stdout, &buf); err != nil { + log.Printf("git: failed to copy stdout: %s", err) + } + + return nil +} + +func (c *ServiceCommand) UploadPack() error { + cmd := exec.Command("git", []string{ + "-c", "uploadpack.allowFilter=true", + "upload-pack", + "--stateless-rpc", + ".", + }...) + cmd.Dir = c.Dir + cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + + stdoutPipe, _ := cmd.StdoutPipe() + cmd.Stderr = cmd.Stdout + defer stdoutPipe.Close() + + stdinPipe, err := cmd.StdinPipe() + if err != nil { + return err + } + defer stdinPipe.Close() + + if err := cmd.Start(); err != nil { + log.Printf("git: failed to start git-upload-pack: %s", err) + return err + } + + if _, err := io.Copy(stdinPipe, c.Stdin); err != nil { + log.Printf("git: failed to copy stdin: %s", err) + return err + } + stdinPipe.Close() + + if _, err := io.Copy(newWriteFlusher(c.Stdout), stdoutPipe); err != nil { + log.Printf("git: failed to copy stdout: %s", err) + return err + } + if err := cmd.Wait(); err != nil { + log.Printf("git: failed to wait for git-upload-pack: %s", err) + return err + } + + return nil +} + +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 +}
A git/service/write_flusher.go

@@ -0,0 +1,25 @@

+package service + +import ( + "io" + "net/http" +) + +func newWriteFlusher(w http.ResponseWriter) io.Writer { + return writeFlusher{w.(interface { + io.Writer + http.Flusher + })} +} + +type writeFlusher struct { + wf interface { + io.Writer + http.Flusher + } +} + +func (w writeFlusher) Write(p []byte) (int, error) { + defer w.wf.Flush() + return w.wf.Write(p) +}
M routes/git.goroutes/git.go

@@ -1,16 +1,13 @@

package routes import ( - "errors" + "compress/gzip" + "io" "log" "net/http" "path/filepath" - "github.com/go-git/go-billy/v5/osfs" - "github.com/go-git/go-git/v5/plumbing/format/pktline" - "github.com/go-git/go-git/v5/plumbing/protocol/packp" - "github.com/go-git/go-git/v5/plumbing/transport" - "github.com/go-git/go-git/v5/plumbing/transport/server" + "git.icyphox.sh/legit/git/service" ) func (d *deps) InfoRefs(w http.ResponseWriter, r *http.Request) {

@@ -20,41 +17,16 @@

repo := filepath.Join(d.c.Repo.ScanPath, name) w.Header().Set("content-type", "application/x-git-upload-pack-advertisement") + w.WriteHeader(http.StatusOK) - ep, err := transport.NewEndpoint("/") - if err != nil { - http.Error(w, err.Error(), 500) - log.Printf("git: %s", err) - return + cmd := service.ServiceCommand{ + Dir: repo, + Stdout: w, } - billyfs := osfs.New(repo) - loader := server.NewFilesystemLoader(billyfs) - srv := server.NewServer(loader) - session, err := srv.NewUploadPackSession(ep, nil) - if err != nil { + if err := cmd.InfoRefs(); err != nil { http.Error(w, err.Error(), 500) - log.Printf("git: %s", err) - return - } - - ar, err := session.AdvertisedReferencesContext(r.Context()) - if errors.Is(err, transport.ErrRepositoryNotFound) { - http.Error(w, err.Error(), 404) - return - } else if err != nil { - http.Error(w, err.Error(), 500) - return - } - - ar.Prefix = [][]byte{ - []byte("# service=git-upload-pack"), - pktline.Flush, - } - - if err = ar.Encode(w); err != nil { - http.Error(w, err.Error(), 500) - log.Printf("git: %s", err) + log.Printf("git: failed to execute git-upload-pack (info/refs) %s", err) return } }

@@ -66,42 +38,32 @@

repo := filepath.Join(d.c.Repo.ScanPath, name) w.Header().Set("content-type", "application/x-git-upload-pack-result") - - upr := packp.NewUploadPackRequest() - err := upr.Decode(r.Body) - if err != nil { - http.Error(w, err.Error(), 400) - log.Printf("git: %s", err) - return - } + w.Header().Set("Connection", "Keep-Alive") + w.Header().Set("Transfer-Encoding", "chunked") + w.WriteHeader(http.StatusOK) - ep, err := transport.NewEndpoint("/") - if err != nil { - http.Error(w, err.Error(), 500) - log.Printf("git: %s", err) - return + cmd := service.ServiceCommand{ + Dir: repo, + Stdout: w, } - billyfs := osfs.New(repo) - loader := server.NewFilesystemLoader(billyfs) - svr := server.NewServer(loader) - session, err := svr.NewUploadPackSession(ep, nil) - if err != nil { - http.Error(w, err.Error(), 500) - log.Printf("git: %s", err) - return - } + var reader io.ReadCloser + reader = r.Body - res, err := session.UploadPack(r.Context(), upr) - if err != nil { - http.Error(w, err.Error(), 500) - log.Printf("git: %s", err) - return + if r.Header.Get("Content-Encoding") == "gzip" { + reader, err := gzip.NewReader(r.Body) + if err != nil { + http.Error(w, err.Error(), 500) + log.Printf("git: failed to create gzip reader: %s", err) + return + } + defer reader.Close() } - if err = res.Encode(w); err != nil { + cmd.Stdin = reader + if err := cmd.UploadPack(); err != nil { http.Error(w, err.Error(), 500) - log.Printf("git: %s", err) + log.Printf("git: failed to execute git-upload-pack %s", err) return } }