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 // TODO: add support for 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 default:
49 return fmt.Errorf("unsupported service: %s", s)
50 }
51}
52
53// ServiceHandler is a git service command handler.
54type ServiceHandler func(ctx context.Context, cmd ServiceCommand) error
55
56// gitServiceHandler is the default service handler using the git binary.
57func gitServiceHandler(ctx context.Context, svc Service, scmd ServiceCommand) error {
58 cmd := exec.CommandContext(ctx, "git")
59 cmd.Dir = scmd.Dir
60 cmd.Args = append(cmd.Args, []string{
61 // Enable partial clones
62 "-c", "uploadpack.allowFilter=true",
63 // Enable push options
64 "-c", "receive.advertisePushOptions=true",
65 // Disable LFS filters
66 "-c", "filter.lfs.required=", "-c", "filter.lfs.smudge=", "-c", "filter.lfs.clean=",
67 svc.Name(),
68 }...)
69 if len(scmd.Args) > 0 {
70 cmd.Args = append(cmd.Args, scmd.Args...)
71 }
72
73 cmd.Args = append(cmd.Args, ".")
74
75 cmd.Env = os.Environ()
76 if len(scmd.Env) > 0 {
77 cmd.Env = append(cmd.Env, scmd.Env...)
78 }
79
80 if scmd.CmdFunc != nil {
81 scmd.CmdFunc(cmd)
82 }
83
84 var (
85 err error
86 stdin io.WriteCloser
87 stdout io.ReadCloser
88 stderr io.ReadCloser
89 )
90
91 if scmd.Stdin != nil {
92 stdin, err = cmd.StdinPipe()
93 if err != nil {
94 return err
95 }
96 }
97
98 if scmd.Stdout != nil {
99 stdout, err = cmd.StdoutPipe()
100 if err != nil {
101 return err
102 }
103 }
104
105 if scmd.Stderr != nil {
106 stderr, err = cmd.StderrPipe()
107 if err != nil {
108 return err
109 }
110 }
111
112 log.Debugf("git service command in %q: %s", cmd.Dir, cmd.String())
113 if err := cmd.Start(); err != nil {
114 if errors.Is(err, os.ErrNotExist) {
115 return ErrInvalidRepo
116 }
117 return err
118 }
119
120 errg, _ := errgroup.WithContext(ctx)
121
122 // stdin
123 if scmd.Stdin != nil {
124 errg.Go(func() error {
125 defer stdin.Close() // nolint: errcheck
126 _, err := io.Copy(stdin, scmd.Stdin)
127 return err
128 })
129 }
130
131 // stdout
132 if scmd.Stdout != nil {
133 errg.Go(func() error {
134 _, err := io.Copy(scmd.Stdout, stdout)
135 return err
136 })
137 }
138
139 // stderr
140 if scmd.Stderr != nil {
141 errg.Go(func() error {
142 _, erro := io.Copy(scmd.Stderr, stderr)
143 return erro
144 })
145 }
146
147 err = errors.Join(errg.Wait(), cmd.Wait())
148 if err != nil && errors.Is(err, os.ErrNotExist) {
149 return ErrInvalidRepo
150 } else if err != nil {
151 return err
152 }
153
154 return nil
155}
156
157// ServiceCommand is used to run a git service command.
158type ServiceCommand struct {
159 Stdin io.Reader
160 Stdout io.Writer
161 Stderr io.Writer
162 Dir string
163 Env []string
164 Args []string
165
166 // Modifier functions
167 CmdFunc func(*exec.Cmd)
168}
169
170// UploadPack runs the git upload-pack protocol against the provided repo.
171func UploadPack(ctx context.Context, cmd ServiceCommand) error {
172 return gitServiceHandler(ctx, UploadPackService, cmd)
173}
174
175// UploadArchive runs the git upload-archive protocol against the provided repo.
176func UploadArchive(ctx context.Context, cmd ServiceCommand) error {
177 return gitServiceHandler(ctx, UploadArchiveService, cmd)
178}
179
180// ReceivePack runs the git receive-pack protocol against the provided repo.
181func ReceivePack(ctx context.Context, cmd ServiceCommand) error {
182 return gitServiceHandler(ctx, ReceivePackService, cmd)
183}