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)
27
28// String returns the string representation of the service.
29func (s Service) String() string {
30 return string(s)
31}
32
33// Name returns the name of the service.
34func (s Service) Name() string {
35 return strings.TrimPrefix(s.String(), "git-")
36}
37
38// Handler is the service handler.
39func (s Service) Handler(ctx context.Context, cmd ServiceCommand) error {
40 switch s {
41 case UploadPackService, UploadArchiveService, ReceivePackService:
42 return gitServiceHandler(ctx, s, cmd)
43 default:
44 return fmt.Errorf("unsupported service: %s", s)
45 }
46}
47
48// ServiceHandler is a git service command handler.
49type ServiceHandler func(ctx context.Context, cmd ServiceCommand) error
50
51// gitServiceHandler is the default service handler using the git binary.
52func gitServiceHandler(ctx context.Context, svc Service, scmd ServiceCommand) error {
53 cmd := exec.CommandContext(ctx, "git", "-c", "uploadpack.allowFilter=true", svc.Name()) // nolint: gosec
54 cmd.Dir = scmd.Dir
55 if len(scmd.Args) > 0 {
56 cmd.Args = append(cmd.Args, scmd.Args...)
57 }
58
59 cmd.Args = append(cmd.Args, ".")
60
61 cmd.Env = os.Environ()
62 if len(scmd.Env) > 0 {
63 cmd.Env = append(cmd.Env, scmd.Env...)
64 }
65
66 if scmd.CmdFunc != nil {
67 scmd.CmdFunc(cmd)
68 }
69
70 var (
71 err error
72 stdin io.WriteCloser
73 stdout io.ReadCloser
74 stderr io.ReadCloser
75 )
76
77 if scmd.Stdin != nil {
78 stdin, err = cmd.StdinPipe()
79 if err != nil {
80 return err
81 }
82 }
83
84 if scmd.Stdout != nil {
85 stdout, err = cmd.StdoutPipe()
86 if err != nil {
87 return err
88 }
89 }
90
91 if scmd.Stderr != nil {
92 stderr, err = cmd.StderrPipe()
93 if err != nil {
94 return err
95 }
96 }
97
98 log.Debugf("git service command in %q: %s", cmd.Dir, cmd.String())
99 if err := cmd.Start(); err != nil {
100 return err
101 }
102
103 errg, ctx := errgroup.WithContext(ctx)
104
105 // stdin
106 if scmd.Stdin != nil {
107 errg.Go(func() error {
108 if scmd.StdinHandler != nil {
109 return scmd.StdinHandler(scmd.Stdin, stdin)
110 } else {
111 return defaultStdinHandler(scmd.Stdin, stdin)
112 }
113 })
114 }
115
116 // stdout
117 if scmd.Stdout != nil {
118 errg.Go(func() error {
119 if scmd.StdoutHandler != nil {
120 return scmd.StdoutHandler(scmd.Stdout, stdout)
121 } else {
122 return defaultStdoutHandler(scmd.Stdout, stdout)
123 }
124 })
125 }
126
127 // stderr
128 if scmd.Stderr != nil {
129 errg.Go(func() error {
130 if scmd.StderrHandler != nil {
131 return scmd.StderrHandler(scmd.Stderr, stderr)
132 } else {
133 return defaultStderrHandler(scmd.Stderr, stderr)
134 }
135 })
136 }
137
138 return errors.Join(errg.Wait(), cmd.Wait())
139}
140
141// ServiceCommand is used to run a git service command.
142type ServiceCommand struct {
143 Stdin io.Reader
144 Stdout io.Writer
145 Stderr io.Writer
146 Dir string
147 Env []string
148 Args []string
149
150 // Modifier functions
151 CmdFunc func(*exec.Cmd)
152 StdinHandler func(io.Reader, io.WriteCloser) error
153 StdoutHandler func(io.Writer, io.ReadCloser) error
154 StderrHandler func(io.Writer, io.ReadCloser) error
155}
156
157func defaultStdinHandler(in io.Reader, stdin io.WriteCloser) error {
158 defer stdin.Close() // nolint: errcheck
159 _, err := io.Copy(stdin, in)
160 return err
161}
162
163func defaultStdoutHandler(out io.Writer, stdout io.ReadCloser) error {
164 _, err := io.Copy(out, stdout)
165 return err
166}
167
168func defaultStderrHandler(err io.Writer, stderr io.ReadCloser) error {
169 _, erro := io.Copy(err, stderr)
170 return erro
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}