s
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
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()
|
||||
}
|
||||
Reference in New Issue
Block a user