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", "-c", "uploadpack.allowFilter=true", 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}