service.go

  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}