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