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) UploadArchive() error {
99 cmd := exec.Command("git", []string{
100 "upload-archive",
101 ".",
102 }...)
103
104 cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
105 cmd.Env = append(cmd.Env, fmt.Sprintf("GIT_PROTOCOL=%s", c.GitProtocol))
106 cmd.Dir = c.Dir
107
108 return c.RunService(cmd)
109}
110
111func (c *ServiceCommand) UploadPack() error {
112 cmd := exec.Command("git", []string{
113 "upload-pack",
114 "--stateless-rpc",
115 ".",
116 }...)
117
118 cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
119 cmd.Env = append(cmd.Env, fmt.Sprintf("GIT_PROTOCOL=%s", c.GitProtocol))
120 cmd.Dir = c.Dir
121
122 return c.RunService(cmd)
123}
124
125func packLine(w io.Writer, s string) error {
126 _, err := fmt.Fprintf(w, "%04x%s", len(s)+4, s)
127 return err
128}
129
130func packFlush(w io.Writer) error {
131 _, err := fmt.Fprint(w, "0000")
132 return err
133}