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	// 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}