1package git
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "io"
8 "os"
9 "os/exec"
10 "strings"
11
12 "github.com/charmbracelet/log"
13 "golang.org/x/sync/errgroup"
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 log.Debugf("git service command in %q: %s", cmd.Dir, cmd.String())
116 if err := cmd.Start(); err != nil {
117 if errors.Is(err, os.ErrNotExist) {
118 return ErrInvalidRepo
119 }
120 return err
121 }
122
123 errg, _ := errgroup.WithContext(ctx)
124
125 // stdin
126 if scmd.Stdin != nil {
127 errg.Go(func() error {
128 defer stdin.Close() // nolint: errcheck
129 _, err := io.Copy(stdin, scmd.Stdin)
130 return err
131 })
132 }
133
134 // stdout
135 if scmd.Stdout != nil {
136 errg.Go(func() error {
137 _, err := io.Copy(scmd.Stdout, stdout)
138 return err
139 })
140 }
141
142 // stderr
143 if scmd.Stderr != nil {
144 errg.Go(func() error {
145 _, erro := io.Copy(scmd.Stderr, stderr)
146 return erro
147 })
148 }
149
150 err = errors.Join(errg.Wait(), cmd.Wait())
151 if err != nil && errors.Is(err, os.ErrNotExist) {
152 return ErrInvalidRepo
153 } else if err != nil {
154 return err
155 }
156
157 return nil
158}
159
160// ServiceCommand is used to run a git service command.
161type ServiceCommand struct {
162 Stdin io.Reader
163 Stdout io.Writer
164 Stderr io.Writer
165 Dir string
166 Env []string
167 Args []string
168
169 // Modifier functions
170 CmdFunc func(*exec.Cmd)
171}
172
173// UploadPack runs the git upload-pack protocol against the provided repo.
174func UploadPack(ctx context.Context, cmd ServiceCommand) error {
175 return gitServiceHandler(ctx, UploadPackService, cmd)
176}
177
178// UploadArchive runs the git upload-archive protocol against the provided repo.
179func UploadArchive(ctx context.Context, cmd ServiceCommand) error {
180 return gitServiceHandler(ctx, UploadArchiveService, cmd)
181}
182
183// ReceivePack runs the git receive-pack protocol against the provided repo.
184func ReceivePack(ctx context.Context, cmd ServiceCommand) error {
185 return gitServiceHandler(ctx, ReceivePackService, cmd)
186}