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