Skip to content

Commit dc8ceda

Browse files
authored
refactor: replace Timeout options with context.Context (#126)
1 parent f2b89e0 commit dc8ceda

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+821
-894
lines changed

blob.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package git
66

77
import (
88
"bytes"
9+
"context"
910
"io"
1011
)
1112

@@ -16,21 +17,23 @@ type Blob struct {
1617

1718
// Bytes reads and returns the content of the blob all at once in bytes. This
1819
// can be very slow and memory consuming for huge content.
19-
func (b *Blob) Bytes() ([]byte, error) {
20+
func (b *Blob) Bytes(ctx context.Context) ([]byte, error) {
2021
stdout := new(bytes.Buffer)
2122
stderr := new(bytes.Buffer)
2223

2324
// Preallocate memory to save ~50% memory usage on big files.
24-
stdout.Grow(int(b.Size()))
25+
if size := b.Size(ctx); size > 0 && size < int64(^uint(0)>>1) {
26+
stdout.Grow(int(size))
27+
}
2528

26-
if err := b.Pipeline(stdout, stderr); err != nil {
29+
if err := b.Pipeline(ctx, stdout, stderr); err != nil {
2730
return nil, concatenateError(err, stderr.String())
2831
}
2932
return stdout.Bytes(), nil
3033
}
3134

3235
// Pipeline reads the content of the blob and pipes stdout and stderr to
3336
// supplied io.Writer.
34-
func (b *Blob) Pipeline(stdout, stderr io.Writer) error {
35-
return NewCommand("show", b.id.String()).RunInDirPipeline(stdout, stderr, b.parent.repo.path)
37+
func (b *Blob) Pipeline(ctx context.Context, stdout, stderr io.Writer) error {
38+
return NewCommand(ctx, "show", b.id.String()).RunInDirPipeline(stdout, stderr, b.parent.repo.path)
3639
}

blob_test.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ package git
66

77
import (
88
"bytes"
9+
"context"
910
"testing"
1011

1112
"github.com/stretchr/testify/assert"
1213
)
1314

1415
func TestBlob(t *testing.T) {
16+
ctx := context.Background()
1517
expOutput := `This is a sample project students can use during Matthew's Git class.
1618
1719
Here is an addition by me
@@ -41,14 +43,14 @@ This demo also includes an image with changes on a branch for examination of ima
4143
}
4244

4345
t.Run("get data all at once", func(t *testing.T) {
44-
p, err := blob.Bytes()
46+
p, err := blob.Bytes(ctx)
4547
assert.Nil(t, err)
4648
assert.Equal(t, expOutput, string(p))
4749
})
4850

4951
t.Run("get data with pipeline", func(t *testing.T) {
5052
stdout := new(bytes.Buffer)
51-
err := blob.Pipeline(stdout, nil)
53+
err := blob.Pipeline(ctx, stdout, nil)
5254
assert.Nil(t, err)
5355
assert.Equal(t, expOutput, stdout.String())
5456
})

command.go

Lines changed: 49 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,16 @@ import (
1717

1818
// Command contains the name, arguments and environment variables of a command.
1919
type Command struct {
20-
name string
21-
args []string
22-
envs []string
23-
timeout time.Duration
24-
ctx context.Context
20+
name string
21+
args []string
22+
envs []string
23+
ctx context.Context
2524
}
2625

2726
// CommandOptions contains options for running a command.
28-
// If timeout is zero, DefaultTimeout will be used.
29-
// If timeout is less than zero, no timeout will be set.
30-
// If context is nil, context.Background() will be used.
3127
type CommandOptions struct {
32-
Args []string
33-
Envs []string
34-
Timeout time.Duration
35-
Context context.Context
28+
Args []string
29+
Envs []string
3630
}
3731

3832
// String returns the string representation of the command.
@@ -44,13 +38,7 @@ func (c *Command) String() string {
4438
}
4539

4640
// NewCommand creates and returns a new Command with given arguments for "git".
47-
func NewCommand(args ...string) *Command {
48-
return NewCommandWithContext(context.Background(), args...)
49-
}
50-
51-
// NewCommandWithContext creates and returns a new Command with given arguments
52-
// and context for "git".
53-
func NewCommandWithContext(ctx context.Context, args ...string) *Command {
41+
func NewCommand(ctx context.Context, args ...string) *Command {
5442
return &Command{
5543
name: "git",
5644
args: args,
@@ -76,23 +64,9 @@ func (c Command) WithContext(ctx context.Context) *Command {
7664
return &c
7765
}
7866

79-
// WithTimeout returns a new Command with given timeout.
80-
func (c Command) WithTimeout(timeout time.Duration) *Command {
81-
c.timeout = timeout
82-
return &c
83-
}
84-
85-
// SetTimeout sets the timeout for the command.
86-
func (c *Command) SetTimeout(timeout time.Duration) {
87-
c.timeout = timeout
88-
}
89-
9067
// AddOptions adds options to the command.
91-
// Note: only the last option will take effect if there are duplicated options.
9268
func (c *Command) AddOptions(opts ...CommandOptions) *Command {
9369
for _, opt := range opts {
94-
c.timeout = opt.Timeout
95-
c.ctx = opt.Context
9670
c.AddArgs(opt.Args...)
9771
c.AddEnvs(opt.Envs...)
9872
}
@@ -105,7 +79,8 @@ func (c *Command) AddCommitter(committer *Signature) *Command {
10579
return c
10680
}
10781

108-
// DefaultTimeout is the default timeout duration for all commands.
82+
// DefaultTimeout is the default timeout duration for all commands. It is
83+
// applied when the context does not already have a deadline.
10984
const DefaultTimeout = time.Minute
11085

11186
// A limitDualWriter writes to W but limits the amount of data written to just N
@@ -144,33 +119,19 @@ type RunInDirOptions struct {
144119
Stdout io.Writer
145120
// Stderr is the error output from the command.
146121
Stderr io.Writer
147-
// Timeout is the duration to wait before timing out.
148-
//
149-
// Deprecated: Use CommandOptions.Timeout or *Command.WithTimeout instead.
150-
Timeout time.Duration
151122
}
152123

153124
// RunInDirWithOptions executes the command in given directory and options. It
154125
// pipes stdin from supplied io.Reader, and pipes stdout and stderr to supplied
155-
// io.Writer. DefaultTimeout will be used if the timeout duration is less than
156-
// time.Nanosecond (i.e. less than or equal to 0). It returns an ErrExecTimeout
157-
// if the execution was timed out.
126+
// io.Writer. If the command's context does not have a deadline, DefaultTimeout
127+
// will be applied automatically. It returns an ErrExecTimeout if the execution
128+
// was timed out.
158129
func (c *Command) RunInDirWithOptions(dir string, opts ...RunInDirOptions) (err error) {
159130
var opt RunInDirOptions
160131
if len(opts) > 0 {
161132
opt = opts[0]
162133
}
163134

164-
timeout := c.timeout
165-
// TODO: remove this in newer version
166-
if opt.Timeout > 0 {
167-
timeout = opt.Timeout
168-
}
169-
170-
if timeout == 0 {
171-
timeout = DefaultTimeout
172-
}
173-
174135
buf := new(bytes.Buffer)
175136
w := opt.Stdout
176137
if logOutput != nil {
@@ -184,26 +145,22 @@ func (c *Command) RunInDirWithOptions(dir string, opts ...RunInDirOptions) (err
184145

185146
defer func() {
186147
if len(dir) == 0 {
187-
log("[timeout: %v] %s\n%s", timeout, c, buf.Bytes())
148+
log("%s\n%s", c, buf.Bytes())
188149
} else {
189-
log("[timeout: %v] %s: %s\n%s", timeout, dir, c, buf.Bytes())
150+
log("%s: %s\n%s", dir, c, buf.Bytes())
190151
}
191152
}()
192153

193-
ctx := context.Background()
194-
if c.ctx != nil {
195-
ctx = c.ctx
154+
ctx := c.ctx
155+
if ctx == nil {
156+
ctx = context.Background()
196157
}
197158

198-
if timeout > 0 {
159+
// Apply default timeout if the context doesn't already have a deadline.
160+
if _, ok := ctx.Deadline(); !ok {
199161
var cancel context.CancelFunc
200-
ctx, cancel = context.WithTimeout(ctx, timeout)
201-
defer func() {
202-
cancel()
203-
if err == context.DeadlineExceeded {
204-
err = ErrExecTimeout
205-
}
206-
}()
162+
ctx, cancel = context.WithTimeout(ctx, DefaultTimeout)
163+
defer cancel()
207164
}
208165

209166
cmd := exec.CommandContext(ctx, c.name, c.args...)
@@ -215,6 +172,11 @@ func (c *Command) RunInDirWithOptions(dir string, opts ...RunInDirOptions) (err
215172
cmd.Stdout = w
216173
cmd.Stderr = opt.Stderr
217174
if err = cmd.Start(); err != nil {
175+
if ctx.Err() == context.DeadlineExceeded {
176+
return ErrExecTimeout
177+
} else if ctx.Err() != nil {
178+
return ctx.Err()
179+
}
218180
return err
219181
}
220182

@@ -225,22 +187,33 @@ func (c *Command) RunInDirWithOptions(dir string, opts ...RunInDirOptions) (err
225187

226188
select {
227189
case <-ctx.Done():
228-
<-result
229-
if cmd.Process != nil && cmd.ProcessState != nil && !cmd.ProcessState.Exited() {
230-
if err := cmd.Process.Kill(); err != nil {
231-
return fmt.Errorf("kill process: %v", err)
232-
}
190+
// Kill the process before waiting so cancellation is enforced promptly.
191+
if cmd.Process != nil {
192+
_ = cmd.Process.Kill()
233193
}
194+
<-result
234195

235-
return ErrExecTimeout
196+
if ctx.Err() == context.DeadlineExceeded {
197+
return ErrExecTimeout
198+
}
199+
return ctx.Err()
236200
case err = <-result:
201+
// Normalize errors when the context may have expired around the same time.
202+
if err != nil {
203+
if ctxErr := ctx.Err(); ctxErr != nil {
204+
if ctxErr == context.DeadlineExceeded {
205+
return ErrExecTimeout
206+
}
207+
return ctxErr
208+
}
209+
}
237210
return err
238211
}
239212

240213
}
241214

242-
// RunInDirPipeline executes the command in given directory and default timeout
243-
// duration. It pipes stdout and stderr to supplied io.Writer.
215+
// RunInDirPipeline executes the command in given directory. It pipes stdout and
216+
// stderr to supplied io.Writer.
244217
func (c *Command) RunInDirPipeline(stdout, stderr io.Writer, dir string) error {
245218
return c.RunInDirWithOptions(dir, RunInDirOptions{
246219
Stdin: nil,
@@ -249,35 +222,8 @@ func (c *Command) RunInDirPipeline(stdout, stderr io.Writer, dir string) error {
249222
})
250223
}
251224

252-
// RunInDirPipelineWithTimeout executes the command in given directory and
253-
// timeout duration. It pipes stdout and stderr to supplied io.Writer.
254-
// DefaultTimeout will be used if the timeout duration is less than
255-
// time.Nanosecond (i.e. less than or equal to 0). It returns an ErrExecTimeout
256-
// if the execution was timed out.
257-
//
258-
// Deprecated: Use RunInDirPipeline and CommandOptions instead.
259-
// TODO: remove this in the next major version
260-
func (c *Command) RunInDirPipelineWithTimeout(timeout time.Duration, stdout, stderr io.Writer, dir string) (err error) {
261-
if timeout != 0 {
262-
c = c.WithTimeout(timeout)
263-
}
264-
return c.RunInDirPipeline(stdout, stderr, dir)
265-
}
266-
267-
// RunInDirWithTimeout executes the command in given directory and timeout
268-
// duration. It returns stdout in []byte and error (combined with stderr).
269-
//
270-
// Deprecated: Use RunInDir and CommandOptions instead.
271-
// TODO: remove this in the next major version
272-
func (c *Command) RunInDirWithTimeout(timeout time.Duration, dir string) ([]byte, error) {
273-
if timeout != 0 {
274-
c = c.WithTimeout(timeout)
275-
}
276-
return c.RunInDir(dir)
277-
}
278-
279-
// RunInDir executes the command in given directory and default timeout
280-
// duration. It returns stdout and error (combined with stderr).
225+
// RunInDir executes the command in given directory. It returns stdout and error
226+
// (combined with stderr).
281227
func (c *Command) RunInDir(dir string) ([]byte, error) {
282228
stdout := new(bytes.Buffer)
283229
stderr := new(bytes.Buffer)
@@ -287,20 +233,8 @@ func (c *Command) RunInDir(dir string) ([]byte, error) {
287233
return stdout.Bytes(), nil
288234
}
289235

290-
// RunWithTimeout executes the command in working directory and given timeout
291-
// duration. It returns stdout in string and error (combined with stderr).
292-
//
293-
// Deprecated: Use RunInDir and CommandOptions instead.
294-
// TODO: remove this in the next major version
295-
func (c *Command) RunWithTimeout(timeout time.Duration) ([]byte, error) {
296-
if timeout != 0 {
297-
c = c.WithTimeout(timeout)
298-
}
299-
return c.Run()
300-
}
301-
302-
// Run executes the command in working directory and default timeout duration.
303-
// It returns stdout in string and error (combined with stderr).
236+
// Run executes the command in working directory. It returns stdout and
237+
// error (combined with stderr).
304238
func (c *Command) Run() ([]byte, error) {
305239
stdout, err := c.RunInDir("")
306240
if err != nil {

0 commit comments

Comments
 (0)