web frontend for git (tangled's grandpa)

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.

+178 -70
+3 -3
config.yaml
··· 12 12 templates: ./templates 13 13 static: ./static 14 14 meta: 15 - title: git good 16 - description: i think it's a skill issue 15 + title: icy does git 16 + description: come get your free software 17 17 server: 18 18 name: git.icyphox.sh 19 - host: 127.0.0.1 19 + host: 0.0.0.0 20 20 port: 5555
+1 -1
flake.nix
··· 38 38 docker = pkgs.dockerTools.buildLayeredImage { 39 39 name = "sini:5000/legit"; 40 40 tag = "latest"; 41 - contents = [ files legit ]; 41 + contents = [ files legit pkgs.git ]; 42 42 config = { 43 43 Entrypoint = [ "${legit}/bin/legit" ]; 44 44 ExposedPorts = { "5555/tcp" = { }; };
+121
git/service/service.go
··· 1 + package service 2 + 3 + import ( 4 + "bytes" 5 + "fmt" 6 + "io" 7 + "log" 8 + "net/http" 9 + "os/exec" 10 + "strings" 11 + "syscall" 12 + ) 13 + 14 + // Mostly from charmbracelet/soft-serve and sosedoff/gitkit. 15 + 16 + type ServiceCommand struct { 17 + Dir string 18 + Stdin io.Reader 19 + Stdout http.ResponseWriter 20 + } 21 + 22 + func (c *ServiceCommand) InfoRefs() error { 23 + cmd := exec.Command("git", []string{ 24 + "upload-pack", 25 + "--stateless-rpc", 26 + "--advertise-refs", 27 + ".", 28 + }...) 29 + 30 + cmd.Dir = c.Dir 31 + cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} 32 + stdoutPipe, _ := cmd.StdoutPipe() 33 + cmd.Stderr = cmd.Stdout 34 + 35 + if err := cmd.Start(); err != nil { 36 + log.Printf("git: failed to start git-upload-pack (info/refs): %s", err) 37 + return err 38 + } 39 + 40 + if err := packLine(c.Stdout, "# service=git-upload-pack\n"); err != nil { 41 + log.Printf("git: failed to write pack line: %s", err) 42 + return err 43 + } 44 + 45 + if err := packFlush(c.Stdout); err != nil { 46 + log.Printf("git: failed to flush pack: %s", err) 47 + return err 48 + } 49 + 50 + buf := bytes.Buffer{} 51 + if _, err := io.Copy(&buf, stdoutPipe); err != nil { 52 + log.Printf("git: failed to copy stdout to tmp buffer: %s", err) 53 + return err 54 + } 55 + 56 + if err := cmd.Wait(); err != nil { 57 + out := strings.Builder{} 58 + _, _ = io.Copy(&out, &buf) 59 + log.Printf("git: failed to run git-upload-pack; err: %s; output: %s", err, out.String()) 60 + return err 61 + } 62 + 63 + if _, err := io.Copy(c.Stdout, &buf); err != nil { 64 + log.Printf("git: failed to copy stdout: %s", err) 65 + } 66 + 67 + return nil 68 + } 69 + 70 + func (c *ServiceCommand) UploadPack() error { 71 + cmd := exec.Command("git", []string{ 72 + "-c", "uploadpack.allowFilter=true", 73 + "upload-pack", 74 + "--stateless-rpc", 75 + ".", 76 + }...) 77 + cmd.Dir = c.Dir 78 + cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} 79 + 80 + stdoutPipe, _ := cmd.StdoutPipe() 81 + cmd.Stderr = cmd.Stdout 82 + defer stdoutPipe.Close() 83 + 84 + stdinPipe, err := cmd.StdinPipe() 85 + if err != nil { 86 + return err 87 + } 88 + defer stdinPipe.Close() 89 + 90 + if err := cmd.Start(); err != nil { 91 + log.Printf("git: failed to start git-upload-pack: %s", err) 92 + return err 93 + } 94 + 95 + if _, err := io.Copy(stdinPipe, c.Stdin); err != nil { 96 + log.Printf("git: failed to copy stdin: %s", err) 97 + return err 98 + } 99 + stdinPipe.Close() 100 + 101 + if _, err := io.Copy(newWriteFlusher(c.Stdout), stdoutPipe); err != nil { 102 + log.Printf("git: failed to copy stdout: %s", err) 103 + return err 104 + } 105 + if err := cmd.Wait(); err != nil { 106 + log.Printf("git: failed to wait for git-upload-pack: %s", err) 107 + return err 108 + } 109 + 110 + return nil 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 + }
+25
git/service/write_flusher.go
··· 1 + package service 2 + 3 + import ( 4 + "io" 5 + "net/http" 6 + ) 7 + 8 + func newWriteFlusher(w http.ResponseWriter) io.Writer { 9 + return writeFlusher{w.(interface { 10 + io.Writer 11 + http.Flusher 12 + })} 13 + } 14 + 15 + type writeFlusher struct { 16 + wf interface { 17 + io.Writer 18 + http.Flusher 19 + } 20 + } 21 + 22 + func (w writeFlusher) Write(p []byte) (int, error) { 23 + defer w.wf.Flush() 24 + return w.wf.Write(p) 25 + }
+28 -66
routes/git.go
··· 1 1 package routes 2 2 3 3 import ( 4 - "errors" 4 + "compress/gzip" 5 + "io" 5 6 "log" 6 7 "net/http" 7 8 "path/filepath" 8 9 9 - "github.com/go-git/go-billy/v5/osfs" 10 - "github.com/go-git/go-git/v5/plumbing/format/pktline" 11 - "github.com/go-git/go-git/v5/plumbing/protocol/packp" 12 - "github.com/go-git/go-git/v5/plumbing/transport" 13 - "github.com/go-git/go-git/v5/plumbing/transport/server" 10 + "git.icyphox.sh/legit/git/service" 14 11 ) 15 12 16 13 func (d *deps) InfoRefs(w http.ResponseWriter, r *http.Request) { ··· 20 17 repo := filepath.Join(d.c.Repo.ScanPath, name) 21 18 22 19 w.Header().Set("content-type", "application/x-git-upload-pack-advertisement") 20 + w.WriteHeader(http.StatusOK) 23 21 24 - ep, err := transport.NewEndpoint("/") 25 - if err != nil { 26 - http.Error(w, err.Error(), 500) 27 - log.Printf("git: %s", err) 28 - return 22 + cmd := service.ServiceCommand{ 23 + Dir: repo, 24 + Stdout: w, 29 25 } 30 26 31 - billyfs := osfs.New(repo) 32 - loader := server.NewFilesystemLoader(billyfs) 33 - srv := server.NewServer(loader) 34 - session, err := srv.NewUploadPackSession(ep, nil) 35 - if err != nil { 27 + if err := cmd.InfoRefs(); err != nil { 36 28 http.Error(w, err.Error(), 500) 37 - log.Printf("git: %s", err) 38 - return 39 - } 40 - 41 - ar, err := session.AdvertisedReferencesContext(r.Context()) 42 - if errors.Is(err, transport.ErrRepositoryNotFound) { 43 - http.Error(w, err.Error(), 404) 44 - return 45 - } else if err != nil { 46 - http.Error(w, err.Error(), 500) 47 - return 48 - } 49 - 50 - ar.Prefix = [][]byte{ 51 - []byte("# service=git-upload-pack"), 52 - pktline.Flush, 53 - } 54 - 55 - if err = ar.Encode(w); err != nil { 56 - http.Error(w, err.Error(), 500) 57 - log.Printf("git: %s", err) 29 + log.Printf("git: failed to execute git-upload-pack (info/refs) %s", err) 58 30 return 59 31 } 60 32 } ··· 66 38 repo := filepath.Join(d.c.Repo.ScanPath, name) 67 39 68 40 w.Header().Set("content-type", "application/x-git-upload-pack-result") 69 - 70 - upr := packp.NewUploadPackRequest() 71 - err := upr.Decode(r.Body) 72 - if err != nil { 73 - http.Error(w, err.Error(), 400) 74 - log.Printf("git: %s", err) 75 - return 76 - } 41 + w.Header().Set("Connection", "Keep-Alive") 42 + w.Header().Set("Transfer-Encoding", "chunked") 43 + w.WriteHeader(http.StatusOK) 77 44 78 - ep, err := transport.NewEndpoint("/") 79 - if err != nil { 80 - http.Error(w, err.Error(), 500) 81 - log.Printf("git: %s", err) 82 - return 45 + cmd := service.ServiceCommand{ 46 + Dir: repo, 47 + Stdout: w, 83 48 } 84 49 85 - billyfs := osfs.New(repo) 86 - loader := server.NewFilesystemLoader(billyfs) 87 - svr := server.NewServer(loader) 88 - session, err := svr.NewUploadPackSession(ep, nil) 89 - if err != nil { 90 - http.Error(w, err.Error(), 500) 91 - log.Printf("git: %s", err) 92 - return 93 - } 50 + var reader io.ReadCloser 51 + reader = r.Body 94 52 95 - res, err := session.UploadPack(r.Context(), upr) 96 - if err != nil { 97 - http.Error(w, err.Error(), 500) 98 - log.Printf("git: %s", err) 99 - return 53 + if r.Header.Get("Content-Encoding") == "gzip" { 54 + reader, err := gzip.NewReader(r.Body) 55 + if err != nil { 56 + http.Error(w, err.Error(), 500) 57 + log.Printf("git: failed to create gzip reader: %s", err) 58 + return 59 + } 60 + defer reader.Close() 100 61 } 101 62 102 - if err = res.Encode(w); err != nil { 63 + cmd.Stdin = reader 64 + if err := cmd.UploadPack(); err != nil { 103 65 http.Error(w, err.Error(), 500) 104 - log.Printf("git: %s", err) 66 + log.Printf("git: failed to execute git-upload-pack %s", err) 105 67 return 106 68 } 107 69 }