1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173
|
// Package file implements the file transport protocol.
package file
import (
"bufio"
"errors"
"io"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/go-git/go-git/v5/plumbing/transport"
"github.com/go-git/go-git/v5/plumbing/transport/internal/common"
"golang.org/x/sys/execabs"
)
// DefaultClient is the default local client.
var DefaultClient = NewClient(
transport.UploadPackServiceName,
transport.ReceivePackServiceName,
)
type runner struct {
UploadPackBin string
ReceivePackBin string
}
// NewClient returns a new local client using the given git-upload-pack and
// git-receive-pack binaries.
func NewClient(uploadPackBin, receivePackBin string) transport.Transport {
return common.NewClient(&runner{
UploadPackBin: uploadPackBin,
ReceivePackBin: receivePackBin,
})
}
func prefixExecPath(cmd string) (string, error) {
// Use `git --exec-path` to find the exec path.
execCmd := execabs.Command("git", "--exec-path")
stdout, err := execCmd.StdoutPipe()
if err != nil {
return "", err
}
stdoutBuf := bufio.NewReader(stdout)
err = execCmd.Start()
if err != nil {
return "", err
}
execPathBytes, isPrefix, err := stdoutBuf.ReadLine()
if err != nil {
return "", err
}
if isPrefix {
return "", errors.New("couldn't read exec-path line all at once")
}
err = execCmd.Wait()
if err != nil {
return "", err
}
execPath := string(execPathBytes)
execPath = strings.TrimSpace(execPath)
cmd = filepath.Join(execPath, cmd)
// Make sure it actually exists.
_, err = execabs.LookPath(cmd)
if err != nil {
return "", err
}
return cmd, nil
}
func (r *runner) Command(cmd string, ep *transport.Endpoint, auth transport.AuthMethod,
) (common.Command, error) {
switch cmd {
case transport.UploadPackServiceName:
cmd = r.UploadPackBin
case transport.ReceivePackServiceName:
cmd = r.ReceivePackBin
}
_, err := execabs.LookPath(cmd)
if err != nil {
if e, ok := err.(*execabs.Error); ok && e.Err == execabs.ErrNotFound {
cmd, err = prefixExecPath(cmd)
if err != nil {
return nil, err
}
} else {
return nil, err
}
}
return &command{cmd: execabs.Command(cmd, adjustPathForWindows(ep.Path))}, nil
}
func isDriveLetter(c byte) bool {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
}
// On Windows, the path that results from a file: URL has a leading slash. This
// has to be removed if there's a drive letter
func adjustPathForWindows(p string) string {
if runtime.GOOS != "windows" {
return p
}
if len(p) >= 3 && p[0] == '/' && isDriveLetter(p[1]) && p[2] == ':' {
return p[1:]
}
return p
}
type command struct {
cmd *execabs.Cmd
stderrCloser io.Closer
closed bool
}
func (c *command) Start() error {
return c.cmd.Start()
}
func (c *command) StderrPipe() (io.Reader, error) {
// Pipe returned by Command.StderrPipe has a race with Read + Command.Wait.
// We use an io.Pipe and close it after the command finishes.
r, w := io.Pipe()
c.cmd.Stderr = w
c.stderrCloser = r
return r, nil
}
func (c *command) StdinPipe() (io.WriteCloser, error) {
return c.cmd.StdinPipe()
}
func (c *command) StdoutPipe() (io.Reader, error) {
return c.cmd.StdoutPipe()
}
func (c *command) Kill() error {
c.cmd.Process.Kill()
return c.Close()
}
// Close waits for the command to exit.
func (c *command) Close() error {
if c.closed {
return nil
}
defer func() {
c.closed = true
_ = c.stderrCloser.Close()
}()
err := c.cmd.Wait()
if _, ok := err.(*os.PathError); ok {
return nil
}
// When a repository does not exist, the command exits with code 128.
if _, ok := err.(*execabs.ExitError); ok {
return nil
}
return err
}
|