diff --git a/blob.go b/blob.go index 399171b4..843e1dbd 100644 --- a/blob.go +++ b/blob.go @@ -15,25 +15,23 @@ type Blob struct { *TreeEntry } -// Bytes reads and returns the content of the blob all at once in bytes. This -// can be very slow and memory consuming for huge content. +// Bytes reads and returns the content of the blob all at once in bytes. This can +// be very slow and memory consuming for huge content. func (b *Blob) Bytes(ctx context.Context) ([]byte, error) { stdout := new(bytes.Buffer) - stderr := new(bytes.Buffer) // Preallocate memory to save ~50% memory usage on big files. if size := b.Size(ctx); size > 0 && size < int64(^uint(0)>>1) { stdout.Grow(int(size)) } - if err := b.Pipeline(ctx, stdout, stderr); err != nil { - return nil, concatenateError(err, stderr.String()) + if err := b.Pipe(ctx, stdout); err != nil { + return nil, err } return stdout.Bytes(), nil } -// Pipeline reads the content of the blob and pipes stdout and stderr to -// supplied io.Writer. -func (b *Blob) Pipeline(ctx context.Context, stdout, stderr io.Writer) error { - return NewCommand(ctx, "show", b.id.String()).RunInDirPipeline(stdout, stderr, b.parent.repo.path) +// Pipe reads the content of the blob and pipes stdout to the supplied io.Writer. +func (b *Blob) Pipe(ctx context.Context, stdout io.Writer) error { + return pipe(ctx, b.parent.repo.path, []string{"show", "--end-of-options", b.id.String()}, nil, stdout) } diff --git a/blob_test.go b/blob_test.go index 6ccc46ee..743c3ed0 100644 --- a/blob_test.go +++ b/blob_test.go @@ -48,9 +48,9 @@ This demo also includes an image with changes on a branch for examination of ima assert.Equal(t, expOutput, string(p)) }) - t.Run("get data with pipeline", func(t *testing.T) { + t.Run("get data with pipe", func(t *testing.T) { stdout := new(bytes.Buffer) - err := blob.Pipeline(ctx, stdout, nil) + err := blob.Pipe(ctx, stdout) assert.Nil(t, err) assert.Equal(t, expOutput, stdout.String()) }) diff --git a/command.go b/command.go index 54e0094f..88a5538b 100644 --- a/command.go +++ b/command.go @@ -7,81 +7,155 @@ package git import ( "bytes" "context" - "fmt" + "errors" "io" "os" - "os/exec" + "strconv" "strings" "time" -) -// Command contains the name, arguments and environment variables of a command. -type Command struct { - name string - args []string - envs []string - ctx context.Context -} + "github.com/sourcegraph/run" +) -// CommandOptions contains options for running a command. +// CommandOptions contains additional options for running a Git command. type CommandOptions struct { - Args []string Envs []string } -// String returns the string representation of the command. -func (c *Command) String() string { - if len(c.args) == 0 { - return c.name +// DefaultTimeout is the default timeout duration for all commands. It is +// applied when the context does not already have a deadline. +const DefaultTimeout = time.Minute + +// cmd builds a *run.Command for git with the given arguments, environment +// variables and working directory. DefaultTimeout will be applied if the context +// does not already have a deadline. +func cmd(ctx context.Context, dir string, args []string, envs []string) (*run.Command, context.CancelFunc) { + cancel := func() {} + if _, ok := ctx.Deadline(); !ok { + var timeoutCancel context.CancelFunc + ctx, timeoutCancel = context.WithTimeout(ctx, DefaultTimeout) + cancel = timeoutCancel } - return fmt.Sprintf("%s %s", c.name, strings.Join(c.args, " ")) -} -// NewCommand creates and returns a new Command with given arguments for "git". -func NewCommand(ctx context.Context, args ...string) *Command { - return &Command{ - name: "git", - args: args, - ctx: ctx, + // run.Cmd joins all parts into a single string and then shell-parses it. We must + // quote each argument so that special characters (spaces, quotes, angle + // brackets, etc.) are preserved correctly. + parts := make([]string, 0, 1+len(args)) + parts = append(parts, "git") + for _, arg := range args { + parts = append(parts, run.Arg(arg)) } -} -// AddArgs appends given arguments to the command. -func (c *Command) AddArgs(args ...string) *Command { - c.args = append(c.args, args...) - return c + c := run.Cmd(ctx, parts...) + if dir != "" { + c = c.Dir(dir) + } + if len(envs) > 0 { + c = c.Environ(append(os.Environ(), envs...)) + } + return c, cancel } -// AddEnvs appends given environment variables to the command. -func (c *Command) AddEnvs(envs ...string) *Command { - c.envs = append(c.envs, envs...) - return c -} +// exec executes a git command in the given directory and returns stdout as +// bytes. Stderr is included in the error message on failure. DefaultTimeout will +// be applied if the context does not already have a deadline. It returns +// ErrExecTimeout if the execution was timed out. +func exec(ctx context.Context, dir string, args []string, envs []string) ([]byte, error) { + c, cancel := cmd(ctx, dir, args, envs) + defer cancel() + + var logBuf *bytes.Buffer + if logOutput != nil { + logBuf = new(bytes.Buffer) + logBuf.Grow(512) + defer func() { + log(dir, args, logBuf.Bytes()) + }() + } + + // Use Stream to a buffer to preserve raw bytes (including NUL bytes from + // commands like "ls-tree -z"). The String/Lines methods process output + // line-by-line which corrupts binary-ish output. + stdout := new(bytes.Buffer) + err := c.StdOut().Run().Stream(stdout) + + // Capture (partial) stdout for logging even on error, so failed commands produce + // a useful log entry rather than an empty one. + if logOutput != nil { + data := stdout.Bytes() + limit := len(data) + if limit > 512 { + limit = 512 + } + logBuf.Write(data[:limit]) + if len(data) > 512 { + logBuf.WriteString("... (more omitted)") + } + } -// WithContext returns a new Command with the given context. -func (c Command) WithContext(ctx context.Context) *Command { - c.ctx = ctx - return &c + if err != nil { + return nil, mapContextError(err, ctx) + } + return stdout.Bytes(), nil } -// AddOptions adds options to the command. -func (c *Command) AddOptions(opts ...CommandOptions) *Command { - for _, opt := range opts { - c.AddArgs(opt.Args...) - c.AddEnvs(opt.Envs...) +// pipe executes a git command in the given directory, streaming stdout to the +// given io.Writer. +func pipe(ctx context.Context, dir string, args []string, envs []string, stdout io.Writer) error { + c, cancel := cmd(ctx, dir, args, envs) + defer cancel() + + var buf *bytes.Buffer + w := stdout + if logOutput != nil { + buf = new(bytes.Buffer) + buf.Grow(512) + w = &limitDualWriter{ + W: buf, + N: int64(buf.Cap()), + w: stdout, + } + + defer func() { + log(dir, args, buf.Bytes()) + }() } - return c + + streamErr := c.StdOut().Run().Stream(w) + if streamErr != nil { + return mapContextError(streamErr, ctx) + } + return nil } -// AddCommitter appends given committer to the command. -func (c *Command) AddCommitter(committer *Signature) *Command { - c.AddEnvs("GIT_COMMITTER_NAME="+committer.Name, "GIT_COMMITTER_EMAIL="+committer.Email) - return c +// committerEnvs returns environment variables for setting the Git committer. +func committerEnvs(committer *Signature) []string { + return []string{ + "GIT_COMMITTER_NAME=" + committer.Name, + "GIT_COMMITTER_EMAIL=" + committer.Email, + } } -// DefaultTimeout is the default timeout duration for all commands. It is -// applied when the context does not already have a deadline. -const DefaultTimeout = time.Minute +// log logs a git command execution with its output. +func log(dir string, args []string, output []byte) { + cmdStr := "git" + if len(args) > 0 { + quoted := make([]string, len(args)) + for i, a := range args { + if strings.ContainsAny(a, " \t\n\"'\\<>") { + quoted[i] = strconv.Quote(a) + } else { + quoted[i] = a + } + } + cmdStr = "git " + strings.Join(quoted, " ") + } + if len(dir) == 0 { + logf("%s\n%s", cmdStr, output) + } else { + logf("%s: %s\n%s", dir, cmdStr, output) + } +} // A limitDualWriter writes to W but limits the amount of data written to just N // bytes. On the other hand, it passes everything to w. @@ -111,134 +185,25 @@ func (w *limitDualWriter) Write(p []byte) (int, error) { return w.w.Write(p) } -// RunInDirOptions contains options for running a command in a directory. -type RunInDirOptions struct { - // Stdin is the input to the command. - Stdin io.Reader - // Stdout is the outputs from the command. - Stdout io.Writer - // Stderr is the error output from the command. - Stderr io.Writer -} - -// RunInDirWithOptions executes the command in given directory and options. It -// pipes stdin from supplied io.Reader, and pipes stdout and stderr to supplied -// io.Writer. If the command's context does not have a deadline, DefaultTimeout -// will be applied automatically. It returns an ErrExecTimeout if the execution -// was timed out. -func (c *Command) RunInDirWithOptions(dir string, opts ...RunInDirOptions) (err error) { - var opt RunInDirOptions - if len(opts) > 0 { - opt = opts[0] - } - - buf := new(bytes.Buffer) - w := opt.Stdout - if logOutput != nil { - buf.Grow(512) - w = &limitDualWriter{ - W: buf, - N: int64(buf.Cap()), - w: opt.Stdout, - } - } - - defer func() { - if len(dir) == 0 { - log("%s\n%s", c, buf.Bytes()) - } else { - log("%s: %s\n%s", dir, c, buf.Bytes()) - } - }() - - ctx := c.ctx +// mapContextError maps context errors to the appropriate sentinel errors used +// by this package. +func mapContextError(err error, ctx context.Context) error { if ctx == nil { - ctx = context.Background() - } - - // Apply default timeout if the context doesn't already have a deadline. - if _, ok := ctx.Deadline(); !ok { - var cancel context.CancelFunc - ctx, cancel = context.WithTimeout(ctx, DefaultTimeout) - defer cancel() - } - - cmd := exec.CommandContext(ctx, c.name, c.args...) - if len(c.envs) > 0 { - cmd.Env = append(os.Environ(), c.envs...) - } - cmd.Dir = dir - cmd.Stdin = opt.Stdin - cmd.Stdout = w - cmd.Stderr = opt.Stderr - if err = cmd.Start(); err != nil { - if ctx.Err() == context.DeadlineExceeded { - return ErrExecTimeout - } else if ctx.Err() != nil { - return ctx.Err() - } return err } - - result := make(chan error) - go func() { - result <- cmd.Wait() - }() - - select { - case <-ctx.Done(): - // Kill the process before waiting so cancellation is enforced promptly. - if cmd.Process != nil { - _ = cmd.Process.Kill() - } - <-result - - if ctx.Err() == context.DeadlineExceeded { + if ctxErr := ctx.Err(); ctxErr != nil { + if errors.Is(ctxErr, context.DeadlineExceeded) { return ErrExecTimeout } - return ctx.Err() - case err = <-result: - // Normalize errors when the context may have expired around the same time. - if err != nil { - if ctxErr := ctx.Err(); ctxErr != nil { - if ctxErr == context.DeadlineExceeded { - return ErrExecTimeout - } - return ctxErr - } - } - return err + return ctxErr } - -} - -// RunInDirPipeline executes the command in given directory. 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, - }) -} - -// RunInDir executes the command in given directory. It returns stdout and error -// (combined with stderr). -func (c *Command) RunInDir(dir string) ([]byte, error) { - 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 + return err } -// Run executes the command in working directory. It returns stdout and -// error (combined with stderr). -func (c *Command) Run() ([]byte, error) { - stdout, err := c.RunInDir("") - if err != nil { - return nil, err - } - return stdout, nil +// isExitStatus reports whether err represents a specific process exit status +// code, using the run.ExitCoder interface provided by sourcegraph/run. +func isExitStatus(err error, code int) bool { + var exitCoder run.ExitCoder + ok := errors.As(err, &exitCoder) + return ok && exitCoder.ExitCode() == code } diff --git a/command_test.go b/command_test.go index e2fb26f5..4c02835d 100644 --- a/command_test.go +++ b/command_test.go @@ -13,63 +13,12 @@ import ( "github.com/stretchr/testify/assert" ) -func TestCommand_String(t *testing.T) { - ctx := context.Background() - tests := []struct { - name string - args []string - expStr string - }{ - { - name: "no args", - args: nil, - expStr: "git", - }, - { - name: "has one arg", - args: []string{"version"}, - expStr: "git version", - }, - { - name: "has more args", - args: []string{"config", "--global", "http.proxy", "http://localhost:8080"}, - expStr: "git config --global http.proxy http://localhost:8080", - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - cmd := NewCommand(ctx, test.args...) - assert.Equal(t, test.expStr, cmd.String()) - }) - } -} - -func TestCommand_AddArgs(t *testing.T) { - ctx := context.Background() - cmd := NewCommand(ctx) - assert.Equal(t, []string(nil), cmd.args) - - cmd.AddArgs("push") - cmd.AddArgs("origin", "master") - assert.Equal(t, []string{"push", "origin", "master"}, cmd.args) -} - -func TestCommand_AddEnvs(t *testing.T) { - ctx := context.Background() - cmd := NewCommand(ctx) - assert.Equal(t, []string(nil), cmd.envs) - - cmd.AddEnvs("GIT_DIR=/tmp") - cmd.AddEnvs("HOME=/Users/unknwon", "GIT_EDITOR=code") - assert.Equal(t, []string{"GIT_DIR=/tmp", "HOME=/Users/unknwon", "GIT_EDITOR=code"}, cmd.envs) -} - -func TestCommand_RunWithContextTimeout(t *testing.T) { +func TestExec_ContextTimeout(t *testing.T) { t.Run("context already expired before start", func(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Nanosecond) defer cancel() time.Sleep(time.Millisecond) // ensure deadline has passed - _, err := NewCommand(ctx, "version").Run() + _, err := exec(ctx, "", []string{"version"}, nil) assert.Equal(t, ErrExecTimeout, err) }) @@ -77,21 +26,21 @@ func TestCommand_RunWithContextTimeout(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) defer cancel() - // Use a blocking reader so the command starts successfully and blocks - // reading stdin until the context deadline fires. - err := NewCommand(ctx, "hash-object", "--stdin").RunInDirWithOptions("", RunInDirOptions{ - Stdin: blockingReader{cancel: ctx.Done()}, - Stdout: io.Discard, - Stderr: io.Discard, - }) + // Use cmd directly with a blocking stdin so the command starts successfully and + // blocks reading until the context deadline fires. + c, timeoutCancel := cmd(ctx, "", []string{"hash-object", "--stdin"}, nil) + defer timeoutCancel() + + err := c.Input(blockingReader{cancel: ctx.Done()}).StdOut().Run().Stream(io.Discard) + err = mapContextError(err, ctx) assert.Equal(t, ErrExecTimeout, err) }) } -// blockingReader is an io.Reader that blocks until its cancel channel is -// closed, simulating a stdin that never provides data. When cancelled it -// returns io.EOF so that exec's stdin copy goroutine can exit cleanly, -// allowing cmd.Wait() to return. +// blockingReader is an io.Reader that blocks until its cancel channel is closed, +// simulating a stdin that never provides data. When canceled it returns io.EOF +// so that the stdin copy goroutine can exit cleanly, allowing cmd.Wait() to +// return. type blockingReader struct { cancel <-chan struct{} } @@ -101,11 +50,11 @@ func (r blockingReader) Read(p []byte) (int, error) { return 0, io.EOF } -func TestCommand_RunWithContextCancellation(t *testing.T) { +func TestCmd_ContextCancellation(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) - // Cancel in the background after a short delay so the command is already - // running when cancellation arrives. Close done to unblock the reader. + // Cancel in the background after a short delay so the command is already running + // when cancellation arrives. Close done to unblock the reader. done := make(chan struct{}) go func() { time.Sleep(50 * time.Millisecond) @@ -113,22 +62,22 @@ func TestCommand_RunWithContextCancellation(t *testing.T) { close(done) }() - err := NewCommand(ctx, "hash-object", "--stdin").RunInDirWithOptions("", RunInDirOptions{ - Stdin: blockingReader{cancel: done}, - Stdout: io.Discard, - Stderr: io.Discard, - }) + c, timeoutCancel := cmd(ctx, "", []string{"hash-object", "--stdin"}, nil) + defer timeoutCancel() + + err := c.Input(blockingReader{cancel: done}).StdOut().Run().Stream(io.Discard) + err = mapContextError(err, ctx) assert.ErrorIs(t, err, context.Canceled) // Must NOT be ErrExecTimeout — cancellation is distinct from deadline. assert.NotEqual(t, ErrExecTimeout, err) } -func TestCommand_DefaultTimeoutApplied(t *testing.T) { - // A plain context.Background() has no deadline. The command should still - // succeed because DefaultTimeout (1 min) is applied automatically and - // "git version" completes well within that. +func TestExec_DefaultTimeoutApplied(t *testing.T) { + // A plain context.Background() has no deadline. The command should still succeed + // because DefaultTimeout is applied automatically and "git version" completes + // well within that. ctx := context.Background() - stdout, err := NewCommand(ctx, "version").Run() + stdout, err := exec(ctx, "", []string{"version"}, nil) assert.NoError(t, err) assert.Contains(t, string(stdout), "git version") } diff --git a/commit.go b/commit.go index 381734e2..134f39fb 100644 --- a/commit.go +++ b/commit.go @@ -154,7 +154,7 @@ func (c *Commit) isImageFile(ctx context.Context, blob *Blob, err error) (bool, N: int64(buf.Cap()), } - err = blob.Pipeline(ctx, stdout, io.Discard) + err = blob.Pipe(ctx, stdout) if err != nil { return false, err } diff --git a/commit_archive.go b/commit_archive.go index 535f6f82..5daba32b 100644 --- a/commit_archive.go +++ b/commit_archive.go @@ -19,14 +19,20 @@ const ( ArchiveTarGz ArchiveFormat = "tar.gz" ) -// CreateArchive creates given format of archive to the destination. -func (c *Commit) CreateArchive(ctx context.Context, format ArchiveFormat, dst string) error { +// Archive creates given format of archive to the destination. +func (c *Commit) Archive(ctx context.Context, format ArchiveFormat, dst string) error { prefix := filepath.Base(strings.TrimSuffix(c.repo.path, ".git")) + "/" - _, err := NewCommand(ctx, "archive", - "--prefix="+prefix, - "--format="+string(format), - "-o", dst, - c.ID.String(), - ).RunInDir(c.repo.path) + _, err := exec(ctx, + c.repo.path, + []string{ + "archive", + "--prefix=" + prefix, + "--format=" + string(format), + "-o", dst, + "--end-of-options", + c.ID.String(), + }, + nil, + ) return err } diff --git a/commit_archive_test.go b/commit_archive_test.go index 2523c73c..892859c8 100644 --- a/commit_archive_test.go +++ b/commit_archive_test.go @@ -19,7 +19,7 @@ func tempPath() string { return filepath.Join(os.TempDir(), strconv.Itoa(int(time.Now().UnixNano()))) } -func TestCommit_CreateArchive(t *testing.T) { +func TestCommit_Archive(t *testing.T) { ctx := context.Background() for _, format := range []ArchiveFormat{ ArchiveZip, @@ -36,7 +36,7 @@ func TestCommit_CreateArchive(t *testing.T) { _ = os.Remove(dst) }() - assert.Nil(t, c.CreateArchive(ctx, format, dst)) + assert.Nil(t, c.Archive(ctx, format, dst)) }) } } diff --git a/git.go b/git.go index ebbfdfd7..e96ab1e9 100644 --- a/git.go +++ b/git.go @@ -29,7 +29,7 @@ func SetPrefix(prefix string) { logPrefix = prefix } -func log(format string, args ...interface{}) { +func logf(format string, args ...interface{}) { if logOutput == nil { return } @@ -58,7 +58,7 @@ func BinVersion(ctx context.Context) (string, error) { return gitVersion, nil } - stdout, err := NewCommand(ctx, "version").Run() + stdout, err := exec(ctx, "", []string{"version"}, nil) if err != nil { return "", err } diff --git a/git_test.go b/git_test.go index 5c0e0df9..c4af3655 100644 --- a/git_test.go +++ b/git_test.go @@ -83,7 +83,7 @@ func Test_log(t *testing.T) { var buf bytes.Buffer SetOutput(&buf) - log(test.format, test.args...) + logf(test.format, test.args...) assert.Equal(t, test.expOutput, buf.String()) }) } diff --git a/go.mod b/go.mod index 0753589a..52cd9060 100644 --- a/go.mod +++ b/go.mod @@ -2,10 +2,23 @@ module github.com/gogs/git-module/v2 go 1.26.0 -require github.com/stretchr/testify v1.11.1 +require ( + github.com/sourcegraph/run v0.12.0 + github.com/stretchr/testify v1.11.1 +) require ( + bitbucket.org/creachadair/shell v0.0.7 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/djherbis/buffer v1.2.0 // indirect + github.com/djherbis/nio/v3 v3.0.1 // indirect + github.com/go-logr/logr v1.2.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/itchyny/gojq v0.12.11 // indirect + github.com/itchyny/timefmt-go v0.1.5 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + go.bobheadxi.dev/streamline v1.2.1 // indirect + go.opentelemetry.io/otel v1.11.0 // indirect + go.opentelemetry.io/otel/trace v1.11.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index c4c1710c..2957c960 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,70 @@ +bitbucket.org/creachadair/shell v0.0.7 h1:Z96pB6DkSb7F3Y3BBnJeOZH2gazyMTWlvecSD4vDqfk= +bitbucket.org/creachadair/shell v0.0.7/go.mod h1:oqtXSSvSYr4624lnnabXHaBsYW6RD80caLi2b3hJk0U= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/djherbis/buffer v1.1.0/go.mod h1:VwN8VdFkMY0DCALdY8o00d3IZ6Amz/UNVMWcSaJT44o= +github.com/djherbis/buffer v1.2.0 h1:PH5Dd2ss0C7CRRhQCZ2u7MssF+No9ide8Ye71nPHcrQ= +github.com/djherbis/buffer v1.2.0/go.mod h1:fjnebbZjCUpPinBRD+TDwXSOeNQ7fPQWLfGQqiAiUyE= +github.com/djherbis/nio/v3 v3.0.1 h1:6wxhnuppteMa6RHA4L81Dq7ThkZH8SwnDzXDYy95vB4= +github.com/djherbis/nio/v3 v3.0.1/go.mod h1:Ng4h80pbZFMla1yKzm61cF0tqqilXZYrogmWgZxOcmg= +github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= +github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/hexops/autogold v1.3.1 h1:YgxF9OHWbEIUjhDbpnLhgVsjUDsiHDTyDfy2lrfdlzo= +github.com/hexops/autogold v1.3.1/go.mod h1:sQO+mQUCVfxOKPht+ipDSkJ2SCJ7BNJVHZexsXqWMx4= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/hexops/valast v1.4.3 h1:oBoGERMJh6UZdRc6cduE1CTPK+VAdXA59Y1HFgu3sm0= +github.com/hexops/valast v1.4.3/go.mod h1:Iqx2kLj3Jn47wuXpj3wX40xn6F93QNFBHuiKBerkTGA= +github.com/itchyny/gojq v0.12.11 h1:YhLueoHhHiN4mkfM+3AyJV6EPcCxKZsOnYf+aVSwaQw= +github.com/itchyny/gojq v0.12.11/go.mod h1:o3FT8Gkbg/geT4pLI0tF3hvip5F3Y/uskjRz9OYa38g= +github.com/itchyny/timefmt-go v0.1.5 h1:G0INE2la8S6ru/ZI5JecgyzbbJNs5lG1RcBqa7Jm6GE= +github.com/itchyny/timefmt-go v0.1.5/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/nightlyone/lockfile v1.0.0 h1:RHep2cFKK4PonZJDdEl4GmkabuhbsRMgk/k3uAmxBiA= +github.com/nightlyone/lockfile v1.0.0/go.mod h1:rywoIealpdNse2r832aiD9jRk8ErCatROs6LzC841CI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/sourcegraph/run v0.12.0 h1:3A8w5e8HIYPfafHekvmdmmh42RHKGVhmiTZAPJclg7I= +github.com/sourcegraph/run v0.12.0/go.mod h1:PwaP936BTnAJC1cqR5rSbG5kOs/EWStTK3lqvMX5GUA= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.bobheadxi.dev/streamline v1.2.1 h1:IqKSA1TbeuDqCzYNAwtlh8sqf3tsQus8XgJdkCWFT8c= +go.bobheadxi.dev/streamline v1.2.1/go.mod h1:yJsVXOSBFLgAKvsnf6WmIzmB2A65nWqkR/sRNxJPa74= +go.opentelemetry.io/otel v1.11.0 h1:kfToEGMDq6TrVrJ9Vht84Y8y9enykSZzDDZglV0kIEk= +go.opentelemetry.io/otel v1.11.0/go.mod h1:H2KtuEphyMvlhZ+F7tg9GRhAOe60moNx61Ex+WmiKkk= +go.opentelemetry.io/otel/sdk v1.11.0 h1:ZnKIL9V9Ztaq+ME43IUi/eo22mNsb6a7tGfzaOWB5fo= +go.opentelemetry.io/otel/sdk v1.11.0/go.mod h1:REusa8RsyKaq0OlyangWXaw97t2VogoO4SSEeKkSTAk= +go.opentelemetry.io/otel/trace v1.11.0 h1:20U/Vj42SX+mASlXLmSGBg6jpI1jQtv682lZtTAOVFI= +go.opentelemetry.io/otel/trace v1.11.0/go.mod h1:nyYjis9jy0gytE9LXGU+/m1sHTKbRY0fX0hulNNDP1U= +golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= +golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/tools v0.4.0 h1:7mTAgkunk3fr4GAloyyCasadO6h9zSsQZbwvcaIciV4= +golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +mvdan.cc/gofumpt v0.4.0 h1:JVf4NN1mIpHogBj7ABpgOyZc65/UUOkKQFkoURsz4MM= +mvdan.cc/gofumpt v0.4.0/go.mod h1:PljLOHDeZqgS8opHRKLzp2It2VBuSdteAgqUfzMTxlQ= diff --git a/repo.go b/repo.go index 4b82c477..7acfeb5a 100644 --- a/repo.go +++ b/repo.go @@ -74,12 +74,12 @@ func Init(ctx context.Context, path string, opts ...InitOptions) error { return err } - cmd := NewCommand(ctx, "init").AddOptions(opt.CommandOptions) + args := []string{"init"} if opt.Bare { - cmd.AddArgs("--bare") + args = append(args, "--bare") } - cmd.AddArgs("--end-of-options") - _, err = cmd.RunInDir(path) + args = append(args, "--end-of-options") + _, err = exec(ctx, path, args, opt.Envs) return err } @@ -131,25 +131,25 @@ func Clone(ctx context.Context, url, dst string, opts ...CloneOptions) error { return err } - cmd := NewCommand(ctx, "clone").AddOptions(opt.CommandOptions) + args := []string{"clone"} if opt.Mirror { - cmd.AddArgs("--mirror") + args = append(args, "--mirror") } if opt.Bare { - cmd.AddArgs("--bare") + args = append(args, "--bare") } if opt.Quiet { - cmd.AddArgs("--quiet") + args = append(args, "--quiet") } if !opt.Bare && opt.Branch != "" { - cmd.AddArgs("-b", opt.Branch) + args = append(args, "-b", opt.Branch) } if opt.Depth > 0 { - cmd.AddArgs("--depth", strconv.FormatUint(opt.Depth, 10)) + args = append(args, "--depth", strconv.FormatUint(opt.Depth, 10)) } - cmd.AddArgs("--end-of-options") - _, err = cmd.AddArgs(url, dst).Run() + args = append(args, "--end-of-options", url, dst) + _, err = exec(ctx, "", args, opt.Envs) return err } @@ -170,12 +170,13 @@ func (r *Repository) Fetch(ctx context.Context, opts ...FetchOptions) error { opt = opts[0] } - cmd := NewCommand(ctx, "fetch").AddOptions(opt.CommandOptions) + args := []string{"fetch"} if opt.Prune { - cmd.AddArgs("--prune") + args = append(args, "--prune") } + args = append(args, "--end-of-options") - _, err := cmd.RunInDir(r.path) + _, err := exec(ctx, r.path, args, opt.Envs) return err } @@ -202,21 +203,22 @@ func (r *Repository) Pull(ctx context.Context, opts ...PullOptions) error { opt = opts[0] } - cmd := NewCommand(ctx, "pull").AddOptions(opt.CommandOptions) + args := []string{"pull"} if opt.Rebase { - cmd.AddArgs("--rebase") + args = append(args, "--rebase") } if opt.All { - cmd.AddArgs("--all") + args = append(args, "--all") } + args = append(args, "--end-of-options") if !opt.All && opt.Remote != "" { - cmd.AddArgs(opt.Remote) + args = append(args, opt.Remote) if opt.Branch != "" { - cmd.AddArgs(opt.Branch) + args = append(args, opt.Branch) } } - _, err := cmd.RunInDir(r.path) + _, err := exec(ctx, r.path, args, opt.Envs) return err } @@ -235,8 +237,8 @@ func (r *Repository) Push(ctx context.Context, remote, branch string, opts ...Pu opt = opts[0] } - cmd := NewCommand(ctx, "push").AddOptions(opt.CommandOptions).AddArgs("--end-of-options", remote, branch) - _, err := cmd.RunInDir(r.path) + args := []string{"push", "--end-of-options", remote, branch} + _, err := exec(ctx, r.path, args, opt.Envs) return err } @@ -257,16 +259,14 @@ func (r *Repository) Checkout(ctx context.Context, branch string, opts ...Checko opt = opts[0] } - cmd := NewCommand(ctx, "checkout").AddOptions(opt.CommandOptions) + args := []string{"checkout"} if opt.BaseBranch != "" { - cmd.AddArgs("-b") - } - cmd.AddArgs(branch) - if opt.BaseBranch != "" { - cmd.AddArgs(opt.BaseBranch) + args = append(args, "-b", branch, "--end-of-options", opt.BaseBranch) + } else { + args = append(args, "--end-of-options", branch) } - _, err := cmd.RunInDir(r.path) + _, err := exec(ctx, r.path, args, opt.Envs) return err } @@ -287,12 +287,13 @@ func (r *Repository) Reset(ctx context.Context, rev string, opts ...ResetOptions opt = opts[0] } - cmd := NewCommand(ctx, "reset") + args := []string{"reset"} if opt.Hard { - cmd.AddArgs("--hard") + args = append(args, "--hard") } + args = append(args, "--end-of-options", rev) - _, err := cmd.AddOptions(opt.CommandOptions).AddArgs("--end-of-options", rev).RunInDir(r.path) + _, err := exec(ctx, r.path, args, opt.Envs) return err } @@ -313,7 +314,8 @@ func (r *Repository) Move(ctx context.Context, src, dst string, opts ...MoveOpti opt = opts[0] } - _, err := NewCommand(ctx, "mv").AddOptions(opt.CommandOptions).AddArgs("--end-of-options", src, dst).RunInDir(r.path) + args := []string{"mv", "--end-of-options", src, dst} + _, err := exec(ctx, r.path, args, opt.Envs) return err } @@ -336,15 +338,15 @@ func (r *Repository) Add(ctx context.Context, opts ...AddOptions) error { opt = opts[0] } - cmd := NewCommand(ctx, "add").AddOptions(opt.CommandOptions) + args := []string{"add"} if opt.All { - cmd.AddArgs("--all") + args = append(args, "--all") } if len(opt.Pathspecs) > 0 { - cmd.AddArgs("--") - cmd.AddArgs(opt.Pathspecs...) + args = append(args, "--") + args = append(args, opt.Pathspecs...) } - _, err := cmd.RunInDir(r.path) + _, err := exec(ctx, r.path, args, opt.Envs) return err } @@ -366,19 +368,21 @@ func (r *Repository) Commit(ctx context.Context, committer *Signature, message s opt = opts[0] } - cmd := NewCommand(ctx, "commit") - cmd.AddCommitter(committer) + envs := committerEnvs(committer) + envs = append(envs, opt.Envs...) if opt.Author == nil { opt.Author = committer } - cmd = cmd.AddArgs(fmt.Sprintf("--author='%s <%s>'", opt.Author.Name, opt.Author.Email)). - AddArgs("-m", message). - AddOptions(opt.CommandOptions) - _, err := cmd.RunInDir(r.path) + args := []string{"commit"} + args = append(args, fmt.Sprintf("--author=%s <%s>", opt.Author.Name, opt.Author.Email)) + args = append(args, "-m", message) + args = append(args, "--end-of-options") + + _, err := exec(ctx, r.path, args, envs) // No stderr but exit status 1 means nothing to commit. - if err != nil && err.Error() == "exit status 1" { + if isExitStatus(err, 1) { return nil } return err @@ -429,14 +433,12 @@ func (r *Repository) ShowNameStatus(ctx context.Context, rev string, opts ...Sho done <- struct{}{} }() - stderr := new(bytes.Buffer) - cmd := NewCommand(ctx, "show", "--name-status", "--pretty=format:''"). - AddOptions(opt.CommandOptions). - AddArgs("--end-of-options", rev) - err := cmd.RunInDirPipeline(w, stderr, r.path) + args := []string{"show", "--name-status", "--pretty=format:''", "--end-of-options", rev} + + err := pipe(ctx, r.path, args, opt.Envs, w) _ = w.Close() // Close writer to exit parsing goroutine if err != nil { - return nil, concatenateError(err, stderr.String()) + return nil, err } <-done @@ -459,12 +461,11 @@ func (r *Repository) RevParse(ctx context.Context, rev string, opts ...RevParseO opt = opts[0] } - commitID, err := NewCommand(ctx, "rev-parse"). - AddOptions(opt.CommandOptions). - AddArgs(rev). - RunInDir(r.path) + args := []string{"rev-parse", rev} + + commitID, err := exec(ctx, r.path, args, opt.Envs) if err != nil { - if strings.Contains(err.Error(), "exit status 128") { + if isExitStatus(err, 128) { return "", ErrRevisionNotExist } return "", err @@ -499,9 +500,9 @@ func (r *Repository) CountObjects(ctx context.Context, opts ...CountObjectsOptio opt = opts[0] } - stdout, err := NewCommand(ctx, "count-objects", "-v"). - AddOptions(opt.CommandOptions). - RunInDir(r.path) + args := []string{"count-objects", "-v", "--end-of-options"} + + stdout, err := exec(ctx, r.path, args, opt.Envs) if err != nil { return nil, err } @@ -552,7 +553,7 @@ func (r *Repository) Fsck(ctx context.Context, opts ...FsckOptions) error { opt = opts[0] } - cmd := NewCommand(ctx, "fsck").AddOptions(opt.CommandOptions) - _, err := cmd.RunInDir(r.path) + args := []string{"fsck", "--end-of-options"} + _, err := exec(ctx, r.path, args, opt.Envs) return err } diff --git a/repo_blame.go b/repo_blame.go index d2ffc062..b10a0d44 100644 --- a/repo_blame.go +++ b/repo_blame.go @@ -24,10 +24,8 @@ func (r *Repository) Blame(ctx context.Context, rev, file string, opts ...BlameO opt = opts[0] } - stdout, err := NewCommand(ctx, "blame"). - AddOptions(opt.CommandOptions). - AddArgs("-l", "-s", rev, "--", file). - RunInDir(r.path) + args := []string{"blame", "-l", "-s", rev, "--", file} + stdout, err := exec(ctx, r.path, args, opt.Envs) if err != nil { return nil, err } diff --git a/repo_commit.go b/repo_commit.go index b2425200..4ffced0f 100644 --- a/repo_commit.go +++ b/repo_commit.go @@ -85,7 +85,7 @@ func (r *Repository) CatFileCommit(ctx context.Context, rev string, opts ...CatF cache, ok := r.cachedCommits.Get(rev) if ok { - log("Cached commit hit: %s", rev) + logf("Cached commit hit: %s", rev) return cache.(*Commit), nil } @@ -94,10 +94,8 @@ func (r *Repository) CatFileCommit(ctx context.Context, rev string, opts ...CatF return nil, err } - stdout, err := NewCommand(ctx, "cat-file"). - AddOptions(opt.CommandOptions). - AddArgs("commit", commitID). - RunInDir(r.path) + args := []string{"cat-file", "commit", commitID} + stdout, err := exec(ctx, r.path, args, opt.Envs) if err != nil { return nil, err } @@ -128,10 +126,8 @@ func (r *Repository) CatFileType(ctx context.Context, rev string, opts ...CatFil opt = opts[0] } - typ, err := NewCommand(ctx, "cat-file"). - AddOptions(opt.CommandOptions). - AddArgs("-t", rev). - RunInDir(r.path) + args := []string{"cat-file", "-t", rev} + typ, err := exec(ctx, r.path, args, opt.Envs) if err != nil { return "", err } @@ -191,30 +187,28 @@ func (r *Repository) Log(ctx context.Context, rev string, opts ...LogOptions) ([ opt = opts[0] } - cmd := NewCommand(ctx, "log"). - AddOptions(opt.CommandOptions). - AddArgs("--pretty=" + LogFormatHashOnly) + args := []string{"log", "--pretty=" + LogFormatHashOnly} if opt.MaxCount > 0 { - cmd.AddArgs("--max-count=" + strconv.Itoa(opt.MaxCount)) + args = append(args, "--max-count="+strconv.Itoa(opt.MaxCount)) } if opt.Skip > 0 { - cmd.AddArgs("--skip=" + strconv.Itoa(opt.Skip)) + args = append(args, "--skip="+strconv.Itoa(opt.Skip)) } if !opt.Since.IsZero() { - cmd.AddArgs("--since=" + opt.Since.Format(time.RFC3339)) + args = append(args, "--since="+opt.Since.Format(time.RFC3339)) } if opt.GrepPattern != "" { - cmd.AddArgs("--grep=" + opt.GrepPattern) + args = append(args, "--grep="+opt.GrepPattern) } if opt.RegexpIgnoreCase { - cmd.AddArgs("--regexp-ignore-case") + args = append(args, "--regexp-ignore-case") } - cmd.AddArgs("--end-of-options", rev, "--") + args = append(args, "--end-of-options", rev, "--") if opt.Path != "" { - cmd.AddArgs(escapePath(opt.Path)) + args = append(args, escapePath(opt.Path)) } - stdout, err := cmd.RunInDir(r.path) + stdout, err := exec(ctx, r.path, args, opt.Envs) if err != nil { return nil, err } @@ -356,21 +350,18 @@ func (r *Repository) DiffNameOnly(ctx context.Context, base, head string, opts . opt = opts[0] } - cmd := NewCommand(ctx, "diff"). - AddOptions(opt.CommandOptions). - AddArgs("--name-only") - cmd.AddArgs("--end-of-options") + args := []string{"diff", "--name-only", "--end-of-options"} if opt.NeedsMergeBase { - cmd.AddArgs(base + "..." + head) + args = append(args, base+"..."+head) } else { - cmd.AddArgs(base, head) + args = append(args, base, head) } - cmd.AddArgs("--") + args = append(args, "--") if opt.Path != "" { - cmd.AddArgs(escapePath(opt.Path)) + args = append(args, escapePath(opt.Path)) } - stdout, err := cmd.RunInDir(r.path) + stdout, err := exec(ctx, r.path, args, opt.Envs) if err != nil { return nil, err } @@ -409,19 +400,14 @@ func (r *Repository) RevListCount(ctx context.Context, refspecs []string, opts . return 0, errors.New("must have at least one refspec") } - cmd := NewCommand(ctx, "rev-list"). - AddOptions(opt.CommandOptions). - AddArgs( - "--count", - "--end-of-options", - ) - cmd.AddArgs(refspecs...) - cmd.AddArgs("--") + args := []string{"rev-list", "--count", "--end-of-options"} + args = append(args, refspecs...) + args = append(args, "--") if opt.Path != "" { - cmd.AddArgs(escapePath(opt.Path)) + args = append(args, escapePath(opt.Path)) } - stdout, err := cmd.RunInDir(r.path) + stdout, err := exec(ctx, r.path, args, opt.Envs) if err != nil { return 0, err } @@ -451,15 +437,14 @@ func (r *Repository) RevList(ctx context.Context, refspecs []string, opts ...Rev return nil, errors.New("must have at least one refspec") } - cmd := NewCommand(ctx, "rev-list").AddOptions(opt.CommandOptions) - cmd.AddArgs("--end-of-options") - cmd.AddArgs(refspecs...) - cmd.AddArgs("--") + args := []string{"rev-list", "--end-of-options"} + args = append(args, refspecs...) + args = append(args, "--") if opt.Path != "" { - cmd.AddArgs(escapePath(opt.Path)) + args = append(args, escapePath(opt.Path)) } - stdout, err := cmd.RunInDir(r.path) + stdout, err := exec(ctx, r.path, args, opt.Envs) if err != nil { return nil, err } @@ -482,18 +467,12 @@ func (r *Repository) LatestCommitTime(ctx context.Context, opts ...LatestCommitT opt = opts[0] } - cmd := NewCommand(ctx, "for-each-ref"). - AddOptions(opt.CommandOptions). - AddArgs( - "--count=1", - "--sort=-committerdate", - "--format=%(committerdate:iso8601)", - ) + args := []string{"for-each-ref", "--count=1", "--sort=-committerdate", "--format=%(committerdate:iso8601)", "--end-of-options"} if opt.Branch != "" { - cmd.AddArgs(RefsHeads + opt.Branch) + args = append(args, RefsHeads+opt.Branch) } - stdout, err := cmd.RunInDir(r.path) + stdout, err := exec(ctx, r.path, args, opt.Envs) if err != nil { return time.Time{}, err } diff --git a/repo_diff.go b/repo_diff.go index 0ea4fdf5..388391aa 100644 --- a/repo_diff.go +++ b/repo_diff.go @@ -5,7 +5,6 @@ package git import ( - "bytes" "context" "fmt" "io" @@ -18,7 +17,7 @@ type DiffOptions struct { // The commit ID to used for computing diff between a range of commits (base, // revision]. When not set, only computes diff for a single commit at revision. Base string - // The additional options to be passed to the underlying git. + // The additional options to be passed to the underlying Git. CommandOptions } @@ -34,37 +33,30 @@ func (r *Repository) Diff(ctx context.Context, rev string, maxFiles, maxFileLine return nil, err } - cmd := NewCommand(ctx) + var args []string if opt.Base == "" { // First commit of repository if commit.ParentsCount() == 0 { - cmd = cmd.AddArgs("show"). - AddOptions(opt.CommandOptions). - AddArgs("--full-index", "--end-of-options", rev) + args = []string{"show", "--full-index", "--end-of-options", rev} } else { c, err := commit.Parent(ctx, 0) if err != nil { return nil, err } - cmd = cmd.AddArgs("diff"). - AddOptions(opt.CommandOptions). - AddArgs("--full-index", "-M", c.ID.String(), "--end-of-options", rev) + args = []string{"diff", "--full-index", "-M", c.ID.String(), "--end-of-options", rev} } } else { - cmd = cmd.AddArgs("diff"). - AddOptions(opt.CommandOptions). - AddArgs("--full-index", "-M", opt.Base, "--end-of-options", rev) + args = []string{"diff", "--full-index", "-M", opt.Base, "--end-of-options", rev} } stdout, w := io.Pipe() done := make(chan SteamParseDiffResult) go StreamParseDiff(stdout, done, maxFiles, maxFileLines, maxLineChars) - stderr := new(bytes.Buffer) - err = cmd.RunInDirPipeline(w, stderr, r.path) + err = pipe(ctx, r.path, args, opt.Envs, w) _ = w.Close() // Close writer to exit parsing goroutine if err != nil { - return nil, concatenateError(err, stderr.String()) + return nil, err } result := <-done @@ -83,7 +75,7 @@ const ( // // Docs: https://git-scm.com/docs/git-format-patch type RawDiffOptions struct { - // The additional options to be passed to the underlying git. + // The additional options to be passed to the underlying Git. CommandOptions } @@ -100,50 +92,41 @@ func (r *Repository) RawDiff(ctx context.Context, rev string, diffType RawDiffFo return err } - cmd := NewCommand(ctx) + var args []string switch diffType { case RawDiffNormal: if commit.ParentsCount() == 0 { - cmd = cmd.AddArgs("show"). - AddOptions(opt.CommandOptions). - AddArgs("--full-index", "--end-of-options", rev) + args = []string{"show", "--full-index", "--end-of-options", rev} } else { c, err := commit.Parent(ctx, 0) if err != nil { return err } - cmd = cmd.AddArgs("diff"). - AddOptions(opt.CommandOptions). - AddArgs("--full-index", "-M", c.ID.String(), "--end-of-options", rev) + args = []string{"diff", "--full-index", "-M", c.ID.String(), "--end-of-options", rev} } case RawDiffPatch: if commit.ParentsCount() == 0 { - cmd = cmd.AddArgs("format-patch"). - AddOptions(opt.CommandOptions). - AddArgs("--full-index", "--no-signoff", "--no-signature", "--stdout", "--root", "--end-of-options", rev) + args = []string{"format-patch", "--full-index", "--no-signoff", "--no-signature", "--stdout", "--root", "--end-of-options", rev} } else { c, err := commit.Parent(ctx, 0) if err != nil { return err } - cmd = cmd.AddArgs("format-patch"). - AddOptions(opt.CommandOptions). - AddArgs("--full-index", "--no-signoff", "--no-signature", "--stdout", "--end-of-options", rev+"..."+c.ID.String()) + args = []string{"format-patch", "--full-index", "--no-signoff", "--no-signature", "--stdout", "--end-of-options", rev + "..." + c.ID.String()} } default: return fmt.Errorf("invalid diffType: %s", diffType) } - stderr := new(bytes.Buffer) - if err = cmd.RunInDirPipeline(w, stderr, r.path); err != nil { - return concatenateError(err, stderr.String()) + if err = pipe(ctx, r.path, args, opt.Envs, w); err != nil { + return err } return nil } // DiffBinaryOptions contains optional arguments for producing binary patch. type DiffBinaryOptions struct { - // The additional options to be passed to the underlying git. + // The additional options to be passed to the underlying Git. CommandOptions } @@ -155,8 +138,6 @@ func (r *Repository) DiffBinary(ctx context.Context, base, head string, opts ... opt = opts[0] } - return NewCommand(ctx, "diff"). - AddOptions(opt.CommandOptions). - AddArgs("--full-index", "--binary", base, head). - RunInDir(r.path) + args := []string{"diff", "--full-index", "--binary", "--end-of-options", base, head} + return exec(ctx, r.path, args, opt.Envs) } diff --git a/repo_grep.go b/repo_grep.go index dd05a64f..3da197c5 100644 --- a/repo_grep.go +++ b/repo_grep.go @@ -25,7 +25,7 @@ type GrepOptions struct { WordRegexp bool // Whether use extended regular expressions. ExtendedRegexp bool - // The additional options to be passed to the underlying git. + // The additional options to be passed to the underlying Git. CommandOptions } @@ -78,29 +78,23 @@ func (r *Repository) Grep(ctx context.Context, pattern string, opts ...GrepOptio opt.Tree = "HEAD" } - cmd := NewCommand(ctx, "grep"). - AddOptions(opt.CommandOptions). - // Display full-name, line number and column number - AddArgs("--full-name", "--line-number", "--column") + args := []string{"grep"} + args = append(args, "--full-name", "--line-number", "--column") if opt.IgnoreCase { - cmd.AddArgs("--ignore-case") + args = append(args, "--ignore-case") } if opt.WordRegexp { - cmd.AddArgs("--word-regexp") + args = append(args, "--word-regexp") } if opt.ExtendedRegexp { - cmd.AddArgs("--extended-regexp") + args = append(args, "--extended-regexp") } - cmd.AddArgs( - "--end-of-options", - pattern, - opt.Tree, - ) + args = append(args, "--end-of-options", pattern, opt.Tree) if opt.Pathspec != "" { - cmd.AddArgs("--", opt.Pathspec) + args = append(args, "--", opt.Pathspec) } - stdout, err := cmd.RunInDir(r.path) + stdout, err := exec(ctx, r.path, args, opt.Envs) if err != nil { return nil } diff --git a/repo_pull.go b/repo_pull.go index 0c9e7775..b83d7083 100644 --- a/repo_pull.go +++ b/repo_pull.go @@ -25,15 +25,10 @@ func (r *Repository) MergeBase(ctx context.Context, base, head string, opts ...M opt = opts[0] } - stdout, err := NewCommand(ctx, "merge-base"). - AddOptions(opt.CommandOptions). - AddArgs( - "--end-of-options", - base, - head, - ).RunInDir(r.path) + args := []string{"merge-base", "--end-of-options", base, head} + stdout, err := exec(ctx, r.path, args, opt.Envs) if err != nil { - if strings.Contains(err.Error(), "exit status 1") { + if isExitStatus(err, 1) { return "", ErrNoMergeBase } return "", err diff --git a/repo_pull_test.go b/repo_pull_test.go index ff231863..737c6c8e 100644 --- a/repo_pull_test.go +++ b/repo_pull_test.go @@ -9,42 +9,42 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestRepository_MergeBase(t *testing.T) { ctx := context.Background() - t.Run("no merge base", func(t *testing.T) { + t.Run("bad revision", func(t *testing.T) { + // "bad_revision" doesn't exist, so git fails with exit status 128 (fatal), + // not exit status 1 (no merge base). mb, err := testrepo.MergeBase(ctx, "0eedd79eba4394bbef888c804e899731644367fe", "bad_revision") - assert.Equal(t, ErrNoMergeBase, err) + assert.Error(t, err) assert.Empty(t, mb) }) tests := []struct { - base string - head string - opt MergeBaseOptions - expMergeBase string + base string + head string + opt MergeBaseOptions + wantMergeBase string }{ { - base: "4e59b72440188e7c2578299fc28ea425fbe9aece", - head: "0eedd79eba4394bbef888c804e899731644367fe", - expMergeBase: "4e59b72440188e7c2578299fc28ea425fbe9aece", + base: "4e59b72440188e7c2578299fc28ea425fbe9aece", + head: "0eedd79eba4394bbef888c804e899731644367fe", + wantMergeBase: "4e59b72440188e7c2578299fc28ea425fbe9aece", }, { - base: "master", - head: "release-1.0", - expMergeBase: "0eedd79eba4394bbef888c804e899731644367fe", + base: "master", + head: "release-1.0", + wantMergeBase: "0eedd79eba4394bbef888c804e899731644367fe", }, } for _, test := range tests { t.Run("", func(t *testing.T) { mb, err := testrepo.MergeBase(ctx, test.base, test.head, test.opt) - if err != nil { - t.Fatal(err) - } - - assert.Equal(t, test.expMergeBase, mb) + require.NoError(t, err) + assert.Equal(t, test.wantMergeBase, mb) }) } } diff --git a/repo_reference.go b/repo_reference.go index eec27159..a3ad2777 100644 --- a/repo_reference.go +++ b/repo_reference.go @@ -37,7 +37,7 @@ type Reference struct { // // Docs: https://git-scm.com/docs/git-show-ref#Documentation/git-show-ref.txt---verify type ShowRefVerifyOptions struct { - // The additional options to be passed to the underlying git. + // The additional options to be passed to the underlying Git. CommandOptions } @@ -51,8 +51,8 @@ func (r *Repository) ShowRefVerify(ctx context.Context, ref string, opts ...Show opt = opts[0] } - cmd := NewCommand(ctx, "show-ref", "--verify", "--end-of-options", ref).AddOptions(opt.CommandOptions) - stdout, err := cmd.RunInDir(r.path) + args := []string{"show-ref", "--verify", "--end-of-options", ref} + stdout, err := exec(ctx, r.path, args, opt.Envs) if err != nil { if strings.Contains(err.Error(), "not a valid ref") { return "", ErrReferenceNotExist @@ -100,7 +100,7 @@ type SymbolicRefOptions struct { // The name of the reference, e.g. "refs/heads/master". When set, it will be // used to update the symbolic ref. Ref string - // The additional options to be passed to the underlying git. + // The additional options to be passed to the underlying Git. CommandOptions } @@ -113,16 +113,16 @@ func (r *Repository) SymbolicRef(ctx context.Context, opts ...SymbolicRefOptions opt = opts[0] } - cmd := NewCommand(ctx, "symbolic-ref").AddOptions(opt.CommandOptions) + args := []string{"symbolic-ref"} if opt.Name == "" { opt.Name = "HEAD" } - cmd.AddArgs("--end-of-options", opt.Name) + args = append(args, "--end-of-options", opt.Name) if opt.Ref != "" { - cmd.AddArgs(opt.Ref) + args = append(args, opt.Ref) } - stdout, err := cmd.RunInDir(r.path) + stdout, err := exec(ctx, r.path, args, opt.Envs) if err != nil { return "", err } @@ -139,7 +139,7 @@ type ShowRefOptions struct { Tags bool // The list of patterns to filter results. Patterns []string - // The additional options to be passed to the underlying git. + // The additional options to be passed to the underlying Git. CommandOptions } @@ -150,19 +150,19 @@ func (r *Repository) ShowRef(ctx context.Context, opts ...ShowRefOptions) ([]*Re opt = opts[0] } - cmd := NewCommand(ctx, "show-ref").AddOptions(opt.CommandOptions) + args := []string{"show-ref"} if opt.Heads { - cmd.AddArgs("--heads") + args = append(args, "--heads") } if opt.Tags { - cmd.AddArgs("--tags") + args = append(args, "--tags") } - cmd.AddArgs("--") + args = append(args, "--") if len(opt.Patterns) > 0 { - cmd.AddArgs(opt.Patterns...) + args = append(args, opt.Patterns...) } - stdout, err := cmd.RunInDir(r.path) + stdout, err := exec(ctx, r.path, args, opt.Envs) if err != nil { return nil, err } @@ -202,7 +202,7 @@ func (r *Repository) Branches(ctx context.Context) ([]string, error) { type DeleteBranchOptions struct { // Indicates whether to force delete the branch. Force bool - // The additional options to be passed to the underlying git. + // The additional options to be passed to the underlying Git. CommandOptions } @@ -213,12 +213,13 @@ func (r *Repository) DeleteBranch(ctx context.Context, name string, opts ...Dele opt = opts[0] } - cmd := NewCommand(ctx, "branch").AddOptions(opt.CommandOptions) + args := []string{"branch"} if opt.Force { - cmd.AddArgs("-D") + args = append(args, "-D") } else { - cmd.AddArgs("-d") + args = append(args, "-d") } - _, err := cmd.AddArgs("--end-of-options", name).RunInDir(r.path) + args = append(args, "--end-of-options", name) + _, err := exec(ctx, r.path, args, opt.Envs) return err } diff --git a/repo_remote.go b/repo_remote.go index 3f13755d..e5438d09 100644 --- a/repo_remote.go +++ b/repo_remote.go @@ -23,33 +23,33 @@ type LsRemoteOptions struct { Refs bool // The list of patterns to filter results. Patterns []string - // The additional options to be passed to the underlying git. + // The additional options to be passed to the underlying Git. CommandOptions } -// LsRemote returns a list references in the remote repository. +// LsRemote returns a list of references in the remote repository. func LsRemote(ctx context.Context, url string, opts ...LsRemoteOptions) ([]*Reference, error) { var opt LsRemoteOptions if len(opts) > 0 { opt = opts[0] } - cmd := NewCommand(ctx, "ls-remote", "--quiet").AddOptions(opt.CommandOptions) + args := []string{"ls-remote", "--quiet"} if opt.Heads { - cmd.AddArgs("--heads") + args = append(args, "--heads") } if opt.Tags { - cmd.AddArgs("--tags") + args = append(args, "--tags") } if opt.Refs { - cmd.AddArgs("--refs") + args = append(args, "--refs") } - cmd.AddArgs("--end-of-options", url) + args = append(args, "--end-of-options", url) if len(opt.Patterns) > 0 { - cmd.AddArgs(opt.Patterns...) + args = append(args, opt.Patterns...) } - stdout, err := cmd.Run() + stdout, err := exec(ctx, "", args, opt.Envs) if err != nil { return nil, err } @@ -88,7 +88,7 @@ type RemoteAddOptions struct { Fetch bool // Indicates whether to add remote as mirror with --mirror=fetch. MirrorFetch bool - // The additional options to be passed to the underlying git. + // The additional options to be passed to the underlying Git. CommandOptions } @@ -99,15 +99,16 @@ func (r *Repository) RemoteAdd(ctx context.Context, name, url string, opts ...Re opt = opts[0] } - cmd := NewCommand(ctx, "remote", "add").AddOptions(opt.CommandOptions) + args := []string{"remote", "add"} if opt.Fetch { - cmd.AddArgs("-f") + args = append(args, "-f") } if opt.MirrorFetch { - cmd.AddArgs("--mirror=fetch") + args = append(args, "--mirror=fetch") } + args = append(args, "--end-of-options", name, url) - _, err := cmd.AddArgs("--end-of-options", name, url).RunInDir(r.path) + _, err := exec(ctx, r.path, args, opt.Envs) return err } @@ -116,7 +117,7 @@ func (r *Repository) RemoteAdd(ctx context.Context, name, url string, opts ...Re // // Docs: https://git-scm.com/docs/git-remote#Documentation/git-remote.txt-emremoveem type RemoteRemoveOptions struct { - // The additional options to be passed to the underlying git. + // The additional options to be passed to the underlying Git. CommandOptions } @@ -127,12 +128,9 @@ func (r *Repository) RemoteRemove(ctx context.Context, name string, opts ...Remo opt = opts[0] } - _, err := NewCommand(ctx, "remote", "remove"). - AddOptions(opt.CommandOptions). - AddArgs("--end-of-options", name). - RunInDir(r.path) + args := []string{"remote", "remove", "--end-of-options", name} + _, err := exec(ctx, r.path, args, opt.Envs) if err != nil { - // the error status may differ from git clients if strings.Contains(err.Error(), "error: No such remote") || strings.Contains(err.Error(), "fatal: No such remote") { return ErrRemoteNotExist @@ -146,7 +144,7 @@ func (r *Repository) RemoteRemove(ctx context.Context, name string, opts ...Remo // / // Docs: https://git-scm.com/docs/git-remote#_commands type RemotesOptions struct { - // The additional options to be passed to the underlying git. + // The additional options to be passed to the underlying Git. CommandOptions } @@ -157,9 +155,8 @@ func (r *Repository) Remotes(ctx context.Context, opts ...RemotesOptions) ([]str opt = opts[0] } - stdout, err := NewCommand(ctx, "remote"). - AddOptions(opt.CommandOptions). - RunInDir(r.path) + args := []string{"remote", "--end-of-options"} + stdout, err := exec(ctx, r.path, args, opt.Envs) if err != nil { return nil, err } @@ -177,7 +174,7 @@ type RemoteGetURLOptions struct { // Indicates whether to get all URLs, including lists that are not part of main // URLs. This option is independent of the Push option. All bool - // The additional options to be passed to the underlying git. + // The additional options to be passed to the underlying Git. CommandOptions } @@ -188,15 +185,16 @@ func (r *Repository) RemoteGetURL(ctx context.Context, name string, opts ...Remo opt = opts[0] } - cmd := NewCommand(ctx, "remote", "get-url").AddOptions(opt.CommandOptions) + args := []string{"remote", "get-url"} if opt.Push { - cmd.AddArgs("--push") + args = append(args, "--push") } if opt.All { - cmd.AddArgs("--all") + args = append(args, "--all") } + args = append(args, "--end-of-options", name) - stdout, err := cmd.AddArgs("--end-of-options", name).RunInDir(r.path) + stdout, err := exec(ctx, r.path, args, opt.Envs) if err != nil { return nil, err } @@ -212,7 +210,7 @@ type RemoteSetURLOptions struct { Push bool // The regex to match existing URLs to replace (instead of first). Regex string - // The additional options to be passed to the underlying git. + // The additional options to be passed to the underlying Git. CommandOptions } @@ -224,18 +222,16 @@ func (r *Repository) RemoteSetURL(ctx context.Context, name, newurl string, opts opt = opts[0] } - cmd := NewCommand(ctx, "remote", "set-url").AddOptions(opt.CommandOptions) + args := []string{"remote", "set-url"} if opt.Push { - cmd.AddArgs("--push") + args = append(args, "--push") } - - cmd.AddArgs("--end-of-options", name, newurl) - + args = append(args, "--end-of-options", name, newurl) if opt.Regex != "" { - cmd.AddArgs(opt.Regex) + args = append(args, opt.Regex) } - _, err := cmd.RunInDir(r.path) + _, err := exec(ctx, r.path, args, opt.Envs) if err != nil { if strings.Contains(err.Error(), "No such URL found") { return ErrURLNotExist @@ -254,7 +250,7 @@ func (r *Repository) RemoteSetURL(ctx context.Context, name, newurl string, opts type RemoteSetURLAddOptions struct { // Indicates whether to get push URLs instead of fetch URLs. Push bool - // The additional options to be passed to the underlying git. + // The additional options to be passed to the underlying Git. CommandOptions } @@ -266,16 +262,13 @@ func (r *Repository) RemoteSetURLAdd(ctx context.Context, name, newurl string, o opt = opts[0] } - cmd := NewCommand(ctx, "remote", "set-url"). - AddOptions(opt.CommandOptions). - AddArgs("--add") + args := []string{"remote", "set-url", "--add"} if opt.Push { - cmd.AddArgs("--push") + args = append(args, "--push") } + args = append(args, "--end-of-options", name, newurl) - cmd.AddArgs("--end-of-options", name, newurl) - - _, err := cmd.RunInDir(r.path) + _, err := exec(ctx, r.path, args, opt.Envs) if err != nil && strings.Contains(err.Error(), "Will not delete all non-push URLs") { return ErrNotDeleteNonPushURLs } @@ -289,7 +282,7 @@ func (r *Repository) RemoteSetURLAdd(ctx context.Context, name, newurl string, o type RemoteSetURLDeleteOptions struct { // Indicates whether to get push URLs instead of fetch URLs. Push bool - // The additional options to be passed to the underlying git. + // The additional options to be passed to the underlying Git. CommandOptions } @@ -301,16 +294,13 @@ func (r *Repository) RemoteSetURLDelete(ctx context.Context, name, regex string, opt = opts[0] } - cmd := NewCommand(ctx, "remote", "set-url"). - AddOptions(opt.CommandOptions). - AddArgs("--delete") + args := []string{"remote", "set-url", "--delete"} if opt.Push { - cmd.AddArgs("--push") + args = append(args, "--push") } + args = append(args, "--end-of-options", name, regex) - cmd.AddArgs("--end-of-options", name, regex) - - _, err := cmd.RunInDir(r.path) + _, err := exec(ctx, r.path, args, opt.Envs) if err != nil && strings.Contains(err.Error(), "Will not delete all non-push URLs") { return ErrNotDeleteNonPushURLs } diff --git a/repo_tag.go b/repo_tag.go index df3b883a..9a57fb3e 100644 --- a/repo_tag.go +++ b/repo_tag.go @@ -55,7 +55,7 @@ l: func (r *Repository) getTag(ctx context.Context, id *SHA1) (*Tag, error) { t, ok := r.cachedTags.Get(id.String()) if ok { - log("Cached tag hit: %s", id) + logf("Cached tag hit: %s", id) return t.(*Tag), nil } @@ -76,7 +76,7 @@ func (r *Repository) getTag(ctx context.Context, id *SHA1) (*Tag, error) { } case ObjectTag: // Tag is an annotation - data, err := NewCommand(ctx, "cat-file", "-p", id.String()).RunInDir(r.path) + data, err := exec(ctx, r.path, []string{"cat-file", "-p", id.String()}, nil) if err != nil { return nil, err } @@ -155,19 +155,18 @@ func (r *Repository) Tags(ctx context.Context, opts ...TagsOptions) ([]string, e opt = opts[0] } - cmd := NewCommand(ctx, "tag", "--list").AddOptions(opt.CommandOptions) - + args := []string{"tag", "--list"} if opt.SortKey != "" { - cmd.AddArgs("--sort=" + opt.SortKey) + args = append(args, "--sort="+opt.SortKey) } else { - cmd.AddArgs("--sort=-creatordate") + args = append(args, "--sort=-creatordate") } - + args = append(args, "--end-of-options") if opt.Pattern != "" { - cmd.AddArgs(opt.Pattern) + args = append(args, opt.Pattern) } - stdout, err := cmd.RunInDir(r.path) + stdout, err := exec(ctx, r.path, args, opt.Envs) if err != nil { return nil, err } @@ -199,21 +198,21 @@ func (r *Repository) CreateTag(ctx context.Context, name, rev string, opts ...Cr opt = opts[0] } - cmd := NewCommand(ctx, "tag").AddOptions(opt.CommandOptions) + args := []string{"tag"} + var envs []string if opt.Annotated { - cmd.AddArgs("-a", name) - cmd.AddArgs("--message", opt.Message) + args = append(args, "-a", name) + args = append(args, "--message", opt.Message) if opt.Author != nil { - cmd.AddCommitter(opt.Author) + envs = committerEnvs(opt.Author) } - cmd.AddArgs("--end-of-options") + args = append(args, "--end-of-options") } else { - cmd.AddArgs("--end-of-options", name) + args = append(args, "--end-of-options", name) } - - cmd.AddArgs(rev) - - _, err := cmd.RunInDir(r.path) + args = append(args, rev) + envs = append(envs, opt.Envs...) + _, err := exec(ctx, r.path, args, envs) return err } @@ -232,8 +231,7 @@ func (r *Repository) DeleteTag(ctx context.Context, name string, opts ...DeleteT opt = opts[0] } - _, err := NewCommand(ctx, "tag", "--delete", "--end-of-options", name). - AddOptions(opt.CommandOptions). - RunInDir(r.path) + args := []string{"tag", "--delete", "--end-of-options", name} + _, err := exec(ctx, r.path, args, opt.Envs) return err } diff --git a/repo_tree.go b/repo_tree.go index 8fb2e663..2c77a6e8 100644 --- a/repo_tree.go +++ b/repo_tree.go @@ -117,7 +117,7 @@ func (r *Repository) LsTree(ctx context.Context, treeID string, opts ...LsTreeOp cache, ok := r.cachedTrees.Get(treeID) if ok { - log("Cached tree hit: %s", treeID) + logf("Cached tree hit: %s", treeID) return cache.(*Tree), nil } @@ -131,14 +131,13 @@ func (r *Repository) LsTree(ctx context.Context, treeID string, opts ...LsTreeOp repo: r, } - cmd := NewCommand(ctx, "ls-tree") + args := []string{"ls-tree"} if opt.Verbatim { - cmd.AddArgs("-z") + args = append(args, "-z") } - stdout, err := cmd. - AddOptions(opt.CommandOptions). - AddArgs(treeID). - RunInDir(r.path) + args = append(args, "--end-of-options", treeID) + + stdout, err := exec(ctx, r.path, args, opt.Envs) if err != nil { return nil, err } diff --git a/server.go b/server.go index 1b4d2356..901f966d 100755 --- a/server.go +++ b/server.go @@ -27,11 +27,13 @@ func UpdateServerInfo(ctx context.Context, path string, opts ...UpdateServerInfo if len(opts) > 0 { opt = opts[0] } - cmd := NewCommand(ctx, "update-server-info").AddOptions(opt.CommandOptions) + + args := []string{"update-server-info"} if opt.Force { - cmd.AddArgs("--force") + args = append(args, "--force") } - _, err := cmd.RunInDir(path) + args = append(args, "--end-of-options") + _, err := exec(ctx, path, args, opt.Envs) return err } @@ -54,15 +56,16 @@ func ReceivePack(ctx context.Context, path string, opts ...ReceivePackOptions) ( if len(opts) > 0 { opt = opts[0] } - cmd := NewCommand(ctx, "receive-pack").AddOptions(opt.CommandOptions) + + args := []string{"receive-pack"} if opt.Quiet { - cmd.AddArgs("--quiet") + args = append(args, "--quiet") } if opt.HTTPBackendInfoRefs { - cmd.AddArgs("--http-backend-info-refs") + args = append(args, "--http-backend-info-refs") } - cmd.AddArgs(".") - return cmd.RunInDir(path) + args = append(args, "--end-of-options", ".") + return exec(ctx, path, args, opt.Envs) } // UploadPackOptions contains optional arguments for sending the packfile to the @@ -91,19 +94,20 @@ func UploadPack(ctx context.Context, path string, opts ...UploadPackOptions) ([] if len(opts) > 0 { opt = opts[0] } - cmd := NewCommand(ctx, "upload-pack").AddOptions(opt.CommandOptions) + + args := []string{"upload-pack"} if opt.StatelessRPC { - cmd.AddArgs("--stateless-rpc") + args = append(args, "--stateless-rpc") } if opt.Strict { - cmd.AddArgs("--strict") + args = append(args, "--strict") } if opt.InactivityTimeout > 0 { - cmd.AddArgs("--timeout", opt.InactivityTimeout.String()) + args = append(args, "--timeout", opt.InactivityTimeout.String()) } if opt.HTTPBackendInfoRefs { - cmd.AddArgs("--http-backend-info-refs") + args = append(args, "--http-backend-info-refs") } - cmd.AddArgs(".") - return cmd.RunInDir(path) + args = append(args, "--end-of-options", ".") + return exec(ctx, path, args, opt.Envs) } diff --git a/tree_entry.go b/tree_entry.go index 30f99641..c854cfc7 100644 --- a/tree_entry.go +++ b/tree_entry.go @@ -98,7 +98,7 @@ func (e *TreeEntry) Size(ctx context.Context) int64 { return e.size } - stdout, err := NewCommand(ctx, "cat-file", "-s", e.id.String()).RunInDir(e.parent.repo.path) + stdout, err := exec(ctx, e.parent.repo.path, []string{"cat-file", "-s", e.id.String()}, nil) if err != nil { return 0 } diff --git a/tree_entry_test.go b/tree_entry_test.go index 0d305b21..3f1ff9fd 100644 --- a/tree_entry_test.go +++ b/tree_entry_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestTreeEntry(t *testing.T) { @@ -31,6 +32,39 @@ func TestTreeEntry(t *testing.T) { assert.Equal(t, "go.mod", e.Name()) } +func TestTreeEntry_Size(t *testing.T) { + ctx := context.Background() + tree, err := testrepo.LsTree(ctx, "0eedd79eba4394bbef888c804e899731644367fe") + require.NoError(t, err) + + es, err := tree.Entries(ctx) + require.NoError(t, err) + + t.Run("blob", func(t *testing.T) { + var entry *TreeEntry + for _, e := range es { + if e.Name() == "README.txt" { + entry = e + break + } + } + require.NotNil(t, entry, "entry README.txt not found") + assert.Equal(t, int64(795), entry.Size(ctx)) + }) + + t.Run("tree returns zero", func(t *testing.T) { + var entry *TreeEntry + for _, e := range es { + if e.IsTree() { + entry = e + break + } + } + require.NotNil(t, entry, "tree entry not found") + assert.Equal(t, int64(0), entry.Size(ctx)) + }) +} + func TestEntries_Sort(t *testing.T) { ctx := context.Background() tree, err := testrepo.LsTree(ctx, "0eedd79eba4394bbef888c804e899731644367fe") diff --git a/utils.go b/utils.go index 79c0e278..305dfc10 100644 --- a/utils.go +++ b/utils.go @@ -5,7 +5,6 @@ package git import ( - "fmt" "os" "strings" "sync" @@ -66,13 +65,6 @@ func isExist(path string) bool { return err == nil || os.IsExist(err) } -func concatenateError(err error, stderr string) error { - if len(stderr) == 0 { - return err - } - return fmt.Errorf("%v - %s", err, stderr) -} - // bytesToStrings splits given bytes into strings by line separator ("\n"). It // returns empty slice if the given bytes only contains line separators. func bytesToStrings(in []byte) []string {