Skip to content

feat: run command with context #95

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 107 additions & 39 deletions command.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,22 @@ import (

// Command contains the name, arguments and environment variables of a command.
type Command struct {
name string
args []string
envs []string
name string
args []string
envs []string
timeout time.Duration
ctx context.Context
}

// CommandOptions contains options for running a command.
// If timeout is zero, DefaultTimeout will be used.
// If timeout is less than zero, no timeout will be set.
// If context is nil, context.Background() will be used.
type CommandOptions struct {
Args []string
Envs []string
Args []string
Envs []string
Timeout time.Duration
Context context.Context
}

// String returns the string representation of the command.
Expand All @@ -38,9 +45,16 @@ func (c *Command) String() string {

// NewCommand creates and returns a new Command with given arguments for "git".
func NewCommand(args ...string) *Command {
return NewCommandWithContext(context.Background(), args...)
}

// NewCommandWithContext creates and returns a new Command with given arguments
// and context for "git".
func NewCommandWithContext(ctx context.Context, args ...string) *Command {
return &Command{
name: "git",
args: args,
ctx: ctx,
}
}

Expand All @@ -56,9 +70,29 @@ func (c *Command) AddEnvs(envs ...string) *Command {
return c
}

// WithContext returns a new Command with the given context.
func (c Command) WithContext(ctx context.Context) *Command {
c.ctx = ctx
return &c
}

// WithTimeout returns a new Command with given timeout.
func (c Command) WithTimeout(timeout time.Duration) *Command {
c.timeout = timeout
return &c
}

// SetTimeout sets the timeout for the command.
func (c *Command) SetTimeout(timeout time.Duration) {
c.timeout = timeout
}

// AddOptions adds options to the command.
// Note: only the last option will take effect if there are duplicated options.
func (c *Command) AddOptions(opts ...CommandOptions) *Command {
for _, opt := range opts {
c.timeout = opt.Timeout
c.ctx = opt.Context
c.AddArgs(opt.Args...)
c.AddEnvs(opt.Envs...)
}
Expand Down Expand Up @@ -111,6 +145,8 @@ type RunInDirOptions struct {
// Stderr is the error output from the command.
Stderr io.Writer
// Timeout is the duration to wait before timing out.
//
// Deprecated: Use CommandOptions.Timeout or *Command.WithTimeout instead.
Timeout time.Duration
}

Expand All @@ -124,8 +160,15 @@ func (c *Command) RunInDirWithOptions(dir string, opts ...RunInDirOptions) (err
if len(opts) > 0 {
opt = opts[0]
}
if opt.Timeout < time.Nanosecond {
opt.Timeout = DefaultTimeout

timeout := c.timeout
// TODO: remove this in newer version
if opt.Timeout > 0 {
timeout = opt.Timeout
}

if timeout == 0 {
timeout = DefaultTimeout
}

buf := new(bytes.Buffer)
Expand All @@ -141,19 +184,27 @@ func (c *Command) RunInDirWithOptions(dir string, opts ...RunInDirOptions) (err

defer func() {
if len(dir) == 0 {
log("[timeout: %v] %s\n%s", opt.Timeout, c, buf.Bytes())
log("[timeout: %v] %s\n%s", timeout, c, buf.Bytes())
} else {
log("[timeout: %v] %s: %s\n%s", opt.Timeout, dir, c, buf.Bytes())
log("[timeout: %v] %s: %s\n%s", timeout, dir, c, buf.Bytes())
}
}()

ctx, cancel := context.WithTimeout(context.Background(), opt.Timeout)
defer func() {
cancel()
if err == context.DeadlineExceeded {
err = ErrExecTimeout
}
}()
ctx := context.Background()
if c.ctx != nil {
ctx = c.ctx
}

if timeout > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, timeout)
defer func() {
cancel()
if err == context.DeadlineExceeded {
err = ErrExecTimeout
}
}()
}

cmd := exec.CommandContext(ctx, c.name, c.args...)
if len(c.envs) > 0 {
Expand Down Expand Up @@ -188,55 +239,72 @@ func (c *Command) RunInDirWithOptions(dir string, opts ...RunInDirOptions) (err

}

// RunInDirPipeline executes the command in given directory and default timeout
// duration. It pipes stdout and stderr to supplied io.Writer.
func (c *Command) RunInDirPipeline(stdout, stderr io.Writer, dir string) error {
return c.RunInDirWithOptions(dir, RunInDirOptions{
Stdin: nil,
Stdout: stdout,
Stderr: stderr,
})
}

// RunInDirPipelineWithTimeout executes the command in given directory and
// timeout duration. It pipes stdout and stderr to supplied io.Writer.
// DefaultTimeout will be used if the timeout duration is less than
// time.Nanosecond (i.e. less than or equal to 0). It returns an ErrExecTimeout
// if the execution was timed out.
//
// Deprecated: Use RunInDirPipeline and CommandOptions instead.
// TODO: remove this in the next major version
func (c *Command) RunInDirPipelineWithTimeout(timeout time.Duration, stdout, stderr io.Writer, dir string) (err error) {
return c.RunInDirWithOptions(dir, RunInDirOptions{
Stdin: nil,
Stdout: stdout,
Stderr: stderr,
Timeout: timeout,
})
}

// RunInDirPipeline executes the command in given directory and default timeout
// duration. It pipes stdout and stderr to supplied io.Writer.
func (c *Command) RunInDirPipeline(stdout, stderr io.Writer, dir string) error {
return c.RunInDirPipelineWithTimeout(DefaultTimeout, stdout, stderr, dir)
if timeout != 0 {
c = c.WithTimeout(timeout)
}
return c.RunInDirPipeline(stdout, stderr, dir)
}

// RunInDirWithTimeout executes the command in given directory and timeout
// duration. It returns stdout in []byte and error (combined with stderr).
//
// Deprecated: Use RunInDir and CommandOptions instead.
// TODO: remove this in the next major version
func (c *Command) RunInDirWithTimeout(timeout time.Duration, dir string) ([]byte, error) {
stdout := new(bytes.Buffer)
stderr := new(bytes.Buffer)
if err := c.RunInDirPipelineWithTimeout(timeout, stdout, stderr, dir); err != nil {
return nil, concatenateError(err, stderr.String())
if timeout != 0 {
c = c.WithTimeout(timeout)
}
return stdout.Bytes(), nil
return c.RunInDir(dir)
}

// RunInDir executes the command in given directory and default timeout
// duration. It returns stdout and error (combined with stderr).
func (c *Command) RunInDir(dir string) ([]byte, error) {
return c.RunInDirWithTimeout(DefaultTimeout, dir)
stdout := new(bytes.Buffer)
stderr := new(bytes.Buffer)
if err := c.RunInDirPipeline(stdout, stderr, dir); err != nil {
return nil, concatenateError(err, stderr.String())
}
return stdout.Bytes(), nil
}

// RunWithTimeout executes the command in working directory and given timeout
// duration. It returns stdout in string and error (combined with stderr).
//
// Deprecated: Use RunInDir and CommandOptions instead.
// TODO: remove this in the next major version
func (c *Command) RunWithTimeout(timeout time.Duration) ([]byte, error) {
stdout, err := c.RunInDirWithTimeout(timeout, "")
if err != nil {
return nil, err
if timeout != 0 {
c = c.WithTimeout(timeout)
}
return stdout, nil
return c.Run()
}

// Run executes the command in working directory and default timeout duration.
// It returns stdout in string and error (combined with stderr).
func (c *Command) Run() ([]byte, error) {
return c.RunWithTimeout(DefaultTimeout)
stdout, err := c.RunInDir("")
if err != nil {
return nil, err
}
return stdout, nil
}
2 changes: 1 addition & 1 deletion command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,6 @@ func TestCommand_AddEnvs(t *testing.T) {
}

func TestCommand_RunWithTimeout(t *testing.T) {
_, err := NewCommand("version").RunWithTimeout(time.Nanosecond)
_, err := NewCommand("version").WithTimeout(time.Nanosecond).Run()
assert.Equal(t, ErrExecTimeout, err)
}
28 changes: 28 additions & 0 deletions repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ type InitOptions struct {
Bare bool
// The timeout duration before giving up for each shell command execution. The
// default timeout duration will be used when not supplied.
//
// Deprecated: Use CommandOptions.Timeout instead.
Timeout time.Duration
// The additional options to be passed to the underlying git.
CommandOptions
Expand Down Expand Up @@ -119,6 +121,8 @@ type CloneOptions struct {
Depth uint64
// The timeout duration before giving up for each shell command execution. The
// default timeout duration will be used when not supplied.
//
// Deprecated: Use CommandOptions.Timeout instead.
Timeout time.Duration
// The additional options to be passed to the underlying git.
CommandOptions
Expand Down Expand Up @@ -165,6 +169,8 @@ type FetchOptions struct {
Prune bool
// The timeout duration before giving up for each shell command execution. The
// default timeout duration will be used when not supplied.
//
// Deprecated: Use CommandOptions.Timeout instead.
Timeout time.Duration
// The additional options to be passed to the underlying git.
CommandOptions
Expand Down Expand Up @@ -200,6 +206,8 @@ type PullOptions struct {
Branch string
// The timeout duration before giving up for each shell command execution. The
// default timeout duration will be used when not supplied.
//
// Deprecated: Use CommandOptions.Timeout instead.
Timeout time.Duration
// The additional options to be passed to the underlying git.
CommandOptions
Expand Down Expand Up @@ -236,6 +244,8 @@ func (r *Repository) Pull(opts ...PullOptions) error {
type PushOptions struct {
// The timeout duration before giving up for each shell command execution. The
// default timeout duration will be used when not supplied.
//
// Deprecated: Use CommandOptions.Timeout instead.
Timeout time.Duration
// The additional options to be passed to the underlying git.
CommandOptions
Expand Down Expand Up @@ -272,6 +282,8 @@ type CheckoutOptions struct {
BaseBranch string
// The timeout duration before giving up for each shell command execution. The
// default timeout duration will be used when not supplied.
//
// Deprecated: Use CommandOptions.Timeout instead.
Timeout time.Duration
// The additional options to be passed to the underlying git.
CommandOptions
Expand Down Expand Up @@ -315,6 +327,8 @@ type ResetOptions struct {
Hard bool
// The timeout duration before giving up for each shell command execution. The
// default timeout duration will be used when not supplied.
//
// Deprecated: Use CommandOptions.Timeout instead.
Timeout time.Duration
// The additional options to be passed to the underlying git.
CommandOptions
Expand Down Expand Up @@ -353,6 +367,8 @@ func (r *Repository) Reset(rev string, opts ...ResetOptions) error {
type MoveOptions struct {
// The timeout duration before giving up for each shell command execution. The
// default timeout duration will be used when not supplied.
//
// Deprecated: Use CommandOptions.Timeout instead.
Timeout time.Duration
// The additional options to be passed to the underlying git.
CommandOptions
Expand Down Expand Up @@ -391,6 +407,8 @@ type AddOptions struct {
Pathspecs []string
// The timeout duration before giving up for each shell command execution. The
// default timeout duration will be used when not supplied.
//
// Deprecated: Use CommandOptions.Timeout instead.
Timeout time.Duration
// The additional options to be passed to the underlying git.
CommandOptions
Expand Down Expand Up @@ -433,6 +451,8 @@ type CommitOptions struct {
Author *Signature
// The timeout duration before giving up for each shell command execution. The
// default timeout duration will be used when not supplied.
//
// Deprecated: Use CommandOptions.Timeout instead.
Timeout time.Duration
// The additional options to be passed to the underlying git.
CommandOptions
Expand Down Expand Up @@ -488,6 +508,8 @@ type NameStatus struct {
type ShowNameStatusOptions struct {
// The timeout duration before giving up for each shell command execution. The
// default timeout duration will be used when not supplied.
//
// Deprecated: Use CommandOptions.Timeout instead.
Timeout time.Duration
// The additional options to be passed to the underlying git.
CommandOptions
Expand Down Expand Up @@ -554,6 +576,8 @@ func (r *Repository) ShowNameStatus(rev string, opts ...ShowNameStatusOptions) (
type RevParseOptions struct {
// The timeout duration before giving up for each shell command execution. The
// default timeout duration will be used when not supplied.
//
// Deprecated: Use CommandOptions.Timeout instead.
Timeout time.Duration
// The additional options to be passed to the underlying git.
CommandOptions
Expand Down Expand Up @@ -598,6 +622,8 @@ type CountObject struct {
type CountObjectsOptions struct {
// The timeout duration before giving up for each shell command execution. The
// default timeout duration will be used when not supplied.
//
// Deprecated: Use CommandOptions.Timeout instead.
Timeout time.Duration
// The additional options to be passed to the underlying git.
CommandOptions
Expand Down Expand Up @@ -663,6 +689,8 @@ func (r *Repository) CountObjects(opts ...CountObjectsOptions) (*CountObject, er
type FsckOptions struct {
// The timeout duration before giving up for each shell command execution. The
// default timeout duration will be used when not supplied.
//
// Deprecated: Use CommandOptions.Timeout instead.
Timeout time.Duration
// The additional options to be passed to the underlying git.
CommandOptions
Expand Down
2 changes: 2 additions & 0 deletions repo_blame.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import (
type BlameOptions struct {
// The timeout duration before giving up for each shell command execution. The
// default timeout duration will be used when not supplied.
//
// Deprecated: Use CommandOptions.Timeout instead.
Timeout time.Duration
// The additional options to be passed to the underlying git.
CommandOptions
Expand Down
2 changes: 2 additions & 0 deletions repo_blob.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import "time"
type CatFileBlobOptions struct {
// The timeout duration before giving up for each shell command execution.
// The default timeout duration will be used when not supplied.
//
// Deprecated: Use CommandOptions.Timeout instead.
Timeout time.Duration
// The additional options to be passed to the underlying git.
CommandOptions
Expand Down
2 changes: 2 additions & 0 deletions repo_commit.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ loop:
type CatFileCommitOptions struct {
// The timeout duration before giving up for each shell command execution.
// The default timeout duration will be used when not supplied.
//
// Deprecated: Use CommandOptions.Timeout instead.
Timeout time.Duration
// The additional options to be passed to the underlying git.
CommandOptions
Expand Down
Loading