1package git23import (4 "context"5 "errors"6 "fmt"7 "io"8 "os"9 "os/exec"10 "strings"11 "sync"1213 "github.com/charmbracelet/log/v2"14)1516// Service is a Git daemon service.17type Service string1819const (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 // LFSAuthenticateService is the LFS authenticate service.29 LFSAuthenticateService = "git-lfs-authenticate"30)3132// String returns the string representation of the service.33func (s Service) String() string {34 return string(s)35}3637// Name returns the name of the service.38func (s Service) Name() string {39 return strings.TrimPrefix(s.String(), "git-")40}4142// Handler is the service handler.43func (s Service) Handler(ctx context.Context, cmd ServiceCommand) error {44 switch s {45 case UploadPackService, UploadArchiveService, ReceivePackService:46 return gitServiceHandler(ctx, s, cmd)47 case LFSTransferService:48 return LFSTransfer(ctx, cmd)49 case LFSAuthenticateService:50 return LFSAuthenticate(ctx, cmd)51 default:52 return fmt.Errorf("unsupported service: %s", s)53 }54}5556// ServiceHandler is a git service command handler.57type ServiceHandler func(ctx context.Context, cmd ServiceCommand) error5859// gitServiceHandler is the default service handler using the git binary.60func gitServiceHandler(ctx context.Context, svc Service, scmd ServiceCommand) error {61 cmd := exec.CommandContext(ctx, "git")62 cmd.Dir = scmd.Dir63 cmd.Args = append(cmd.Args, []string{64 // Enable partial clones65 "-c", "uploadpack.allowFilter=true",66 // Enable push options67 "-c", "receive.advertisePushOptions=true",68 // Disable LFS filters69 "-c", "filter.lfs.required=", "-c", "filter.lfs.smudge=", "-c", "filter.lfs.clean=",70 svc.Name(),71 }...)72 if len(scmd.Args) > 0 {73 cmd.Args = append(cmd.Args, scmd.Args...)74 }7576 cmd.Args = append(cmd.Args, ".")7778 cmd.Env = os.Environ()79 if len(scmd.Env) > 0 {80 cmd.Env = append(cmd.Env, scmd.Env...)81 }8283 if scmd.CmdFunc != nil {84 scmd.CmdFunc(cmd)85 }8687 var (88 err error89 stdin io.WriteCloser90 stdout io.ReadCloser91 stderr io.ReadCloser92 )9394 if scmd.Stdin != nil {95 stdin, err = cmd.StdinPipe()96 if err != nil {97 return err98 }99 }100101 if scmd.Stdout != nil {102 stdout, err = cmd.StdoutPipe()103 if err != nil {104 return err105 }106 }107108 if scmd.Stderr != nil {109 stderr, err = cmd.StderrPipe()110 if err != nil {111 return err112 }113 }114115 if err := cmd.Start(); err != nil {116 if errors.Is(err, os.ErrNotExist) {117 return ErrInvalidRepo118 }119 return err120 }121122 wg := &sync.WaitGroup{}123124 // stdin125 if scmd.Stdin != nil {126 go func() {127 defer stdin.Close() // nolint: errcheck128 if _, err := io.Copy(stdin, scmd.Stdin); err != nil {129 log.Errorf("gitServiceHandler: failed to copy stdin: %v", err)130 }131 }()132 }133134 // stdout135 if scmd.Stdout != nil {136 wg.Add(1)137 go func() {138 defer wg.Done()139 if _, err := io.Copy(scmd.Stdout, stdout); err != nil {140 log.Errorf("gitServiceHandler: failed to copy stdout: %v", err)141 }142 }()143 }144145 // stderr146 if scmd.Stderr != nil {147 wg.Add(1)148 go func() {149 defer wg.Done()150 if _, erro := io.Copy(scmd.Stderr, stderr); err != nil {151 log.Errorf("gitServiceHandler: failed to copy stderr: %v", erro)152 }153 }()154 }155156 // Ensure all the output is written before waiting for the command to157 // finish.158 // Stdin is handled by the client side.159 wg.Wait()160161 err = cmd.Wait()162 if err != nil && errors.Is(err, os.ErrNotExist) {163 return ErrInvalidRepo164 } else if err != nil {165 var exitErr *exec.ExitError166 if errors.As(err, &exitErr) && len(exitErr.Stderr) > 0 {167 return fmt.Errorf("%s: %s", exitErr, exitErr.Stderr)168 }169170 return err171 }172173 return nil174}175176// ServiceCommand is used to run a git service command.177type ServiceCommand struct {178 Stdin io.Reader179 Stdout io.Writer180 Stderr io.Writer181 Dir string182 Env []string183 Args []string184185 // Modifier functions186 CmdFunc func(*exec.Cmd)187}188189// UploadPack runs the git upload-pack protocol against the provided repo.190func UploadPack(ctx context.Context, cmd ServiceCommand) error {191 return gitServiceHandler(ctx, UploadPackService, cmd)192}193194// UploadArchive runs the git upload-archive protocol against the provided repo.195func UploadArchive(ctx context.Context, cmd ServiceCommand) error {196 return gitServiceHandler(ctx, UploadArchiveService, cmd)197}198199// ReceivePack runs the git receive-pack protocol against the provided repo.200func ReceivePack(ctx context.Context, cmd ServiceCommand) error {201 return gitServiceHandler(ctx, ReceivePackService, cmd)202}