diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index a862f7df..42c99ae5 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -1,7 +1,7 @@ name: Go on: push: - branches: [ master ] + branches: [ master, v2 ] paths: - '**.go' - 'go.mod' @@ -29,7 +29,7 @@ jobs: - name: Install Go uses: actions/setup-go@v5 with: - go-version: 1.24.x + go-version: 1.26.x - name: Check Go module tidiness shell: bash run: | @@ -42,7 +42,7 @@ jobs: exit 1 fi - name: Run golangci-lint - uses: golangci/golangci-lint-action@v3 + uses: golangci/golangci-lint-action@v9 with: version: latest args: --timeout=30m @@ -51,7 +51,7 @@ jobs: name: Test strategy: matrix: - go-version: [ 1.24.x ] + go-version: [ 1.26.x ] platform: [ ubuntu-latest, macos-latest, windows-latest ] runs-on: ${{ matrix.platform }} steps: diff --git a/.golangci.yml b/.golangci.yml index 479ac812..f4e977cb 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,24 +1,41 @@ -linters-settings: - staticcheck: - checks: [ - "all", - "-SA1019" # There are valid use cases of strings.Title - ] - nakedret: - max-func-lines: 0 # Disallow any unnamed return statement - +version: "2" linters: enable: - - unused - - errcheck - - gosimple - - govet - - ineffassign - - staticcheck - - typecheck - nakedret - - gofmt - rowserrcheck - unconvert - - goimports - unparam + settings: + govet: + disable: + # printf: non-constant format string in call to fmt.Errorf (govet) + # showing up since golangci-lint version 1.60.1 + - printf + staticcheck: + checks: + - all + - "-QF1001" # I'm a math noob + - "-ST1016" # Some legit code uses this pattern + nakedret: + max-func-lines: 0 # Disallow any unnamed return statement + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + paths: + - third_party$ + - builtin$ + - examples$ +formatters: + enable: + - gofmt + - goimports + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/README.md b/README.md index 4849e21f..7af17359 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,18 @@ # Git Module -[![GitHub Workflow Status](https://img.shields.io/github/workflow/status/gogs/git-module/Go?logo=github&style=for-the-badge)](https://github.com/gogs/git-module/actions?query=workflow%3AGo) -[![GoDoc](https://img.shields.io/badge/GoDoc-Reference-blue?style=for-the-badge&logo=go)](https://pkg.go.dev/github.com/gogs/git-module?tab=doc) +[![GoDoc](https://img.shields.io/badge/GoDoc-Reference-blue?style=for-the-badge&logo=go)](https://pkg.go.dev/github.com/gogs/git-module/v2?tab=doc) Package git-module is a Go module for Git access through shell commands. ## Requirements -- Git version must be no less than **1.8.3**. -- For Windows users, try to use the latest version of both. +- Git version must be no less than **2.4.9**. + +## Installation + +```zsh +go get github.com/gogs/git-module/v2 +``` ## License diff --git a/blame.go b/blame.go index defd9438..2fbb7bac 100644 --- a/blame.go +++ b/blame.go @@ -1,7 +1,3 @@ -// Copyright 2020 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - package git // Blame contains information of a Git file blame. diff --git a/blob.go b/blob.go index 9bb85466..8c8535f1 100644 --- a/blob.go +++ b/blob.go @@ -1,11 +1,8 @@ -// Copyright 2015 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - package git import ( "bytes" + "context" "io" ) @@ -14,23 +11,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. -func (b *Blob) Bytes() ([]byte, error) { +// 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. - stdout.Grow(int(b.Size())) + if size := b.Size(ctx); size > 0 && size < int64(^uint(0)>>1) { + stdout.Grow(int(size)) + } - if err := b.Pipeline(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(stdout, stderr io.Writer) error { - return NewCommand("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 a9273b7d..cc02c0ba 100644 --- a/blob_test.go +++ b/blob_test.go @@ -1,17 +1,15 @@ -// Copyright 2019 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - package git import ( "bytes" + "context" "testing" "github.com/stretchr/testify/assert" ) func TestBlob(t *testing.T) { + ctx := context.Background() expOutput := `This is a sample project students can use during Matthew's Git class. Here is an addition by me @@ -41,14 +39,14 @@ This demo also includes an image with changes on a branch for examination of ima } t.Run("get data all at once", func(t *testing.T) { - p, err := blob.Bytes() + p, err := blob.Bytes(ctx) assert.Nil(t, err) 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(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 4dc5f017..5ff78fb2 100644 --- a/command.go +++ b/command.go @@ -1,112 +1,157 @@ -// Copyright 2015 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - 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 - timeout time.Duration - ctx context.Context -} + "github.com/sourcegraph/run" +) -// 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. +// CommandOptions contains additional options for running a Git command. type CommandOptions struct { - Args []string - Envs []string - Timeout time.Duration - Context context.Context + 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(args ...string) *Command { - return NewCommandWithContext(context.Background(), args...) -} + // 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)) + } -// 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, + 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 } -// AddArgs appends given arguments to the command. -func (c *Command) AddArgs(args ...string) *Command { - c.args = append(c.args, args...) - 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() -// AddEnvs appends given environment variables to the command. -func (c *Command) AddEnvs(envs ...string) *Command { - c.envs = append(c.envs, envs...) - return c -} + var logBuf *bytes.Buffer + if logOutput != nil { + logBuf = new(bytes.Buffer) + logBuf.Grow(512) + defer func() { + log(dir, args, logBuf.Bytes()) + }() + } -// WithContext returns a new Command with the given context. -func (c Command) WithContext(ctx context.Context) *Command { - c.ctx = ctx - return &c -} + // 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) -// WithTimeout returns a new Command with given timeout. -func (c Command) WithTimeout(timeout time.Duration) *Command { - c.timeout = timeout - return &c -} + // 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)") + } + } -// SetTimeout sets the timeout for the command. -func (c *Command) SetTimeout(timeout time.Duration) { - c.timeout = timeout + if err != nil { + return nil, mapContextError(err, ctx) + } + return stdout.Bytes(), nil } -// 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...) +// 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()) + }() + } + + streamErr := c.StdOut().Run().Stream(w) + if streamErr != nil { + return mapContextError(streamErr, ctx) } - return c + 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. -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. @@ -136,175 +181,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 - // Timeout is the duration to wait before timing out. - // - // Deprecated: Use CommandOptions.Timeout or *Command.WithTimeout instead. - Timeout time.Duration -} - -// 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. 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. -func (c *Command) RunInDirWithOptions(dir string, opts ...RunInDirOptions) (err error) { - var opt RunInDirOptions - if len(opts) > 0 { - opt = opts[0] - } - - 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) - 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("[timeout: %v] %s\n%s", timeout, c, buf.Bytes()) - } else { - log("[timeout: %v] %s: %s\n%s", timeout, dir, c, buf.Bytes()) - } - }() - - 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 { - 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 { +// mapContextError maps context errors to the appropriate sentinel errors used +// by this package. +func mapContextError(err error, ctx context.Context) error { + if ctx == nil { return err } - - result := make(chan error) - go func() { - result <- cmd.Wait() - }() - - select { - case <-ctx.Done(): - <-result - if cmd.Process != nil && cmd.ProcessState != nil && !cmd.ProcessState.Exited() { - if err := cmd.Process.Kill(); err != nil { - return fmt.Errorf("kill process: %v", err) - } + if ctxErr := ctx.Err(); ctxErr != nil { + if errors.Is(ctxErr, context.DeadlineExceeded) { + return ErrExecTimeout } - - return ErrExecTimeout - case err = <-result: - return 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) { - if timeout != 0 { - c = c.WithTimeout(timeout) + return ctxErr } - return c.RunInDirPipeline(stdout, stderr, dir) + return err } -// 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) { - if timeout != 0 { - c = c.WithTimeout(timeout) - } - 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) { - 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) { - if timeout != 0 { - c = c.WithTimeout(timeout) - } - 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) { - 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 53e2bf21..f38b0c22 100644 --- a/command_test.go +++ b/command_test.go @@ -1,65 +1,79 @@ -// Copyright 2020 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - package git import ( + "context" + "io" "testing" "time" "github.com/stretchr/testify/assert" ) -func TestCommand_String(t *testing.T) { - 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(test.args...) - assert.Equal(t, test.expStr, cmd.String()) - }) - } +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 := exec(ctx, "", []string{"version"}, nil) + assert.Equal(t, ErrExecTimeout, err) + }) + + t.Run("context deadline fires mid-execution", func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) + defer cancel() + + // 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) + }) } -func TestCommand_AddArgs(t *testing.T) { - cmd := NewCommand() - assert.Equal(t, []string(nil), cmd.args) +// 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{} +} - cmd.AddArgs("push") - cmd.AddArgs("origin", "master") - assert.Equal(t, []string{"push", "origin", "master"}, cmd.args) +func (r blockingReader) Read(p []byte) (int, error) { + <-r.cancel + return 0, io.EOF } -func TestCommand_AddEnvs(t *testing.T) { - cmd := NewCommand() - assert.Equal(t, []string(nil), cmd.envs) +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. + done := make(chan struct{}) + go func() { + time.Sleep(50 * time.Millisecond) + cancel() + close(done) + }() + + c, timeoutCancel := cmd(ctx, "", []string{"hash-object", "--stdin"}, nil) + defer timeoutCancel() - 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) + 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_RunWithTimeout(t *testing.T) { - _, err := NewCommand("version").WithTimeout(time.Nanosecond).Run() - assert.Equal(t, ErrExecTimeout, err) +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 := 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 e287df65..0edda64a 100644 --- a/commit.go +++ b/commit.go @@ -1,13 +1,9 @@ -// Copyright 2015 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - package git import ( "bytes" + "context" "io" - "io/ioutil" "net/http" "strings" "sync" @@ -27,9 +23,9 @@ type Commit struct { parents []*SHA1 *Tree - submodules Submodules - submodulesOnce sync.Once - submodulesErr error + submodules Submodules + submodulesMu sync.Mutex + submodulesSet bool } // Summary returns first line of commit message. @@ -54,56 +50,56 @@ func (c *Commit) ParentID(n int) (*SHA1, error) { // Parent returns the n-th parent commit (0-based) of this commit. It returns // ErrRevisionNotExist if no such parent exists. -func (c *Commit) Parent(n int, opts ...CatFileCommitOptions) (*Commit, error) { +func (c *Commit) Parent(ctx context.Context, n int, opts ...CatFileCommitOptions) (*Commit, error) { id, err := c.ParentID(n) if err != nil { return nil, err } - return c.repo.CatFileCommit(id.String(), opts...) + return c.repo.CatFileCommit(ctx, id.String(), opts...) } // CommitByPath returns the commit of the path in the state of this commit. -func (c *Commit) CommitByPath(opts ...CommitByRevisionOptions) (*Commit, error) { - return c.repo.CommitByRevision(c.ID.String(), opts...) +func (c *Commit) CommitByPath(ctx context.Context, opts ...CommitByRevisionOptions) (*Commit, error) { + return c.repo.CommitByRevision(ctx, c.ID.String(), opts...) } // CommitsByPage returns a paginated list of commits in the state of this // commit. The returned list is in reverse chronological order. -func (c *Commit) CommitsByPage(page, size int, opts ...CommitsByPageOptions) ([]*Commit, error) { - return c.repo.CommitsByPage(c.ID.String(), page, size, opts...) +func (c *Commit) CommitsByPage(ctx context.Context, page, size int, opts ...CommitsByPageOptions) ([]*Commit, error) { + return c.repo.CommitsByPage(ctx, c.ID.String(), page, size, opts...) } // SearchCommits searches commit message with given pattern. The returned list // is in reverse chronological order. -func (c *Commit) SearchCommits(pattern string, opts ...SearchCommitsOptions) ([]*Commit, error) { - return c.repo.SearchCommits(c.ID.String(), pattern, opts...) +func (c *Commit) SearchCommits(ctx context.Context, pattern string, opts ...SearchCommitsOptions) ([]*Commit, error) { + return c.repo.SearchCommits(ctx, c.ID.String(), pattern, opts...) } // ShowNameStatus returns name status of the commit. -func (c *Commit) ShowNameStatus(opts ...ShowNameStatusOptions) (*NameStatus, error) { - return c.repo.ShowNameStatus(c.ID.String(), opts...) +func (c *Commit) ShowNameStatus(ctx context.Context, opts ...ShowNameStatusOptions) (*NameStatus, error) { + return c.repo.ShowNameStatus(ctx, c.ID.String(), opts...) } // CommitsCount returns number of total commits up to this commit. -func (c *Commit) CommitsCount(opts ...RevListCountOptions) (int64, error) { - return c.repo.RevListCount([]string{c.ID.String()}, opts...) +func (c *Commit) CommitsCount(ctx context.Context, opts ...RevListCountOptions) (int64, error) { + return c.repo.RevListCount(ctx, []string{c.ID.String()}, opts...) } // FilesChangedAfter returns a list of files changed after given commit ID. -func (c *Commit) FilesChangedAfter(after string, opts ...DiffNameOnlyOptions) ([]string, error) { - return c.repo.DiffNameOnly(after, c.ID.String(), opts...) +func (c *Commit) FilesChangedAfter(ctx context.Context, after string, opts ...DiffNameOnlyOptions) ([]string, error) { + return c.repo.DiffNameOnly(ctx, after, c.ID.String(), opts...) } // CommitsAfter returns a list of commits after given commit ID up to this // commit. The returned list is in reverse chronological order. -func (c *Commit) CommitsAfter(after string, opts ...RevListOptions) ([]*Commit, error) { - return c.repo.RevList([]string{after + "..." + c.ID.String()}, opts...) +func (c *Commit) CommitsAfter(ctx context.Context, after string, opts ...RevListOptions) ([]*Commit, error) { + return c.repo.RevList(ctx, []string{after + "..." + c.ID.String()}, opts...) } // Ancestors returns a list of ancestors of this commit in reverse chronological // order. -func (c *Commit) Ancestors(opts ...LogOptions) ([]*Commit, error) { +func (c *Commit) Ancestors(ctx context.Context, opts ...LogOptions) ([]*Commit, error) { if c.ParentsCount() == 0 { return []*Commit{}, nil } @@ -115,7 +111,7 @@ func (c *Commit) Ancestors(opts ...LogOptions) ([]*Commit, error) { opt.Skip++ - return c.repo.Log(c.ID.String(), opt) + return c.repo.Log(ctx, c.ID.String(), opt) } type limitWriter struct { @@ -139,7 +135,7 @@ func (w *limitWriter) Write(p []byte) (int, error) { return len(p), err } -func (c *Commit) isImageFile(blob *Blob, err error) (bool, error) { +func (c *Commit) isImageFile(ctx context.Context, blob *Blob, err error) (bool, error) { if err != nil { if err == ErrNotBlob { return false, nil @@ -154,7 +150,7 @@ func (c *Commit) isImageFile(blob *Blob, err error) (bool, error) { N: int64(buf.Cap()), } - err = blob.Pipeline(stdout, ioutil.Discard) + err = blob.Pipe(ctx, stdout) if err != nil { return false, err } @@ -163,12 +159,14 @@ func (c *Commit) isImageFile(blob *Blob, err error) (bool, error) { } // IsImageFile returns true if the blob of the commit is an image by subpath. -func (c *Commit) IsImageFile(subpath string) (bool, error) { - return c.isImageFile(c.Blob(subpath)) +func (c *Commit) IsImageFile(ctx context.Context, subpath string) (bool, error) { + blob, err := c.Blob(ctx, subpath) + return c.isImageFile(ctx, blob, err) } // IsImageFileByIndex returns true if the blob of the commit is an image by // index. -func (c *Commit) IsImageFileByIndex(index string) (bool, error) { - return c.isImageFile(c.BlobByIndex(index)) +func (c *Commit) IsImageFileByIndex(ctx context.Context, index string) (bool, error) { + blob, err := c.BlobByIndex(ctx, index) + return c.isImageFile(ctx, blob, err) } diff --git a/commit_archive.go b/commit_archive.go index cbc45750..9bb34bfe 100644 --- a/commit_archive.go +++ b/commit_archive.go @@ -1,10 +1,7 @@ -// Copyright 2015 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - package git import ( + "context" "path/filepath" "strings" ) @@ -18,14 +15,20 @@ const ( ArchiveTarGz ArchiveFormat = "tar.gz" ) -// CreateArchive creates given format of archive to the destination. -func (c *Commit) CreateArchive(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("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 f9376821..8e29580f 100644 --- a/commit_archive_test.go +++ b/commit_archive_test.go @@ -1,10 +1,7 @@ -// Copyright 2020 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - package git import ( + "context" "os" "path/filepath" "strconv" @@ -18,13 +15,14 @@ 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, ArchiveTarGz, } { t.Run(string(format), func(t *testing.T) { - c, err := testrepo.CatFileCommit("755fd577edcfd9209d0ac072eed3b022cbe4d39b") + c, err := testrepo.CatFileCommit(ctx, "755fd577edcfd9209d0ac072eed3b022cbe4d39b") if err != nil { t.Fatal(err) } @@ -34,7 +32,7 @@ func TestCommit_CreateArchive(t *testing.T) { _ = os.Remove(dst) }() - assert.Nil(t, c.CreateArchive(format, dst)) + assert.Nil(t, c.Archive(ctx, format, dst)) }) } } diff --git a/commit_submodule.go b/commit_submodule.go index da63231e..59e85777 100644 --- a/commit_submodule.go +++ b/commit_submodule.go @@ -1,12 +1,9 @@ -// Copyright 2020 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - package git import ( "bufio" "bytes" + "context" "strings" ) @@ -23,68 +20,82 @@ type Submodule struct { // Submodules contains information of submodules. type Submodules = *objectCache -// Submodules returns submodules found in this commit. -func (c *Commit) Submodules() (Submodules, error) { - c.submodulesOnce.Do(func() { - var e *TreeEntry - e, c.submodulesErr = c.TreeEntry(".gitmodules") - if c.submodulesErr != nil { - return +// Submodules returns submodules found in this commit. Successful results are +// cached; failed attempts are not cached, allowing retries with a fresh context. +func (c *Commit) Submodules(ctx context.Context) (Submodules, error) { + c.submodulesMu.Lock() + defer c.submodulesMu.Unlock() + + if c.submodulesSet { + return c.submodules, nil + } + + e, err := c.TreeEntry(ctx, ".gitmodules") + if err != nil { + return nil, err + } + + p, err := e.Blob().Bytes(ctx) + if err != nil { + return nil, err + } + + scanner := bufio.NewScanner(bytes.NewReader(p)) + submodules := newObjectCache() + var inSection bool + var path string + var url string + for scanner.Scan() { + if strings.HasPrefix(scanner.Text(), "[submodule") { + inSection = true + path = "" + url = "" + continue + } else if !inSection { + continue } - var p []byte - p, c.submodulesErr = e.Blob().Bytes() - if c.submodulesErr != nil { - return + key, value, ok := strings.Cut(scanner.Text(), "=") + if !ok { + continue } - scanner := bufio.NewScanner(bytes.NewReader(p)) - c.submodules = newObjectCache() - var inSection bool - var path string - var url string - for scanner.Scan() { - if strings.HasPrefix(scanner.Text(), "[submodule") { - inSection = true - path = "" - url = "" - continue - } else if !inSection { - continue - } + switch strings.TrimSpace(key) { + case "path": + path = strings.TrimSpace(value) + case "url": + url = strings.TrimSpace(value) + } - fields := strings.Split(scanner.Text(), "=") - switch strings.TrimSpace(fields[0]) { - case "path": - path = strings.TrimSpace(fields[1]) - case "url": - url = strings.TrimSpace(fields[1]) + if len(path) > 0 && len(url) > 0 { + mod := &Submodule{ + Name: path, + URL: url, } - if len(path) > 0 && len(url) > 0 { - mod := &Submodule{ - Name: path, - URL: url, - } - - mod.Commit, c.submodulesErr = c.repo.RevParse(c.id.String() + ":" + mod.Name) - if c.submodulesErr != nil { - return - } - - c.submodules.Set(path, mod) - inSection = false + mod.Commit, err = c.repo.RevParse(ctx, c.id.String()+":"+mod.Name) + if err != nil { + return nil, err } + + submodules.Set(path, mod) + inSection = false } - }) + } + + if err := scanner.Err(); err != nil { + return nil, err + } - return c.submodules, c.submodulesErr + c.submodules = submodules + c.submodulesSet = true + return c.submodules, nil } // Submodule returns submodule by given name. It returns an ErrSubmoduleNotExist // if the path does not exist as a submodule. -func (c *Commit) Submodule(path string) (*Submodule, error) { - mods, err := c.Submodules() +func (c *Commit) Submodule(ctx context.Context, path string) (*Submodule, error) { + mods, err := c.Submodules(ctx) if err != nil { return nil, err } diff --git a/commit_submodule_test.go b/commit_submodule_test.go index cecc661d..b9516796 100644 --- a/commit_submodule_test.go +++ b/commit_submodule_test.go @@ -1,22 +1,21 @@ -// Copyright 2020 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - package git import ( + "context" "testing" "github.com/stretchr/testify/assert" ) func TestCommit_Submodule(t *testing.T) { - c, err := testrepo.CatFileCommit("4e59b72440188e7c2578299fc28ea425fbe9aece") + ctx := context.Background() + + c, err := testrepo.CatFileCommit(ctx, "4e59b72440188e7c2578299fc28ea425fbe9aece") if err != nil { t.Fatal(err) } - mod, err := c.Submodule("gogs/docs-api") + mod, err := c.Submodule(ctx, "gogs/docs-api") if err != nil { t.Fatal(err) } @@ -24,6 +23,6 @@ func TestCommit_Submodule(t *testing.T) { assert.Equal(t, "https://github.com/gogs/docs-api.git", mod.URL) assert.Equal(t, "6b08f76a5313fa3d26859515b30aa17a5faa2807", mod.Commit) - _, err = c.Submodule("404") + _, err = c.Submodule(ctx, "404") assert.Equal(t, ErrSubmoduleNotExist, err) } diff --git a/commit_test.go b/commit_test.go index a9d9cbfb..3a752451 100644 --- a/commit_test.go +++ b/commit_test.go @@ -1,17 +1,15 @@ -// Copyright 2020 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - package git import ( + "context" "testing" "github.com/stretchr/testify/assert" ) func TestCommit(t *testing.T) { - c, err := testrepo.CatFileCommit("435ffceb7ba576c937e922766e37d4f7abdcc122") + ctx := context.Background() + c, err := testrepo.CatFileCommit(ctx, "435ffceb7ba576c937e922766e37d4f7abdcc122") if err != nil { t.Fatal(err) } @@ -25,7 +23,8 @@ func TestCommit(t *testing.T) { } func TestCommit_Parent(t *testing.T) { - c, err := testrepo.CatFileCommit("435ffceb7ba576c937e922766e37d4f7abdcc122") + ctx := context.Background() + c, err := testrepo.CatFileCommit(ctx, "435ffceb7ba576c937e922766e37d4f7abdcc122") if err != nil { t.Fatal(err) } @@ -36,7 +35,7 @@ func TestCommit_Parent(t *testing.T) { t.Run("Parent", func(t *testing.T) { t.Run("no such parent", func(t *testing.T) { - _, err := c.Parent(c.ParentsCount() + 1) + _, err := c.Parent(ctx, c.ParentsCount()+1) assert.Equal(t, ErrParentNotExist, err) }) @@ -55,7 +54,7 @@ func TestCommit_Parent(t *testing.T) { } for _, test := range tests { t.Run("", func(t *testing.T) { - p, err := c.Parent(test.n) + p, err := c.Parent(ctx, test.n) if err != nil { t.Fatal(err) } @@ -66,6 +65,7 @@ func TestCommit_Parent(t *testing.T) { } func TestCommit_CommitByPath(t *testing.T) { + ctx := context.Background() tests := []struct { id string opt CommitByRevisionOptions @@ -88,12 +88,12 @@ func TestCommit_CommitByPath(t *testing.T) { } for _, test := range tests { t.Run("", func(t *testing.T) { - c, err := testrepo.CatFileCommit(test.id) + c, err := testrepo.CatFileCommit(ctx, test.id) if err != nil { t.Fatal(err) } - cc, err := c.CommitByPath(test.opt) + cc, err := c.CommitByPath(ctx, test.opt) if err != nil { t.Fatal(err) } @@ -113,8 +113,9 @@ func commitsToIDs(commits []*Commit) []string { } func TestCommit_CommitsByPage(t *testing.T) { + ctx := context.Background() // There are at most 5 commits can be used for pagination before this commit. - c, err := testrepo.CatFileCommit("f5ed01959cffa4758ca0a49bf4c34b138d7eab0a") + c, err := testrepo.CatFileCommit(ctx, "f5ed01959cffa4758ca0a49bf4c34b138d7eab0a") if err != nil { t.Fatal(err) } @@ -175,7 +176,7 @@ func TestCommit_CommitsByPage(t *testing.T) { } for _, test := range tests { t.Run("", func(t *testing.T) { - commits, err := c.CommitsByPage(test.page, test.size, test.opt) + commits, err := c.CommitsByPage(ctx, test.page, test.size, test.opt) if err != nil { t.Fatal(err) } @@ -186,6 +187,7 @@ func TestCommit_CommitsByPage(t *testing.T) { } func TestCommit_SearchCommits(t *testing.T) { + ctx := context.Background() tests := []struct { id string pattern string @@ -271,12 +273,12 @@ func TestCommit_SearchCommits(t *testing.T) { } for _, test := range tests { t.Run("", func(t *testing.T) { - c, err := testrepo.CatFileCommit(test.id) + c, err := testrepo.CatFileCommit(ctx, test.id) if err != nil { t.Fatal(err) } - commits, err := c.SearchCommits(test.pattern, test.opt) + commits, err := c.SearchCommits(ctx, test.pattern, test.opt) if err != nil { t.Fatal(err) } @@ -287,6 +289,7 @@ func TestCommit_SearchCommits(t *testing.T) { } func TestCommit_ShowNameStatus(t *testing.T) { + ctx := context.Background() tests := []struct { id string opt ShowNameStatusOptions @@ -332,12 +335,12 @@ func TestCommit_ShowNameStatus(t *testing.T) { } for _, test := range tests { t.Run("", func(t *testing.T) { - c, err := testrepo.CatFileCommit(test.id) + c, err := testrepo.CatFileCommit(ctx, test.id) if err != nil { t.Fatal(err) } - status, err := c.ShowNameStatus(test.opt) + status, err := c.ShowNameStatus(ctx, test.opt) if err != nil { t.Fatal(err) } @@ -348,6 +351,7 @@ func TestCommit_ShowNameStatus(t *testing.T) { } func TestCommit_CommitsCount(t *testing.T) { + ctx := context.Background() tests := []struct { id string opt RevListCountOptions @@ -383,12 +387,12 @@ func TestCommit_CommitsCount(t *testing.T) { } for _, test := range tests { t.Run("", func(t *testing.T) { - c, err := testrepo.CatFileCommit(test.id) + c, err := testrepo.CatFileCommit(ctx, test.id) if err != nil { t.Fatal(err) } - count, err := c.CommitsCount(test.opt) + count, err := c.CommitsCount(ctx, test.opt) if err != nil { t.Fatal(err) } @@ -399,6 +403,7 @@ func TestCommit_CommitsCount(t *testing.T) { } func TestCommit_FilesChangedAfter(t *testing.T) { + ctx := context.Background() tests := []struct { id string after string @@ -435,12 +440,12 @@ func TestCommit_FilesChangedAfter(t *testing.T) { } for _, test := range tests { t.Run("", func(t *testing.T) { - c, err := testrepo.CatFileCommit(test.id) + c, err := testrepo.CatFileCommit(ctx, test.id) if err != nil { t.Fatal(err) } - files, err := c.FilesChangedAfter(test.after, test.opt) + files, err := c.FilesChangedAfter(ctx, test.after, test.opt) if err != nil { t.Fatal(err) } @@ -451,6 +456,7 @@ func TestCommit_FilesChangedAfter(t *testing.T) { } func TestCommit_CommitsAfter(t *testing.T) { + ctx := context.Background() tests := []struct { id string after string @@ -479,12 +485,12 @@ func TestCommit_CommitsAfter(t *testing.T) { } for _, test := range tests { t.Run("", func(t *testing.T) { - c, err := testrepo.CatFileCommit(test.id) + c, err := testrepo.CatFileCommit(ctx, test.id) if err != nil { t.Fatal(err) } - commits, err := c.CommitsAfter(test.after, test.opt) + commits, err := c.CommitsAfter(ctx, test.after, test.opt) if err != nil { t.Fatal(err) } @@ -495,6 +501,7 @@ func TestCommit_CommitsAfter(t *testing.T) { } func TestCommit_Ancestors(t *testing.T) { + ctx := context.Background() tests := []struct { id string opt LogOptions @@ -518,12 +525,12 @@ func TestCommit_Ancestors(t *testing.T) { } for _, test := range tests { t.Run("", func(t *testing.T) { - c, err := testrepo.CatFileCommit(test.id) + c, err := testrepo.CatFileCommit(ctx, test.id) if err != nil { t.Fatal(err) } - commits, err := c.Ancestors(test.opt) + commits, err := c.Ancestors(ctx, test.opt) if err != nil { t.Fatal(err) } @@ -534,13 +541,15 @@ func TestCommit_Ancestors(t *testing.T) { } func TestCommit_IsImageFile(t *testing.T) { + ctx := context.Background() + t.Run("not a blob", func(t *testing.T) { - c, err := testrepo.CatFileCommit("4e59b72440188e7c2578299fc28ea425fbe9aece") + c, err := testrepo.CatFileCommit(ctx, "4e59b72440188e7c2578299fc28ea425fbe9aece") if err != nil { t.Fatal(err) } - isImage, err := c.IsImageFile("gogs/docs-api") + isImage, err := c.IsImageFile(ctx, "gogs/docs-api") if err != nil { t.Fatal(err) } @@ -565,12 +574,12 @@ func TestCommit_IsImageFile(t *testing.T) { } for _, test := range tests { t.Run("", func(t *testing.T) { - c, err := testrepo.CatFileCommit(test.id) + c, err := testrepo.CatFileCommit(ctx, test.id) if err != nil { t.Fatal(err) } - isImage, err := c.IsImageFile(test.name) + isImage, err := c.IsImageFile(ctx, test.name) if err != nil { t.Fatal(err) } @@ -581,13 +590,15 @@ func TestCommit_IsImageFile(t *testing.T) { } func TestCommit_IsImageFileByIndex(t *testing.T) { + ctx := context.Background() + t.Run("not a blob", func(t *testing.T) { - c, err := testrepo.CatFileCommit("4e59b72440188e7c2578299fc28ea425fbe9aece") + c, err := testrepo.CatFileCommit(ctx, "4e59b72440188e7c2578299fc28ea425fbe9aece") if err != nil { t.Fatal(err) } - isImage, err := c.IsImageFileByIndex("fcf7087e732bfe3c25328248a9bf8c3ccd85bed4") // "gogs" + isImage, err := c.IsImageFileByIndex(ctx, "fcf7087e732bfe3c25328248a9bf8c3ccd85bed4") // "gogs" if err != nil { t.Fatal(err) } @@ -612,12 +623,12 @@ func TestCommit_IsImageFileByIndex(t *testing.T) { } for _, test := range tests { t.Run("", func(t *testing.T) { - c, err := testrepo.CatFileCommit(test.id) + c, err := testrepo.CatFileCommit(ctx, test.id) if err != nil { t.Fatal(err) } - isImage, err := c.IsImageFileByIndex(test.index) + isImage, err := c.IsImageFileByIndex(ctx, test.index) if err != nil { t.Fatal(err) } diff --git a/diff.go b/diff.go index 3dab3733..8b339c09 100644 --- a/diff.go +++ b/diff.go @@ -1,7 +1,3 @@ -// Copyright 2020 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - package git import ( @@ -10,7 +6,6 @@ import ( "errors" "fmt" "io" - "io/ioutil" "strconv" "strings" ) @@ -482,7 +477,7 @@ func (p *diffParser) parse() (*Diff, error) { // Check if reached maximum number of files if p.maxFiles > 0 && len(diff.Files) >= p.maxFiles { diff.isIncomplete = true - _, _ = io.Copy(ioutil.Discard, p) + _, _ = io.Copy(io.Discard, p) break } diff --git a/diff_test.go b/diff_test.go index db8a7b51..05d38333 100644 --- a/diff_test.go +++ b/diff_test.go @@ -1,7 +1,3 @@ -// Copyright 2020 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - package git import ( diff --git a/error.go b/error.go index dbbf7769..85a2c90a 100644 --- a/error.go +++ b/error.go @@ -1,7 +1,3 @@ -// Copyright 2015 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - package git import ( diff --git a/git.go b/git.go index 31955b3c..5de2d7a1 100644 --- a/git.go +++ b/git.go @@ -1,10 +1,7 @@ -// Copyright 2015 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - package git import ( + "context" "fmt" "io" "strings" @@ -28,7 +25,7 @@ func SetPrefix(prefix string) { logPrefix = prefix } -func log(format string, args ...interface{}) { +func logf(format string, args ...interface{}) { if logOutput == nil { return } @@ -41,35 +38,40 @@ func log(format string, args ...interface{}) { var ( // gitVersion stores the Git binary version. // NOTE: To check Git version should call BinVersion not this global variable. - gitVersion string - gitVersionOnce sync.Once - gitVersionErr error + gitVersion string + gitVersionMu sync.Mutex + gitVersionSet bool ) // BinVersion returns current Git binary version that is used by this module. -func BinVersion() (string, error) { - gitVersionOnce.Do(func() { - var stdout []byte - stdout, gitVersionErr = NewCommand("version").Run() - if gitVersionErr != nil { - return - } +// Successful results are cached; failed attempts are not cached, allowing +// retries with a fresh context. +func BinVersion(ctx context.Context) (string, error) { + gitVersionMu.Lock() + defer gitVersionMu.Unlock() + + if gitVersionSet { + return gitVersion, nil + } - fields := strings.Fields(string(stdout)) - if len(fields) < 3 { - gitVersionErr = fmt.Errorf("not enough output: %s", stdout) - return - } + stdout, err := exec(ctx, "", []string{"version"}, nil) + if err != nil { + return "", err + } - // Handle special case on Windows. - i := strings.Index(fields[2], "windows") - if i >= 1 { - gitVersion = fields[2][:i-1] - return - } + fields := strings.Fields(string(stdout)) + if len(fields) < 3 { + return "", fmt.Errorf("not enough output: %s", stdout) + } + // Handle special case on Windows. + i := strings.Index(fields[2], "windows") + if i >= 1 { + gitVersion = fields[2][:i-1] + } else { gitVersion = fields[2] - }) + } - return gitVersion, gitVersionErr + gitVersionSet = true + return gitVersion, nil } diff --git a/git_test.go b/git_test.go index 13adc294..165f64e8 100644 --- a/git_test.go +++ b/git_test.go @@ -1,20 +1,14 @@ -// Copyright 2020 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - package git import ( "bytes" + "context" "flag" - "fmt" stdlog "log" "os" "testing" - goversion "github.com/mcuadros/go-version" "github.com/stretchr/testify/assert" - "golang.org/x/sync/errgroup" ) const repoPath = "testdata/testrepo.git" @@ -28,9 +22,11 @@ func TestMain(m *testing.M) { SetOutput(os.Stdout) } + ctx := context.Background() + // Set up the test repository if !isExist(repoPath) { - if err := Clone("https://github.com/gogs/git-module-testrepo.git", repoPath, CloneOptions{ + if err := Clone(ctx, "https://github.com/gogs/git-module-testrepo.git", repoPath, CloneOptions{ Bare: true, }); err != nil { stdlog.Fatal(err) @@ -83,26 +79,8 @@ 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()) }) } } - -func TestBinVersion(t *testing.T) { - g := errgroup.Group{} - for i := 0; i < 30; i++ { - g.Go(func() error { - version, err := BinVersion() - assert.Nil(t, err) - - if !goversion.Compare(version, "1.8.3", ">=") { - return fmt.Errorf("version: expected >= 1.8.3 but got %q", version) - } - return nil - }) - } - if err := g.Wait(); err != nil { - t.Fatal(err) - } -} diff --git a/go.mod b/go.mod index de2137ae..52cd9060 100644 --- a/go.mod +++ b/go.mod @@ -1,15 +1,24 @@ -module github.com/gogs/git-module +module github.com/gogs/git-module/v2 -go 1.24.0 +go 1.26.0 require ( - github.com/mcuadros/go-version v0.0.0-20190308113854-92cdf37c5b75 + github.com/sourcegraph/run v0.12.0 github.com/stretchr/testify v1.11.1 - golang.org/x/sync v0.19.0 ) 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 bce0f20a..2957c960 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +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/mcuadros/go-version v0.0.0-20190308113854-92cdf37c5b75 h1:Pijfgr7ZuvX7QIQiEwLdRVr3RoMG+i0SbBO1Qu+7yVk= -github.com/mcuadros/go-version v0.0.0-20190308113854-92cdf37c5b75/go.mod h1:76rfSfYPWj01Z85hUf/ituArm797mNKcvINh1OlsZKo= +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= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +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/hook.go b/hook.go index 566d5381..0c602f8b 100644 --- a/hook.go +++ b/hook.go @@ -1,11 +1,6 @@ -// Copyright 2015 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - package git import ( - "io/ioutil" "os" "path" "strings" @@ -235,11 +230,11 @@ func (h *Hook) Content() string { // memory copy of the content as well. func (h *Hook) Update(content string) error { h.content = strings.TrimSpace(content) - h.content = strings.Replace(h.content, "\r", "", -1) + h.content = strings.ReplaceAll(h.content, "\r", "") if err := os.MkdirAll(path.Dir(h.path), os.ModePerm); err != nil { return err - } else if err = ioutil.WriteFile(h.path, []byte(h.content), os.ModePerm); err != nil { + } else if err = os.WriteFile(h.path, []byte(h.content), os.ModePerm); err != nil { return err } diff --git a/hook_test.go b/hook_test.go index 0eda6b4b..83596a3e 100644 --- a/hook_test.go +++ b/hook_test.go @@ -1,11 +1,6 @@ -// Copyright 2020 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - package git import ( - "io/ioutil" "os" "testing" @@ -43,7 +38,7 @@ func TestHook_Update(t *testing.T) { t.Fatal(err) } - p, err := ioutil.ReadFile(path) + p, err := os.ReadFile(path) if err != nil { t.Fatal(err) } diff --git a/object.go b/object.go index d85f9514..af2b491c 100644 --- a/object.go +++ b/object.go @@ -1,7 +1,3 @@ -// Copyright 2014 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - package git // ObjectType is the type of a Git objet. diff --git a/repo.go b/repo.go index dd327131..140ece35 100644 --- a/repo.go +++ b/repo.go @@ -1,12 +1,9 @@ -// Copyright 2015 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - package git import ( "bufio" "bytes" + "context" "fmt" "io" "os" @@ -14,7 +11,6 @@ import ( "path/filepath" "strconv" "strings" - "time" ) // Repository contains information of a Git repository. @@ -35,7 +31,7 @@ const LogFormatHashOnly = `format:%H` // parsePrettyFormatLogToList returns a list of commits parsed from given logs // that are formatted in LogFormatHashOnly. -func (r *Repository) parsePrettyFormatLogToList(timeout time.Duration, logs []byte) ([]*Commit, error) { +func (r *Repository) parsePrettyFormatLogToList(ctx context.Context, logs []byte) ([]*Commit, error) { if len(logs) == 0 { return []*Commit{}, nil } @@ -44,7 +40,7 @@ func (r *Repository) parsePrettyFormatLogToList(timeout time.Duration, logs []by ids := bytes.Split(logs, []byte{'\n'}) commits := make([]*Commit, len(ids)) for i, id := range ids { - commits[i], err = r.CatFileCommit(string(id), CatFileCommitOptions{Timeout: timeout}) + commits[i], err = r.CatFileCommit(ctx, string(id)) if err != nil { return nil, err } @@ -58,17 +54,11 @@ func (r *Repository) parsePrettyFormatLogToList(timeout time.Duration, logs []by type InitOptions struct { // Indicates whether the repository should be initialized in bare format. 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 } // Init initializes a new Git repository. -func Init(path string, opts ...InitOptions) error { +func Init(ctx context.Context, path string, opts ...InitOptions) error { var opt InitOptions if len(opts) > 0 { opt = opts[0] @@ -79,12 +69,12 @@ func Init(path string, opts ...InitOptions) error { return err } - cmd := NewCommand("init").AddOptions(opt.CommandOptions) + args := []string{"init"} if opt.Bare { - cmd.AddArgs("--bare") + args = append(args, "--bare") } - cmd.AddArgs("--end-of-options") - _, err = cmd.RunInDirWithTimeout(opt.Timeout, path) + args = append(args, "--end-of-options") + _, err = exec(ctx, path, args, opt.Envs) return err } @@ -120,17 +110,11 @@ type CloneOptions struct { Branch string // The number of revisions to clone. 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 } // Clone clones the repository from remote URL to the destination. -func Clone(url, dst string, opts ...CloneOptions) error { +func Clone(ctx context.Context, url, dst string, opts ...CloneOptions) error { var opt CloneOptions if len(opts) > 0 { opt = opts[0] @@ -141,25 +125,25 @@ func Clone(url, dst string, opts ...CloneOptions) error { return err } - cmd := NewCommand("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).RunWithTimeout(opt.Timeout) + args = append(args, "--end-of-options", url, dst) + _, err = exec(ctx, "", args, opt.Envs) return err } @@ -169,28 +153,23 @@ func Clone(url, dst string, opts ...CloneOptions) error { type FetchOptions struct { // Indicates whether to prune during fetching. 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 } // Fetch fetches updates for the repository. -func (r *Repository) Fetch(opts ...FetchOptions) error { +func (r *Repository) Fetch(ctx context.Context, opts ...FetchOptions) error { var opt FetchOptions if len(opts) > 0 { opt = opts[0] } - cmd := NewCommand("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.RunInDirWithTimeout(opt.Timeout, r.path) + _, err := exec(ctx, r.path, args, opt.Envs) return err } @@ -206,37 +185,32 @@ type PullOptions struct { Remote string // The branch to pull updates from when All=false and Remote is supplied. 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 } // Pull pulls updates for the repository. -func (r *Repository) Pull(opts ...PullOptions) error { +func (r *Repository) Pull(ctx context.Context, opts ...PullOptions) error { var opt PullOptions if len(opts) > 0 { opt = opts[0] } - cmd := NewCommand("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.RunInDirWithTimeout(opt.Timeout, r.path) + _, err := exec(ctx, r.path, args, opt.Envs) return err } @@ -244,161 +218,101 @@ func (r *Repository) Pull(opts ...PullOptions) error { // // Docs: https://git-scm.com/docs/git-push 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. + // Indicates whether to set upstream tracking for the branch. + SetUpstream bool CommandOptions } -// Push pushes local changes to given remote and branch for the repository in -// given path. -func Push(repoPath, remote, branch string, opts ...PushOptions) error { +// Push pushes local changes to given remote and branch for the repository. +func (r *Repository) Push(ctx context.Context, remote, branch string, opts ...PushOptions) error { var opt PushOptions if len(opts) > 0 { opt = opts[0] } - cmd := NewCommand("push").AddOptions(opt.CommandOptions).AddArgs("--end-of-options", remote, branch) - _, err := cmd.RunInDirWithTimeout(opt.Timeout, repoPath) + args := []string{"push"} + if opt.SetUpstream { + args = append(args, "-u") + } + args = append(args, "--end-of-options", remote, branch) + _, err := exec(ctx, r.path, args, opt.Envs) return err } -// Deprecated: Use Push instead. -func RepoPush(repoPath, remote, branch string, opts ...PushOptions) error { - return Push(repoPath, remote, branch, opts...) -} - -// Push pushes local changes to given remote and branch for the repository. -func (r *Repository) Push(remote, branch string, opts ...PushOptions) error { - return Push(r.path, remote, branch, opts...) -} - // CheckoutOptions contains optional arguments for checking out to a branch. // // Docs: https://git-scm.com/docs/git-checkout type CheckoutOptions struct { // The base branch if checks out to a new branch. 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 } -// Checkout checks out to given branch for the repository in given path. -func Checkout(repoPath, branch string, opts ...CheckoutOptions) error { +// Checkout checks out to given branch for the repository. +func (r *Repository) Checkout(ctx context.Context, branch string, opts ...CheckoutOptions) error { var opt CheckoutOptions if len(opts) > 0 { opt = opts[0] } - cmd := NewCommand("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.RunInDirWithTimeout(opt.Timeout, repoPath) + _, err := exec(ctx, r.path, args, opt.Envs) return err } -// Deprecated: Use Checkout instead. -func RepoCheckout(repoPath, branch string, opts ...CheckoutOptions) error { - return Checkout(repoPath, branch, opts...) -} - -// Checkout checks out to given branch for the repository. -func (r *Repository) Checkout(branch string, opts ...CheckoutOptions) error { - return Checkout(r.path, branch, opts...) -} - // ResetOptions contains optional arguments for resetting a branch. // // Docs: https://git-scm.com/docs/git-reset type ResetOptions struct { // Indicates whether to perform a hard reset. 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 } -// Reset resets working tree to given revision for the repository in given path. -func Reset(repoPath, rev string, opts ...ResetOptions) error { +// Reset resets working tree to given revision for the repository. +func (r *Repository) Reset(ctx context.Context, rev string, opts ...ResetOptions) error { var opt ResetOptions if len(opts) > 0 { opt = opts[0] } - cmd := NewCommand("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(repoPath) + _, err := exec(ctx, r.path, args, opt.Envs) return err } -// Deprecated: Use Reset instead. -func RepoReset(repoPath, rev string, opts ...ResetOptions) error { - return Reset(repoPath, rev, opts...) -} - -// Reset resets working tree to given revision for the repository. -func (r *Repository) Reset(rev string, opts ...ResetOptions) error { - return Reset(r.path, rev, opts...) -} - // MoveOptions contains optional arguments for moving a file, a directory, or a // symlink. // // Docs: https://git-scm.com/docs/git-mv 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 } // Move moves a file, a directory, or a symlink file or directory from source to -// destination for the repository in given path. -func Move(repoPath, src, dst string, opts ...MoveOptions) error { +// destination for the repository. +func (r *Repository) Move(ctx context.Context, src, dst string, opts ...MoveOptions) error { var opt MoveOptions if len(opts) > 0 { opt = opts[0] } - _, err := NewCommand("mv").AddOptions(opt.CommandOptions).AddArgs("--end-of-options", src, dst).RunInDirWithTimeout(opt.Timeout, repoPath) + args := []string{"mv", "--end-of-options", src, dst} + _, err := exec(ctx, r.path, args, opt.Envs) return err } -// Deprecated: Use Move instead. -func RepoMove(repoPath, src, dst string, opts ...MoveOptions) error { - return Move(repoPath, src, dst, opts...) -} - -// Move moves a file, a directory, or a symlink file or directory from source to -// destination for the repository. -func (r *Repository) Move(src, dst string, opts ...MoveOptions) error { - return Move(r.path, src, dst, opts...) -} - // AddOptions contains optional arguments for adding local changes. // // Docs: https://git-scm.com/docs/git-add @@ -407,96 +321,65 @@ type AddOptions struct { All bool // The specific pathspecs to be added to index. 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 } -// Add adds local changes to index for the repository in given path. -func Add(repoPath string, opts ...AddOptions) error { +// Add adds local changes to index for the repository. +func (r *Repository) Add(ctx context.Context, opts ...AddOptions) error { var opt AddOptions if len(opts) > 0 { opt = opts[0] } - cmd := NewCommand("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.RunInDirWithTimeout(opt.Timeout, repoPath) + _, err := exec(ctx, r.path, args, opt.Envs) return err } -// Deprecated: Use Add instead. -func RepoAdd(repoPath string, opts ...AddOptions) error { - return Add(repoPath, opts...) -} - -// Add adds local changes to index for the repository. -func (r *Repository) Add(opts ...AddOptions) error { - return Add(r.path, opts...) -} - // CommitOptions contains optional arguments to commit changes. // // Docs: https://git-scm.com/docs/git-commit type CommitOptions struct { // Author is the author of the changes if that's not the same as committer. 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 } -// CreateCommit commits local changes with given author, committer and message -// for the repository in given path. -func CreateCommit(repoPath string, committer *Signature, message string, opts ...CommitOptions) error { +// Commit commits local changes with given author, committer and message for the +// repository. +func (r *Repository) Commit(ctx context.Context, committer *Signature, message string, opts ...CommitOptions) error { var opt CommitOptions if len(opts) > 0 { opt = opts[0] } - cmd := NewCommand("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.RunInDirWithTimeout(opt.Timeout, repoPath) + 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 } -// Deprecated: Use CreateCommit instead. -func RepoCommit(repoPath string, committer *Signature, message string, opts ...CommitOptions) error { - return CreateCommit(repoPath, committer, message, opts...) -} - -// Commit commits local changes with given author, committer and message for the -// repository. -func (r *Repository) Commit(committer *Signature, message string, opts ...CommitOptions) error { - return CreateCommit(r.path, committer, message, opts...) -} - // NameStatus contains name status of a commit. type NameStatus struct { Added []string @@ -508,18 +391,11 @@ type NameStatus struct { // // Docs: https://git-scm.com/docs/git-show#Documentation/git-show.txt---name-status 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 } -// ShowNameStatus returns name status of given revision of the repository in -// given path. -func ShowNameStatus(repoPath, rev string, opts ...ShowNameStatusOptions) (*NameStatus, error) { +// ShowNameStatus returns name status of given revision of the repository. +func (r *Repository) ShowNameStatus(ctx context.Context, rev string, opts ...ShowNameStatusOptions) (*NameStatus, error) { var opt ShowNameStatusOptions if len(opts) > 0 { opt = opts[0] @@ -548,57 +424,38 @@ func ShowNameStatus(repoPath, rev string, opts ...ShowNameStatusOptions) (*NameS done <- struct{}{} }() - stderr := new(bytes.Buffer) - cmd := NewCommand("show", "--name-status", "--pretty=format:''"). - AddOptions(opt.CommandOptions). - AddArgs("--end-of-options", rev) - err := cmd.RunInDirPipelineWithTimeout(opt.Timeout, w, stderr, repoPath) + 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 return fileStatus, nil } -// Deprecated: Use ShowNameStatus instead. -func RepoShowNameStatus(repoPath, rev string, opts ...ShowNameStatusOptions) (*NameStatus, error) { - return ShowNameStatus(repoPath, rev, opts...) -} - -// ShowNameStatus returns name status of given revision of the repository. -func (r *Repository) ShowNameStatus(rev string, opts ...ShowNameStatusOptions) (*NameStatus, error) { - return ShowNameStatus(r.path, rev, opts...) -} - // RevParseOptions contains optional arguments for parsing revision. // // Docs: https://git-scm.com/docs/git-rev-parse 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 } // RevParse returns full length (40) commit ID by given revision in the // repository. -func (r *Repository) RevParse(rev string, opts ...RevParseOptions) (string, error) { +func (r *Repository) RevParse(ctx context.Context, rev string, opts ...RevParseOptions) (string, error) { var opt RevParseOptions if len(opts) > 0 { opt = opts[0] } - commitID, err := NewCommand("rev-parse"). - AddOptions(opt.CommandOptions). - AddArgs(rev). - RunInDirWithTimeout(opt.Timeout, 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 @@ -622,25 +479,19 @@ type CountObject struct { // // Docs: https://git-scm.com/docs/git-count-objects 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 } -// CountObjects returns disk usage report of the repository in given path. -func CountObjects(repoPath string, opts ...CountObjectsOptions) (*CountObject, error) { +// CountObjects returns disk usage report of the repository. +func (r *Repository) CountObjects(ctx context.Context, opts ...CountObjectsOptions) (*CountObject, error) { var opt CountObjectsOptions if len(opts) > 0 { opt = opts[0] } - stdout, err := NewCommand("count-objects", "-v"). - AddOptions(opt.CommandOptions). - RunInDirWithTimeout(opt.Timeout, repoPath) + args := []string{"count-objects", "-v", "--end-of-options"} + + stdout, err := exec(ctx, r.path, args, opt.Envs) if err != nil { return nil, err } @@ -675,49 +526,22 @@ func CountObjects(repoPath string, opts ...CountObjectsOptions) (*CountObject, e return countObject, nil } -// Deprecated: Use CountObjects instead. -func RepoCountObjects(repoPath string, opts ...CountObjectsOptions) (*CountObject, error) { - return CountObjects(repoPath, opts...) -} - -// CountObjects returns disk usage report of the repository. -func (r *Repository) CountObjects(opts ...CountObjectsOptions) (*CountObject, error) { - return CountObjects(r.path, opts...) -} - // FsckOptions contains optional arguments for verifying the objects. // // Docs: https://git-scm.com/docs/git-fsck 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 } // Fsck verifies the connectivity and validity of the objects in the database -// for the repository in given path. -func Fsck(repoPath string, opts ...FsckOptions) error { +// for the repository. +func (r *Repository) Fsck(ctx context.Context, opts ...FsckOptions) error { var opt FsckOptions if len(opts) > 0 { opt = opts[0] } - cmd := NewCommand("fsck").AddOptions(opt.CommandOptions) - _, err := cmd.RunInDirWithTimeout(opt.Timeout, repoPath) + args := []string{"fsck", "--end-of-options"} + _, err := exec(ctx, r.path, args, opt.Envs) return err } - -// Deprecated: Use Fsck instead. -func RepoFsck(repoPath string, opts ...FsckOptions) error { - return Fsck(repoPath, opts...) -} - -// Fsck verifies the connectivity and validity of the objects in the database -// for the repository. -func (r *Repository) Fsck(opts ...FsckOptions) error { - return Fsck(r.path, opts...) -} diff --git a/repo_blame.go b/repo_blame.go index 9ac4caee..efdd368c 100644 --- a/repo_blame.go +++ b/repo_blame.go @@ -1,38 +1,26 @@ -// Copyright 2020 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - package git import ( "bytes" - "time" + "context" ) // BlameOptions contains optional arguments for blaming a file. // Docs: https://git-scm.com/docs/git-blame 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 } -// BlameFile returns blame results of the file with the given revision of the +// Blame returns blame results of the file with the given revision of the // repository. -func (r *Repository) BlameFile(rev, file string, opts ...BlameOptions) (*Blame, error) { +func (r *Repository) Blame(ctx context.Context, rev, file string, opts ...BlameOptions) (*Blame, error) { var opt BlameOptions if len(opts) > 0 { opt = opts[0] } - stdout, err := NewCommand("blame"). - AddOptions(opt.CommandOptions). - AddArgs("-l", "-s", rev, "--", file). - RunInDirWithTimeout(opt.Timeout, r.path) + args := []string{"blame", "-l", "-s", rev, "--", file} + stdout, err := exec(ctx, r.path, args, opt.Envs) if err != nil { return nil, err } @@ -51,7 +39,7 @@ func (r *Repository) BlameFile(rev, file string, opts ...BlameOptions) (*Blame, if id[0] == '^' { id = id[1:] } - commit, err := r.CatFileCommit(string(id), CatFileCommitOptions{Timeout: opt.Timeout}) //nolint + commit, err := r.CatFileCommit(ctx, string(id)) //nolint if err != nil { return nil, err } diff --git a/repo_blame_test.go b/repo_blame_test.go index ec9b45db..133ea4c5 100644 --- a/repo_blame_test.go +++ b/repo_blame_test.go @@ -1,23 +1,22 @@ -// Copyright 2020 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - package git import ( + "context" "fmt" "testing" "github.com/stretchr/testify/assert" ) -func TestRepository_BlameFile(t *testing.T) { +func TestRepository_Blame(t *testing.T) { + ctx := context.Background() + t.Run("bad file", func(t *testing.T) { - _, err := testrepo.BlameFile("", "404.txt") + _, err := testrepo.Blame(ctx, "", "404.txt") assert.Error(t, err) }) - blame, err := testrepo.BlameFile("cfc3b2993f74726356887a5ec093de50486dc617", "README.txt") + blame, err := testrepo.Blame(ctx, "cfc3b2993f74726356887a5ec093de50486dc617", "README.txt") assert.Nil(t, err) // Assert representative commits diff --git a/repo_blob.go b/repo_blob.go index 4d9bfc24..ce521e5c 100644 --- a/repo_blob.go +++ b/repo_blob.go @@ -1,37 +1,29 @@ -// Copyright 2022 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - package git -import "time" +import "context" // CatFileBlobOptions contains optional arguments for verifying the objects. // // Docs: https://git-scm.com/docs/git-cat-file#Documentation/git-cat-file.txt 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 } // CatFileBlob returns the blob corresponding to the given revision of the repository. -func (r *Repository) CatFileBlob(rev string, opts ...CatFileBlobOptions) (*Blob, error) { +func (r *Repository) CatFileBlob(ctx context.Context, rev string, opts ...CatFileBlobOptions) (*Blob, error) { var opt CatFileBlobOptions if len(opts) > 0 { opt = opts[0] } - rev, err := r.RevParse(rev, RevParseOptions{Timeout: opt.Timeout}) //nolint + // Type conversions work because all three option types share the same + // underlying structure (CommandOptions only). + rev, err := r.RevParse(ctx, rev, RevParseOptions(opt)) //nolint if err != nil { return nil, err } - typ, err := r.CatFileType(rev) + typ, err := r.CatFileType(ctx, rev, CatFileTypeOptions(opt)) if err != nil { return nil, err } diff --git a/repo_blob_test.go b/repo_blob_test.go index 7def8039..4818145e 100644 --- a/repo_blob_test.go +++ b/repo_blob_test.go @@ -1,10 +1,7 @@ -// Copyright 2022 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - package git import ( + "context" "testing" "github.com/stretchr/testify/assert" @@ -12,19 +9,21 @@ import ( ) func TestRepository_CatFileBlob(t *testing.T) { + ctx := context.Background() + t.Run("not a blob", func(t *testing.T) { - _, err := testrepo.CatFileBlob("007cb92318c7bd3b56908ea8c2e54370245562f8") + _, err := testrepo.CatFileBlob(ctx, "007cb92318c7bd3b56908ea8c2e54370245562f8") assert.Equal(t, ErrNotBlob, err) }) t.Run("get a blob, no full rev hash", func(t *testing.T) { - b, err := testrepo.CatFileBlob("021a") + b, err := testrepo.CatFileBlob(ctx, "021a") require.NoError(t, err) assert.True(t, b.IsBlob()) }) t.Run("get a blob", func(t *testing.T) { - b, err := testrepo.CatFileBlob("021a721a61a1de65865542c405796d1eb985f784") + b, err := testrepo.CatFileBlob(ctx, "021a721a61a1de65865542c405796d1eb985f784") require.NoError(t, err) assert.True(t, b.IsBlob()) }) diff --git a/repo_commit.go b/repo_commit.go index d4e8c1fd..04d1ce38 100644 --- a/repo_commit.go +++ b/repo_commit.go @@ -1,13 +1,9 @@ -// Copyright 2015 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - package git import ( "bytes" + "context" "errors" - "fmt" "strconv" "strings" "time" @@ -70,19 +66,13 @@ loop: // // Docs: https://git-scm.com/docs/git-cat-file#Documentation/git-cat-file.txt-lttypegt 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 } // CatFileCommit returns the commit corresponding to the given revision of the // repository. The revision could be a commit ID or full refspec (e.g. // "refs/heads/master"). -func (r *Repository) CatFileCommit(rev string, opts ...CatFileCommitOptions) (*Commit, error) { +func (r *Repository) CatFileCommit(ctx context.Context, rev string, opts ...CatFileCommitOptions) (*Commit, error) { var opt CatFileCommitOptions if len(opts) > 0 { opt = opts[0] @@ -90,19 +80,17 @@ func (r *Repository) CatFileCommit(rev string, opts ...CatFileCommitOptions) (*C cache, ok := r.cachedCommits.Get(rev) if ok { - log("Cached commit hit: %s", rev) + logf("Cached commit hit: %s", rev) return cache.(*Commit), nil } - commitID, err := r.RevParse(rev, RevParseOptions{Timeout: opt.Timeout}) //nolint + commitID, err := r.RevParse(ctx, rev) //nolint if err != nil { return nil, err } - stdout, err := NewCommand("cat-file"). - AddOptions(opt.CommandOptions). - AddArgs("commit", commitID). - RunInDirWithTimeout(opt.Timeout, r.path) + args := []string{"cat-file", "commit", commitID} + stdout, err := exec(ctx, r.path, args, opt.Envs) if err != nil { return nil, err } @@ -122,24 +110,18 @@ func (r *Repository) CatFileCommit(rev string, opts ...CatFileCommitOptions) (*C // // Docs: https://git-scm.com/docs/git-cat-file#Documentation/git-cat-file.txt--t type CatFileTypeOptions struct { - // The timeout duration before giving up for each shell command execution. The - // default timeout duration will be used when not supplied. - Timeout time.Duration - // The additional options to be passed to the underlying git. CommandOptions } // CatFileType returns the object type of given revision of the repository. -func (r *Repository) CatFileType(rev string, opts ...CatFileTypeOptions) (ObjectType, error) { +func (r *Repository) CatFileType(ctx context.Context, rev string, opts ...CatFileTypeOptions) (ObjectType, error) { var opt CatFileTypeOptions if len(opts) > 0 { opt = opts[0] } - typ, err := NewCommand("cat-file"). - AddOptions(opt.CommandOptions). - AddArgs("-t", rev). - RunInDirWithTimeout(opt.Timeout, r.path) + args := []string{"cat-file", "-t", rev} + typ, err := exec(ctx, r.path, args, opt.Envs) if err != nil { return "", err } @@ -149,14 +131,14 @@ func (r *Repository) CatFileType(rev string, opts ...CatFileTypeOptions) (Object // BranchCommit returns the latest commit of given branch of the repository. The // branch must be given in short name e.g. "master". -func (r *Repository) BranchCommit(branch string, opts ...CatFileCommitOptions) (*Commit, error) { - return r.CatFileCommit(RefsHeads+branch, opts...) +func (r *Repository) BranchCommit(ctx context.Context, branch string, opts ...CatFileCommitOptions) (*Commit, error) { + return r.CatFileCommit(ctx, RefsHeads+branch, opts...) } // TagCommit returns the latest commit of given tag of the repository. The tag // must be given in short name e.g. "v1.0.0". -func (r *Repository) TagCommit(tag string, opts ...CatFileCommitOptions) (*Commit, error) { - return r.CatFileCommit(RefsTags+tag, opts...) +func (r *Repository) TagCommit(ctx context.Context, tag string, opts ...CatFileCommitOptions) (*Commit, error) { + return r.CatFileCommit(ctx, RefsTags+tag, opts...) } // LogOptions contains optional arguments for listing commits. @@ -175,10 +157,6 @@ type LogOptions struct { RegexpIgnoreCase bool // The relative path of the repository. Path string - // The timeout duration before giving up for each shell command execution. The - // default timeout duration will be used when not supplied. - Timeout time.Duration - // The additional options to be passed to the underlying git. CommandOptions } @@ -194,59 +172,40 @@ func escapePath(path string) string { return path } -// Log returns a list of commits in the state of given revision of the -// repository in given path. The returned list is in reverse chronological -// order. -func Log(repoPath, rev string, opts ...LogOptions) ([]*Commit, error) { - r, err := Open(repoPath) - if err != nil { - return nil, fmt.Errorf("open: %v", err) - } - - return r.Log(rev, opts...) -} - -// Deprecated: Use Log instead. -func RepoLog(repoPath, rev string, opts ...LogOptions) ([]*Commit, error) { - return Log(repoPath, rev, opts...) -} - // Log returns a list of commits in the state of given revision of the repository. // The returned list is in reverse chronological order. -func (r *Repository) Log(rev string, opts ...LogOptions) ([]*Commit, error) { +func (r *Repository) Log(ctx context.Context, rev string, opts ...LogOptions) ([]*Commit, error) { var opt LogOptions if len(opts) > 0 { opt = opts[0] } - cmd := NewCommand("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.RunInDirWithTimeout(opt.Timeout, r.path) + stdout, err := exec(ctx, r.path, args, opt.Envs) if err != nil { return nil, err } - return r.parsePrettyFormatLogToList(opt.Timeout, stdout) + return r.parsePrettyFormatLogToList(ctx, stdout) } // CommitByRevisionOptions contains optional arguments for getting a commit. @@ -255,24 +214,19 @@ func (r *Repository) Log(rev string, opts ...LogOptions) ([]*Commit, error) { type CommitByRevisionOptions struct { // The relative path of the repository. Path string - // The timeout duration before giving up for each shell command execution. The - // default timeout duration will be used when not supplied. - Timeout time.Duration - // The additional options to be passed to the underlying git. CommandOptions } // CommitByRevision returns a commit by given revision. -func (r *Repository) CommitByRevision(rev string, opts ...CommitByRevisionOptions) (*Commit, error) { +func (r *Repository) CommitByRevision(ctx context.Context, rev string, opts ...CommitByRevisionOptions) (*Commit, error) { var opt CommitByRevisionOptions if len(opts) > 0 { opt = opts[0] } - commits, err := r.Log(rev, LogOptions{ + commits, err := r.Log(ctx, rev, LogOptions{ MaxCount: 1, Path: opt.Path, - Timeout: opt.Timeout, CommandOptions: opt.CommandOptions, }) if err != nil { @@ -293,26 +247,21 @@ func (r *Repository) CommitByRevision(rev string, opts ...CommitByRevisionOption type CommitsByPageOptions struct { // The relative path of the repository. Path string - // The timeout duration before giving up for each shell command execution. The - // default timeout duration will be used when not supplied. - Timeout time.Duration - // The additional options to be passed to the underlying git. CommandOptions } // CommitsByPage returns a paginated list of commits in the state of given // revision. The pagination starts from the newest to the oldest commit. -func (r *Repository) CommitsByPage(rev string, page, size int, opts ...CommitsByPageOptions) ([]*Commit, error) { +func (r *Repository) CommitsByPage(ctx context.Context, rev string, page, size int, opts ...CommitsByPageOptions) ([]*Commit, error) { var opt CommitsByPageOptions if len(opts) > 0 { opt = opts[0] } - return r.Log(rev, LogOptions{ + return r.Log(ctx, rev, LogOptions{ MaxCount: size, Skip: (page - 1) * size, Path: opt.Path, - Timeout: opt.Timeout, CommandOptions: opt.CommandOptions, }) } @@ -325,27 +274,22 @@ type SearchCommitsOptions struct { MaxCount int // The relative path of the repository. Path string - // The timeout duration before giving up for each shell command execution. The - // default timeout duration will be used when not supplied. - Timeout time.Duration - // The additional options to be passed to the underlying git. CommandOptions } // SearchCommits searches commit message with given pattern in the state of // given revision. The returned list is in reverse chronological order. -func (r *Repository) SearchCommits(rev, pattern string, opts ...SearchCommitsOptions) ([]*Commit, error) { +func (r *Repository) SearchCommits(ctx context.Context, rev, pattern string, opts ...SearchCommitsOptions) ([]*Commit, error) { var opt SearchCommitsOptions if len(opts) > 0 { opt = opts[0] } - return r.Log(rev, LogOptions{ + return r.Log(ctx, rev, LogOptions{ MaxCount: opt.MaxCount, GrepPattern: pattern, RegexpIgnoreCase: true, Path: opt.Path, - Timeout: opt.Timeout, CommandOptions: opt.CommandOptions, }) } @@ -357,25 +301,20 @@ func (r *Repository) SearchCommits(rev, pattern string, opts ...SearchCommitsOpt type CommitsSinceOptions struct { // The relative path of the repository. Path string - // The timeout duration before giving up for each shell command execution. The - // default timeout duration will be used when not supplied. - Timeout time.Duration - // The additional options to be passed to the underlying git. CommandOptions } // CommitsSince returns a list of commits since given time. The returned list is // in reverse chronological order. -func (r *Repository) CommitsSince(rev string, since time.Time, opts ...CommitsSinceOptions) ([]*Commit, error) { +func (r *Repository) CommitsSince(ctx context.Context, rev string, since time.Time, opts ...CommitsSinceOptions) ([]*Commit, error) { var opt CommitsSinceOptions if len(opts) > 0 { opt = opts[0] } - return r.Log(rev, LogOptions{ + return r.Log(ctx, rev, LogOptions{ Since: since, Path: opt.Path, - Timeout: opt.Timeout, CommandOptions: opt.CommandOptions, }) } @@ -388,36 +327,29 @@ type DiffNameOnlyOptions struct { NeedsMergeBase bool // The relative path of the repository. Path string - // The timeout duration before giving up for each shell command execution. The - // default timeout duration will be used when not supplied. - Timeout time.Duration - // The additional options to be passed to the underlying git. CommandOptions } -// DiffNameOnly returns a list of changed files between base and head revisions -// of the repository in given path. -func DiffNameOnly(repoPath, base, head string, opts ...DiffNameOnlyOptions) ([]string, error) { +// DiffNameOnly returns a list of changed files between base and head revisions of the +// repository. +func (r *Repository) DiffNameOnly(ctx context.Context, base, head string, opts ...DiffNameOnlyOptions) ([]string, error) { var opt DiffNameOnlyOptions if len(opts) > 0 { opt = opts[0] } - cmd := NewCommand("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.RunInDirWithTimeout(opt.Timeout, repoPath) + stdout, err := exec(ctx, r.path, args, opt.Envs) if err != nil { return nil, err } @@ -434,33 +366,18 @@ func DiffNameOnly(repoPath, base, head string, opts ...DiffNameOnlyOptions) ([]s return names, nil } -// Deprecated: Use DiffNameOnly instead. -func RepoDiffNameOnly(repoPath, base, head string, opts ...DiffNameOnlyOptions) ([]string, error) { - return DiffNameOnly(repoPath, base, head, opts...) -} - -// DiffNameOnly returns a list of changed files between base and head revisions of the -// repository. -func (r *Repository) DiffNameOnly(base, head string, opts ...DiffNameOnlyOptions) ([]string, error) { - return DiffNameOnly(r.path, base, head, opts...) -} - // RevListCountOptions contains optional arguments for counting commits. // // Docs: https://git-scm.com/docs/git-rev-list#Documentation/git-rev-list.txt---count type RevListCountOptions struct { // The relative path of the repository. Path string - // The timeout duration before giving up for each shell command execution. The - // default timeout duration will be used when not supplied. - Timeout time.Duration - // The additional options to be passed to the underlying git. CommandOptions } // RevListCount returns number of total commits up to given refspec of the // repository. -func (r *Repository) RevListCount(refspecs []string, opts ...RevListCountOptions) (int64, error) { +func (r *Repository) RevListCount(ctx context.Context, refspecs []string, opts ...RevListCountOptions) (int64, error) { var opt RevListCountOptions if len(opts) > 0 { opt = opts[0] @@ -470,19 +387,14 @@ func (r *Repository) RevListCount(refspecs []string, opts ...RevListCountOptions return 0, errors.New("must have at least one refspec") } - cmd := NewCommand("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.RunInDirWithTimeout(opt.Timeout, r.path) + stdout, err := exec(ctx, r.path, args, opt.Envs) if err != nil { return 0, err } @@ -496,16 +408,12 @@ func (r *Repository) RevListCount(refspecs []string, opts ...RevListCountOptions type RevListOptions struct { // The relative path of the repository. Path string - // The timeout duration before giving up for each shell command execution. The - // default timeout duration will be used when not supplied. - Timeout time.Duration - // The additional options to be passed to the underlying git. CommandOptions } // RevList returns a list of commits based on given refspecs in reverse // chronological order. -func (r *Repository) RevList(refspecs []string, opts ...RevListOptions) ([]*Commit, error) { +func (r *Repository) RevList(ctx context.Context, refspecs []string, opts ...RevListOptions) ([]*Commit, error) { var opt RevListOptions if len(opts) > 0 { opt = opts[0] @@ -515,19 +423,18 @@ func (r *Repository) RevList(refspecs []string, opts ...RevListOptions) ([]*Comm return nil, errors.New("must have at least one refspec") } - cmd := NewCommand("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.RunInDirWithTimeout(opt.Timeout, r.path) + stdout, err := exec(ctx, r.path, args, opt.Envs) if err != nil { return nil, err } - return r.parsePrettyFormatLogToList(opt.Timeout, bytes.TrimSpace(stdout)) + return r.parsePrettyFormatLogToList(ctx, bytes.TrimSpace(stdout)) } // LatestCommitTimeOptions contains optional arguments for getting the latest @@ -535,32 +442,22 @@ func (r *Repository) RevList(refspecs []string, opts ...RevListOptions) ([]*Comm type LatestCommitTimeOptions struct { // To get the latest commit time of the branch. When not set, it checks all branches. Branch string - // The timeout duration before giving up for each shell command execution. The - // default timeout duration will be used when not supplied. - Timeout time.Duration - // The additional options to be passed to the underlying git. CommandOptions } // LatestCommitTime returns the time of latest commit of the repository. -func (r *Repository) LatestCommitTime(opts ...LatestCommitTimeOptions) (time.Time, error) { +func (r *Repository) LatestCommitTime(ctx context.Context, opts ...LatestCommitTimeOptions) (time.Time, error) { var opt LatestCommitTimeOptions if len(opts) > 0 { opt = opts[0] } - cmd := NewCommand("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.RunInDirWithTimeout(opt.Timeout, r.path) + stdout, err := exec(ctx, r.path, args, opt.Envs) if err != nil { return time.Time{}, err } diff --git a/repo_commit_test.go b/repo_commit_test.go index f8b6e5a4..b4ccac77 100644 --- a/repo_commit_test.go +++ b/repo_commit_test.go @@ -1,10 +1,7 @@ -// Copyright 2020 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - package git import ( + "context" "errors" "testing" "time" @@ -38,13 +35,15 @@ func Test_escapePath(t *testing.T) { } func TestRepository_CatFileCommit(t *testing.T) { + ctx := context.Background() + t.Run("invalid revision", func(t *testing.T) { - c, err := testrepo.CatFileCommit("bad_revision") + c, err := testrepo.CatFileCommit(ctx, "bad_revision") assert.Equal(t, ErrRevisionNotExist, err) assert.Nil(t, c) }) - c, err := testrepo.CatFileCommit("d58e3ef9f123eea6857161c79275ee22b228f659") + c, err := testrepo.CatFileCommit(ctx, "d58e3ef9f123eea6857161c79275ee22b228f659") if err != nil { t.Fatal(err) } @@ -54,13 +53,15 @@ func TestRepository_CatFileCommit(t *testing.T) { } func TestRepository_BranchCommit(t *testing.T) { + ctx := context.Background() + t.Run("invalid branch", func(t *testing.T) { - c, err := testrepo.BranchCommit("refs/heads/release-1.0") + c, err := testrepo.BranchCommit(ctx, "refs/heads/release-1.0") assert.Equal(t, ErrRevisionNotExist, err) assert.Nil(t, c) }) - c, err := testrepo.BranchCommit("release-1.0") + c, err := testrepo.BranchCommit(ctx, "release-1.0") if err != nil { t.Fatal(err) } @@ -70,13 +71,15 @@ func TestRepository_BranchCommit(t *testing.T) { } func TestRepository_TagCommit(t *testing.T) { + ctx := context.Background() + t.Run("invalid branch", func(t *testing.T) { - c, err := testrepo.BranchCommit("refs/tags/v1.0.0") + c, err := testrepo.BranchCommit(ctx, "refs/tags/v1.0.0") assert.Equal(t, ErrRevisionNotExist, err) assert.Nil(t, c) }) - c, err := testrepo.BranchCommit("release-1.0") + c, err := testrepo.BranchCommit(ctx, "release-1.0") if err != nil { t.Fatal(err) } @@ -86,6 +89,7 @@ func TestRepository_TagCommit(t *testing.T) { } func TestRepository_Log(t *testing.T) { + ctx := context.Background() tests := []struct { rev string opt LogOptions @@ -111,14 +115,7 @@ func TestRepository_Log(t *testing.T) { } for _, test := range tests { t.Run("", func(t *testing.T) { - commits, err := testrepo.Log(test.rev, test.opt) - if err != nil { - t.Fatal(err) - } - - assert.Equal(t, test.expCommitIDs, commitsToIDs(commits)) - - commits, err = Log(testrepo.path, test.rev, test.opt) + commits, err := testrepo.Log(ctx, test.rev, test.opt) if err != nil { t.Fatal(err) } @@ -129,8 +126,10 @@ func TestRepository_Log(t *testing.T) { } func TestRepository_CommitByRevision(t *testing.T) { + ctx := context.Background() + t.Run("invalid revision", func(t *testing.T) { - c, err := testrepo.CommitByRevision("bad_revision") + c, err := testrepo.CommitByRevision(ctx, "bad_revision") assert.Equal(t, ErrRevisionNotExist, err) assert.Nil(t, c) }) @@ -147,7 +146,7 @@ func TestRepository_CommitByRevision(t *testing.T) { } for _, test := range tests { t.Run("", func(t *testing.T) { - c, err := testrepo.CommitByRevision(test.rev, test.opt) + c, err := testrepo.CommitByRevision(ctx, test.rev, test.opt) if err != nil { t.Fatal(err) } @@ -158,6 +157,7 @@ func TestRepository_CommitByRevision(t *testing.T) { } func TestRepository_CommitsSince(t *testing.T) { + ctx := context.Background() tests := []struct { rev string since time.Time @@ -180,7 +180,7 @@ func TestRepository_CommitsSince(t *testing.T) { } for _, test := range tests { t.Run("", func(t *testing.T) { - commits, err := testrepo.CommitsSince(test.rev, test.since, test.opt) + commits, err := testrepo.CommitsSince(ctx, test.rev, test.since, test.opt) if err != nil { t.Fatal(err) } @@ -191,6 +191,7 @@ func TestRepository_CommitsSince(t *testing.T) { } func TestRepository_DiffNameOnly(t *testing.T) { + ctx := context.Background() tests := []struct { base string head string @@ -230,7 +231,7 @@ func TestRepository_DiffNameOnly(t *testing.T) { } for _, test := range tests { t.Run("", func(t *testing.T) { - files, err := testrepo.DiffNameOnly(test.base, test.head, test.opt) + files, err := testrepo.DiffNameOnly(ctx, test.base, test.head, test.opt) if err != nil { t.Fatal(err) } @@ -241,8 +242,10 @@ func TestRepository_DiffNameOnly(t *testing.T) { } func TestRepository_RevListCount(t *testing.T) { + ctx := context.Background() + t.Run("no refspecs", func(t *testing.T) { - count, err := testrepo.RevListCount([]string{}) + count, err := testrepo.RevListCount(ctx, []string{}) assert.Equal(t, errors.New("must have at least one refspec"), err) assert.Zero(t, count) }) @@ -282,7 +285,7 @@ func TestRepository_RevListCount(t *testing.T) { } for _, test := range tests { t.Run("", func(t *testing.T) { - count, err := testrepo.RevListCount(test.refspecs, test.opt) + count, err := testrepo.RevListCount(ctx, test.refspecs, test.opt) if err != nil { t.Fatal(err) } @@ -293,8 +296,10 @@ func TestRepository_RevListCount(t *testing.T) { } func TestRepository_RevList(t *testing.T) { + ctx := context.Background() + t.Run("no refspecs", func(t *testing.T) { - commits, err := testrepo.RevList([]string{}) + commits, err := testrepo.RevList(ctx, []string{}) assert.Equal(t, errors.New("must have at least one refspec"), err) assert.Nil(t, commits) }) @@ -324,7 +329,7 @@ func TestRepository_RevList(t *testing.T) { } for _, test := range tests { t.Run("", func(t *testing.T) { - commits, err := testrepo.RevList(test.refspecs, test.opt) + commits, err := testrepo.RevList(ctx, test.refspecs, test.opt) if err != nil { t.Fatal(err) } @@ -335,6 +340,7 @@ func TestRepository_RevList(t *testing.T) { } func TestRepository_LatestCommitTime(t *testing.T) { + ctx := context.Background() tests := []struct { opt LatestCommitTimeOptions expTime time.Time @@ -348,7 +354,7 @@ func TestRepository_LatestCommitTime(t *testing.T) { } for _, test := range tests { t.Run("", func(t *testing.T) { - got, err := testrepo.LatestCommitTime(test.opt) + got, err := testrepo.LatestCommitTime(ctx, test.opt) if err != nil { t.Fatal(err) } diff --git a/repo_diff.go b/repo_diff.go index ffe9d6ca..d65bda22 100644 --- a/repo_diff.go +++ b/repo_diff.go @@ -1,14 +1,9 @@ -// Copyright 2017 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - package git import ( - "bytes" + "context" "fmt" "io" - "time" ) // DiffOptions contains optional arguments for parsing diff. @@ -18,58 +13,45 @@ 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 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 } // Diff returns a parsed diff object between given commits of the repository. -func (r *Repository) Diff(rev string, maxFiles, maxFileLines, maxLineChars int, opts ...DiffOptions) (*Diff, error) { +func (r *Repository) Diff(ctx context.Context, rev string, maxFiles, maxFileLines, maxLineChars int, opts ...DiffOptions) (*Diff, error) { var opt DiffOptions if len(opts) > 0 { opt = opts[0] } - commit, err := r.CatFileCommit(rev, CatFileCommitOptions{Timeout: opt.Timeout}) + commit, err := r.CatFileCommit(ctx, rev) if err != nil { return nil, err } - cmd := NewCommand() + 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(0) + 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.RunInDirPipelineWithTimeout(opt.Timeout, 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 @@ -88,86 +70,67 @@ const ( // // Docs: https://git-scm.com/docs/git-format-patch type RawDiffOptions struct { - // The timeout duration before giving up for each shell command execution. The - // default timeout duration will be used when not supplied. - Timeout time.Duration - // The additional options to be passed to the underlying git. CommandOptions } // RawDiff dumps diff of repository in given revision directly to given // io.Writer. -func (r *Repository) RawDiff(rev string, diffType RawDiffFormat, w io.Writer, opts ...RawDiffOptions) error { +func (r *Repository) RawDiff(ctx context.Context, rev string, diffType RawDiffFormat, w io.Writer, opts ...RawDiffOptions) error { var opt RawDiffOptions if len(opts) > 0 { opt = opts[0] } - commit, err := r.CatFileCommit(rev, CatFileCommitOptions{Timeout: opt.Timeout}) //nolint + commit, err := r.CatFileCommit(ctx, rev) //nolint if err != nil { return err } - cmd := NewCommand() + 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(0) + 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(0) + 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.RunInDirPipelineWithTimeout(opt.Timeout, 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 timeout duration before giving up for each shell command execution. The - // default timeout duration will be used when not supplied. - Timeout time.Duration - // The additional options to be passed to the underlying git. CommandOptions } // DiffBinary returns binary patch between base and head revisions that could be // used for git-apply. -func (r *Repository) DiffBinary(base, head string, opts ...DiffBinaryOptions) ([]byte, error) { +func (r *Repository) DiffBinary(ctx context.Context, base, head string, opts ...DiffBinaryOptions) ([]byte, error) { var opt DiffBinaryOptions if len(opts) > 0 { opt = opts[0] } - return NewCommand("diff"). - AddOptions(opt.CommandOptions). - AddArgs("--full-index", "--binary", base, head). - RunInDirWithTimeout(opt.Timeout, 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_diff_test.go b/repo_diff_test.go index ec15c133..4fcbd57e 100644 --- a/repo_diff_test.go +++ b/repo_diff_test.go @@ -1,11 +1,8 @@ -// Copyright 2020 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - package git import ( "bytes" + "context" "errors" "strings" "testing" @@ -14,6 +11,7 @@ import ( ) func TestRepository_Diff(t *testing.T) { + ctx := context.Background() tests := []struct { rev string maxFiles int @@ -289,7 +287,7 @@ func TestRepository_Diff(t *testing.T) { } for _, test := range tests { t.Run("", func(t *testing.T) { - diff, err := testrepo.Diff(test.rev, test.maxFiles, test.maxFileLines, test.maxLineChars, test.opt) + diff, err := testrepo.Diff(ctx, test.rev, test.maxFiles, test.maxFileLines, test.maxLineChars, test.opt) if err != nil { t.Fatal(err) } @@ -300,13 +298,15 @@ func TestRepository_Diff(t *testing.T) { } func TestRepository_RawDiff(t *testing.T) { + ctx := context.Background() + t.Run("invalid revision", func(t *testing.T) { - err := testrepo.RawDiff("bad_revision", "bad_diff_type", nil) + err := testrepo.RawDiff(ctx, "bad_revision", "bad_diff_type", nil) assert.Equal(t, ErrRevisionNotExist, err) }) t.Run("invalid diffType", func(t *testing.T) { - err := testrepo.RawDiff("978fb7f6388b49b532fbef8b856681cfa6fcaa0a", "bad_diff_type", nil) + err := testrepo.RawDiff(ctx, "978fb7f6388b49b532fbef8b856681cfa6fcaa0a", "bad_diff_type", nil) assert.Equal(t, errors.New("invalid diffType: bad_diff_type"), err) }) @@ -461,7 +461,7 @@ index 0000000000000000000000000000000000000000..51680791956b43effdb2f16bccd2b475 for _, test := range tests { t.Run("", func(t *testing.T) { buf := new(bytes.Buffer) - err := testrepo.RawDiff(test.rev, test.diffType, buf, test.opt) + err := testrepo.RawDiff(ctx, test.rev, test.diffType, buf, test.opt) if err != nil { t.Fatal(err) } @@ -479,6 +479,7 @@ index 0000000000000000000000000000000000000000..51680791956b43effdb2f16bccd2b475 } func TestRepository_DiffBinary(t *testing.T) { + ctx := context.Background() tests := []struct { base string head string @@ -509,7 +510,7 @@ index 0000000000000000000000000000000000000000..6b08f76a5313fa3d26859515b30aa17a } for _, test := range tests { t.Run("", func(t *testing.T) { - p, err := testrepo.DiffBinary(test.base, test.head, test.opt) + p, err := testrepo.DiffBinary(ctx, test.base, test.head, test.opt) if err != nil { t.Fatal(err) } diff --git a/repo_grep.go b/repo_grep.go index f6394bce..3e83e3f7 100644 --- a/repo_grep.go +++ b/repo_grep.go @@ -1,14 +1,10 @@ -// Copyright 2022 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - package git import ( + "context" "fmt" "strconv" "strings" - "time" ) // GrepOptions contains optional arguments for grep search over repository files. @@ -25,12 +21,6 @@ type GrepOptions struct { WordRegexp bool // Whether use extended regular expressions. ExtendedRegexp 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 } @@ -74,7 +64,7 @@ func parseGrepLine(line string) (*GrepResult, error) { } // Grep returns the results of a grep search in the repository. -func (r *Repository) Grep(pattern string, opts ...GrepOptions) []*GrepResult { +func (r *Repository) Grep(ctx context.Context, pattern string, opts ...GrepOptions) []*GrepResult { var opt GrepOptions if len(opts) > 0 { opt = opts[0] @@ -83,29 +73,23 @@ func (r *Repository) Grep(pattern string, opts ...GrepOptions) []*GrepResult { opt.Tree = "HEAD" } - cmd := NewCommand("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.RunInDirWithTimeout(opt.Timeout, r.path) + stdout, err := exec(ctx, r.path, args, opt.Envs) if err != nil { return nil } diff --git a/repo_grep_test.go b/repo_grep_test.go index 2c0be390..c5e954cf 100644 --- a/repo_grep_test.go +++ b/repo_grep_test.go @@ -1,10 +1,7 @@ -// Copyright 2022 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - package git import ( + "context" "runtime" "testing" @@ -12,6 +9,7 @@ import ( ) func TestRepository_Grep_Simple(t *testing.T) { + ctx := context.Background() want := []*GrepResult{ { Tree: "HEAD", @@ -51,11 +49,12 @@ func TestRepository_Grep_Simple(t *testing.T) { Text: `println "${programmingPoints} plus 3 bonus points is ${sum(programmingPoints, 3)}"`, }, } - got := testrepo.Grep("programmingPoints") + got := testrepo.Grep(ctx, "programmingPoints") assert.Equal(t, want, got) } func TestRepository_Grep_IgnoreCase(t *testing.T) { + ctx := context.Background() want := []*GrepResult{ { Tree: "HEAD", @@ -107,11 +106,12 @@ func TestRepository_Grep_IgnoreCase(t *testing.T) { Text: ` System.out.println( "Hello World!" );`, }, } - got := testrepo.Grep("Hello", GrepOptions{IgnoreCase: true}) + got := testrepo.Grep(ctx, "Hello", GrepOptions{IgnoreCase: true}) assert.Equal(t, want, got) } func TestRepository_Grep_ExtendedRegexp(t *testing.T) { + ctx := context.Background() if runtime.GOOS == "darwin" { t.Skip("Skipping testing on macOS") return @@ -125,11 +125,12 @@ func TestRepository_Grep_ExtendedRegexp(t *testing.T) { Text: ` System.out.println( "Hello World!" );`, }, } - got := testrepo.Grep(`Hello\sW\w+`, GrepOptions{ExtendedRegexp: true}) + got := testrepo.Grep(ctx, `Hello\sW\w+`, GrepOptions{ExtendedRegexp: true}) assert.Equal(t, want, got) } func TestRepository_Grep_WordRegexp(t *testing.T) { + ctx := context.Background() want := []*GrepResult{ { Tree: "HEAD", @@ -139,6 +140,6 @@ func TestRepository_Grep_WordRegexp(t *testing.T) { Text: ` * Hello world!`, }, } - got := testrepo.Grep("world", GrepOptions{WordRegexp: true}) + got := testrepo.Grep(ctx, "world", GrepOptions{WordRegexp: true}) assert.Equal(t, want, got) } diff --git a/repo_hook.go b/repo_hook.go index 8794876f..8592d4c5 100644 --- a/repo_hook.go +++ b/repo_hook.go @@ -1,11 +1,6 @@ -// Copyright 2015 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - package git import ( - "io/ioutil" "os" "path/filepath" ) @@ -32,7 +27,7 @@ func (r *Repository) Hook(dir string, name HookName) (*Hook, error) { // 1. Check if there is an active hook. fpath := filepath.Join(r.path, dir, string(name)) if isFile(fpath) { - p, err := ioutil.ReadFile(fpath) + p, err := os.ReadFile(fpath) if err != nil { return nil, err } diff --git a/repo_hook_test.go b/repo_hook_test.go index abc10075..200251b4 100644 --- a/repo_hook_test.go +++ b/repo_hook_test.go @@ -1,7 +1,3 @@ -// Copyright 2020 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - package git import ( diff --git a/repo_pull.go b/repo_pull.go index 5d2cfb1e..d5eebaeb 100644 --- a/repo_pull.go +++ b/repo_pull.go @@ -1,58 +1,32 @@ -// Copyright 2015 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - package git import ( + "context" "strings" - "time" ) // MergeBaseOptions contains optional arguments for getting merge base. // // Docs: https://git-scm.com/docs/git-merge-base type MergeBaseOptions 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 } // MergeBase returns merge base between base and head revisions of the -// repository in given path. -func MergeBase(repoPath, base, head string, opts ...MergeBaseOptions) (string, error) { +// repository. +func (r *Repository) MergeBase(ctx context.Context, base, head string, opts ...MergeBaseOptions) (string, error) { var opt MergeBaseOptions if len(opts) > 0 { opt = opts[0] } - stdout, err := NewCommand("merge-base"). - AddOptions(opt.CommandOptions). - AddArgs( - "--end-of-options", - base, - head, - ).RunInDirWithTimeout(opt.Timeout, repoPath) + 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 } return strings.TrimSpace(string(stdout)), nil } - -// Deprecated: Use MergeBase instead. -func RepoMergeBase(repoPath, base, head string, opts ...MergeBaseOptions) (string, error) { - return MergeBase(repoPath, base, head, opts...) -} - -// MergeBase returns merge base between base and head revisions of the -// repository. -func (r *Repository) MergeBase(base, head string, opts ...MergeBaseOptions) (string, error) { - return MergeBase(r.path, base, head, opts...) -} diff --git a/repo_pull_test.go b/repo_pull_test.go index 3ec9dcb7..082e987b 100644 --- a/repo_pull_test.go +++ b/repo_pull_test.go @@ -1,47 +1,46 @@ -// Copyright 2020 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - package git import ( + "context" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestRepository_MergeBase(t *testing.T) { - t.Run("no merge base", func(t *testing.T) { - mb, err := testrepo.MergeBase("0eedd79eba4394bbef888c804e899731644367fe", "bad_revision") - assert.Equal(t, ErrNoMergeBase, err) + ctx := context.Background() + + 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.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(test.base, test.head, test.opt) - if err != nil { - t.Fatal(err) - } - - assert.Equal(t, test.expMergeBase, mb) + mb, err := testrepo.MergeBase(ctx, test.base, test.head, test.opt) + require.NoError(t, err) + assert.Equal(t, test.wantMergeBase, mb) }) } } diff --git a/repo_reference.go b/repo_reference.go index 43dfb3f9..b525ae9b 100644 --- a/repo_reference.go +++ b/repo_reference.go @@ -1,13 +1,9 @@ -// Copyright 2015 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - package git import ( + "context" "errors" "strings" - "time" ) const ( @@ -37,27 +33,21 @@ type Reference struct { // // Docs: https://git-scm.com/docs/git-show-ref#Documentation/git-show-ref.txt---verify type ShowRefVerifyOptions 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 } var ErrReferenceNotExist = errors.New("reference does not exist") -// ShowRefVerify returns the commit ID of given reference if it exists in the -// repository in given path. -func ShowRefVerify(repoPath, ref string, opts ...ShowRefVerifyOptions) (string, error) { +// ShowRefVerify returns the commit ID of given reference (e.g. +// "refs/heads/master") if it exists in the repository. +func (r *Repository) ShowRefVerify(ctx context.Context, ref string, opts ...ShowRefVerifyOptions) (string, error) { var opt ShowRefVerifyOptions if len(opts) > 0 { opt = opts[0] } - cmd := NewCommand("show-ref", "--verify", "--end-of-options", ref).AddOptions(opt.CommandOptions) - stdout, err := cmd.RunInDirWithTimeout(opt.Timeout, repoPath) + 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 @@ -67,70 +57,35 @@ func ShowRefVerify(repoPath, ref string, opts ...ShowRefVerifyOptions) (string, return strings.Split(string(stdout), " ")[0], nil } -// Deprecated: Use ShowRefVerify instead. -func RepoShowRefVerify(repoPath, ref string, opts ...ShowRefVerifyOptions) (string, error) { - return ShowRefVerify(repoPath, ref, opts...) -} - -// ShowRefVerify returns the commit ID of given reference (e.g. -// "refs/heads/master") if it exists in the repository. -func (r *Repository) ShowRefVerify(ref string, opts ...ShowRefVerifyOptions) (string, error) { - return ShowRefVerify(r.path, ref, opts...) -} - // BranchCommitID returns the commit ID of given branch if it exists in the // repository. The branch must be given in short name e.g. "master". -func (r *Repository) BranchCommitID(branch string, opts ...ShowRefVerifyOptions) (string, error) { - return r.ShowRefVerify(RefsHeads+branch, opts...) +func (r *Repository) BranchCommitID(ctx context.Context, branch string, opts ...ShowRefVerifyOptions) (string, error) { + return r.ShowRefVerify(ctx, RefsHeads+branch, opts...) } // TagCommitID returns the commit ID of given tag if it exists in the // repository. The tag must be given in short name e.g. "v1.0.0". -func (r *Repository) TagCommitID(tag string, opts ...ShowRefVerifyOptions) (string, error) { - return r.ShowRefVerify(RefsTags+tag, opts...) -} - -// RepoHasReference returns true if given reference exists in the repository in -// given path. The reference must be given in full refspec, e.g. -// "refs/heads/master". -func RepoHasReference(repoPath, ref string, opts ...ShowRefVerifyOptions) bool { - _, err := ShowRefVerify(repoPath, ref, opts...) - return err == nil -} - -// RepoHasBranch returns true if given branch exists in the repository in given -// path. The branch must be given in short name e.g. "master". -func RepoHasBranch(repoPath, branch string, opts ...ShowRefVerifyOptions) bool { - return RepoHasReference(repoPath, RefsHeads+branch, opts...) -} - -// HasTag returns true if given tag exists in the repository in given path. The -// tag must be given in short name e.g. "v1.0.0". -func HasTag(repoPath, tag string, opts ...ShowRefVerifyOptions) bool { - return RepoHasReference(repoPath, RefsTags+tag, opts...) -} - -// Deprecated: Use HasTag instead. -func RepoHasTag(repoPath, tag string, opts ...ShowRefVerifyOptions) bool { - return HasTag(repoPath, tag, opts...) +func (r *Repository) TagCommitID(ctx context.Context, tag string, opts ...ShowRefVerifyOptions) (string, error) { + return r.ShowRefVerify(ctx, RefsTags+tag, opts...) } // HasReference returns true if given reference exists in the repository. The // reference must be given in full refspec, e.g. "refs/heads/master". -func (r *Repository) HasReference(ref string, opts ...ShowRefVerifyOptions) bool { - return RepoHasReference(r.path, ref, opts...) +func (r *Repository) HasReference(ctx context.Context, ref string, opts ...ShowRefVerifyOptions) bool { + _, err := r.ShowRefVerify(ctx, ref, opts...) + return err == nil } // HasBranch returns true if given branch exists in the repository. The branch // must be given in short name e.g. "master". -func (r *Repository) HasBranch(branch string, opts ...ShowRefVerifyOptions) bool { - return RepoHasBranch(r.path, branch, opts...) +func (r *Repository) HasBranch(ctx context.Context, branch string, opts ...ShowRefVerifyOptions) bool { + return r.HasReference(ctx, RefsHeads+branch, opts...) } // HasTag returns true if given tag exists in the repository. The tag must be // given in short name e.g. "v1.0.0". -func (r *Repository) HasTag(tag string, opts ...ShowRefVerifyOptions) bool { - return HasTag(r.path, tag, opts...) +func (r *Repository) HasTag(ctx context.Context, tag string, opts ...ShowRefVerifyOptions) bool { + return r.HasReference(ctx, RefsTags+tag, opts...) } // SymbolicRefOptions contains optional arguments for get and set symbolic ref. @@ -140,47 +95,34 @@ 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 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 } // SymbolicRef returns the reference name (e.g. "refs/heads/master") pointed by -// the symbolic ref in the repository in given path. It returns an empty string -// and nil error when doing set operation. -func SymbolicRef(repoPath string, opts ...SymbolicRefOptions) (string, error) { +// the symbolic ref. It returns an empty string and nil error when doing set +// operation. +func (r *Repository) SymbolicRef(ctx context.Context, opts ...SymbolicRefOptions) (string, error) { var opt SymbolicRefOptions if len(opts) > 0 { opt = opts[0] } - cmd := NewCommand("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.RunInDirWithTimeout(opt.Timeout, repoPath) + stdout, err := exec(ctx, r.path, args, opt.Envs) if err != nil { return "", err } return strings.TrimSpace(string(stdout)), nil } -// SymbolicRef returns the reference name (e.g. "refs/heads/master") pointed by -// the symbolic ref. It returns an empty string and nil error when doing set -// operation. -func (r *Repository) SymbolicRef(opts ...SymbolicRefOptions) (string, error) { - return SymbolicRef(r.path, opts...) -} - // ShowRefOptions contains optional arguments for listing references. // // Docs: https://git-scm.com/docs/git-show-ref @@ -191,35 +133,29 @@ type ShowRefOptions struct { Tags bool // The list of patterns to filter results. Patterns []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 } // ShowRef returns a list of references in the repository. -func (r *Repository) ShowRef(opts ...ShowRefOptions) ([]*Reference, error) { +func (r *Repository) ShowRef(ctx context.Context, opts ...ShowRefOptions) ([]*Reference, error) { var opt ShowRefOptions if len(opts) > 0 { opt = opts[0] } - cmd := NewCommand("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.RunInDirWithTimeout(opt.Timeout, r.path) + stdout, err := exec(ctx, r.path, args, opt.Envs) if err != nil { return nil, err } @@ -240,8 +176,8 @@ func (r *Repository) ShowRef(opts ...ShowRefOptions) ([]*Reference, error) { } // Branches returns a list of all branches in the repository. -func (r *Repository) Branches() ([]string, error) { - heads, err := r.ShowRef(ShowRefOptions{Heads: true}) +func (r *Repository) Branches(ctx context.Context) ([]string, error) { + heads, err := r.ShowRef(ctx, ShowRefOptions{Heads: true}) if err != nil { return nil, err } @@ -259,38 +195,23 @@ func (r *Repository) Branches() ([]string, error) { type DeleteBranchOptions struct { // Indicates whether to force delete the branch. Force 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 } -// DeleteBranch deletes the branch from the repository in given path. -func DeleteBranch(repoPath, name string, opts ...DeleteBranchOptions) error { +// DeleteBranch deletes the branch from the repository. +func (r *Repository) DeleteBranch(ctx context.Context, name string, opts ...DeleteBranchOptions) error { var opt DeleteBranchOptions if len(opts) > 0 { opt = opts[0] } - cmd := NewCommand("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).RunInDirWithTimeout(opt.Timeout, repoPath) + args = append(args, "--end-of-options", name) + _, err := exec(ctx, r.path, args, opt.Envs) return err } - -// Deprecated: Use DeleteBranch instead. -func RepoDeleteBranch(repoPath, name string, opts ...DeleteBranchOptions) error { - return DeleteBranch(repoPath, name, opts...) -} - -// DeleteBranch deletes the branch from the repository. -func (r *Repository) DeleteBranch(name string, opts ...DeleteBranchOptions) error { - return DeleteBranch(r.path, name, opts...) -} diff --git a/repo_reference_test.go b/repo_reference_test.go index d0e90e17..05a243c0 100644 --- a/repo_reference_test.go +++ b/repo_reference_test.go @@ -1,10 +1,7 @@ -// Copyright 2020 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - package git import ( + "context" "strconv" "testing" "time" @@ -38,13 +35,15 @@ func TestRefShortName(t *testing.T) { } func TestRepository_ShowRefVerify(t *testing.T) { - t.Run("reference does not exsit", func(t *testing.T) { - rev, err := testrepo.ShowRefVerify("bad_reference") + ctx := context.Background() + + t.Run("reference does not exist", func(t *testing.T) { + rev, err := testrepo.ShowRefVerify(ctx, "bad_reference") assert.NotNil(t, err) assert.Empty(t, rev) }) - rev, err := testrepo.ShowRefVerify("refs/heads/release-1.0") + rev, err := testrepo.ShowRefVerify(ctx, "refs/heads/release-1.0") if err != nil { t.Fatal(err) } @@ -53,13 +52,15 @@ func TestRepository_ShowRefVerify(t *testing.T) { } func TestRepository_BranchCommitID(t *testing.T) { - t.Run("branch does not exsit", func(t *testing.T) { - rev, err := testrepo.BranchCommitID("bad_branch") + ctx := context.Background() + + t.Run("branch does not exist", func(t *testing.T) { + rev, err := testrepo.BranchCommitID(ctx, "bad_branch") assert.NotNil(t, err) assert.Empty(t, rev) }) - rev, err := testrepo.BranchCommitID("release-1.0") + rev, err := testrepo.BranchCommitID(ctx, "release-1.0") if err != nil { t.Fatal(err) } @@ -68,13 +69,15 @@ func TestRepository_BranchCommitID(t *testing.T) { } func TestRepository_TagCommitID(t *testing.T) { - t.Run("tag does not exsit", func(t *testing.T) { - rev, err := testrepo.TagCommitID("bad_tag") + ctx := context.Background() + + t.Run("tag does not exist", func(t *testing.T) { + rev, err := testrepo.TagCommitID(ctx, "bad_tag") assert.NotNil(t, err) assert.Empty(t, rev) }) - rev, err := testrepo.TagCommitID("v1.0.0") + rev, err := testrepo.TagCommitID(ctx, "v1.0.0") if err != nil { t.Fatal(err) } @@ -83,6 +86,7 @@ func TestRepository_TagCommitID(t *testing.T) { } func TestRepository_HasReference(t *testing.T) { + ctx := context.Background() tests := []struct { ref string opt ShowRefVerifyOptions @@ -103,12 +107,13 @@ func TestRepository_HasReference(t *testing.T) { } for _, test := range tests { t.Run("", func(t *testing.T) { - assert.Equal(t, test.expVal, testrepo.HasReference(test.ref, test.opt)) + assert.Equal(t, test.expVal, testrepo.HasReference(ctx, test.ref, test.opt)) }) } } func TestRepository_HasBranch(t *testing.T) { + ctx := context.Background() tests := []struct { ref string opt ShowRefVerifyOptions @@ -125,12 +130,13 @@ func TestRepository_HasBranch(t *testing.T) { } for _, test := range tests { t.Run("", func(t *testing.T) { - assert.Equal(t, test.expVal, testrepo.HasBranch(test.ref, test.opt)) + assert.Equal(t, test.expVal, testrepo.HasBranch(ctx, test.ref, test.opt)) }) } } func TestRepository_HasTag(t *testing.T) { + ctx := context.Background() tests := []struct { ref string opt ShowRefVerifyOptions @@ -147,12 +153,13 @@ func TestRepository_HasTag(t *testing.T) { } for _, test := range tests { t.Run("", func(t *testing.T) { - assert.Equal(t, test.expVal, testrepo.HasTag(test.ref, test.opt)) + assert.Equal(t, test.expVal, testrepo.HasTag(ctx, test.ref, test.opt)) }) } } func TestRepository_SymbolicRef(t *testing.T) { + ctx := context.Background() r, cleanup, err := setupTempRepo() if err != nil { t.Fatal(err) @@ -160,14 +167,14 @@ func TestRepository_SymbolicRef(t *testing.T) { defer cleanup() // Get HEAD - ref, err := r.SymbolicRef() + ref, err := r.SymbolicRef(ctx) if err != nil { t.Fatal(err) } assert.Equal(t, RefsHeads+"master", ref) // Set a symbolic reference - _, err = r.SymbolicRef(SymbolicRefOptions{ + _, err = r.SymbolicRef(ctx, SymbolicRefOptions{ Name: "TEST_REF", Ref: RefsHeads + "develop", }) @@ -176,7 +183,7 @@ func TestRepository_SymbolicRef(t *testing.T) { } // Get the symbolic reference we just set - ref, err = r.SymbolicRef(SymbolicRefOptions{ + ref, err = r.SymbolicRef(ctx, SymbolicRefOptions{ Name: "TEST_REF", }) if err != nil { @@ -186,6 +193,7 @@ func TestRepository_SymbolicRef(t *testing.T) { } func TestRepository_ShowRef(t *testing.T) { + ctx := context.Background() tests := []struct { opt ShowRefOptions expRefs []*Reference @@ -216,7 +224,7 @@ func TestRepository_ShowRef(t *testing.T) { } for _, test := range tests { t.Run("", func(t *testing.T) { - refs, err := testrepo.ShowRef(test.opt) + refs, err := testrepo.ShowRef(ctx, test.opt) if err != nil { t.Fatal(err) } @@ -227,12 +235,13 @@ func TestRepository_ShowRef(t *testing.T) { } func TestRepository_Branches(t *testing.T) { + ctx := context.Background() expBranches := map[string]bool{ "master": true, "develop": true, "release-1.0": true, } - branches, err := testrepo.Branches() + branches, err := testrepo.Branches(ctx) if err != nil { t.Fatal(err) } @@ -246,6 +255,7 @@ func TestRepository_Branches(t *testing.T) { } func TestRepository_DeleteBranch(t *testing.T) { + ctx := context.Background() r, cleanup, err := setupTempRepo() if err != nil { t.Fatal(err) @@ -269,26 +279,26 @@ func TestRepository_DeleteBranch(t *testing.T) { for _, test := range tests { t.Run("", func(t *testing.T) { branch := strconv.Itoa(int(time.Now().UnixNano())) - err := r.Checkout(branch, CheckoutOptions{ + err := r.Checkout(ctx, branch, CheckoutOptions{ BaseBranch: "master", }) if err != nil { t.Fatal(err) } - assert.True(t, r.HasReference(RefsHeads+branch)) + assert.True(t, r.HasReference(ctx, RefsHeads+branch)) - err = r.Checkout("master") + err = r.Checkout(ctx, "master") if err != nil { t.Fatal(err) } - err = r.DeleteBranch(branch, test.opt) + err = r.DeleteBranch(ctx, branch, test.opt) if err != nil { t.Fatal(err) } - assert.False(t, r.HasReference(RefsHeads+branch)) + assert.False(t, r.HasReference(ctx, RefsHeads+branch)) }) } } diff --git a/repo_remote.go b/repo_remote.go index 363b8998..5b0379a9 100644 --- a/repo_remote.go +++ b/repo_remote.go @@ -1,13 +1,9 @@ -// Copyright 2019 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - package git import ( "bytes" + "context" "strings" - "time" ) // LsRemoteOptions contains arguments for listing references in a remote @@ -23,38 +19,32 @@ type LsRemoteOptions struct { Refs bool // The list of patterns to filter results. Patterns []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 } -// LsRemote returns a list references in the remote repository. -func LsRemote(url string, opts ...LsRemoteOptions) ([]*Reference, error) { +// 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("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.RunWithTimeout(opt.Timeout) + stdout, err := exec(ctx, "", args, opt.Envs) if err != nil { return nil, err } @@ -75,12 +65,11 @@ func LsRemote(url string, opts ...LsRemoteOptions) ([]*Reference, error) { return refs, nil } -// IsURLAccessible returns true if given remote URL is accessible via Git within -// given timeout. -func IsURLAccessible(timeout time.Duration, url string) bool { - _, err := LsRemote(url, LsRemoteOptions{ +// IsURLAccessible returns true if given remote URL is accessible via Git. The +// caller should use context.WithTimeout to control the timeout. +func IsURLAccessible(ctx context.Context, url string) bool { + _, err := LsRemote(ctx, url, LsRemoteOptions{ Patterns: []string{"HEAD"}, - Timeout: timeout, }) return err == nil } @@ -94,82 +83,47 @@ type RemoteAddOptions struct { Fetch bool // Indicates whether to add remote as mirror with --mirror=fetch. MirrorFetch 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 } -// Deprecated: Use RemoteAddOptions instead. -type AddRemoteOptions = RemoteAddOptions - -// RemoteAdd adds a new remote to the repository in given path. -func RemoteAdd(repoPath, name, url string, opts ...RemoteAddOptions) error { +// RemoteAdd adds a new remote to the repository. +func (r *Repository) RemoteAdd(ctx context.Context, name, url string, opts ...RemoteAddOptions) error { var opt RemoteAddOptions if len(opts) > 0 { opt = opts[0] } - cmd := NewCommand("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).RunInDirWithTimeout(opt.Timeout, repoPath) + _, err := exec(ctx, r.path, args, opt.Envs) return err } -// Deprecated: Use RemoteAdd instead. -func RepoAddRemote(repoPath, name, url string, opts ...RemoteAddOptions) error { - return RemoteAdd(repoPath, name, url, opts...) -} - -// RemoteAdd adds a new remote to the repository. -func (r *Repository) RemoteAdd(name, url string, opts ...RemoteAddOptions) error { - return RemoteAdd(r.path, name, url, opts...) -} - -// Deprecated: Use RemoteAdd instead. -func (r *Repository) AddRemote(name, url string, opts ...RemoteAddOptions) error { - return RemoteAdd(r.path, name, url, opts...) -} - // RemoteRemoveOptions contains arguments for removing a remote from the // repository. // // Docs: https://git-scm.com/docs/git-remote#Documentation/git-remote.txt-emremoveem type RemoteRemoveOptions 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 } -// Deprecated: Use RemoteRemoveOptions instead. -type RemoveRemoteOptions = RemoteRemoveOptions - -// RemoteRemove removes a remote from the repository in given path. -func RemoteRemove(repoPath, name string, opts ...RemoteRemoveOptions) error { +// RemoteRemove removes a remote from the repository. +func (r *Repository) RemoteRemove(ctx context.Context, name string, opts ...RemoteRemoveOptions) error { var opt RemoteRemoveOptions if len(opts) > 0 { opt = opts[0] } - _, err := NewCommand("remote", "remove"). - AddOptions(opt.CommandOptions). - AddArgs("--end-of-options", name). - RunInDirWithTimeout(opt.Timeout, repoPath) + 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 @@ -179,44 +133,22 @@ func RemoteRemove(repoPath, name string, opts ...RemoteRemoveOptions) error { return nil } -// Deprecated: Use RemoteRemove instead. -func RepoRemoveRemote(repoPath, name string, opts ...RemoteRemoveOptions) error { - return RemoteRemove(repoPath, name, opts...) -} - -// RemoteRemove removes a remote from the repository. -func (r *Repository) RemoteRemove(name string, opts ...RemoteRemoveOptions) error { - return RemoteRemove(r.path, name, opts...) -} - -// Deprecated: Use RemoteRemove instead. -func (r *Repository) RemoveRemote(name string, opts ...RemoteRemoveOptions) error { - return RemoteRemove(r.path, name, opts...) -} - // RemotesOptions contains arguments for listing remotes of the repository. // / // Docs: https://git-scm.com/docs/git-remote#_commands type RemotesOptions 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 } -// Remotes lists remotes of the repository in given path. -func Remotes(repoPath string, opts ...RemotesOptions) ([]string, error) { +// Remotes lists remotes of the repository. +func (r *Repository) Remotes(ctx context.Context, opts ...RemotesOptions) ([]string, error) { var opt RemotesOptions if len(opts) > 0 { opt = opts[0] } - stdout, err := NewCommand("remote"). - AddOptions(opt.CommandOptions). - RunInDirWithTimeout(opt.Timeout, repoPath) + args := []string{"remote", "--end-of-options"} + stdout, err := exec(ctx, r.path, args, opt.Envs) if err != nil { return nil, err } @@ -224,11 +156,6 @@ func Remotes(repoPath string, opts ...RemotesOptions) ([]string, error) { return bytesToStrings(stdout), nil } -// Remotes lists remotes of the repository. -func (r *Repository) Remotes(opts ...RemotesOptions) ([]string, error) { - return Remotes(r.path, opts...) -} - // RemoteGetURLOptions contains arguments for retrieving URL(s) of a remote of // the repository. // @@ -239,42 +166,32 @@ 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 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 } -// RemoteGetURL retrieves URL(s) of a remote of the repository in given path. -func RemoteGetURL(repoPath, name string, opts ...RemoteGetURLOptions) ([]string, error) { +// RemoteGetURL retrieves URL(s) of a remote of the repository. +func (r *Repository) RemoteGetURL(ctx context.Context, name string, opts ...RemoteGetURLOptions) ([]string, error) { var opt RemoteGetURLOptions if len(opts) > 0 { opt = opts[0] } - cmd := NewCommand("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).RunInDirWithTimeout(opt.Timeout, repoPath) + stdout, err := exec(ctx, r.path, args, opt.Envs) if err != nil { return nil, err } return bytesToStrings(stdout), nil } -// RemoteGetURL retrieves URL(s) of a remote of the repository in given path. -func (r *Repository) RemoteGetURL(name string, opts ...RemoteGetURLOptions) ([]string, error) { - return RemoteGetURL(r.path, name, opts...) -} - // RemoteSetURLOptions contains arguments for setting an URL of a remote of the // repository. // @@ -284,35 +201,27 @@ type RemoteSetURLOptions struct { Push bool // The regex to match existing URLs to replace (instead of first). Regex 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 } -// RemoteSetURL sets first URL of the remote with given name of the repository -// in given path. -func RemoteSetURL(repoPath, name, newurl string, opts ...RemoteSetURLOptions) error { +// RemoteSetURL sets the first URL of the remote with given name of the +// repository. +func (r *Repository) RemoteSetURL(ctx context.Context, name, newurl string, opts ...RemoteSetURLOptions) error { var opt RemoteSetURLOptions if len(opts) > 0 { opt = opts[0] } - cmd := NewCommand("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.RunInDirWithTimeout(opt.Timeout, repoPath) + _, err := exec(ctx, r.path, args, opt.Envs) if err != nil { if strings.Contains(err.Error(), "No such URL found") { return ErrURLNotExist @@ -324,12 +233,6 @@ func RemoteSetURL(repoPath, name, newurl string, opts ...RemoteSetURLOptions) er return nil } -// RemoteSetURL sets the first URL of the remote with given name of the -// repository. -func (r *Repository) RemoteSetURL(name, newurl string, opts ...RemoteSetURLOptions) error { - return RemoteSetURL(r.path, name, newurl, opts...) -} - // RemoteSetURLAddOptions contains arguments for appending an URL to a remote // of the repository. // @@ -337,45 +240,30 @@ func (r *Repository) RemoteSetURL(name, newurl string, opts ...RemoteSetURLOptio type RemoteSetURLAddOptions struct { // Indicates whether to get push URLs instead of fetch URLs. Push 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 } // RemoteSetURLAdd appends an URL to the remote with given name of the -// repository in given path. Use RemoteSetURL to overwrite the URL(s) instead. -func RemoteSetURLAdd(repoPath, name, newurl string, opts ...RemoteSetURLAddOptions) error { +// repository. Use RemoteSetURL to overwrite the URL(s) instead. +func (r *Repository) RemoteSetURLAdd(ctx context.Context, name, newurl string, opts ...RemoteSetURLAddOptions) error { var opt RemoteSetURLAddOptions if len(opts) > 0 { opt = opts[0] } - cmd := NewCommand("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.RunInDirWithTimeout(opt.Timeout, repoPath) + _, err := exec(ctx, r.path, args, opt.Envs) if err != nil && strings.Contains(err.Error(), "Will not delete all non-push URLs") { return ErrNotDeleteNonPushURLs } return err } -// RemoteSetURLAdd appends an URL to the remote with given name of the -// repository. Use RemoteSetURL to overwrite the URL(s) instead. -func (r *Repository) RemoteSetURLAdd(name, newurl string, opts ...RemoteSetURLAddOptions) error { - return RemoteSetURLAdd(r.path, name, newurl, opts...) -} - // RemoteSetURLDeleteOptions contains arguments for deleting an URL of a remote // of the repository. // @@ -383,41 +271,26 @@ func (r *Repository) RemoteSetURLAdd(name, newurl string, opts ...RemoteSetURLAd type RemoteSetURLDeleteOptions struct { // Indicates whether to get push URLs instead of fetch URLs. Push 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 } -// RemoteSetURLDelete deletes the remote with given name of the repository in -// given path. -func RemoteSetURLDelete(repoPath, name, regex string, opts ...RemoteSetURLDeleteOptions) error { +// RemoteSetURLDelete deletes all URLs matching regex of the remote with given +// name of the repository. +func (r *Repository) RemoteSetURLDelete(ctx context.Context, name, regex string, opts ...RemoteSetURLDeleteOptions) error { var opt RemoteSetURLDeleteOptions if len(opts) > 0 { opt = opts[0] } - cmd := NewCommand("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.RunInDirWithTimeout(opt.Timeout, repoPath) + _, err := exec(ctx, r.path, args, opt.Envs) if err != nil && strings.Contains(err.Error(), "Will not delete all non-push URLs") { return ErrNotDeleteNonPushURLs } return err } - -// RemoteSetURLDelete deletes all URLs matching regex of the remote with given -// name of the repository. -func (r *Repository) RemoteSetURLDelete(name, regex string, opts ...RemoteSetURLDeleteOptions) error { - return RemoteSetURLDelete(r.path, name, regex, opts...) -} diff --git a/repo_remote_test.go b/repo_remote_test.go index 51ee3ab0..d4e0962a 100644 --- a/repo_remote_test.go +++ b/repo_remote_test.go @@ -1,10 +1,7 @@ -// Copyright 2020 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - package git import ( + "context" "os" "testing" @@ -12,6 +9,7 @@ import ( ) func TestLsRemote(t *testing.T) { + ctx := context.Background() tests := []struct { url string opt LsRemoteOptions @@ -57,7 +55,7 @@ func TestLsRemote(t *testing.T) { } for _, test := range tests { t.Run("", func(t *testing.T) { - refs, err := LsRemote(test.url, test.opt) + refs, err := LsRemote(ctx, test.url, test.opt) if err != nil { t.Fatal(err) } @@ -68,6 +66,7 @@ func TestLsRemote(t *testing.T) { } func TestIsURLAccessible(t *testing.T) { + ctx := context.Background() tests := []struct { url string expVal bool @@ -82,18 +81,19 @@ func TestIsURLAccessible(t *testing.T) { } for _, test := range tests { t.Run("", func(t *testing.T) { - assert.Equal(t, test.expVal, IsURLAccessible(DefaultTimeout, test.url)) + assert.Equal(t, test.expVal, IsURLAccessible(ctx, test.url)) }) } } func TestRepository_RemoteAdd(t *testing.T) { + ctx := context.Background() path := tempPath() defer func() { _ = os.RemoveAll(path) }() - err := Init(path, InitOptions{ + err := Init(ctx, path, InitOptions{ Bare: true, }) if err != nil { @@ -106,7 +106,7 @@ func TestRepository_RemoteAdd(t *testing.T) { } // Add testrepo as the mirror remote and fetch right away - err = r.RemoteAdd("origin", testrepo.Path(), RemoteAddOptions{ + err = r.RemoteAdd(ctx, "origin", testrepo.Path(), RemoteAddOptions{ Fetch: true, MirrorFetch: true, }) @@ -115,24 +115,26 @@ func TestRepository_RemoteAdd(t *testing.T) { } // Check a non-default branch: release-1.0 - assert.True(t, r.HasReference(RefsHeads+"release-1.0")) + assert.True(t, r.HasReference(ctx, RefsHeads+"release-1.0")) } func TestRepository_RemoteRemove(t *testing.T) { + ctx := context.Background() r, cleanup, err := setupTempRepo() if err != nil { t.Fatal(err) } defer cleanup() - err = r.RemoteRemove("origin", RemoteRemoveOptions{}) + err = r.RemoteRemove(ctx, "origin", RemoteRemoveOptions{}) assert.Nil(t, err) - err = r.RemoteRemove("origin", RemoteRemoveOptions{}) + err = r.RemoteRemove(ctx, "origin", RemoteRemoveOptions{}) assert.Equal(t, ErrRemoteNotExist, err) } func TestRepository_Remotes(t *testing.T) { + ctx := context.Background() r, cleanup, err := setupTempRepo() if err != nil { t.Fatal(err) @@ -140,69 +142,70 @@ func TestRepository_Remotes(t *testing.T) { defer cleanup() // 1 remote - remotes, err := r.Remotes() + remotes, err := r.Remotes(ctx) assert.Nil(t, err) assert.Equal(t, []string{"origin"}, remotes) // 2 remotes - err = r.RemoteAdd("t", "t") + err = r.RemoteAdd(ctx, "t", "t") assert.Nil(t, err) - remotes, err = r.Remotes() + remotes, err = r.Remotes(ctx) assert.Nil(t, err) assert.Equal(t, []string{"origin", "t"}, remotes) assert.Len(t, remotes, 2) // 0 remotes - err = r.RemoteRemove("t") + err = r.RemoteRemove(ctx, "t") assert.Nil(t, err) - err = r.RemoteRemove("origin") + err = r.RemoteRemove(ctx, "origin") assert.Nil(t, err) - remotes, err = r.Remotes() + remotes, err = r.Remotes(ctx) assert.Nil(t, err) assert.Equal(t, []string{}, remotes) assert.Len(t, remotes, 0) } func TestRepository_RemoteURLFamily(t *testing.T) { + ctx := context.Background() r, cleanup, err := setupTempRepo() if err != nil { t.Fatal(err) } defer cleanup() - err = r.RemoteSetURLDelete("origin", ".*") + err = r.RemoteSetURLDelete(ctx, "origin", ".*") assert.Equal(t, ErrNotDeleteNonPushURLs, err) - err = r.RemoteSetURL("notexist", "t") + err = r.RemoteSetURL(ctx, "notexist", "t") assert.Equal(t, ErrRemoteNotExist, err) - err = r.RemoteSetURL("notexist", "t", RemoteSetURLOptions{Regex: "t"}) + err = r.RemoteSetURL(ctx, "notexist", "t", RemoteSetURLOptions{Regex: "t"}) assert.Equal(t, ErrRemoteNotExist, err) // Default origin URL is not easily testable - err = r.RemoteSetURL("origin", "t") + err = r.RemoteSetURL(ctx, "origin", "t") assert.Nil(t, err) - urls, err := r.RemoteGetURL("origin") + urls, err := r.RemoteGetURL(ctx, "origin") assert.Nil(t, err) assert.Equal(t, []string{"t"}, urls) - err = r.RemoteSetURLAdd("origin", "e") + err = r.RemoteSetURLAdd(ctx, "origin", "e") assert.Nil(t, err) - urls, err = r.RemoteGetURL("origin", RemoteGetURLOptions{All: true}) + urls, err = r.RemoteGetURL(ctx, "origin", RemoteGetURLOptions{All: true}) assert.Nil(t, err) assert.Equal(t, []string{"t", "e"}, urls) - err = r.RemoteSetURL("origin", "s", RemoteSetURLOptions{Regex: "e"}) + err = r.RemoteSetURL(ctx, "origin", "s", RemoteSetURLOptions{Regex: "e"}) assert.Nil(t, err) - urls, err = r.RemoteGetURL("origin", RemoteGetURLOptions{All: true}) + urls, err = r.RemoteGetURL(ctx, "origin", RemoteGetURLOptions{All: true}) assert.Nil(t, err) assert.Equal(t, []string{"t", "s"}, urls) - err = r.RemoteSetURLDelete("origin", "t") + err = r.RemoteSetURLDelete(ctx, "origin", "t") assert.Nil(t, err) - urls, err = r.RemoteGetURL("origin", RemoteGetURLOptions{All: true}) + urls, err = r.RemoteGetURL(ctx, "origin", RemoteGetURLOptions{All: true}) assert.Nil(t, err) assert.Equal(t, []string{"s"}, urls) } diff --git a/repo_tag.go b/repo_tag.go index fd00deac..3d309897 100644 --- a/repo_tag.go +++ b/repo_tag.go @@ -1,16 +1,10 @@ -// Copyright 2015 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - package git import ( "bytes" + "context" "fmt" "strings" - "time" - - goversion "github.com/mcuadros/go-version" ) // parseTag parses tag information from the (uncompressed) raw data of the tag @@ -54,15 +48,15 @@ l: } // getTag returns a tag by given SHA1 hash. -func (r *Repository) getTag(timeout time.Duration, id *SHA1) (*Tag, error) { +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 } // Check tag type - typ, err := r.CatFileType(id.String(), CatFileTypeOptions{Timeout: timeout}) + typ, err := r.CatFileType(ctx, id.String()) if err != nil { return nil, err } @@ -78,7 +72,7 @@ func (r *Repository) getTag(timeout time.Duration, id *SHA1) (*Tag, error) { } case ObjectTag: // Tag is an annotation - data, err := NewCommand("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 } @@ -102,27 +96,20 @@ func (r *Repository) getTag(timeout time.Duration, id *SHA1) (*Tag, error) { // // Docs: https://git-scm.com/docs/git-cat-file type TagOptions 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 } // Tag returns a Git tag by given name, e.g. "v1.0.0". -func (r *Repository) Tag(name string, opts ...TagOptions) (*Tag, error) { +func (r *Repository) Tag(ctx context.Context, name string, opts ...TagOptions) (*Tag, error) { var opt TagOptions if len(opts) > 0 { opt = opts[0] } - refsepc := RefsTags + name - refs, err := r.ShowRef(ShowRefOptions{ + refspec := RefsTags + name + refs, err := r.ShowRef(ctx, ShowRefOptions{ Tags: true, - Patterns: []string{refsepc}, - Timeout: opt.Timeout, + Patterns: []string{refspec}, CommandOptions: opt.CommandOptions, }) if err != nil { @@ -136,11 +123,11 @@ func (r *Repository) Tag(name string, opts ...TagOptions) (*Tag, error) { return nil, err } - tag, err := r.getTag(opt.Timeout, id) + tag, err := r.getTag(ctx, id) if err != nil { return nil, err } - tag.refspec = refsepc + tag.refspec = refspec return tag, nil } @@ -152,43 +139,28 @@ type TagsOptions struct { SortKey string // Pattern filters tags matching the specified pattern. Pattern 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 } -// RepoTags returns a list of tags of the repository in given path. -func RepoTags(repoPath string, opts ...TagsOptions) ([]string, error) { +// Tags returns a list of tags of the repository. +func (r *Repository) Tags(ctx context.Context, opts ...TagsOptions) ([]string, error) { var opt TagsOptions if len(opts) > 0 { opt = opts[0] } - version, err := BinVersion() - if err != nil { - return nil, err - } - - cmd := NewCommand("tag", "--list").AddOptions(opt.CommandOptions) - - var sorted bool + args := []string{"tag", "--list"} if opt.SortKey != "" { - cmd.AddArgs("--sort=" + opt.SortKey) - sorted = true - } else if goversion.Compare(version, "2.4.9", ">=") { - cmd.AddArgs("--sort=-creatordate") - sorted = true + args = append(args, "--sort="+opt.SortKey) + } else { + 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.RunInDirWithTimeout(opt.Timeout, repoPath) + stdout, err := exec(ctx, r.path, args, opt.Envs) if err != nil { return nil, err } @@ -196,24 +168,9 @@ func RepoTags(repoPath string, opts ...TagsOptions) ([]string, error) { tags := strings.Split(string(stdout), "\n") tags = tags[:len(tags)-1] - if !sorted { - goversion.Sort(tags) - - // Reverse order - for i := 0; i < len(tags)/2; i++ { - j := len(tags) - i - 1 - tags[i], tags[j] = tags[j], tags[i] - } - } - return tags, nil } -// Tags returns a list of tags of the repository. -func (r *Repository) Tags(opts ...TagsOptions) ([]string, error) { - return RepoTags(r.path, opts...) -} - // CreateTagOptions contains optional arguments for creating a tag. // // Docs: https://git-scm.com/docs/git-tag @@ -224,37 +181,31 @@ type CreateTagOptions struct { Message string // Author is the author of the tag. It is ignored when tag is not annotated. 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 } // CreateTag creates a new tag on given revision. -func (r *Repository) CreateTag(name, rev string, opts ...CreateTagOptions) error { +func (r *Repository) CreateTag(ctx context.Context, name, rev string, opts ...CreateTagOptions) error { var opt CreateTagOptions if len(opts) > 0 { opt = opts[0] } - cmd := NewCommand("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.RunInDirWithTimeout(opt.Timeout, r.path) + args = append(args, rev) + envs = append(envs, opt.Envs...) + _, err := exec(ctx, r.path, args, envs) return err } @@ -262,24 +213,17 @@ func (r *Repository) CreateTag(name, rev string, opts ...CreateTagOptions) error // // Docs: https://git-scm.com/docs/git-tag#Documentation/git-tag.txt---delete type DeleteTagOptions 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 } // DeleteTag deletes a tag from the repository. -func (r *Repository) DeleteTag(name string, opts ...DeleteTagOptions) error { +func (r *Repository) DeleteTag(ctx context.Context, name string, opts ...DeleteTagOptions) error { var opt DeleteTagOptions if len(opts) > 0 { opt = opts[0] } - _, err := NewCommand("tag", "--delete", "--end-of-options", name). - AddOptions(opt.CommandOptions). - RunInDirWithTimeout(opt.Timeout, r.path) + args := []string{"tag", "--delete", "--end-of-options", name} + _, err := exec(ctx, r.path, args, opt.Envs) return err } diff --git a/repo_tag_test.go b/repo_tag_test.go index bd5bc79b..9905c927 100644 --- a/repo_tag_test.go +++ b/repo_tag_test.go @@ -1,16 +1,14 @@ -// Copyright 2020 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - package git import ( + "context" "testing" "github.com/stretchr/testify/assert" ) func TestRepository_Tag(t *testing.T) { + ctx := context.Background() tests := []struct { name string opt TagOptions @@ -36,7 +34,7 @@ func TestRepository_Tag(t *testing.T) { } for _, test := range tests { t.Run("", func(t *testing.T) { - tag, err := testrepo.Tag(test.name, test.opt) + tag, err := testrepo.Tag(ctx, test.name, test.opt) if err != nil { t.Fatal(err) } @@ -50,8 +48,9 @@ func TestRepository_Tag(t *testing.T) { } func TestRepository_Tags(t *testing.T) { + ctx := context.Background() // Make sure it does not blow up - tags, err := testrepo.Tags(TagsOptions{}) + tags, err := testrepo.Tags(ctx, TagsOptions{}) if err != nil { t.Fatal(err) } @@ -59,22 +58,23 @@ func TestRepository_Tags(t *testing.T) { } func TestRepository_Tags_VersionSort(t *testing.T) { + ctx := context.Background() r, cleanup, err := setupTempRepo() if err != nil { t.Fatal(err) } defer cleanup() - err = r.CreateTag("v3.0.0", "master") + err = r.CreateTag(ctx, "v3.0.0", "master") if err != nil { t.Fatal(err) } - err = r.CreateTag("v2.999.0", "master") + err = r.CreateTag(ctx, "v2.999.0", "master") if err != nil { t.Fatal(err) } - tags, err := r.Tags(TagsOptions{ + tags, err := r.Tags(ctx, TagsOptions{ SortKey: "-version:refname", Pattern: "v*", }) @@ -90,32 +90,34 @@ func TestRepository_Tags_VersionSort(t *testing.T) { } func TestRepository_CreateTag(t *testing.T) { + ctx := context.Background() r, cleanup, err := setupTempRepo() if err != nil { t.Fatal(err) } defer cleanup() - assert.False(t, r.HasReference(RefsTags+"v2.0.0")) + assert.False(t, r.HasReference(ctx, RefsTags+"v2.0.0")) - err = r.CreateTag("v2.0.0", "master", CreateTagOptions{}) + err = r.CreateTag(ctx, "v2.0.0", "master", CreateTagOptions{}) if err != nil { t.Fatal(err) } - assert.True(t, r.HasReference(RefsTags+"v2.0.0")) + assert.True(t, r.HasReference(ctx, RefsTags+"v2.0.0")) } func TestRepository_CreateAnnotatedTag(t *testing.T) { + ctx := context.Background() r, cleanup, err := setupTempRepo() if err != nil { t.Fatal(err) } defer cleanup() - assert.False(t, r.HasReference(RefsTags+"v2.0.0")) + assert.False(t, r.HasReference(ctx, RefsTags+"v2.0.0")) - err = r.CreateTag("v2.0.0", "master", CreateTagOptions{ + err = r.CreateTag(ctx, "v2.0.0", "master", CreateTagOptions{ Annotated: true, Author: &Signature{ Name: "alice", @@ -126,9 +128,9 @@ func TestRepository_CreateAnnotatedTag(t *testing.T) { t.Fatal(err) } - assert.True(t, r.HasReference(RefsTags+"v2.0.0")) + assert.True(t, r.HasReference(ctx, RefsTags+"v2.0.0")) - tag, err := r.Tag("v2.0.0") + tag, err := r.Tag(ctx, "v2.0.0") if err != nil { t.Fatal(err) } @@ -139,18 +141,19 @@ func TestRepository_CreateAnnotatedTag(t *testing.T) { } func TestRepository_DeleteTag(t *testing.T) { + ctx := context.Background() r, cleanup, err := setupTempRepo() if err != nil { t.Fatal(err) } defer cleanup() - assert.True(t, r.HasReference(RefsTags+"v1.0.0")) + assert.True(t, r.HasReference(ctx, RefsTags+"v1.0.0")) - err = r.DeleteTag("v1.0.0", DeleteTagOptions{}) + err = r.DeleteTag(ctx, "v1.0.0", DeleteTagOptions{}) if err != nil { t.Fatal(err) } - assert.False(t, r.HasReference(RefsTags+"v1.0.0")) + assert.False(t, r.HasReference(ctx, RefsTags+"v1.0.0")) } diff --git a/repo_test.go b/repo_test.go index 7f4ef1c4..6f7e9bd8 100644 --- a/repo_test.go +++ b/repo_test.go @@ -1,11 +1,7 @@ -// Copyright 2020 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - package git import ( - "io/ioutil" + "context" "os" "path/filepath" "testing" @@ -23,6 +19,7 @@ func TestRepository(t *testing.T) { } func TestInit(t *testing.T) { + ctx := context.Background() tests := []struct { opt InitOptions }{ @@ -43,7 +40,7 @@ func TestInit(t *testing.T) { _ = os.RemoveAll(path) }() - if err := Init(path, test.opt); err != nil { + if err := Init(ctx, path, test.opt); err != nil { t.Fatal(err) } }) @@ -59,6 +56,7 @@ func TestOpen(t *testing.T) { } func TestClone(t *testing.T) { + ctx := context.Background() tests := []struct { opt CloneOptions }{ @@ -97,7 +95,7 @@ func TestClone(t *testing.T) { _ = os.RemoveAll(path) }() - if err := Clone(testrepo.Path(), path, test.opt); err != nil { + if err := Clone(ctx, testrepo.Path(), path, test.opt); err != nil { t.Fatal(err) } }) @@ -105,6 +103,7 @@ func TestClone(t *testing.T) { } func setupTempRepo() (_ *Repository, cleanup func(), err error) { + ctx := context.Background() path := tempPath() cleanup = func() { _ = os.RemoveAll(path) @@ -115,7 +114,7 @@ func setupTempRepo() (_ *Repository, cleanup func(), err error) { } }() - if err = Clone(testrepo.Path(), path); err != nil { + if err = Clone(ctx, testrepo.Path(), path); err != nil { return nil, cleanup, err } @@ -127,6 +126,7 @@ func setupTempRepo() (_ *Repository, cleanup func(), err error) { } func TestRepository_Fetch(t *testing.T) { + ctx := context.Background() r, cleanup, err := setupTempRepo() if err != nil { t.Fatal(err) @@ -148,7 +148,7 @@ func TestRepository_Fetch(t *testing.T) { for _, test := range tests { t.Run("", func(t *testing.T) { // Make sure it does not blow up - if err := r.Fetch(test.opt); err != nil { + if err := r.Fetch(ctx, test.opt); err != nil { t.Fatal(err) } }) @@ -156,6 +156,7 @@ func TestRepository_Fetch(t *testing.T) { } func TestRepository_Pull(t *testing.T) { + ctx := context.Background() r, cleanup, err := setupTempRepo() if err != nil { t.Fatal(err) @@ -188,7 +189,7 @@ func TestRepository_Pull(t *testing.T) { for _, test := range tests { t.Run("", func(t *testing.T) { // Make sure it does not blow up - if err := r.Pull(test.opt); err != nil { + if err := r.Pull(ctx, test.opt); err != nil { t.Fatal(err) } }) @@ -196,6 +197,7 @@ func TestRepository_Pull(t *testing.T) { } func TestRepository_Push(t *testing.T) { + ctx := context.Background() r, cleanup, err := setupTempRepo() if err != nil { t.Fatal(err) @@ -212,11 +214,16 @@ func TestRepository_Push(t *testing.T) { branch: "master", opt: PushOptions{}, }, + { + remote: "origin", + branch: "master", + opt: PushOptions{SetUpstream: true}, + }, } for _, test := range tests { t.Run("", func(t *testing.T) { // Make sure it does not blow up - if err := r.Push(test.remote, test.branch, test.opt); err != nil { + if err := r.Push(ctx, test.remote, test.branch, test.opt); err != nil { t.Fatal(err) } }) @@ -224,6 +231,7 @@ func TestRepository_Push(t *testing.T) { } func TestRepository_Checkout(t *testing.T) { + ctx := context.Background() r, cleanup, err := setupTempRepo() if err != nil { t.Fatal(err) @@ -248,7 +256,7 @@ func TestRepository_Checkout(t *testing.T) { for _, test := range tests { t.Run("", func(t *testing.T) { // Make sure it does not blow up - if err := r.Checkout(test.branch, test.opt); err != nil { + if err := r.Checkout(ctx, test.branch, test.opt); err != nil { t.Fatal(err) } }) @@ -256,6 +264,7 @@ func TestRepository_Checkout(t *testing.T) { } func TestRepository_Reset(t *testing.T) { + ctx := context.Background() r, cleanup, err := setupTempRepo() if err != nil { t.Fatal(err) @@ -276,7 +285,7 @@ func TestRepository_Reset(t *testing.T) { for _, test := range tests { t.Run("", func(t *testing.T) { // Make sure it does not blow up - if err := r.Reset(test.rev, test.opt); err != nil { + if err := r.Reset(ctx, test.rev, test.opt); err != nil { t.Fatal(err) } }) @@ -284,6 +293,7 @@ func TestRepository_Reset(t *testing.T) { } func TestRepository_Move(t *testing.T) { + ctx := context.Background() r, cleanup, err := setupTempRepo() if err != nil { t.Fatal(err) @@ -304,7 +314,7 @@ func TestRepository_Move(t *testing.T) { for _, test := range tests { t.Run("", func(t *testing.T) { // Make sure it does not blow up - if err := r.Move(test.src, test.dst, test.opt); err != nil { + if err := r.Move(ctx, test.src, test.dst, test.opt); err != nil { t.Fatal(err) } }) @@ -312,6 +322,7 @@ func TestRepository_Move(t *testing.T) { } func TestRepository_Add(t *testing.T) { + ctx := context.Background() r, cleanup, err := setupTempRepo() if err != nil { t.Fatal(err) @@ -320,13 +331,13 @@ func TestRepository_Add(t *testing.T) { // Generate a file fpath := filepath.Join(r.Path(), "TESTFILE") - err = ioutil.WriteFile(fpath, []byte("something"), 0600) + err = os.WriteFile(fpath, []byte("something"), 0600) if err != nil { t.Fatal(err) } // Make sure it does not blow up - if err := r.Add(AddOptions{ + if err := r.Add(ctx, AddOptions{ All: true, Pathspecs: []string{"TESTFILE"}, }); err != nil { @@ -335,6 +346,7 @@ func TestRepository_Add(t *testing.T) { } func TestRepository_Commit(t *testing.T) { + ctx := context.Background() r, cleanup, err := setupTempRepo() if err != nil { t.Fatal(err) @@ -352,7 +364,7 @@ func TestRepository_Commit(t *testing.T) { message := "Add a file" t.Run("nothing to commit", func(t *testing.T) { - if err = r.Commit(committer, message, CommitOptions{ + if err = r.Commit(ctx, committer, message, CommitOptions{ Author: author, }); err != nil { t.Fatal(err) @@ -362,24 +374,24 @@ func TestRepository_Commit(t *testing.T) { t.Run("committer is also the author", func(t *testing.T) { // Generate a file and add to index fpath := filepath.Join(r.Path(), "COMMITTER_IS_AUTHOR") - err = ioutil.WriteFile(fpath, []byte("something"), 0600) + err = os.WriteFile(fpath, []byte("something"), 0600) if err != nil { t.Fatal(err) } - if err := r.Add(AddOptions{ + if err := r.Add(ctx, AddOptions{ All: true, }); err != nil { t.Fatal(err) } // Make sure it does not blow up - if err = r.Commit(committer, message); err != nil { + if err = r.Commit(ctx, committer, message); err != nil { t.Fatal(err) } // Verify the result - c, err := r.CatFileCommit("master") + c, err := r.CatFileCommit(ctx, "master") if err != nil { t.Fatal(err) } @@ -394,24 +406,24 @@ func TestRepository_Commit(t *testing.T) { t.Run("committer is not the author", func(t *testing.T) { // Generate a file and add to index fpath := filepath.Join(r.Path(), "COMMITTER_IS_NOT_AUTHOR") - err = ioutil.WriteFile(fpath, []byte("something"), 0600) + err = os.WriteFile(fpath, []byte("something"), 0600) if err != nil { t.Fatal(err) } - if err := r.Add(AddOptions{ + if err := r.Add(ctx, AddOptions{ All: true, }); err != nil { t.Fatal(err) } // Make sure it does not blow up - if err = r.Commit(committer, message, CommitOptions{Author: author}); err != nil { + if err = r.Commit(ctx, committer, message, CommitOptions{Author: author}); err != nil { t.Fatal(err) } // Verify the result - c, err := r.CatFileCommit("master") + c, err := r.CatFileCommit(ctx, "master") if err != nil { t.Fatal(err) } @@ -425,6 +437,7 @@ func TestRepository_Commit(t *testing.T) { } func TestRepository_RevParse(t *testing.T) { + ctx := context.Background() tests := []struct { rev string expID string @@ -464,7 +477,7 @@ func TestRepository_RevParse(t *testing.T) { } for _, test := range tests { t.Run("", func(t *testing.T) { - id, err := testrepo.RevParse(test.rev) + id, err := testrepo.RevParse(ctx, test.rev) assert.Equal(t, test.expErr, err) assert.Equal(t, test.expID, id) }) @@ -472,16 +485,18 @@ func TestRepository_RevParse(t *testing.T) { } func TestRepository_CountObjects(t *testing.T) { + ctx := context.Background() // Make sure it does not blow up - _, err := testrepo.CountObjects(CountObjectsOptions{}) + _, err := testrepo.CountObjects(ctx, CountObjectsOptions{}) if err != nil { t.Fatal(err) } } func TestRepository_Fsck(t *testing.T) { + ctx := context.Background() // Make sure it does not blow up - err := testrepo.Fsck(FsckOptions{}) + err := testrepo.Fsck(ctx, FsckOptions{}) if err != nil { t.Fatal(err) } diff --git a/repo_tree.go b/repo_tree.go index fe434d41..98badf2e 100644 --- a/repo_tree.go +++ b/repo_tree.go @@ -1,13 +1,9 @@ -// Copyright 2015 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - package git import ( "bytes" + "context" "fmt" - "time" ) // UnescapeChars reverses escaped characters in quoted output from Git. @@ -104,17 +100,11 @@ type LsTreeOptions struct { // Verbatim outputs filenames unquoted using the -z flag. This avoids issues // with special characters in filenames that would otherwise be quoted by Git. Verbatim 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 } // LsTree returns the tree object in the repository by given tree ID. -func (r *Repository) LsTree(treeID string, opts ...LsTreeOptions) (*Tree, error) { +func (r *Repository) LsTree(ctx context.Context, treeID string, opts ...LsTreeOptions) (*Tree, error) { var opt LsTreeOptions if len(opts) > 0 { opt = opts[0] @@ -122,12 +112,12 @@ func (r *Repository) LsTree(treeID string, opts ...LsTreeOptions) (*Tree, error) cache, ok := r.cachedTrees.Get(treeID) if ok { - log("Cached tree hit: %s", treeID) + logf("Cached tree hit: %s", treeID) return cache.(*Tree), nil } var err error - treeID, err = r.RevParse(treeID, RevParseOptions{Timeout: opt.Timeout}) //nolint + treeID, err = r.RevParse(ctx, treeID) //nolint if err != nil { return nil, err } @@ -136,14 +126,13 @@ func (r *Repository) LsTree(treeID string, opts ...LsTreeOptions) (*Tree, error) repo: r, } - cmd := NewCommand("ls-tree") + args := []string{"ls-tree"} if opt.Verbatim { - cmd.AddArgs("-z") + args = append(args, "-z") } - stdout, err := cmd. - AddOptions(opt.CommandOptions). - AddArgs(treeID). - RunInDirWithTimeout(opt.Timeout, 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/repo_tree_test.go b/repo_tree_test.go index 16beaa4a..bcc708c3 100644 --- a/repo_tree_test.go +++ b/repo_tree_test.go @@ -1,10 +1,7 @@ -// Copyright 2020 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - package git import ( + "context" "os" "path/filepath" "runtime" @@ -55,6 +52,7 @@ func TestUnescapeChars(t *testing.T) { } func TestRepository_LsTree(t *testing.T) { + ctx := context.Background() if runtime.GOOS == "windows" { t.Skip(`Windows does not allow '"' in filenames`) } @@ -62,33 +60,33 @@ func TestRepository_LsTree(t *testing.T) { path := tempPath() defer os.RemoveAll(path) - err := Init(path) + err := Init(ctx, path) require.NoError(t, err) specialName := `Test "Wiki" Page.md` err = os.WriteFile(filepath.Join(path, specialName), []byte("content"), 0o644) require.NoError(t, err) - err = Add(path, AddOptions{All: true}) + repo, err := Open(path) require.NoError(t, err) - err = CreateCommit(path, &Signature{Name: "test", Email: "test@test.com"}, "initial commit") + err = repo.Add(ctx, AddOptions{All: true}) require.NoError(t, err) - repo, err := Open(path) + err = repo.Commit(ctx, &Signature{Name: "test", Email: "test@test.com"}, "initial commit") require.NoError(t, err) - commit, err := repo.CatFileCommit("HEAD") + commit, err := repo.CatFileCommit(ctx, "HEAD") require.NoError(t, err) // Without Verbatim, Git quotes and escapes the filename. - entries, err := commit.Entries() + entries, err := commit.Entries(ctx) require.NoError(t, err) require.Len(t, entries, 1) assert.Equal(t, specialName, entries[0].Name()) // With Verbatim, Git outputs the filename as-is. - entries, err = commit.Entries(LsTreeOptions{Verbatim: true}) + entries, err = commit.Entries(ctx, LsTreeOptions{Verbatim: true}) require.NoError(t, err) require.Len(t, entries, 1) assert.Equal(t, specialName, entries[0].Name()) diff --git a/repo_worktree.go b/repo_worktree.go new file mode 100644 index 00000000..edbccf8f --- /dev/null +++ b/repo_worktree.go @@ -0,0 +1,56 @@ +package git + +import "context" + +// WorktreeAddOptions contains optional arguments for adding a worktree. +// +// Docs: https://git-scm.com/docs/git-worktree#Documentation/git-worktree.txt-add +type WorktreeAddOptions struct { + // The new branch name to create and checkout in the worktree. + Branch string + CommandOptions +} + +// WorktreeAdd creates a new worktree at the given path linked to this +// repository. The commitIsh determines the HEAD of the new worktree. +func (r *Repository) WorktreeAdd(ctx context.Context, path, commitIsh string, opts ...WorktreeAddOptions) error { + var opt WorktreeAddOptions + if len(opts) > 0 { + opt = opts[0] + } + + args := []string{"worktree", "add"} + if opt.Branch != "" { + args = append(args, "-b", opt.Branch) + } + args = append(args, "--end-of-options", path, commitIsh) + + _, err := exec(ctx, r.path, args, opt.Envs) + return err +} + +// WorktreeRemoveOptions contains optional arguments for removing a worktree. +// +// Docs: https://git-scm.com/docs/git-worktree#Documentation/git-worktree.txt-remove +type WorktreeRemoveOptions struct { + // Indicates whether to force removal even if the worktree is dirty. + Force bool + CommandOptions +} + +// WorktreeRemove removes a worktree at the given path. +func (r *Repository) WorktreeRemove(ctx context.Context, path string, opts ...WorktreeRemoveOptions) error { + var opt WorktreeRemoveOptions + if len(opts) > 0 { + opt = opts[0] + } + + args := []string{"worktree", "remove"} + if opt.Force { + args = append(args, "--force") + } + args = append(args, "--end-of-options", path) + + _, err := exec(ctx, r.path, args, opt.Envs) + return err +} diff --git a/repo_worktree_test.go b/repo_worktree_test.go new file mode 100644 index 00000000..2f05ca46 --- /dev/null +++ b/repo_worktree_test.go @@ -0,0 +1,73 @@ +package git + +import ( + "context" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRepository_WorktreeAdd(t *testing.T) { + ctx := context.Background() + r, cleanup, err := setupTempRepo() + require.NoError(t, err) + defer cleanup() + + t.Run("detached HEAD", func(t *testing.T) { + path := tempPath() + defer func() { _ = os.RemoveAll(path) }() + + sha, err := r.RevParse(ctx, "master") + require.NoError(t, err) + + err = r.WorktreeAdd(ctx, path, sha) + assert.NoError(t, err) + }) + + t.Run("new branch", func(t *testing.T) { + path := tempPath() + defer func() { _ = os.RemoveAll(path) }() + + err := r.WorktreeAdd(ctx, path, "master", WorktreeAddOptions{ + Branch: "test-worktree-branch", + }) + assert.NoError(t, err) + }) +} + +func TestRepository_WorktreeRemove(t *testing.T) { + ctx := context.Background() + r, cleanup, err := setupTempRepo() + require.NoError(t, err) + defer cleanup() + + t.Run("remove worktree", func(t *testing.T) { + path := tempPath() + + err := r.WorktreeAdd(ctx, path, "master", WorktreeAddOptions{ + Branch: "test-remove-branch", + }) + require.NoError(t, err) + + err = r.WorktreeRemove(ctx, path) + assert.NoError(t, err) + }) + + t.Run("force remove", func(t *testing.T) { + path := tempPath() + + err := r.WorktreeAdd(ctx, path, "master", WorktreeAddOptions{ + Branch: "test-force-remove", + }) + require.NoError(t, err) + + // Write an untracked file to make the worktree dirty. + err = os.WriteFile(path+"/dirty-file", []byte("dirty"), 0600) + require.NoError(t, err) + + err = r.WorktreeRemove(ctx, path, WorktreeRemoveOptions{Force: true}) + assert.NoError(t, err) + }) +} diff --git a/server.go b/server.go index 0c2d275e..92d9d25d 100755 --- a/server.go +++ b/server.go @@ -1,10 +1,7 @@ -// Copyright 2023 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - package git import ( + "context" "time" ) @@ -15,25 +12,23 @@ import ( type UpdateServerInfoOptions struct { // Indicates whether to overwrite the existing server info. Force bool - // The timeout duration before giving up for each shell command execution. The - // default timeout duration will be used when not supplied. - Timeout time.Duration - // The additional options to be passed to the underlying git. CommandOptions } // UpdateServerInfo updates the auxiliary info file on the server side for the // repository in given path. -func UpdateServerInfo(path string, opts ...UpdateServerInfoOptions) error { +func UpdateServerInfo(ctx context.Context, path string, opts ...UpdateServerInfoOptions) error { var opt UpdateServerInfoOptions if len(opts) > 0 { opt = opts[0] } - cmd := NewCommand("update-server-info").AddOptions(opt.CommandOptions) + + args := []string{"update-server-info"} if opt.Force { - cmd.AddArgs("--force") + args = append(args, "--force") } - _, err := cmd.RunInDirWithTimeout(opt.Timeout, path) + args = append(args, "--end-of-options") + _, err := exec(ctx, path, args, opt.Envs) return err } @@ -46,28 +41,25 @@ type ReceivePackOptions struct { Quiet bool // Indicates whether to generate the "info/refs" used by the "git http-backend". HTTPBackendInfoRefs bool - // The timeout duration before giving up for each shell command execution. The - // default timeout duration will be used when not supplied. - Timeout time.Duration - // The additional options to be passed to the underlying git. CommandOptions } // ReceivePack receives what is pushed into the repository in given path. -func ReceivePack(path string, opts ...ReceivePackOptions) ([]byte, error) { +func ReceivePack(ctx context.Context, path string, opts ...ReceivePackOptions) ([]byte, error) { var opt ReceivePackOptions if len(opts) > 0 { opt = opts[0] } - cmd := NewCommand("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.RunInDirWithTimeout(opt.Timeout, path) + args = append(args, "--end-of-options", ".") + return exec(ctx, path, args, opt.Envs) } // UploadPackOptions contains optional arguments for sending the packfile to the @@ -82,32 +74,33 @@ type UploadPackOptions struct { Strict bool // Indicates whether to generate the "info/refs" used by the "git http-backend". HTTPBackendInfoRefs bool - // The timeout duration before giving up for each shell command execution. The - // default timeout duration will be used when not supplied. - Timeout time.Duration - // The additional options to be passed to the underlying git. + // The git-level inactivity timeout passed to git-upload-pack's --timeout flag. + // This is separate from the command execution timeout which is controlled via + // context.Context. + InactivityTimeout time.Duration CommandOptions } // UploadPack sends the packfile to the client for the repository in given path. -func UploadPack(path string, opts ...UploadPackOptions) ([]byte, error) { +func UploadPack(ctx context.Context, path string, opts ...UploadPackOptions) ([]byte, error) { var opt UploadPackOptions if len(opts) > 0 { opt = opts[0] } - cmd := NewCommand("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.Timeout > 0 { - cmd.AddArgs("--timeout", opt.Timeout.String()) + if opt.InactivityTimeout > 0 { + 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.RunInDirWithTimeout(opt.Timeout, path) + args = append(args, "--end-of-options", ".") + return exec(ctx, path, args, opt.Envs) } diff --git a/server_test.go b/server_test.go index b8d95dc2..32baf1e3 100755 --- a/server_test.go +++ b/server_test.go @@ -1,10 +1,7 @@ -// Copyright 2023 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - package git import ( + "context" "os" "path/filepath" "testing" @@ -14,22 +11,25 @@ import ( ) func TestUpdateServerInfo(t *testing.T) { + ctx := context.Background() err := os.RemoveAll(filepath.Join(repoPath, "info")) require.NoError(t, err) - err = UpdateServerInfo(repoPath, UpdateServerInfoOptions{Force: true}) + err = UpdateServerInfo(ctx, repoPath, UpdateServerInfoOptions{Force: true}) require.NoError(t, err) assert.True(t, isFile(filepath.Join(repoPath, "info", "refs"))) } func TestReceivePack(t *testing.T) { - got, err := ReceivePack(repoPath, ReceivePackOptions{HTTPBackendInfoRefs: true}) + ctx := context.Background() + got, err := ReceivePack(ctx, repoPath, ReceivePackOptions{HTTPBackendInfoRefs: true}) require.NoError(t, err) const contains = "report-status report-status-v2 delete-refs side-band-64k quiet atomic ofs-delta object-format=sha1 agent=git/" assert.Contains(t, string(got), contains) } func TestUploadPack(t *testing.T) { - got, err := UploadPack(repoPath, + ctx := context.Background() + got, err := UploadPack(ctx, repoPath, UploadPackOptions{ StatelessRPC: true, Strict: true, diff --git a/sha1.go b/sha1.go index 4709345d..c977166c 100644 --- a/sha1.go +++ b/sha1.go @@ -1,7 +1,3 @@ -// Copyright 2015 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - package git import ( diff --git a/sha1_test.go b/sha1_test.go index b3778b37..be98b0d0 100644 --- a/sha1_test.go +++ b/sha1_test.go @@ -1,7 +1,3 @@ -// Copyright 2020 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - package git import ( diff --git a/signature.go b/signature.go index a35f7bab..ebc1b232 100644 --- a/signature.go +++ b/signature.go @@ -1,7 +1,3 @@ -// Copyright 2015 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - package git import ( diff --git a/signature_test.go b/signature_test.go index 2793a50a..d38ea466 100644 --- a/signature_test.go +++ b/signature_test.go @@ -1,7 +1,3 @@ -// Copyright 2020 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - package git import ( diff --git a/tag.go b/tag.go index e26cb1b3..40efd5a0 100644 --- a/tag.go +++ b/tag.go @@ -1,9 +1,7 @@ -// Copyright 2015 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - package git +import "context" + // Tag contains information of a Git tag. type Tag struct { typ ObjectType @@ -47,6 +45,6 @@ func (t *Tag) Message() string { } // Commit returns the underlying commit of the tag. -func (t *Tag) Commit(opts ...CatFileCommitOptions) (*Commit, error) { - return t.repo.CatFileCommit(t.commitID.String(), opts...) +func (t *Tag) Commit(ctx context.Context, opts ...CatFileCommitOptions) (*Commit, error) { + return t.repo.CatFileCommit(ctx, t.commitID.String(), opts...) } diff --git a/tag_test.go b/tag_test.go index fdd63cc9..c43c1510 100644 --- a/tag_test.go +++ b/tag_test.go @@ -1,17 +1,15 @@ -// Copyright 2020 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - package git import ( + "context" "testing" "github.com/stretchr/testify/assert" ) func TestTag(t *testing.T) { - tag, err := testrepo.Tag("v1.1.0") + ctx := context.Background() + tag, err := testrepo.Tag(ctx, "v1.1.0") if err != nil { t.Fatal(err) } @@ -31,12 +29,13 @@ func TestTag(t *testing.T) { } func TestTag_Commit(t *testing.T) { - tag, err := testrepo.Tag("v1.1.0") + ctx := context.Background() + tag, err := testrepo.Tag(ctx, "v1.1.0") if err != nil { t.Fatal(err) } - c, err := tag.Commit() + c, err := tag.Commit(ctx) if err != nil { t.Fatal(err) } diff --git a/tree.go b/tree.go index ee16e370..539027b4 100644 --- a/tree.go +++ b/tree.go @@ -1,10 +1,7 @@ -// Copyright 2015 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - package git import ( + "context" "strings" "sync" ) @@ -16,13 +13,13 @@ type Tree struct { repo *Repository - entries Entries - entriesOnce sync.Once - entriesErr error + entries Entries + entriesMu sync.Mutex + entriesSet bool } // Subtree returns a subtree by given subpath of the tree. -func (t *Tree) Subtree(subpath string, opts ...LsTreeOptions) (*Tree, error) { +func (t *Tree) Subtree(ctx context.Context, subpath string, opts ...LsTreeOptions) (*Tree, error) { if len(subpath) == 0 { return t, nil } @@ -35,7 +32,7 @@ func (t *Tree) Subtree(subpath string, opts ...LsTreeOptions) (*Tree, error) { e *TreeEntry ) for _, name := range paths { - e, err = p.TreeEntry(name, opts...) + e, err = p.TreeEntry(ctx, name, opts...) if err != nil { return nil, err } @@ -50,20 +47,21 @@ func (t *Tree) Subtree(subpath string, opts ...LsTreeOptions) (*Tree, error) { return g, nil } -// Entries returns all entries of the tree. -func (t *Tree) Entries(opts ...LsTreeOptions) (Entries, error) { - t.entriesOnce.Do(func() { - if t.entries != nil { - return - } +// Entries returns all entries of the tree. Successful results are cached; +// failed attempts are not cached, allowing retries with a fresh context. +func (t *Tree) Entries(ctx context.Context, opts ...LsTreeOptions) (Entries, error) { + t.entriesMu.Lock() + defer t.entriesMu.Unlock() - var tt *Tree - tt, t.entriesErr = t.repo.LsTree(t.id.String(), opts...) - if t.entriesErr != nil { - return - } - t.entries = tt.entries - }) + if t.entriesSet { + return t.entries, nil + } - return t.entries, t.entriesErr + tt, err := t.repo.LsTree(ctx, t.id.String(), opts...) + if err != nil { + return nil, err + } + t.entries = tt.entries + t.entriesSet = true + return t.entries, nil } diff --git a/tree_blob.go b/tree_blob.go index 57fea178..a50b08fa 100644 --- a/tree_blob.go +++ b/tree_blob.go @@ -1,16 +1,13 @@ -// Copyright 2015 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - package git import ( + "context" "path" "strings" ) // TreeEntry returns the TreeEntry by given subpath of the tree. -func (t *Tree) TreeEntry(subpath string, opts ...LsTreeOptions) (*TreeEntry, error) { +func (t *Tree) TreeEntry(ctx context.Context, subpath string, opts ...LsTreeOptions) (*TreeEntry, error) { if len(subpath) == 0 { return &TreeEntry{ id: t.id, @@ -26,7 +23,7 @@ func (t *Tree) TreeEntry(subpath string, opts ...LsTreeOptions) (*TreeEntry, err for i, name := range paths { // Reached end of the loop if i == len(paths)-1 { - entries, err := tree.Entries(opts...) + entries, err := tree.Entries(ctx, opts...) if err != nil { return nil, err } @@ -37,7 +34,7 @@ func (t *Tree) TreeEntry(subpath string, opts ...LsTreeOptions) (*TreeEntry, err } } } else { - tree, err = tree.Subtree(name, opts...) + tree, err = tree.Subtree(ctx, name, opts...) if err != nil { return nil, err } @@ -47,8 +44,8 @@ func (t *Tree) TreeEntry(subpath string, opts ...LsTreeOptions) (*TreeEntry, err } // Blob returns the blob object by given subpath of the tree. -func (t *Tree) Blob(subpath string, opts ...LsTreeOptions) (*Blob, error) { - e, err := t.TreeEntry(subpath, opts...) +func (t *Tree) Blob(ctx context.Context, subpath string, opts ...LsTreeOptions) (*Blob, error) { + e, err := t.TreeEntry(ctx, subpath, opts...) if err != nil { return nil, err } @@ -61,8 +58,8 @@ func (t *Tree) Blob(subpath string, opts ...LsTreeOptions) (*Blob, error) { } // BlobByIndex returns blob object by given index. -func (t *Tree) BlobByIndex(index string) (*Blob, error) { - typ, err := t.repo.CatFileType(index) +func (t *Tree) BlobByIndex(ctx context.Context, index string) (*Blob, error) { + typ, err := t.repo.CatFileType(ctx, index) if err != nil { return nil, err } @@ -71,7 +68,7 @@ func (t *Tree) BlobByIndex(index string) (*Blob, error) { return nil, ErrNotBlob } - id, err := t.repo.RevParse(index) + id, err := t.repo.RevParse(ctx, index) if err != nil { return nil, err } diff --git a/tree_blob_test.go b/tree_blob_test.go index 6138be5b..ed9d509e 100644 --- a/tree_blob_test.go +++ b/tree_blob_test.go @@ -1,22 +1,20 @@ -// Copyright 2020 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - package git import ( + "context" "testing" "github.com/stretchr/testify/assert" ) func TestTree_TreeEntry(t *testing.T) { - tree, err := testrepo.LsTree("master") + ctx := context.Background() + tree, err := testrepo.LsTree(ctx, "master") if err != nil { t.Fatal(err) } - e, err := tree.TreeEntry("") + e, err := tree.TreeEntry(ctx, "") if err != nil { t.Fatal(err) } @@ -27,18 +25,19 @@ func TestTree_TreeEntry(t *testing.T) { } func TestTree_Blob(t *testing.T) { - tree, err := testrepo.LsTree("d58e3ef9f123eea6857161c79275ee22b228f659") + ctx := context.Background() + tree, err := testrepo.LsTree(ctx, "d58e3ef9f123eea6857161c79275ee22b228f659") if err != nil { t.Fatal(err) } t.Run("not a blob", func(t *testing.T) { - _, err := tree.Blob("src") + _, err := tree.Blob(ctx, "src") assert.Equal(t, ErrNotBlob, err) }) t.Run("get a blob", func(t *testing.T) { - b, err := tree.Blob("README.txt") + b, err := tree.Blob(ctx, "README.txt") if err != nil { t.Fatal(err) } @@ -47,7 +46,7 @@ func TestTree_Blob(t *testing.T) { }) t.Run("get an executable as blob", func(t *testing.T) { - b, err := tree.Blob("run.sh") + b, err := tree.Blob(ctx, "run.sh") if err != nil { t.Fatal(err) } diff --git a/tree_entry.go b/tree_entry.go index 8fdf251a..071d9abf 100644 --- a/tree_entry.go +++ b/tree_entry.go @@ -1,10 +1,7 @@ -// Copyright 2015 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - package git import ( + "context" "fmt" "path" "runtime" @@ -13,7 +10,6 @@ import ( "strings" "sync" "sync/atomic" - "time" ) // EntryMode is the unix file mode of a tree entry. @@ -37,8 +33,9 @@ type TreeEntry struct { parent *Tree - size int64 - sizeOnce sync.Once + size int64 + sizeMu sync.Mutex + sizeSet bool } // Mode returns the entry mode if the tree entry. @@ -86,20 +83,23 @@ func (e *TreeEntry) Name() string { return e.name } -// Size returns the size of thr entry. -func (e *TreeEntry) Size() int64 { - e.sizeOnce.Do(func() { - if e.IsTree() { - return - } +// Size returns the size of the entry. It runs a git command to determine the +// size on first call. Successful results are cached; failed attempts are not +// cached, allowing retries with a fresh context. +func (e *TreeEntry) Size(ctx context.Context) int64 { + e.sizeMu.Lock() + defer e.sizeMu.Unlock() - stdout, err := NewCommand("cat-file", "-s", e.id.String()).RunInDir(e.parent.repo.path) - if err != nil { - return - } - e.size, _ = strconv.ParseInt(strings.TrimSpace(string(stdout)), 10, 64) - }) + if e.sizeSet || e.IsTree() { + return e.size + } + stdout, err := exec(ctx, e.parent.repo.path, []string{"cat-file", "-s", e.id.String()}, nil) + if err != nil { + return 0 + } + e.size, _ = strconv.ParseInt(strings.TrimSpace(string(stdout)), 10, 64) + e.sizeSet = true return e.size } @@ -159,9 +159,6 @@ type CommitsInfoOptions struct { // The maximum number of goroutines to be used for getting commits information. // When not set (i.e. <=0), runtime.GOMAXPROCS is used to determine the value. MaxConcurrency int - // The timeout duration before giving up for each shell command execution. - // The default timeout duration will be used when not supplied. - Timeout time.Duration } var defaultConcurrency = runtime.GOMAXPROCS(0) @@ -170,7 +167,7 @@ var defaultConcurrency = runtime.GOMAXPROCS(0) // the state of given commit and subpath. It takes advantages of concurrency to // speed up the process. The returned list has the same number of items as tree // entries, so the caller can access them via slice indices. -func (es Entries) CommitsInfo(commit *Commit, opts ...CommitsInfoOptions) ([]*EntryCommitInfo, error) { +func (es Entries) CommitsInfo(ctx context.Context, commit *Commit, opts ...CommitsInfoOptions) ([]*EntryCommitInfo, error) { if len(es) == 0 { return []*EntryCommitInfo{}, nil } @@ -232,9 +229,8 @@ func (es Entries) CommitsInfo(commit *Commit, opts ...CommitsInfoOptions) ([]*En epath := path.Join(opt.Path, e.Name()) var err error - info.Commit, err = commit.CommitByPath(CommitByRevisionOptions{ - Path: epath, - Timeout: opt.Timeout, + info.Commit, err = commit.CommitByPath(ctx, CommitByRevisionOptions{ + Path: epath, }) if err != nil { setError(fmt.Errorf("get commit by path %q: %v", epath, err)) @@ -244,7 +240,7 @@ func (es Entries) CommitsInfo(commit *Commit, opts ...CommitsInfoOptions) ([]*En // Get extra information for submodules if e.IsCommit() { // Be tolerant to implicit submodules - info.Submodule, err = commit.Submodule(epath) + info.Submodule, err = commit.Submodule(ctx, epath) if err != nil { info.Submodule = &Submodule{Name: epath} } diff --git a/tree_entry_test.go b/tree_entry_test.go index 50b09ef0..e8c3707d 100644 --- a/tree_entry_test.go +++ b/tree_entry_test.go @@ -1,13 +1,11 @@ -// Copyright 2020 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - package git import ( + "context" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestTreeEntry(t *testing.T) { @@ -30,13 +28,47 @@ 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) { - tree, err := testrepo.LsTree("0eedd79eba4394bbef888c804e899731644367fe") + ctx := context.Background() + tree, err := testrepo.LsTree(ctx, "0eedd79eba4394bbef888c804e899731644367fe") if err != nil { t.Fatal(err) } - es, err := tree.Entries() + es, err := tree.Entries(ctx) if err != nil { t.Fatal(err) } @@ -120,23 +152,24 @@ func TestEntries_Sort(t *testing.T) { } func TestEntries_CommitsInfo(t *testing.T) { - tree, err := testrepo.LsTree("cfc3b2993f74726356887a5ec093de50486dc617") + ctx := context.Background() + tree, err := testrepo.LsTree(ctx, "cfc3b2993f74726356887a5ec093de50486dc617") if err != nil { t.Fatal(err) } - c, err := testrepo.CatFileCommit(tree.id.String()) + c, err := testrepo.CatFileCommit(ctx, tree.id.String()) if err != nil { t.Fatal(err) } t.Run("general directory", func(t *testing.T) { - es, err := tree.Entries() + es, err := tree.Entries(ctx) if err != nil { t.Fatal(err) } - infos, err := es.CommitsInfo(c) + infos, err := es.CommitsInfo(ctx, c) if err != nil { t.Fatal(err) } @@ -249,17 +282,17 @@ func TestEntries_CommitsInfo(t *testing.T) { }) t.Run("directory with submodule", func(t *testing.T) { - subtree, err := tree.Subtree("gogs") + subtree, err := tree.Subtree(ctx, "gogs") if err != nil { t.Fatal(err) } - es, err := subtree.Entries() + es, err := subtree.Entries(ctx) if err != nil { t.Fatal(err) } - infos, err := es.CommitsInfo(c, CommitsInfoOptions{ + infos, err := es.CommitsInfo(ctx, c, CommitsInfoOptions{ Path: "gogs", }) if err != nil { @@ -282,18 +315,18 @@ func TestEntries_CommitsInfo(t *testing.T) { } }) - t.Run("direcotry with files have same SHA", func(t *testing.T) { - subtree, err := tree.Subtree("sameSHAs") + t.Run("directory with files that have the same SHA", func(t *testing.T) { + subtree, err := tree.Subtree(ctx, "sameSHAs") if err != nil { t.Fatal(err) } - es, err := subtree.Entries() + es, err := subtree.Entries(ctx) if err != nil { t.Fatal(err) } - infos, err := es.CommitsInfo(c, CommitsInfoOptions{ + infos, err := es.CommitsInfo(ctx, c, CommitsInfoOptions{ Path: "sameSHAs", }) if err != nil { diff --git a/utils.go b/utils.go index 79c0e278..29aade21 100644 --- a/utils.go +++ b/utils.go @@ -1,11 +1,6 @@ -// Copyright 2015 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - package git import ( - "fmt" "os" "strings" "sync" @@ -66,13 +61,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 {