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