1package git
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "io"
8 "os"
9 "os/exec"
10 "strings"
11 "sync"
12
13 log "github.com/charmbracelet/log/v2"
14)
15
16// Service is a Git daemon service.
17type Service string
18
19const (
20 // UploadPackService is the upload-pack service.
21 UploadPackService Service = "git-upload-pack"
22 // UploadArchiveService is the upload-archive service.
23 UploadArchiveService Service = "git-upload-archive"
24 // ReceivePackService is the receive-pack service.
25 ReceivePackService Service = "git-receive-pack"
26 // LFSTransferService is the LFS transfer service.
27 LFSTransferService Service = "git-lfs-transfer"
28 // LFSAuthenticateService is the LFS authenticate service.
29 LFSAuthenticateService = "git-lfs-authenticate"
30)
31
32// String returns the string representation of the service.
33func (s Service) String() string {
34 return string(s)
35}
36
37// Name returns the name of the service.
38func (s Service) Name() string {
39 return strings.TrimPrefix(s.String(), "git-")
40}
41
42// Handler is the service handler.
43func (s Service) Handler(ctx context.Context, cmd ServiceCommand) error {
44 switch s {
45 case UploadPackService, UploadArchiveService, ReceivePackService:
46 return gitServiceHandler(ctx, s, cmd)
47 case LFSTransferService:
48 return LFSTransfer(ctx, cmd)
49 case LFSAuthenticateService:
50 return LFSAuthenticate(ctx, cmd)
51 default:
52 return fmt.Errorf("unsupported service: %s", s)
53 }
54}
55
56// ServiceHandler is a git service command handler.
57type ServiceHandler func(ctx context.Context, cmd ServiceCommand) error
58
59// gitServiceHandler is the default service handler using the git binary.
60func gitServiceHandler(ctx context.Context, svc Service, scmd ServiceCommand) error {
61 cmd := exec.CommandContext(ctx, "git")
62 cmd.Dir = scmd.Dir
63 cmd.Args = append(cmd.Args, []string{
64 // Enable partial clones
65 "-c", "uploadpack.allowFilter=true",
66 // Enable push options
67 "-c", "receive.advertisePushOptions=true",
68 // Disable LFS filters
69 "-c", "filter.lfs.required=", "-c", "filter.lfs.smudge=", "-c", "filter.lfs.clean=",
70 svc.Name(),
71 }...)
72 if len(scmd.Args) > 0 {
73 cmd.Args = append(cmd.Args, scmd.Args...)
74 }
75
76 cmd.Args = append(cmd.Args, ".")
77
78 cmd.Env = os.Environ()
79 if len(scmd.Env) > 0 {
80 cmd.Env = append(cmd.Env, scmd.Env...)
81 }
82
83 if scmd.CmdFunc != nil {
84 scmd.CmdFunc(cmd)
85 }
86
87 var (
88 err error
89 stdin io.WriteCloser
90 stdout io.ReadCloser
91 stderr io.ReadCloser
92 )
93
94 if scmd.Stdin != nil {
95 stdin, err = cmd.StdinPipe()
96 if err != nil {
97 return err
98 }
99 }
100
101 if scmd.Stdout != nil {
102 stdout, err = cmd.StdoutPipe()
103 if err != nil {
104 return err
105 }
106 }
107
108 if scmd.Stderr != nil {
109 stderr, err = cmd.StderrPipe()
110 if err != nil {
111 return err
112 }
113 }
114
115 if err := cmd.Start(); err != nil {
116 if errors.Is(err, os.ErrNotExist) {
117 return ErrInvalidRepo
118 }
119 return err
120 }
121
122 wg := &sync.WaitGroup{}
123
124 // stdin
125 if scmd.Stdin != nil {
126 go func() {
127 defer stdin.Close()
128 if _, err := io.Copy(stdin, scmd.Stdin); err != nil {
129 log.Errorf("gitServiceHandler: failed to copy stdin: %v", err)
130 }
131 }()
132 }
133
134 // stdout
135 if scmd.Stdout != nil {
136 wg.Add(1)
137 go func() {
138 defer wg.Done()
139 if _, err := io.Copy(scmd.Stdout, stdout); err != nil {
140 log.Errorf("gitServiceHandler: failed to copy stdout: %v", err)
141 }
142 }()
143 }
144
145 // stderr
146 if scmd.Stderr != nil {
147 wg.Add(1)
148 go func() {
149 defer wg.Done()
150 if _, erro := io.Copy(scmd.Stderr, stderr); err != nil {
151 log.Errorf("gitServiceHandler: failed to copy stderr: %v", erro)
152 }
153 }()
154 }
155
156 // Ensure all the output is written before waiting for the command to
157 // finish.
158 // Stdin is handled by the client side.
159 wg.Wait()
160
161 err = cmd.Wait()
162 if err != nil && errors.Is(err, os.ErrNotExist) {
163 return ErrInvalidRepo
164 } else if err != nil {
165 var exitErr *exec.ExitError
166 if errors.As(err, &exitErr) && len(exitErr.Stderr) > 0 {
167 return fmt.Errorf("%s: %s", exitErr, exitErr.Stderr)
168 }
169
170 return err
171 }
172
173 return nil
174}
175
176// ServiceCommand is used to run a git service command.
177type ServiceCommand struct {
178 Stdin io.Reader
179 Stdout io.Writer
180 Stderr io.Writer
181 Dir string
182 Env []string
183 Args []string
184
185 // Modifier functions
186 CmdFunc func(*exec.Cmd)
187}
188
189// UploadPack runs the git upload-pack protocol against the provided repo.
190func UploadPack(ctx context.Context, cmd ServiceCommand) error {
191 return gitServiceHandler(ctx, UploadPackService, cmd)
192}
193
194// UploadArchive runs the git upload-archive protocol against the provided repo.
195func UploadArchive(ctx context.Context, cmd ServiceCommand) error {
196 return gitServiceHandler(ctx, UploadArchiveService, cmd)
197}
198
199// ReceivePack runs the git receive-pack protocol against the provided repo.
200func ReceivePack(ctx context.Context, cmd ServiceCommand) error {
201 return gitServiceHandler(ctx, ReceivePackService, cmd)
202}