192 lines
3.9 KiB
Go
192 lines
3.9 KiB
Go
package gitcmd
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type Command struct {
|
|
args []string
|
|
dir string
|
|
env []string
|
|
timeout time.Duration
|
|
stdin io.Reader
|
|
preErrs []error
|
|
}
|
|
|
|
func NewCommand(args ...string) *Command {
|
|
c := &Command{}
|
|
for _, arg := range args {
|
|
if !isTrustedArg(arg) {
|
|
c.preErrs = append(c.preErrs, fmt.Errorf("untrusted argument in NewCommand: %q", arg))
|
|
}
|
|
}
|
|
c.args = append(c.args, args...)
|
|
return c
|
|
}
|
|
|
|
func isTrustedArg(arg string) bool {
|
|
if arg == "" {
|
|
return true
|
|
}
|
|
for _, r := range arg {
|
|
switch r {
|
|
case ';', '`', '$', '|', '&', '\n', '\r':
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func isSafeValue(arg string) bool {
|
|
return !strings.HasPrefix(arg, "-")
|
|
}
|
|
|
|
func (c *Command) Add(args ...string) *Command {
|
|
for _, arg := range args {
|
|
if !isTrustedArg(arg) {
|
|
c.preErrs = append(c.preErrs, fmt.Errorf("untrusted argument in Add: %q", arg))
|
|
}
|
|
}
|
|
if len(c.preErrs) == 0 {
|
|
c.args = append(c.args, args...)
|
|
}
|
|
return c
|
|
}
|
|
|
|
func (c *Command) AddOptionValues(opt string, values ...string) *Command {
|
|
if !isTrustedArg(opt) || !strings.HasPrefix(opt, "-") {
|
|
c.preErrs = append(c.preErrs, fmt.Errorf("invalid option in AddOptionValues: %q", opt))
|
|
return c
|
|
}
|
|
c.args = append(c.args, opt)
|
|
c.AddDynamicArguments(values...)
|
|
return c
|
|
}
|
|
|
|
func (c *Command) AddDynamicArguments(args ...string) *Command {
|
|
for _, arg := range args {
|
|
if !isSafeValue(arg) {
|
|
c.preErrs = append(c.preErrs, fmt.Errorf("user argument cannot start with '-': %q", arg))
|
|
}
|
|
}
|
|
if len(c.preErrs) == 0 {
|
|
c.args = append(c.args, args...)
|
|
}
|
|
return c
|
|
}
|
|
|
|
func (c *Command) AddDashesAndList(list ...string) *Command {
|
|
c.args = append(c.args, "--")
|
|
c.args = append(c.args, list...)
|
|
return c
|
|
}
|
|
|
|
func (c *Command) WithDir(dir string) *Command {
|
|
c.dir = dir
|
|
return c
|
|
}
|
|
|
|
func (c *Command) WithEnv(env ...string) *Command {
|
|
c.env = append(c.env, env...)
|
|
return c
|
|
}
|
|
|
|
func (c *Command) WithTimeout(d time.Duration) *Command {
|
|
c.timeout = d
|
|
return c
|
|
}
|
|
|
|
func (c *Command) WithStdin(r io.Reader) *Command {
|
|
c.stdin = r
|
|
return c
|
|
}
|
|
|
|
func (c *Command) CmdString() string {
|
|
exe, err := Executable()
|
|
if err != nil {
|
|
exe = "git"
|
|
}
|
|
parts := append([]string{exe}, c.args...)
|
|
for i, p := range parts {
|
|
if strings.ContainsAny(p, " `'\"\t\n") {
|
|
parts[i] = fmt.Sprintf("%q", p)
|
|
}
|
|
}
|
|
return strings.Join(parts, " ")
|
|
}
|
|
|
|
func (c *Command) Run(ctx context.Context) error {
|
|
_, _, err := c.RunStdString(ctx)
|
|
return err
|
|
}
|
|
|
|
func (c *Command) RunStdString(ctx context.Context) (stdout, stderr string, err error) {
|
|
outb, errb, err := c.RunStdBytes(ctx)
|
|
return string(outb), string(errb), err
|
|
}
|
|
|
|
func (c *Command) RunStdBytes(ctx context.Context) (stdout, stderr []byte, err error) {
|
|
if len(c.preErrs) > 0 {
|
|
return nil, nil, errors.Join(c.preErrs...)
|
|
}
|
|
|
|
exe, err := Executable()
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("git executable not found: %w", err)
|
|
}
|
|
|
|
var cancel context.CancelFunc
|
|
if c.timeout > 0 {
|
|
ctx, cancel = context.WithTimeout(ctx, c.timeout)
|
|
defer cancel()
|
|
}
|
|
|
|
cmd := exec.CommandContext(ctx, exe, c.args...)
|
|
cmd.Dir = c.dir
|
|
cmd.Env = append(os.Environ(), CommonEnvs()...)
|
|
cmd.Env = append(cmd.Env, c.env...)
|
|
cmd.Stdin = c.stdin
|
|
|
|
var stdoutBuf, stderrBuf bytes.Buffer
|
|
cmd.Stdout = &stdoutBuf
|
|
cmd.Stderr = &stderrBuf
|
|
|
|
err = cmd.Run()
|
|
return stdoutBuf.Bytes(), stderrBuf.Bytes(), err
|
|
}
|
|
|
|
func (c *Command) RunPiped(ctx context.Context, stdout, stderr io.Writer) error {
|
|
if len(c.preErrs) > 0 {
|
|
return errors.Join(c.preErrs...)
|
|
}
|
|
|
|
exe, err := Executable()
|
|
if err != nil {
|
|
return fmt.Errorf("git executable not found: %w", err)
|
|
}
|
|
|
|
var cancel context.CancelFunc
|
|
if c.timeout > 0 {
|
|
ctx, cancel = context.WithTimeout(ctx, c.timeout)
|
|
defer cancel()
|
|
}
|
|
|
|
cmd := exec.CommandContext(ctx, exe, c.args...)
|
|
cmd.Dir = c.dir
|
|
cmd.Env = append(os.Environ(), CommonEnvs()...)
|
|
cmd.Env = append(cmd.Env, c.env...)
|
|
cmd.Stdin = c.stdin
|
|
cmd.Stdout = stdout
|
|
cmd.Stderr = stderr
|
|
|
|
return cmd.Run()
|
|
}
|