1package service
2
3import (
4 "bytes"
5 "fmt"
6 "io"
7 "log"
8 "net/http"
9 "os/exec"
10 "strings"
11 "sync"
12 "syscall"
13)
14
15// Mostly from charmbracelet/soft-serve and sosedoff/gitkit.
16
17type ServiceCommand struct {
18 Dir string
19 Stdin io.Reader
20 Stdout http.ResponseWriter
21}
22
23func (c *ServiceCommand) InfoRefs() error {
24 cmd := exec.Command("git", []string{
25 "upload-pack",
26 "--stateless-rpc",
27 "--advertise-refs",
28 ".",
29 }...)
30
31 cmd.Dir = c.Dir
32 cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
33 stdoutPipe, _ := cmd.StdoutPipe()
34 cmd.Stderr = cmd.Stdout
35
36 if err := cmd.Start(); err != nil {
37 log.Printf("git: failed to start git-upload-pack (info/refs): %s", err)
38 return err
39 }
40
41 if err := packLine(c.Stdout, "# service=git-upload-pack\n"); err != nil {
42 log.Printf("git: failed to write pack line: %s", err)
43 return err
44 }
45
46 if err := packFlush(c.Stdout); err != nil {
47 log.Printf("git: failed to flush pack: %s", err)
48 return err
49 }
50
51 buf := bytes.Buffer{}
52 if _, err := io.Copy(&buf, stdoutPipe); err != nil {
53 log.Printf("git: failed to copy stdout to tmp buffer: %s", err)
54 return err
55 }
56
57 if err := cmd.Wait(); err != nil {
58 out := strings.Builder{}
59 _, _ = io.Copy(&out, &buf)
60 log.Printf("git: failed to run git-upload-pack; err: %s; output: %s", err, out.String())
61 return err
62 }
63
64 if _, err := io.Copy(c.Stdout, &buf); err != nil {
65 log.Printf("git: failed to copy stdout: %s", err)
66 }
67
68 return nil
69}
70
71func (c *ServiceCommand) UploadPack() error {
72 var stderr bytes.Buffer
73
74 cmd := exec.Command("git", "-c", "uploadpack.allowFilter=true",
75 "upload-pack", "--stateless-rpc", ".")
76 cmd.Dir = c.Dir
77 cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
78
79 stdoutPipe, err := cmd.StdoutPipe()
80 if err != nil {
81 return fmt.Errorf("failed to create stdout pipe: %w", err)
82 }
83
84 cmd.Stderr = &stderr
85
86 stdinPipe, err := cmd.StdinPipe()
87 if err != nil {
88 return fmt.Errorf("failed to create stdin pipe: %w", err)
89 }
90
91 if err := cmd.Start(); err != nil {
92 return fmt.Errorf("failed to start git-upload-pack: %w", err)
93 }
94
95 var wg sync.WaitGroup
96
97 wg.Add(1)
98 go func() {
99 defer wg.Done()
100 defer stdinPipe.Close()
101 io.Copy(stdinPipe, c.Stdin)
102 }()
103
104 wg.Add(1)
105 go func() {
106 defer wg.Done()
107 io.Copy(newWriteFlusher(c.Stdout), stdoutPipe)
108 stdoutPipe.Close()
109 }()
110
111 wg.Wait()
112
113 if err := cmd.Wait(); err != nil {
114 return fmt.Errorf("git-upload-pack failed: %w, stderr: %s", err, stderr.String())
115 }
116
117 return nil
118}
119
120func packLine(w io.Writer, s string) error {
121 _, err := fmt.Fprintf(w, "%04x%s", len(s)+4, s)
122 return err
123}
124
125func packFlush(w io.Writer) error {
126 _, err := fmt.Fprint(w, "0000")
127 return err
128}