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