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