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