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