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)
26
27// String returns the string representation of the service.
28func (s Service) String() string {
29 return string(s)
30}
31
32// Name returns the name of the service.
33func (s Service) Name() string {
34 return strings.TrimPrefix(s.String(), "git-")
35}
36
37// Handler is the service handler.
38func (s Service) Handler(ctx context.Context, cmd ServiceCommand) error {
39 switch s {
40 case UploadPackService, UploadArchiveService, ReceivePackService:
41 return gitServiceHandler(ctx, s, cmd)
42 default:
43 return fmt.Errorf("unsupported service: %s", s)
44 }
45}
46
47// ServiceHandler is a git service command handler.
48type ServiceHandler func(ctx context.Context, cmd ServiceCommand) error
49
50// gitServiceHandler is the default service handler using the git binary.
51func gitServiceHandler(ctx context.Context, svc Service, scmd ServiceCommand) error {
52 cmd := exec.CommandContext(ctx, "git", svc.Name()) // nolint: gosec
53 cmd.Dir = scmd.Dir
54 if len(scmd.Args) > 0 {
55 cmd.Args = append(cmd.Args, scmd.Args...)
56 }
57
58 cmd.Args = append(cmd.Args, ".")
59
60 cmd.Env = os.Environ()
61 if len(scmd.Env) > 0 {
62 cmd.Env = append(cmd.Env, scmd.Env...)
63 }
64
65 if scmd.CmdFunc != nil {
66 scmd.CmdFunc(cmd)
67 }
68
69 stdin, err := cmd.StdinPipe()
70 if err != nil {
71 return err
72 }
73
74 stdout, err := cmd.StdoutPipe()
75 if err != nil {
76 return err
77 }
78
79 stderr, err := cmd.StderrPipe()
80 if err != nil {
81 return err
82 }
83
84 if err := cmd.Start(); err != nil {
85 return err
86 }
87
88 errg, ctx := errgroup.WithContext(ctx)
89
90 // stdin
91 errg.Go(func() error {
92 defer stdin.Close() // nolint: errcheck
93 _, err := io.Copy(stdin, scmd.Stdin)
94 return err
95 })
96
97 // stdout
98 errg.Go(func() error {
99 _, err := io.Copy(scmd.Stdout, stdout)
100 return err
101 })
102
103 // stderr
104 errg.Go(func() error {
105 _, err := io.Copy(scmd.Stderr, stderr)
106 return err
107 })
108
109 return errors.Join(errg.Wait(), cmd.Wait())
110}
111
112// ServiceCommand is used to run a git service command.
113type ServiceCommand struct {
114 Stdin io.Reader
115 Stdout io.Writer
116 Stderr io.Writer
117 Dir string
118 Env []string
119 Args []string
120 CmdFunc func(*exec.Cmd)
121}
122
123// UploadPack runs the git upload-pack protocol against the provided repo.
124func UploadPack(ctx context.Context, cmd ServiceCommand) error {
125 return gitServiceHandler(ctx, UploadPackService, cmd)
126}
127
128// UploadArchive runs the git upload-archive protocol against the provided repo.
129func UploadArchive(ctx context.Context, cmd ServiceCommand) error {
130 return gitServiceHandler(ctx, UploadArchiveService, cmd)
131}
132
133// ReceivePack runs the git receive-pack protocol against the provided repo.
134func ReceivePack(ctx context.Context, cmd ServiceCommand) error {
135 return gitServiceHandler(ctx, ReceivePackService, cmd)
136}