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 GitProtocol string
19 Dir string
20 Stdin io.Reader
21 Stdout http.ResponseWriter
22}
23
24func (c *ServiceCommand) RunService(cmd *exec.Cmd) error {
25 cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
26 cmd.Dir = c.Dir
27 cmd.Env = append(cmd.Env, fmt.Sprintf("GIT_PROTOCOL=%s", c.GitProtocol))
28
29 var stderr bytes.Buffer
30 cmd.Stderr = &stderr
31
32 stdoutPipe, err := cmd.StdoutPipe()
33 if err != nil {
34 return fmt.Errorf("failed to create stdout pipe: %w", err)
35 }
36
37 stdinPipe, err := cmd.StdinPipe()
38 if err != nil {
39 return fmt.Errorf("failed to create stdin pipe: %w", err)
40 }
41
42 if err := cmd.Start(); err != nil {
43 return fmt.Errorf("failed to start '%s': %w", cmd.String(), err)
44 }
45
46 var wg sync.WaitGroup
47
48 if c.Stdin != nil {
49 wg.Add(1)
50 go func() {
51 defer wg.Done()
52 defer stdinPipe.Close()
53 io.Copy(stdinPipe, c.Stdin)
54 }()
55 }
56
57 if c.Stdout != nil {
58 wg.Add(1)
59 go func() {
60 defer wg.Done()
61 io.Copy(newWriteFlusher(c.Stdout), stdoutPipe)
62 stdoutPipe.Close()
63 }()
64 }
65
66 wg.Wait()
67
68 if err := cmd.Wait(); err != nil {
69 return fmt.Errorf("'%s' failed: %w, stderr: %s", cmd.String(), err, stderr.String())
70 }
71
72 return nil
73}
74
75func (c *ServiceCommand) InfoRefs() error {
76 cmd := exec.Command("git", []string{
77 "upload-pack",
78 "--stateless-rpc",
79 "--http-backend-info-refs",
80 ".",
81 }...)
82
83 if !strings.Contains(c.GitProtocol, "version=2") {
84 if err := packLine(c.Stdout, "# service=git-upload-pack\n"); err != nil {
85 log.Printf("git: failed to write pack line: %s", err)
86 return err
87 }
88
89 if err := packFlush(c.Stdout); err != nil {
90 log.Printf("git: failed to flush pack: %s", err)
91 return err
92 }
93 }
94
95 return c.RunService(cmd)
96}
97
98func (c *ServiceCommand) UploadPack() error {
99 cmd := exec.Command("git", []string{
100 "-c", "uploadpack.allowFilter=true",
101 "upload-pack",
102 "--stateless-rpc",
103 ".",
104 }...)
105
106 cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
107 cmd.Env = append(cmd.Env, fmt.Sprintf("GIT_PROTOCOL=%s", c.GitProtocol))
108 cmd.Dir = c.Dir
109
110 return c.RunService(cmd)
111}
112
113func packLine(w io.Writer, s string) error {
114 _, err := fmt.Fprintf(w, "%04x%s", len(s)+4, s)
115 return err
116}
117
118func packFlush(w io.Writer) error {
119 _, err := fmt.Fprint(w, "0000")
120 return err
121}