From 20722b96c523c5ffbc31fbd5680f1362c45def4a Mon Sep 17 00:00:00 2001 From: Joe Chen Date: Fri, 13 Feb 2026 21:20:19 -0500 Subject: [PATCH 1/7] v2: start of a new era --- README.md | 6 ++---- git_test.go | 21 --------------------- go.mod | 8 ++------ go.sum | 4 ---- repo_tag.go | 22 +--------------------- 5 files changed, 5 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index 4849e21f..22d84576 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,12 @@ # 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**. ## License diff --git a/git_test.go b/git_test.go index 13adc294..10289006 100644 --- a/git_test.go +++ b/git_test.go @@ -7,14 +7,11 @@ package git import ( "bytes" "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" @@ -88,21 +85,3 @@ func Test_log(t *testing.T) { }) } } - -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..83a77403 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,8 @@ -module github.com/gogs/git-module +module github.com/gogs/git-module/v2 go 1.24.0 -require ( - github.com/mcuadros/go-version v0.0.0-20190308113854-92cdf37c5b75 - github.com/stretchr/testify v1.11.1 - golang.org/x/sync v0.19.0 -) +require github.com/stretchr/testify v1.11.1 require ( github.com/davecgh/go-spew v1.1.1 // indirect diff --git a/go.sum b/go.sum index bce0f20a..c4c1710c 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,9 @@ 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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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= 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= diff --git a/repo_tag.go b/repo_tag.go index fd00deac..931b6dc6 100644 --- a/repo_tag.go +++ b/repo_tag.go @@ -9,8 +9,6 @@ import ( "fmt" "strings" "time" - - goversion "github.com/mcuadros/go-version" ) // parseTag parses tag information from the (uncompressed) raw data of the tag @@ -168,20 +166,12 @@ func RepoTags(repoPath string, opts ...TagsOptions) ([]string, error) { opt = opts[0] } - version, err := BinVersion() - if err != nil { - return nil, err - } - cmd := NewCommand("tag", "--list").AddOptions(opt.CommandOptions) - var sorted bool if opt.SortKey != "" { cmd.AddArgs("--sort=" + opt.SortKey) - sorted = true - } else if goversion.Compare(version, "2.4.9", ">=") { + } else { cmd.AddArgs("--sort=-creatordate") - sorted = true } if opt.Pattern != "" { @@ -196,16 +186,6 @@ 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 } From f2b89e08df936d0d9da27eb0017e7e003cf321c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=B4=8A=E1=B4=8F=E1=B4=87=20=E1=B4=84=CA=9C=E1=B4=87?= =?UTF-8?q?=C9=B4?= Date: Fri, 13 Feb 2026 22:17:55 -0500 Subject: [PATCH 2/7] cleanup: consolidate all repo operations into Repository methods (#125) --- .github/workflows/go.yml | 8 +-- .golangci.yml | 53 +++++++++----- commit.go | 3 +- diff.go | 3 +- go.mod | 2 +- hook.go | 5 +- hook_test.go | 3 +- repo.go | 151 ++++++++------------------------------- repo_blame.go | 4 +- repo_blame_test.go | 6 +- repo_commit.go | 37 ++-------- repo_commit_test.go | 7 -- repo_hook.go | 3 +- repo_pull.go | 17 +---- repo_reference.go | 82 ++++----------------- repo_remote.go | 110 ++++++---------------------- repo_tag.go | 11 +-- repo_test.go | 7 +- repo_tree_test.go | 6 +- 19 files changed, 133 insertions(+), 385 deletions(-) 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/commit.go b/commit.go index e287df65..301be259 100644 --- a/commit.go +++ b/commit.go @@ -7,7 +7,6 @@ package git import ( "bytes" "io" - "io/ioutil" "net/http" "strings" "sync" @@ -154,7 +153,7 @@ func (c *Commit) isImageFile(blob *Blob, err error) (bool, error) { N: int64(buf.Cap()), } - err = blob.Pipeline(stdout, ioutil.Discard) + err = blob.Pipeline(stdout, io.Discard) if err != nil { return false, err } diff --git a/diff.go b/diff.go index 3dab3733..e70932e6 100644 --- a/diff.go +++ b/diff.go @@ -10,7 +10,6 @@ import ( "errors" "fmt" "io" - "io/ioutil" "strconv" "strings" ) @@ -482,7 +481,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/go.mod b/go.mod index 83a77403..0753589a 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/gogs/git-module/v2 -go 1.24.0 +go 1.26.0 require github.com/stretchr/testify v1.11.1 diff --git a/hook.go b/hook.go index 566d5381..7083c1ad 100644 --- a/hook.go +++ b/hook.go @@ -5,7 +5,6 @@ package git import ( - "io/ioutil" "os" "path" "strings" @@ -235,11 +234,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..92cd4866 100644 --- a/hook_test.go +++ b/hook_test.go @@ -5,7 +5,6 @@ package git import ( - "io/ioutil" "os" "testing" @@ -43,7 +42,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/repo.go b/repo.go index dd327131..1eee7b2d 100644 --- a/repo.go +++ b/repo.go @@ -253,29 +253,18 @@ type PushOptions struct { 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(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) + _, err := cmd.RunInDirWithTimeout(opt.Timeout, r.path) 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 @@ -291,8 +280,8 @@ type CheckoutOptions struct { 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(branch string, opts ...CheckoutOptions) error { var opt CheckoutOptions if len(opts) > 0 { opt = opts[0] @@ -307,20 +296,10 @@ func Checkout(repoPath, branch string, opts ...CheckoutOptions) error { cmd.AddArgs(opt.BaseBranch) } - _, err := cmd.RunInDirWithTimeout(opt.Timeout, repoPath) + _, err := cmd.RunInDirWithTimeout(opt.Timeout, r.path) 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 @@ -336,8 +315,8 @@ type ResetOptions struct { 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(rev string, opts ...ResetOptions) error { var opt ResetOptions if len(opts) > 0 { opt = opts[0] @@ -348,20 +327,10 @@ func Reset(repoPath, rev string, opts ...ResetOptions) error { cmd.AddArgs("--hard") } - _, err := cmd.AddOptions(opt.CommandOptions).AddArgs("--end-of-options", rev).RunInDir(repoPath) + _, err := cmd.AddOptions(opt.CommandOptions).AddArgs("--end-of-options", rev).RunInDir(r.path) 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. // @@ -377,28 +346,17 @@ type MoveOptions struct { } // 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(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) + _, err := NewCommand("mv").AddOptions(opt.CommandOptions).AddArgs("--end-of-options", src, dst).RunInDirWithTimeout(opt.Timeout, r.path) 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 @@ -416,8 +374,8 @@ type AddOptions struct { 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(opts ...AddOptions) error { var opt AddOptions if len(opts) > 0 { opt = opts[0] @@ -431,20 +389,10 @@ func Add(repoPath string, opts ...AddOptions) error { cmd.AddArgs("--") cmd.AddArgs(opt.Pathspecs...) } - _, err := cmd.RunInDirWithTimeout(opt.Timeout, repoPath) + _, err := cmd.RunInDirWithTimeout(opt.Timeout, r.path) 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 @@ -460,9 +408,9 @@ type CommitOptions struct { 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(committer *Signature, message string, opts ...CommitOptions) error { var opt CommitOptions if len(opts) > 0 { opt = opts[0] @@ -478,7 +426,7 @@ func CreateCommit(repoPath string, committer *Signature, message string, opts .. AddArgs("-m", message). AddOptions(opt.CommandOptions) - _, err := cmd.RunInDirWithTimeout(opt.Timeout, repoPath) + _, err := cmd.RunInDirWithTimeout(opt.Timeout, r.path) // No stderr but exit status 1 means nothing to commit. if err != nil && err.Error() == "exit status 1" { return nil @@ -486,17 +434,6 @@ func CreateCommit(repoPath string, committer *Signature, message string, opts .. 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 @@ -517,9 +454,8 @@ type ShowNameStatusOptions struct { 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(rev string, opts ...ShowNameStatusOptions) (*NameStatus, error) { var opt ShowNameStatusOptions if len(opts) > 0 { opt = opts[0] @@ -552,7 +488,7 @@ func ShowNameStatus(repoPath, rev string, opts ...ShowNameStatusOptions) (*NameS cmd := NewCommand("show", "--name-status", "--pretty=format:''"). AddOptions(opt.CommandOptions). AddArgs("--end-of-options", rev) - err := cmd.RunInDirPipelineWithTimeout(opt.Timeout, w, stderr, repoPath) + err := cmd.RunInDirPipelineWithTimeout(opt.Timeout, w, stderr, r.path) _ = w.Close() // Close writer to exit parsing goroutine if err != nil { return nil, concatenateError(err, stderr.String()) @@ -562,16 +498,6 @@ func ShowNameStatus(repoPath, rev string, opts ...ShowNameStatusOptions) (*NameS 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 @@ -631,8 +557,8 @@ type CountObjectsOptions struct { 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(opts ...CountObjectsOptions) (*CountObject, error) { var opt CountObjectsOptions if len(opts) > 0 { opt = opts[0] @@ -640,7 +566,7 @@ func CountObjects(repoPath string, opts ...CountObjectsOptions) (*CountObject, e stdout, err := NewCommand("count-objects", "-v"). AddOptions(opt.CommandOptions). - RunInDirWithTimeout(opt.Timeout, repoPath) + RunInDirWithTimeout(opt.Timeout, r.path) if err != nil { return nil, err } @@ -675,16 +601,6 @@ 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 @@ -699,25 +615,14 @@ type FsckOptions struct { } // 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(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) + _, err := cmd.RunInDirWithTimeout(opt.Timeout, r.path) 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..047fa950 100644 --- a/repo_blame.go +++ b/repo_blame.go @@ -21,9 +21,9 @@ type BlameOptions struct { 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(rev, file string, opts ...BlameOptions) (*Blame, error) { var opt BlameOptions if len(opts) > 0 { opt = opts[0] diff --git a/repo_blame_test.go b/repo_blame_test.go index ec9b45db..6073b78d 100644 --- a/repo_blame_test.go +++ b/repo_blame_test.go @@ -11,13 +11,13 @@ import ( "github.com/stretchr/testify/assert" ) -func TestRepository_BlameFile(t *testing.T) { +func TestRepository_Blame(t *testing.T) { t.Run("bad file", func(t *testing.T) { - _, err := testrepo.BlameFile("", "404.txt") + _, err := testrepo.Blame("", "404.txt") assert.Error(t, err) }) - blame, err := testrepo.BlameFile("cfc3b2993f74726356887a5ec093de50486dc617", "README.txt") + blame, err := testrepo.Blame("cfc3b2993f74726356887a5ec093de50486dc617", "README.txt") assert.Nil(t, err) // Assert representative commits diff --git a/repo_commit.go b/repo_commit.go index d4e8c1fd..a8ee370c 100644 --- a/repo_commit.go +++ b/repo_commit.go @@ -7,7 +7,6 @@ package git import ( "bytes" "errors" - "fmt" "strconv" "strings" "time" @@ -194,23 +193,6 @@ 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) { @@ -395,9 +377,9 @@ type DiffNameOnlyOptions struct { 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(base, head string, opts ...DiffNameOnlyOptions) ([]string, error) { var opt DiffNameOnlyOptions if len(opts) > 0 { opt = opts[0] @@ -417,7 +399,7 @@ func DiffNameOnly(repoPath, base, head string, opts ...DiffNameOnlyOptions) ([]s cmd.AddArgs(escapePath(opt.Path)) } - stdout, err := cmd.RunInDirWithTimeout(opt.Timeout, repoPath) + stdout, err := cmd.RunInDirWithTimeout(opt.Timeout, r.path) if err != nil { return nil, err } @@ -434,17 +416,6 @@ 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 diff --git a/repo_commit_test.go b/repo_commit_test.go index f8b6e5a4..809b6ffc 100644 --- a/repo_commit_test.go +++ b/repo_commit_test.go @@ -117,13 +117,6 @@ func TestRepository_Log(t *testing.T) { } assert.Equal(t, test.expCommitIDs, commitsToIDs(commits)) - - commits, err = Log(testrepo.path, test.rev, test.opt) - if err != nil { - t.Fatal(err) - } - - assert.Equal(t, test.expCommitIDs, commitsToIDs(commits)) }) } } diff --git a/repo_hook.go b/repo_hook.go index 8794876f..32fffc85 100644 --- a/repo_hook.go +++ b/repo_hook.go @@ -5,7 +5,6 @@ package git import ( - "io/ioutil" "os" "path/filepath" ) @@ -32,7 +31,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_pull.go b/repo_pull.go index 5d2cfb1e..2b42229e 100644 --- a/repo_pull.go +++ b/repo_pull.go @@ -23,8 +23,8 @@ type MergeBaseOptions struct { } // 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(base, head string, opts ...MergeBaseOptions) (string, error) { var opt MergeBaseOptions if len(opts) > 0 { opt = opts[0] @@ -36,7 +36,7 @@ func MergeBase(repoPath, base, head string, opts ...MergeBaseOptions) (string, e "--end-of-options", base, head, - ).RunInDirWithTimeout(opt.Timeout, repoPath) + ).RunInDirWithTimeout(opt.Timeout, r.path) if err != nil { if strings.Contains(err.Error(), "exit status 1") { return "", ErrNoMergeBase @@ -45,14 +45,3 @@ func MergeBase(repoPath, base, head string, opts ...MergeBaseOptions) (string, e } 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_reference.go b/repo_reference.go index 43dfb3f9..1b90e7b8 100644 --- a/repo_reference.go +++ b/repo_reference.go @@ -48,16 +48,16 @@ type ShowRefVerifyOptions struct { 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(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) + stdout, err := cmd.RunInDirWithTimeout(opt.Timeout, r.path) if err != nil { if strings.Contains(err.Error(), "not a valid ref") { return "", ErrReferenceNotExist @@ -67,17 +67,6 @@ 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) { @@ -90,47 +79,23 @@ func (r *Repository) TagCommitID(tag string, opts ...ShowRefVerifyOptions) (stri 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...) -} - // 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...) + _, err := r.ShowRefVerify(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...) + return r.HasReference(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...) + return r.HasReference(RefsTags+tag, opts...) } // SymbolicRefOptions contains optional arguments for get and set symbolic ref. @@ -150,9 +115,9 @@ type SymbolicRefOptions struct { } // 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(opts ...SymbolicRefOptions) (string, error) { var opt SymbolicRefOptions if len(opts) > 0 { opt = opts[0] @@ -167,20 +132,13 @@ func SymbolicRef(repoPath string, opts ...SymbolicRefOptions) (string, error) { cmd.AddArgs(opt.Ref) } - stdout, err := cmd.RunInDirWithTimeout(opt.Timeout, repoPath) + stdout, err := cmd.RunInDirWithTimeout(opt.Timeout, r.path) 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 @@ -268,8 +226,8 @@ type DeleteBranchOptions struct { 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(name string, opts ...DeleteBranchOptions) error { var opt DeleteBranchOptions if len(opts) > 0 { opt = opts[0] @@ -281,16 +239,6 @@ func DeleteBranch(repoPath, name string, opts ...DeleteBranchOptions) error { } else { cmd.AddArgs("-d") } - _, err := cmd.AddArgs("--end-of-options", name).RunInDirWithTimeout(opt.Timeout, repoPath) + _, err := cmd.AddArgs("--end-of-options", name).RunInDirWithTimeout(opt.Timeout, r.path) 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_remote.go b/repo_remote.go index 363b8998..9ff23c51 100644 --- a/repo_remote.go +++ b/repo_remote.go @@ -103,11 +103,8 @@ type RemoteAddOptions struct { 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(name, url string, opts ...RemoteAddOptions) error { var opt RemoteAddOptions if len(opts) > 0 { opt = opts[0] @@ -121,25 +118,10 @@ func RemoteAdd(repoPath, name, url string, opts ...RemoteAddOptions) error { cmd.AddArgs("--mirror=fetch") } - _, err := cmd.AddArgs("--end-of-options", name, url).RunInDirWithTimeout(opt.Timeout, repoPath) + _, err := cmd.AddArgs("--end-of-options", name, url).RunInDirWithTimeout(opt.Timeout, r.path) 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. // @@ -154,11 +136,8 @@ type RemoteRemoveOptions struct { 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(name string, opts ...RemoteRemoveOptions) error { var opt RemoteRemoveOptions if len(opts) > 0 { opt = opts[0] @@ -167,7 +146,7 @@ func RemoteRemove(repoPath, name string, opts ...RemoteRemoveOptions) error { _, err := NewCommand("remote", "remove"). AddOptions(opt.CommandOptions). AddArgs("--end-of-options", name). - RunInDirWithTimeout(opt.Timeout, repoPath) + RunInDirWithTimeout(opt.Timeout, r.path) if err != nil { // the error status may differ from git clients if strings.Contains(err.Error(), "error: No such remote") || @@ -179,21 +158,6 @@ 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 @@ -207,8 +171,8 @@ type RemotesOptions struct { 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(opts ...RemotesOptions) ([]string, error) { var opt RemotesOptions if len(opts) > 0 { opt = opts[0] @@ -216,7 +180,7 @@ func Remotes(repoPath string, opts ...RemotesOptions) ([]string, error) { stdout, err := NewCommand("remote"). AddOptions(opt.CommandOptions). - RunInDirWithTimeout(opt.Timeout, repoPath) + RunInDirWithTimeout(opt.Timeout, r.path) if err != nil { return nil, err } @@ -224,11 +188,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. // @@ -248,8 +207,8 @@ type RemoteGetURLOptions struct { 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(name string, opts ...RemoteGetURLOptions) ([]string, error) { var opt RemoteGetURLOptions if len(opts) > 0 { opt = opts[0] @@ -263,18 +222,13 @@ func RemoteGetURL(repoPath, name string, opts ...RemoteGetURLOptions) ([]string, cmd.AddArgs("--all") } - stdout, err := cmd.AddArgs("--end-of-options", name).RunInDirWithTimeout(opt.Timeout, repoPath) + stdout, err := cmd.AddArgs("--end-of-options", name).RunInDirWithTimeout(opt.Timeout, r.path) 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. // @@ -293,9 +247,9 @@ type RemoteSetURLOptions struct { 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(name, newurl string, opts ...RemoteSetURLOptions) error { var opt RemoteSetURLOptions if len(opts) > 0 { opt = opts[0] @@ -312,7 +266,7 @@ func RemoteSetURL(repoPath, name, newurl string, opts ...RemoteSetURLOptions) er cmd.AddArgs(opt.Regex) } - _, err := cmd.RunInDirWithTimeout(opt.Timeout, repoPath) + _, err := cmd.RunInDirWithTimeout(opt.Timeout, r.path) if err != nil { if strings.Contains(err.Error(), "No such URL found") { return ErrURLNotExist @@ -324,12 +278,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. // @@ -347,8 +295,8 @@ type RemoteSetURLAddOptions struct { } // 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(name, newurl string, opts ...RemoteSetURLAddOptions) error { var opt RemoteSetURLAddOptions if len(opts) > 0 { opt = opts[0] @@ -363,19 +311,13 @@ func RemoteSetURLAdd(repoPath, name, newurl string, opts ...RemoteSetURLAddOptio cmd.AddArgs("--end-of-options", name, newurl) - _, err := cmd.RunInDirWithTimeout(opt.Timeout, repoPath) + _, err := cmd.RunInDirWithTimeout(opt.Timeout, r.path) 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. // @@ -392,9 +334,9 @@ type RemoteSetURLDeleteOptions struct { 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(name, regex string, opts ...RemoteSetURLDeleteOptions) error { var opt RemoteSetURLDeleteOptions if len(opts) > 0 { opt = opts[0] @@ -409,15 +351,9 @@ func RemoteSetURLDelete(repoPath, name, regex string, opts ...RemoteSetURLDelete cmd.AddArgs("--end-of-options", name, regex) - _, err := cmd.RunInDirWithTimeout(opt.Timeout, repoPath) + _, err := cmd.RunInDirWithTimeout(opt.Timeout, r.path) 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_tag.go b/repo_tag.go index 931b6dc6..26142b27 100644 --- a/repo_tag.go +++ b/repo_tag.go @@ -159,8 +159,8 @@ type TagsOptions struct { 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(opts ...TagsOptions) ([]string, error) { var opt TagsOptions if len(opts) > 0 { opt = opts[0] @@ -178,7 +178,7 @@ func RepoTags(repoPath string, opts ...TagsOptions) ([]string, error) { cmd.AddArgs(opt.Pattern) } - stdout, err := cmd.RunInDirWithTimeout(opt.Timeout, repoPath) + stdout, err := cmd.RunInDirWithTimeout(opt.Timeout, r.path) if err != nil { return nil, err } @@ -189,11 +189,6 @@ func RepoTags(repoPath string, opts ...TagsOptions) ([]string, error) { 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 diff --git a/repo_test.go b/repo_test.go index 7f4ef1c4..42ae99c9 100644 --- a/repo_test.go +++ b/repo_test.go @@ -5,7 +5,6 @@ package git import ( - "io/ioutil" "os" "path/filepath" "testing" @@ -320,7 +319,7 @@ 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) } @@ -362,7 +361,7 @@ 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) } @@ -394,7 +393,7 @@ 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) } diff --git a/repo_tree_test.go b/repo_tree_test.go index 16beaa4a..51965f6c 100644 --- a/repo_tree_test.go +++ b/repo_tree_test.go @@ -69,13 +69,13 @@ func TestRepository_LsTree(t *testing.T) { 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(AddOptions{All: true}) require.NoError(t, err) - repo, err := Open(path) + err = repo.Commit(&Signature{Name: "test", Email: "test@test.com"}, "initial commit") require.NoError(t, err) commit, err := repo.CatFileCommit("HEAD") From dc8ceda699f1f36698debe8e2046a7dd49aa684f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=B4=8A=E1=B4=8F=E1=B4=87=20=E1=B4=84=CA=9C=E1=B4=87?= =?UTF-8?q?=C9=B4?= Date: Sat, 14 Feb 2026 11:12:45 -0500 Subject: [PATCH 3/7] refactor: replace Timeout options with context.Context (#126) --- blob.go | 13 ++-- blob_test.go | 6 +- command.go | 164 ++++++++++++--------------------------- command_test.go | 81 +++++++++++++++++-- commit.go | 57 +++++++------- commit_archive.go | 5 +- commit_archive_test.go | 6 +- commit_submodule.go | 111 ++++++++++++++------------ commit_submodule_test.go | 9 ++- commit_test.go | 71 ++++++++++------- git.go | 52 +++++++------ git_test.go | 5 +- repo.go | 156 ++++++++++--------------------------- repo_blame.go | 15 ++-- repo_blame_test.go | 7 +- repo_blob.go | 15 ++-- repo_blob_test.go | 9 ++- repo_commit.go | 112 +++++++++----------------- repo_commit_test.go | 49 ++++++++---- repo_diff.go | 41 ++++------ repo_diff_test.go | 15 ++-- repo_grep.go | 13 +--- repo_grep_test.go | 13 +++- repo_pull.go | 13 +--- repo_pull_test.go | 7 +- repo_reference.go | 70 ++++++----------- repo_reference_test.go | 58 ++++++++------ repo_remote.go | 99 +++++++---------------- repo_remote_test.go | 55 +++++++------ repo_tag.go | 59 +++++--------- repo_tag_test.go | 37 +++++---- repo_test.go | 55 ++++++++----- repo_tree.go | 15 ++-- repo_tree_test.go | 14 ++-- server.go | 36 ++++----- server_test.go | 10 ++- tag.go | 6 +- tag_test.go | 9 ++- tree.go | 40 +++++----- tree_blob.go | 17 ++-- tree_blob_test.go | 15 ++-- tree_entry.go | 46 +++++------ tree_entry_test.go | 29 +++---- 43 files changed, 821 insertions(+), 894 deletions(-) diff --git a/blob.go b/blob.go index 9bb85466..399171b4 100644 --- a/blob.go +++ b/blob.go @@ -6,6 +6,7 @@ package git import ( "bytes" + "context" "io" ) @@ -16,14 +17,16 @@ type Blob struct { // 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) { +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 { + if err := b.Pipeline(ctx, stdout, stderr); err != nil { return nil, concatenateError(err, stderr.String()) } return stdout.Bytes(), nil @@ -31,6 +34,6 @@ func (b *Blob) Bytes() ([]byte, error) { // 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) +func (b *Blob) Pipeline(ctx context.Context, stdout, stderr io.Writer) error { + return NewCommand(ctx, "show", b.id.String()).RunInDirPipeline(stdout, stderr, b.parent.repo.path) } diff --git a/blob_test.go b/blob_test.go index a9273b7d..6ccc46ee 100644 --- a/blob_test.go +++ b/blob_test.go @@ -6,12 +6,14 @@ 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 +43,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) { stdout := new(bytes.Buffer) - err := blob.Pipeline(stdout, nil) + err := blob.Pipeline(ctx, stdout, nil) assert.Nil(t, err) assert.Equal(t, expOutput, stdout.String()) }) diff --git a/command.go b/command.go index 4dc5f017..54e0094f 100644 --- a/command.go +++ b/command.go @@ -17,22 +17,16 @@ import ( // 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 + name string + args []string + envs []string + ctx context.Context } // CommandOptions contains options for running a command. -// If timeout is zero, DefaultTimeout will be used. -// If timeout is less than zero, no timeout will be set. -// If context is nil, context.Background() will be used. type CommandOptions struct { - Args []string - Envs []string - Timeout time.Duration - Context context.Context + Args []string + Envs []string } // String returns the string representation of the command. @@ -44,13 +38,7 @@ func (c *Command) String() string { } // NewCommand creates and returns a new Command with given arguments for "git". -func NewCommand(args ...string) *Command { - return NewCommandWithContext(context.Background(), args...) -} - -// NewCommandWithContext creates and returns a new Command with given arguments -// and context for "git". -func NewCommandWithContext(ctx context.Context, args ...string) *Command { +func NewCommand(ctx context.Context, args ...string) *Command { return &Command{ name: "git", args: args, @@ -76,23 +64,9 @@ func (c Command) WithContext(ctx context.Context) *Command { return &c } -// WithTimeout returns a new Command with given timeout. -func (c Command) WithTimeout(timeout time.Duration) *Command { - c.timeout = timeout - return &c -} - -// SetTimeout sets the timeout for the command. -func (c *Command) SetTimeout(timeout time.Duration) { - c.timeout = timeout -} - // AddOptions adds options to the command. -// Note: only the last option will take effect if there are duplicated options. func (c *Command) AddOptions(opts ...CommandOptions) *Command { for _, opt := range opts { - c.timeout = opt.Timeout - c.ctx = opt.Context c.AddArgs(opt.Args...) c.AddEnvs(opt.Envs...) } @@ -105,7 +79,8 @@ func (c *Command) AddCommitter(committer *Signature) *Command { return c } -// DefaultTimeout is the default timeout duration for all commands. +// 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 // A limitDualWriter writes to W but limits the amount of data written to just N @@ -144,33 +119,19 @@ type RunInDirOptions struct { 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. +// io.Writer. If the command's context does not have a deadline, DefaultTimeout +// will be applied automatically. It returns an ErrExecTimeout if the execution +// was timed out. func (c *Command) RunInDirWithOptions(dir string, opts ...RunInDirOptions) (err error) { var opt RunInDirOptions if len(opts) > 0 { opt = opts[0] } - 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 { @@ -184,26 +145,22 @@ func (c *Command) RunInDirWithOptions(dir string, opts ...RunInDirOptions) (err defer func() { if len(dir) == 0 { - log("[timeout: %v] %s\n%s", timeout, c, buf.Bytes()) + log("%s\n%s", c, buf.Bytes()) } else { - log("[timeout: %v] %s: %s\n%s", timeout, dir, c, buf.Bytes()) + log("%s: %s\n%s", dir, c, buf.Bytes()) } }() - ctx := context.Background() - if c.ctx != nil { - ctx = c.ctx + ctx := c.ctx + if ctx == nil { + ctx = context.Background() } - if timeout > 0 { + // Apply default timeout if the context doesn't already have a deadline. + if _, ok := ctx.Deadline(); !ok { var cancel context.CancelFunc - ctx, cancel = context.WithTimeout(ctx, timeout) - defer func() { - cancel() - if err == context.DeadlineExceeded { - err = ErrExecTimeout - } - }() + ctx, cancel = context.WithTimeout(ctx, DefaultTimeout) + defer cancel() } cmd := exec.CommandContext(ctx, c.name, c.args...) @@ -215,6 +172,11 @@ func (c *Command) RunInDirWithOptions(dir string, opts ...RunInDirOptions) (err cmd.Stdout = w cmd.Stderr = opt.Stderr if err = cmd.Start(); err != nil { + if ctx.Err() == context.DeadlineExceeded { + return ErrExecTimeout + } else if ctx.Err() != nil { + return ctx.Err() + } return err } @@ -225,22 +187,33 @@ func (c *Command) RunInDirWithOptions(dir string, opts ...RunInDirOptions) (err 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) - } + // Kill the process before waiting so cancellation is enforced promptly. + if cmd.Process != nil { + _ = cmd.Process.Kill() } + <-result - return ErrExecTimeout + if ctx.Err() == context.DeadlineExceeded { + return ErrExecTimeout + } + return ctx.Err() case err = <-result: + // Normalize errors when the context may have expired around the same time. + if err != nil { + if ctxErr := ctx.Err(); ctxErr != nil { + if ctxErr == context.DeadlineExceeded { + return ErrExecTimeout + } + return ctxErr + } + } return err } } -// RunInDirPipeline executes the command in given directory and default timeout -// duration. It pipes stdout and stderr to supplied io.Writer. +// RunInDirPipeline executes the command in given directory. It pipes stdout and +// stderr to supplied io.Writer. func (c *Command) RunInDirPipeline(stdout, stderr io.Writer, dir string) error { return c.RunInDirWithOptions(dir, RunInDirOptions{ Stdin: nil, @@ -249,35 +222,8 @@ func (c *Command) RunInDirPipeline(stdout, stderr io.Writer, dir string) error { }) } -// 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 c.RunInDirPipeline(stdout, stderr, dir) -} - -// RunInDirWithTimeout executes the command in given directory and timeout -// duration. It returns stdout in []byte and error (combined with stderr). -// -// Deprecated: Use RunInDir and CommandOptions instead. -// TODO: remove this in the next major version -func (c *Command) RunInDirWithTimeout(timeout time.Duration, dir string) ([]byte, error) { - 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). +// RunInDir executes the command in given directory. It returns stdout and error +// (combined with stderr). func (c *Command) RunInDir(dir string) ([]byte, error) { stdout := new(bytes.Buffer) stderr := new(bytes.Buffer) @@ -287,20 +233,8 @@ func (c *Command) RunInDir(dir string) ([]byte, error) { 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). +// Run executes the command in working directory. It returns stdout and +// error (combined with stderr). func (c *Command) Run() ([]byte, error) { stdout, err := c.RunInDir("") if err != nil { diff --git a/command_test.go b/command_test.go index 53e2bf21..e2fb26f5 100644 --- a/command_test.go +++ b/command_test.go @@ -5,6 +5,8 @@ package git import ( + "context" + "io" "testing" "time" @@ -12,6 +14,7 @@ import ( ) func TestCommand_String(t *testing.T) { + ctx := context.Background() tests := []struct { name string args []string @@ -35,14 +38,15 @@ func TestCommand_String(t *testing.T) { } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - cmd := NewCommand(test.args...) + cmd := NewCommand(ctx, test.args...) assert.Equal(t, test.expStr, cmd.String()) }) } } func TestCommand_AddArgs(t *testing.T) { - cmd := NewCommand() + ctx := context.Background() + cmd := NewCommand(ctx) assert.Equal(t, []string(nil), cmd.args) cmd.AddArgs("push") @@ -51,7 +55,8 @@ func TestCommand_AddArgs(t *testing.T) { } func TestCommand_AddEnvs(t *testing.T) { - cmd := NewCommand() + ctx := context.Background() + cmd := NewCommand(ctx) assert.Equal(t, []string(nil), cmd.envs) cmd.AddEnvs("GIT_DIR=/tmp") @@ -59,7 +64,71 @@ func TestCommand_AddEnvs(t *testing.T) { assert.Equal(t, []string{"GIT_DIR=/tmp", "HOME=/Users/unknwon", "GIT_EDITOR=code"}, cmd.envs) } -func TestCommand_RunWithTimeout(t *testing.T) { - _, err := NewCommand("version").WithTimeout(time.Nanosecond).Run() - assert.Equal(t, ErrExecTimeout, err) +func TestCommand_RunWithContextTimeout(t *testing.T) { + t.Run("context already expired before start", func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Nanosecond) + defer cancel() + time.Sleep(time.Millisecond) // ensure deadline has passed + _, err := NewCommand(ctx, "version").Run() + 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 a blocking reader so the command starts successfully and blocks + // reading stdin until the context deadline fires. + err := NewCommand(ctx, "hash-object", "--stdin").RunInDirWithOptions("", RunInDirOptions{ + Stdin: blockingReader{cancel: ctx.Done()}, + Stdout: io.Discard, + Stderr: io.Discard, + }) + assert.Equal(t, ErrExecTimeout, err) + }) +} + +// blockingReader is an io.Reader that blocks until its cancel channel is +// closed, simulating a stdin that never provides data. When cancelled it +// returns io.EOF so that exec's stdin copy goroutine can exit cleanly, +// allowing cmd.Wait() to return. +type blockingReader struct { + cancel <-chan struct{} +} + +func (r blockingReader) Read(p []byte) (int, error) { + <-r.cancel + return 0, io.EOF +} + +func TestCommand_RunWithContextCancellation(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) + }() + + err := NewCommand(ctx, "hash-object", "--stdin").RunInDirWithOptions("", RunInDirOptions{ + Stdin: blockingReader{cancel: done}, + Stdout: io.Discard, + Stderr: io.Discard, + }) + assert.ErrorIs(t, err, context.Canceled) + // Must NOT be ErrExecTimeout — cancellation is distinct from deadline. + assert.NotEqual(t, ErrExecTimeout, err) +} + +func TestCommand_DefaultTimeoutApplied(t *testing.T) { + // A plain context.Background() has no deadline. The command should still + // succeed because DefaultTimeout (1 min) is applied automatically and + // "git version" completes well within that. + ctx := context.Background() + stdout, err := NewCommand(ctx, "version").Run() + assert.NoError(t, err) + assert.Contains(t, string(stdout), "git version") } diff --git a/commit.go b/commit.go index 301be259..381734e2 100644 --- a/commit.go +++ b/commit.go @@ -6,6 +6,7 @@ package git import ( "bytes" + "context" "io" "net/http" "strings" @@ -26,9 +27,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. @@ -53,56 +54,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 } @@ -114,7 +115,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 { @@ -138,7 +139,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 @@ -153,7 +154,7 @@ func (c *Commit) isImageFile(blob *Blob, err error) (bool, error) { N: int64(buf.Cap()), } - err = blob.Pipeline(stdout, io.Discard) + err = blob.Pipeline(ctx, stdout, io.Discard) if err != nil { return false, err } @@ -162,12 +163,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..535f6f82 100644 --- a/commit_archive.go +++ b/commit_archive.go @@ -5,6 +5,7 @@ package git import ( + "context" "path/filepath" "strings" ) @@ -19,9 +20,9 @@ const ( ) // CreateArchive creates given format of archive to the destination. -func (c *Commit) CreateArchive(format ArchiveFormat, dst string) error { +func (c *Commit) CreateArchive(ctx context.Context, format ArchiveFormat, dst string) error { prefix := filepath.Base(strings.TrimSuffix(c.repo.path, ".git")) + "/" - _, err := NewCommand("archive", + _, err := NewCommand(ctx, "archive", "--prefix="+prefix, "--format="+string(format), "-o", dst, diff --git a/commit_archive_test.go b/commit_archive_test.go index f9376821..2523c73c 100644 --- a/commit_archive_test.go +++ b/commit_archive_test.go @@ -5,6 +5,7 @@ package git import ( + "context" "os" "path/filepath" "strconv" @@ -19,12 +20,13 @@ func tempPath() string { } func TestCommit_CreateArchive(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 +36,7 @@ func TestCommit_CreateArchive(t *testing.T) { _ = os.Remove(dst) }() - assert.Nil(t, c.CreateArchive(format, dst)) + assert.Nil(t, c.CreateArchive(ctx, format, dst)) }) } } diff --git a/commit_submodule.go b/commit_submodule.go index da63231e..3c4da05c 100644 --- a/commit_submodule.go +++ b/commit_submodule.go @@ -7,6 +7,7 @@ package git import ( "bufio" "bytes" + "context" "strings" ) @@ -23,68 +24,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..2861fc0f 100644 --- a/commit_submodule_test.go +++ b/commit_submodule_test.go @@ -5,18 +5,21 @@ 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 +27,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..ca256804 100644 --- a/commit_test.go +++ b/commit_test.go @@ -5,13 +5,15 @@ 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 +27,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 +39,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 +58,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 +69,7 @@ func TestCommit_Parent(t *testing.T) { } func TestCommit_CommitByPath(t *testing.T) { + ctx := context.Background() tests := []struct { id string opt CommitByRevisionOptions @@ -88,12 +92,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 +117,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 +180,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 +191,7 @@ func TestCommit_CommitsByPage(t *testing.T) { } func TestCommit_SearchCommits(t *testing.T) { + ctx := context.Background() tests := []struct { id string pattern string @@ -271,12 +277,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 +293,7 @@ func TestCommit_SearchCommits(t *testing.T) { } func TestCommit_ShowNameStatus(t *testing.T) { + ctx := context.Background() tests := []struct { id string opt ShowNameStatusOptions @@ -332,12 +339,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 +355,7 @@ func TestCommit_ShowNameStatus(t *testing.T) { } func TestCommit_CommitsCount(t *testing.T) { + ctx := context.Background() tests := []struct { id string opt RevListCountOptions @@ -383,12 +391,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 +407,7 @@ func TestCommit_CommitsCount(t *testing.T) { } func TestCommit_FilesChangedAfter(t *testing.T) { + ctx := context.Background() tests := []struct { id string after string @@ -435,12 +444,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 +460,7 @@ func TestCommit_FilesChangedAfter(t *testing.T) { } func TestCommit_CommitsAfter(t *testing.T) { + ctx := context.Background() tests := []struct { id string after string @@ -479,12 +489,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 +505,7 @@ func TestCommit_CommitsAfter(t *testing.T) { } func TestCommit_Ancestors(t *testing.T) { + ctx := context.Background() tests := []struct { id string opt LogOptions @@ -518,12 +529,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 +545,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 +578,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 +594,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 +627,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/git.go b/git.go index 31955b3c..ebbfdfd7 100644 --- a/git.go +++ b/git.go @@ -5,6 +5,7 @@ package git import ( + "context" "fmt" "io" "strings" @@ -41,35 +42,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() - fields := strings.Fields(string(stdout)) - if len(fields) < 3 { - gitVersionErr = fmt.Errorf("not enough output: %s", stdout) - return - } + if gitVersionSet { + return gitVersion, nil + } - // Handle special case on Windows. - i := strings.Index(fields[2], "windows") - if i >= 1 { - gitVersion = fields[2][:i-1] - return - } + stdout, err := NewCommand(ctx, "version").Run() + if err != nil { + return "", err + } + 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 10289006..5c0e0df9 100644 --- a/git_test.go +++ b/git_test.go @@ -6,6 +6,7 @@ package git import ( "bytes" + "context" "flag" stdlog "log" "os" @@ -25,9 +26,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) diff --git a/repo.go b/repo.go index 1eee7b2d..4b82c477 100644 --- a/repo.go +++ b/repo.go @@ -7,6 +7,7 @@ package git import ( "bufio" "bytes" + "context" "fmt" "io" "os" @@ -14,7 +15,6 @@ import ( "path/filepath" "strconv" "strings" - "time" ) // Repository contains information of a Git repository. @@ -35,7 +35,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 +44,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 +58,12 @@ 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 +74,12 @@ func Init(path string, opts ...InitOptions) error { return err } - cmd := NewCommand("init").AddOptions(opt.CommandOptions) + cmd := NewCommand(ctx, "init").AddOptions(opt.CommandOptions) if opt.Bare { cmd.AddArgs("--bare") } cmd.AddArgs("--end-of-options") - _, err = cmd.RunInDirWithTimeout(opt.Timeout, path) + _, err = cmd.RunInDir(path) return err } @@ -120,17 +115,12 @@ 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,7 +131,7 @@ func Clone(url, dst string, opts ...CloneOptions) error { return err } - cmd := NewCommand("clone").AddOptions(opt.CommandOptions) + cmd := NewCommand(ctx, "clone").AddOptions(opt.CommandOptions) if opt.Mirror { cmd.AddArgs("--mirror") } @@ -159,7 +149,7 @@ func Clone(url, dst string, opts ...CloneOptions) error { } cmd.AddArgs("--end-of-options") - _, err = cmd.AddArgs(url, dst).RunWithTimeout(opt.Timeout) + _, err = cmd.AddArgs(url, dst).Run() return err } @@ -169,28 +159,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) + cmd := NewCommand(ctx, "fetch").AddOptions(opt.CommandOptions) if opt.Prune { cmd.AddArgs("--prune") } - _, err := cmd.RunInDirWithTimeout(opt.Timeout, r.path) + _, err := cmd.RunInDir(r.path) return err } @@ -206,23 +191,18 @@ 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) + cmd := NewCommand(ctx, "pull").AddOptions(opt.CommandOptions) if opt.Rebase { cmd.AddArgs("--rebase") } @@ -236,7 +216,7 @@ func (r *Repository) Pull(opts ...PullOptions) error { } } - _, err := cmd.RunInDirWithTimeout(opt.Timeout, r.path) + _, err := cmd.RunInDir(r.path) return err } @@ -244,24 +224,19 @@ 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. CommandOptions } // Push pushes local changes to given remote and branch for the repository. -func (r *Repository) Push(remote, branch string, opts ...PushOptions) error { +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, r.path) + cmd := NewCommand(ctx, "push").AddOptions(opt.CommandOptions).AddArgs("--end-of-options", remote, branch) + _, err := cmd.RunInDir(r.path) return err } @@ -271,23 +246,18 @@ func (r *Repository) Push(remote, branch string, opts ...PushOptions) error { 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. -func (r *Repository) Checkout(branch string, opts ...CheckoutOptions) error { +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) + cmd := NewCommand(ctx, "checkout").AddOptions(opt.CommandOptions) if opt.BaseBranch != "" { cmd.AddArgs("-b") } @@ -296,7 +266,7 @@ func (r *Repository) Checkout(branch string, opts ...CheckoutOptions) error { cmd.AddArgs(opt.BaseBranch) } - _, err := cmd.RunInDirWithTimeout(opt.Timeout, r.path) + _, err := cmd.RunInDir(r.path) return err } @@ -306,23 +276,18 @@ func (r *Repository) Checkout(branch string, opts ...CheckoutOptions) error { 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. -func (r *Repository) Reset(rev string, opts ...ResetOptions) error { +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") + cmd := NewCommand(ctx, "reset") if opt.Hard { cmd.AddArgs("--hard") } @@ -336,24 +301,19 @@ func (r *Repository) Reset(rev string, opts ...ResetOptions) error { // // 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. -func (r *Repository) Move(src, dst string, opts ...MoveOptions) error { +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, r.path) + _, err := NewCommand(ctx, "mv").AddOptions(opt.CommandOptions).AddArgs("--end-of-options", src, dst).RunInDir(r.path) return err } @@ -365,23 +325,18 @@ 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. -func (r *Repository) Add(opts ...AddOptions) error { +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) + cmd := NewCommand(ctx, "add").AddOptions(opt.CommandOptions) if opt.All { cmd.AddArgs("--all") } @@ -389,7 +344,7 @@ func (r *Repository) Add(opts ...AddOptions) error { cmd.AddArgs("--") cmd.AddArgs(opt.Pathspecs...) } - _, err := cmd.RunInDirWithTimeout(opt.Timeout, r.path) + _, err := cmd.RunInDir(r.path) return err } @@ -399,24 +354,19 @@ func (r *Repository) Add(opts ...AddOptions) error { 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 } // Commit commits local changes with given author, committer and message for the // repository. -func (r *Repository) Commit(committer *Signature, message string, opts ...CommitOptions) error { +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 := NewCommand(ctx, "commit") cmd.AddCommitter(committer) if opt.Author == nil { @@ -426,7 +376,7 @@ func (r *Repository) Commit(committer *Signature, message string, opts ...Commit AddArgs("-m", message). AddOptions(opt.CommandOptions) - _, err := cmd.RunInDirWithTimeout(opt.Timeout, r.path) + _, err := cmd.RunInDir(r.path) // No stderr but exit status 1 means nothing to commit. if err != nil && err.Error() == "exit status 1" { return nil @@ -445,17 +395,12 @@ 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. -func (r *Repository) ShowNameStatus(rev string, opts ...ShowNameStatusOptions) (*NameStatus, error) { +func (r *Repository) ShowNameStatus(ctx context.Context, rev string, opts ...ShowNameStatusOptions) (*NameStatus, error) { var opt ShowNameStatusOptions if len(opts) > 0 { opt = opts[0] @@ -485,10 +430,10 @@ func (r *Repository) ShowNameStatus(rev string, opts ...ShowNameStatusOptions) ( }() stderr := new(bytes.Buffer) - cmd := NewCommand("show", "--name-status", "--pretty=format:''"). + cmd := NewCommand(ctx, "show", "--name-status", "--pretty=format:''"). AddOptions(opt.CommandOptions). AddArgs("--end-of-options", rev) - err := cmd.RunInDirPipelineWithTimeout(opt.Timeout, w, stderr, r.path) + err := cmd.RunInDirPipeline(w, stderr, r.path) _ = w.Close() // Close writer to exit parsing goroutine if err != nil { return nil, concatenateError(err, stderr.String()) @@ -502,27 +447,22 @@ func (r *Repository) ShowNameStatus(rev string, opts ...ShowNameStatusOptions) ( // // 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"). + commitID, err := NewCommand(ctx, "rev-parse"). AddOptions(opt.CommandOptions). AddArgs(rev). - RunInDirWithTimeout(opt.Timeout, r.path) + RunInDir(r.path) if err != nil { if strings.Contains(err.Error(), "exit status 128") { return "", ErrRevisionNotExist @@ -548,25 +488,20 @@ 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. -func (r *Repository) CountObjects(opts ...CountObjectsOptions) (*CountObject, error) { +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"). + stdout, err := NewCommand(ctx, "count-objects", "-v"). AddOptions(opt.CommandOptions). - RunInDirWithTimeout(opt.Timeout, r.path) + RunInDir(r.path) if err != nil { return nil, err } @@ -605,24 +540,19 @@ func (r *Repository) CountObjects(opts ...CountObjectsOptions) (*CountObject, er // // 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. -func (r *Repository) Fsck(opts ...FsckOptions) error { +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, r.path) + cmd := NewCommand(ctx, "fsck").AddOptions(opt.CommandOptions) + _, err := cmd.RunInDir(r.path) return err } diff --git a/repo_blame.go b/repo_blame.go index 047fa950..d2ffc062 100644 --- a/repo_blame.go +++ b/repo_blame.go @@ -6,33 +6,28 @@ 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 } // Blame returns blame results of the file with the given revision of the // repository. -func (r *Repository) Blame(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"). + stdout, err := NewCommand(ctx, "blame"). AddOptions(opt.CommandOptions). AddArgs("-l", "-s", rev, "--", file). - RunInDirWithTimeout(opt.Timeout, r.path) + RunInDir(r.path) if err != nil { return nil, err } @@ -51,7 +46,7 @@ func (r *Repository) Blame(rev, file string, opts ...BlameOptions) (*Blame, erro 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 6073b78d..85fe38b7 100644 --- a/repo_blame_test.go +++ b/repo_blame_test.go @@ -5,6 +5,7 @@ package git import ( + "context" "fmt" "testing" @@ -12,12 +13,14 @@ import ( ) func TestRepository_Blame(t *testing.T) { + ctx := context.Background() + t.Run("bad file", func(t *testing.T) { - _, err := testrepo.Blame("", "404.txt") + _, err := testrepo.Blame(ctx, "", "404.txt") assert.Error(t, err) }) - blame, err := testrepo.Blame("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..906d18d5 100644 --- a/repo_blob.go +++ b/repo_blob.go @@ -4,34 +4,31 @@ 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..434a1969 100644 --- a/repo_blob_test.go +++ b/repo_blob_test.go @@ -5,6 +5,7 @@ package git import ( + "context" "testing" "github.com/stretchr/testify/assert" @@ -12,19 +13,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 a8ee370c..b2425200 100644 --- a/repo_commit.go +++ b/repo_commit.go @@ -6,6 +6,7 @@ package git import ( "bytes" + "context" "errors" "strconv" "strings" @@ -69,11 +70,6 @@ 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 } @@ -81,7 +77,7 @@ type CatFileCommitOptions struct { // 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] @@ -93,15 +89,15 @@ func (r *Repository) CatFileCommit(rev string, opts ...CatFileCommitOptions) (*C 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"). + stdout, err := NewCommand(ctx, "cat-file"). AddOptions(opt.CommandOptions). AddArgs("commit", commitID). - RunInDirWithTimeout(opt.Timeout, r.path) + RunInDir(r.path) if err != nil { return nil, err } @@ -121,24 +117,21 @@ 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"). + typ, err := NewCommand(ctx, "cat-file"). AddOptions(opt.CommandOptions). AddArgs("-t", rev). - RunInDirWithTimeout(opt.Timeout, r.path) + RunInDir(r.path) if err != nil { return "", err } @@ -148,14 +141,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. @@ -174,9 +167,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 } @@ -195,13 +185,13 @@ func escapePath(path string) string { // 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"). + cmd := NewCommand(ctx, "log"). AddOptions(opt.CommandOptions). AddArgs("--pretty=" + LogFormatHashOnly) if opt.MaxCount > 0 { @@ -224,11 +214,11 @@ func (r *Repository) Log(rev string, opts ...LogOptions) ([]*Commit, error) { cmd.AddArgs(escapePath(opt.Path)) } - stdout, err := cmd.RunInDirWithTimeout(opt.Timeout, r.path) + stdout, err := cmd.RunInDir(r.path) 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. @@ -237,24 +227,20 @@ 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 { @@ -275,26 +261,22 @@ 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, }) } @@ -307,27 +289,23 @@ 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, }) } @@ -339,25 +317,21 @@ 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, }) } @@ -370,22 +344,19 @@ 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. -func (r *Repository) DiffNameOnly(base, head string, opts ...DiffNameOnlyOptions) ([]string, error) { +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"). + cmd := NewCommand(ctx, "diff"). AddOptions(opt.CommandOptions). AddArgs("--name-only") cmd.AddArgs("--end-of-options") @@ -399,7 +370,7 @@ func (r *Repository) DiffNameOnly(base, head string, opts ...DiffNameOnlyOptions cmd.AddArgs(escapePath(opt.Path)) } - stdout, err := cmd.RunInDirWithTimeout(opt.Timeout, r.path) + stdout, err := cmd.RunInDir(r.path) if err != nil { return nil, err } @@ -422,16 +393,13 @@ func (r *Repository) DiffNameOnly(base, head string, opts ...DiffNameOnlyOptions 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] @@ -441,7 +409,7 @@ func (r *Repository) RevListCount(refspecs []string, opts ...RevListCountOptions return 0, errors.New("must have at least one refspec") } - cmd := NewCommand("rev-list"). + cmd := NewCommand(ctx, "rev-list"). AddOptions(opt.CommandOptions). AddArgs( "--count", @@ -453,7 +421,7 @@ func (r *Repository) RevListCount(refspecs []string, opts ...RevListCountOptions cmd.AddArgs(escapePath(opt.Path)) } - stdout, err := cmd.RunInDirWithTimeout(opt.Timeout, r.path) + stdout, err := cmd.RunInDir(r.path) if err != nil { return 0, err } @@ -467,16 +435,13 @@ 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] @@ -486,7 +451,7 @@ 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 := NewCommand(ctx, "rev-list").AddOptions(opt.CommandOptions) cmd.AddArgs("--end-of-options") cmd.AddArgs(refspecs...) cmd.AddArgs("--") @@ -494,11 +459,11 @@ func (r *Repository) RevList(refspecs []string, opts ...RevListOptions) ([]*Comm cmd.AddArgs(escapePath(opt.Path)) } - stdout, err := cmd.RunInDirWithTimeout(opt.Timeout, r.path) + stdout, err := cmd.RunInDir(r.path) 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 @@ -506,21 +471,18 @@ 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"). + cmd := NewCommand(ctx, "for-each-ref"). AddOptions(opt.CommandOptions). AddArgs( "--count=1", @@ -531,7 +493,7 @@ func (r *Repository) LatestCommitTime(opts ...LatestCommitTimeOptions) (time.Tim cmd.AddArgs(RefsHeads + opt.Branch) } - stdout, err := cmd.RunInDirWithTimeout(opt.Timeout, r.path) + stdout, err := cmd.RunInDir(r.path) if err != nil { return time.Time{}, err } diff --git a/repo_commit_test.go b/repo_commit_test.go index 809b6ffc..c7108993 100644 --- a/repo_commit_test.go +++ b/repo_commit_test.go @@ -5,6 +5,7 @@ package git import ( + "context" "errors" "testing" "time" @@ -38,13 +39,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 +57,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 +75,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 +93,7 @@ func TestRepository_TagCommit(t *testing.T) { } func TestRepository_Log(t *testing.T) { + ctx := context.Background() tests := []struct { rev string opt LogOptions @@ -111,7 +119,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) + commits, err := testrepo.Log(ctx, test.rev, test.opt) if err != nil { t.Fatal(err) } @@ -122,8 +130,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) }) @@ -140,7 +150,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) } @@ -151,6 +161,7 @@ func TestRepository_CommitByRevision(t *testing.T) { } func TestRepository_CommitsSince(t *testing.T) { + ctx := context.Background() tests := []struct { rev string since time.Time @@ -173,7 +184,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) } @@ -184,6 +195,7 @@ func TestRepository_CommitsSince(t *testing.T) { } func TestRepository_DiffNameOnly(t *testing.T) { + ctx := context.Background() tests := []struct { base string head string @@ -223,7 +235,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) } @@ -234,8 +246,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) }) @@ -275,7 +289,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) } @@ -286,8 +300,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) }) @@ -317,7 +333,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) } @@ -328,6 +344,7 @@ func TestRepository_RevList(t *testing.T) { } func TestRepository_LatestCommitTime(t *testing.T) { + ctx := context.Background() tests := []struct { opt LatestCommitTimeOptions expTime time.Time @@ -341,7 +358,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..0ea4fdf5 100644 --- a/repo_diff.go +++ b/repo_diff.go @@ -6,9 +6,9 @@ package git import ( "bytes" + "context" "fmt" "io" - "time" ) // DiffOptions contains optional arguments for parsing diff. @@ -18,28 +18,23 @@ 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() + cmd := NewCommand(ctx) if opt.Base == "" { // First commit of repository if commit.ParentsCount() == 0 { @@ -47,7 +42,7 @@ func (r *Repository) Diff(rev string, maxFiles, maxFileLines, maxLineChars int, AddOptions(opt.CommandOptions). AddArgs("--full-index", "--end-of-options", rev) } else { - c, err := commit.Parent(0) + c, err := commit.Parent(ctx, 0) if err != nil { return nil, err } @@ -66,7 +61,7 @@ func (r *Repository) Diff(rev string, maxFiles, maxFileLines, maxLineChars int, go StreamParseDiff(stdout, done, maxFiles, maxFileLines, maxLineChars) stderr := new(bytes.Buffer) - err = cmd.RunInDirPipelineWithTimeout(opt.Timeout, w, stderr, r.path) + err = cmd.RunInDirPipeline(w, stderr, r.path) _ = w.Close() // Close writer to exit parsing goroutine if err != nil { return nil, concatenateError(err, stderr.String()) @@ -88,27 +83,24 @@ 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() + cmd := NewCommand(ctx) switch diffType { case RawDiffNormal: if commit.ParentsCount() == 0 { @@ -116,7 +108,7 @@ func (r *Repository) RawDiff(rev string, diffType RawDiffFormat, w io.Writer, op AddOptions(opt.CommandOptions). AddArgs("--full-index", "--end-of-options", rev) } else { - c, err := commit.Parent(0) + c, err := commit.Parent(ctx, 0) if err != nil { return err } @@ -130,7 +122,7 @@ func (r *Repository) RawDiff(rev string, diffType RawDiffFormat, w io.Writer, op AddOptions(opt.CommandOptions). AddArgs("--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 } @@ -143,7 +135,7 @@ func (r *Repository) RawDiff(rev string, diffType RawDiffFormat, w io.Writer, op } stderr := new(bytes.Buffer) - if err = cmd.RunInDirPipelineWithTimeout(opt.Timeout, w, stderr, r.path); err != nil { + if err = cmd.RunInDirPipeline(w, stderr, r.path); err != nil { return concatenateError(err, stderr.String()) } return nil @@ -151,23 +143,20 @@ func (r *Repository) RawDiff(rev string, diffType RawDiffFormat, w io.Writer, op // 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"). + return NewCommand(ctx, "diff"). AddOptions(opt.CommandOptions). AddArgs("--full-index", "--binary", base, head). - RunInDirWithTimeout(opt.Timeout, r.path) + RunInDir(r.path) } diff --git a/repo_diff_test.go b/repo_diff_test.go index ec15c133..96a619e9 100644 --- a/repo_diff_test.go +++ b/repo_diff_test.go @@ -6,6 +6,7 @@ package git import ( "bytes" + "context" "errors" "strings" "testing" @@ -14,6 +15,7 @@ import ( ) func TestRepository_Diff(t *testing.T) { + ctx := context.Background() tests := []struct { rev string maxFiles int @@ -289,7 +291,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 +302,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 +465,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 +483,7 @@ index 0000000000000000000000000000000000000000..51680791956b43effdb2f16bccd2b475 } func TestRepository_DiffBinary(t *testing.T) { + ctx := context.Background() tests := []struct { base string head string @@ -509,7 +514,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..dd05a64f 100644 --- a/repo_grep.go +++ b/repo_grep.go @@ -5,10 +5,10 @@ package git import ( + "context" "fmt" "strconv" "strings" - "time" ) // GrepOptions contains optional arguments for grep search over repository files. @@ -25,11 +25,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 +69,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,7 +78,7 @@ func (r *Repository) Grep(pattern string, opts ...GrepOptions) []*GrepResult { opt.Tree = "HEAD" } - cmd := NewCommand("grep"). + cmd := NewCommand(ctx, "grep"). AddOptions(opt.CommandOptions). // Display full-name, line number and column number AddArgs("--full-name", "--line-number", "--column") @@ -105,7 +100,7 @@ func (r *Repository) Grep(pattern string, opts ...GrepOptions) []*GrepResult { cmd.AddArgs("--", opt.Pathspec) } - stdout, err := cmd.RunInDirWithTimeout(opt.Timeout, r.path) + stdout, err := cmd.RunInDir(r.path) if err != nil { return nil } diff --git a/repo_grep_test.go b/repo_grep_test.go index 2c0be390..1402c71d 100644 --- a/repo_grep_test.go +++ b/repo_grep_test.go @@ -5,6 +5,7 @@ package git import ( + "context" "runtime" "testing" @@ -12,6 +13,7 @@ import ( ) func TestRepository_Grep_Simple(t *testing.T) { + ctx := context.Background() want := []*GrepResult{ { Tree: "HEAD", @@ -51,11 +53,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 +110,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 +129,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 +144,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_pull.go b/repo_pull.go index 2b42229e..0c9e7775 100644 --- a/repo_pull.go +++ b/repo_pull.go @@ -5,38 +5,33 @@ 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. -func (r *Repository) MergeBase(base, head string, opts ...MergeBaseOptions) (string, error) { +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"). + stdout, err := NewCommand(ctx, "merge-base"). AddOptions(opt.CommandOptions). AddArgs( "--end-of-options", base, head, - ).RunInDirWithTimeout(opt.Timeout, r.path) + ).RunInDir(r.path) if err != nil { if strings.Contains(err.Error(), "exit status 1") { return "", ErrNoMergeBase diff --git a/repo_pull_test.go b/repo_pull_test.go index 3ec9dcb7..ff231863 100644 --- a/repo_pull_test.go +++ b/repo_pull_test.go @@ -5,14 +5,17 @@ package git import ( + "context" "testing" "github.com/stretchr/testify/assert" ) func TestRepository_MergeBase(t *testing.T) { + ctx := context.Background() + t.Run("no merge base", func(t *testing.T) { - mb, err := testrepo.MergeBase("0eedd79eba4394bbef888c804e899731644367fe", "bad_revision") + mb, err := testrepo.MergeBase(ctx, "0eedd79eba4394bbef888c804e899731644367fe", "bad_revision") assert.Equal(t, ErrNoMergeBase, err) assert.Empty(t, mb) }) @@ -36,7 +39,7 @@ func TestRepository_MergeBase(t *testing.T) { } for _, test := range tests { t.Run("", func(t *testing.T) { - mb, err := testrepo.MergeBase(test.base, test.head, test.opt) + mb, err := testrepo.MergeBase(ctx, test.base, test.head, test.opt) if err != nil { t.Fatal(err) } diff --git a/repo_reference.go b/repo_reference.go index 1b90e7b8..eec27159 100644 --- a/repo_reference.go +++ b/repo_reference.go @@ -5,9 +5,9 @@ package git import ( + "context" "errors" "strings" - "time" ) const ( @@ -37,11 +37,6 @@ 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 } @@ -50,14 +45,14 @@ var ErrReferenceNotExist = errors.New("reference does not exist") // 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) { +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, r.path) + cmd := NewCommand(ctx, "show-ref", "--verify", "--end-of-options", ref).AddOptions(opt.CommandOptions) + stdout, err := cmd.RunInDir(r.path) if err != nil { if strings.Contains(err.Error(), "not a valid ref") { return "", ErrReferenceNotExist @@ -69,33 +64,33 @@ func (r *Repository) ShowRefVerify(ref string, opts ...ShowRefVerifyOptions) (st // 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...) +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 { - _, err := r.ShowRefVerify(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 r.HasReference(RefsHeads+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 r.HasReference(RefsTags+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. @@ -105,11 +100,6 @@ 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 } @@ -117,13 +107,13 @@ type SymbolicRefOptions struct { // 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) { +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) + cmd := NewCommand(ctx, "symbolic-ref").AddOptions(opt.CommandOptions) if opt.Name == "" { opt.Name = "HEAD" } @@ -132,7 +122,7 @@ func (r *Repository) SymbolicRef(opts ...SymbolicRefOptions) (string, error) { cmd.AddArgs(opt.Ref) } - stdout, err := cmd.RunInDirWithTimeout(opt.Timeout, r.path) + stdout, err := cmd.RunInDir(r.path) if err != nil { return "", err } @@ -149,23 +139,18 @@ 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) + cmd := NewCommand(ctx, "show-ref").AddOptions(opt.CommandOptions) if opt.Heads { cmd.AddArgs("--heads") } @@ -177,7 +162,7 @@ func (r *Repository) ShowRef(opts ...ShowRefOptions) ([]*Reference, error) { cmd.AddArgs(opt.Patterns...) } - stdout, err := cmd.RunInDirWithTimeout(opt.Timeout, r.path) + stdout, err := cmd.RunInDir(r.path) if err != nil { return nil, err } @@ -198,8 +183,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 } @@ -217,28 +202,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. -func (r *Repository) DeleteBranch(name string, opts ...DeleteBranchOptions) error { +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) + cmd := NewCommand(ctx, "branch").AddOptions(opt.CommandOptions) if opt.Force { cmd.AddArgs("-D") } else { cmd.AddArgs("-d") } - _, err := cmd.AddArgs("--end-of-options", name).RunInDirWithTimeout(opt.Timeout, r.path) + _, err := cmd.AddArgs("--end-of-options", name).RunInDir(r.path) return err } diff --git a/repo_reference_test.go b/repo_reference_test.go index d0e90e17..3299ddf2 100644 --- a/repo_reference_test.go +++ b/repo_reference_test.go @@ -5,6 +5,7 @@ package git import ( + "context" "strconv" "testing" "time" @@ -38,13 +39,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 +56,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 +73,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 +90,7 @@ func TestRepository_TagCommitID(t *testing.T) { } func TestRepository_HasReference(t *testing.T) { + ctx := context.Background() tests := []struct { ref string opt ShowRefVerifyOptions @@ -103,12 +111,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 +134,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 +157,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 +171,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 +187,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 +197,7 @@ func TestRepository_SymbolicRef(t *testing.T) { } func TestRepository_ShowRef(t *testing.T) { + ctx := context.Background() tests := []struct { opt ShowRefOptions expRefs []*Reference @@ -216,7 +228,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 +239,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 +259,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 +283,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 9ff23c51..3f13755d 100644 --- a/repo_remote.go +++ b/repo_remote.go @@ -6,8 +6,8 @@ package git import ( "bytes" + "context" "strings" - "time" ) // LsRemoteOptions contains arguments for listing references in a remote @@ -23,23 +23,18 @@ 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) { +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) + cmd := NewCommand(ctx, "ls-remote", "--quiet").AddOptions(opt.CommandOptions) if opt.Heads { cmd.AddArgs("--heads") } @@ -54,7 +49,7 @@ func LsRemote(url string, opts ...LsRemoteOptions) ([]*Reference, error) { cmd.AddArgs(opt.Patterns...) } - stdout, err := cmd.RunWithTimeout(opt.Timeout) + stdout, err := cmd.Run() if err != nil { return nil, err } @@ -75,12 +70,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,23 +88,18 @@ 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 } // RemoteAdd adds a new remote to the repository. -func (r *Repository) RemoteAdd(name, url string, opts ...RemoteAddOptions) error { +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) + cmd := NewCommand(ctx, "remote", "add").AddOptions(opt.CommandOptions) if opt.Fetch { cmd.AddArgs("-f") } @@ -118,7 +107,7 @@ func (r *Repository) RemoteAdd(name, url string, opts ...RemoteAddOptions) error cmd.AddArgs("--mirror=fetch") } - _, err := cmd.AddArgs("--end-of-options", name, url).RunInDirWithTimeout(opt.Timeout, r.path) + _, err := cmd.AddArgs("--end-of-options", name, url).RunInDir(r.path) return err } @@ -127,26 +116,21 @@ func (r *Repository) RemoteAdd(name, url string, opts ...RemoteAddOptions) error // // 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 } // RemoteRemove removes a remote from the repository. -func (r *Repository) RemoteRemove(name string, opts ...RemoteRemoveOptions) error { +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"). + _, err := NewCommand(ctx, "remote", "remove"). AddOptions(opt.CommandOptions). AddArgs("--end-of-options", name). - RunInDirWithTimeout(opt.Timeout, r.path) + RunInDir(r.path) if err != nil { // the error status may differ from git clients if strings.Contains(err.Error(), "error: No such remote") || @@ -162,25 +146,20 @@ func (r *Repository) RemoteRemove(name string, opts ...RemoteRemoveOptions) erro // / // 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. -func (r *Repository) Remotes(opts ...RemotesOptions) ([]string, error) { +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"). + stdout, err := NewCommand(ctx, "remote"). AddOptions(opt.CommandOptions). - RunInDirWithTimeout(opt.Timeout, r.path) + RunInDir(r.path) if err != nil { return nil, err } @@ -198,23 +177,18 @@ 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. -func (r *Repository) RemoteGetURL(name string, opts ...RemoteGetURLOptions) ([]string, error) { +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) + cmd := NewCommand(ctx, "remote", "get-url").AddOptions(opt.CommandOptions) if opt.Push { cmd.AddArgs("--push") } @@ -222,7 +196,7 @@ func (r *Repository) RemoteGetURL(name string, opts ...RemoteGetURLOptions) ([]s cmd.AddArgs("--all") } - stdout, err := cmd.AddArgs("--end-of-options", name).RunInDirWithTimeout(opt.Timeout, r.path) + stdout, err := cmd.AddArgs("--end-of-options", name).RunInDir(r.path) if err != nil { return nil, err } @@ -238,24 +212,19 @@ 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 the first URL of the remote with given name of the // repository. -func (r *Repository) RemoteSetURL(name, newurl string, opts ...RemoteSetURLOptions) error { +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) + cmd := NewCommand(ctx, "remote", "set-url").AddOptions(opt.CommandOptions) if opt.Push { cmd.AddArgs("--push") } @@ -266,7 +235,7 @@ func (r *Repository) RemoteSetURL(name, newurl string, opts ...RemoteSetURLOptio cmd.AddArgs(opt.Regex) } - _, err := cmd.RunInDirWithTimeout(opt.Timeout, r.path) + _, err := cmd.RunInDir(r.path) if err != nil { if strings.Contains(err.Error(), "No such URL found") { return ErrURLNotExist @@ -285,24 +254,19 @@ 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. Use RemoteSetURL to overwrite the URL(s) instead. -func (r *Repository) RemoteSetURLAdd(name, newurl string, opts ...RemoteSetURLAddOptions) error { +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"). + cmd := NewCommand(ctx, "remote", "set-url"). AddOptions(opt.CommandOptions). AddArgs("--add") if opt.Push { @@ -311,7 +275,7 @@ func (r *Repository) RemoteSetURLAdd(name, newurl string, opts ...RemoteSetURLAd cmd.AddArgs("--end-of-options", name, newurl) - _, err := cmd.RunInDirWithTimeout(opt.Timeout, r.path) + _, err := cmd.RunInDir(r.path) if err != nil && strings.Contains(err.Error(), "Will not delete all non-push URLs") { return ErrNotDeleteNonPushURLs } @@ -325,24 +289,19 @@ 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 all URLs matching regex of the remote with given // name of the repository. -func (r *Repository) RemoteSetURLDelete(name, regex string, opts ...RemoteSetURLDeleteOptions) error { +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"). + cmd := NewCommand(ctx, "remote", "set-url"). AddOptions(opt.CommandOptions). AddArgs("--delete") if opt.Push { @@ -351,7 +310,7 @@ func (r *Repository) RemoteSetURLDelete(name, regex string, opts ...RemoteSetURL cmd.AddArgs("--end-of-options", name, regex) - _, err := cmd.RunInDirWithTimeout(opt.Timeout, r.path) + _, err := cmd.RunInDir(r.path) if err != nil && strings.Contains(err.Error(), "Will not delete all non-push URLs") { return ErrNotDeleteNonPushURLs } diff --git a/repo_remote_test.go b/repo_remote_test.go index 51ee3ab0..b75e9a58 100644 --- a/repo_remote_test.go +++ b/repo_remote_test.go @@ -5,6 +5,7 @@ package git import ( + "context" "os" "testing" @@ -12,6 +13,7 @@ import ( ) func TestLsRemote(t *testing.T) { + ctx := context.Background() tests := []struct { url string opt LsRemoteOptions @@ -57,7 +59,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 +70,7 @@ func TestLsRemote(t *testing.T) { } func TestIsURLAccessible(t *testing.T) { + ctx := context.Background() tests := []struct { url string expVal bool @@ -82,18 +85,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 +110,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 +119,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 +146,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 26142b27..df3b883a 100644 --- a/repo_tag.go +++ b/repo_tag.go @@ -6,9 +6,9 @@ package git import ( "bytes" + "context" "fmt" "strings" - "time" ) // parseTag parses tag information from the (uncompressed) raw data of the tag @@ -52,7 +52,7 @@ 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) @@ -60,7 +60,7 @@ func (r *Repository) getTag(timeout time.Duration, id *SHA1) (*Tag, error) { } // 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 } @@ -76,7 +76,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 := NewCommand(ctx, "cat-file", "-p", id.String()).RunInDir(r.path) if err != nil { return nil, err } @@ -100,27 +100,21 @@ 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 { @@ -134,11 +128,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 } @@ -150,23 +144,18 @@ 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 } // Tags returns a list of tags of the repository. -func (r *Repository) Tags(opts ...TagsOptions) ([]string, error) { +func (r *Repository) Tags(ctx context.Context, opts ...TagsOptions) ([]string, error) { var opt TagsOptions if len(opts) > 0 { opt = opts[0] } - cmd := NewCommand("tag", "--list").AddOptions(opt.CommandOptions) + cmd := NewCommand(ctx, "tag", "--list").AddOptions(opt.CommandOptions) if opt.SortKey != "" { cmd.AddArgs("--sort=" + opt.SortKey) @@ -178,7 +167,7 @@ func (r *Repository) Tags(opts ...TagsOptions) ([]string, error) { cmd.AddArgs(opt.Pattern) } - stdout, err := cmd.RunInDirWithTimeout(opt.Timeout, r.path) + stdout, err := cmd.RunInDir(r.path) if err != nil { return nil, err } @@ -199,23 +188,18 @@ 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) + cmd := NewCommand(ctx, "tag").AddOptions(opt.CommandOptions) if opt.Annotated { cmd.AddArgs("-a", name) cmd.AddArgs("--message", opt.Message) @@ -229,7 +213,7 @@ func (r *Repository) CreateTag(name, rev string, opts ...CreateTagOptions) error cmd.AddArgs(rev) - _, err := cmd.RunInDirWithTimeout(opt.Timeout, r.path) + _, err := cmd.RunInDir(r.path) return err } @@ -237,24 +221,19 @@ 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). + _, err := NewCommand(ctx, "tag", "--delete", "--end-of-options", name). AddOptions(opt.CommandOptions). - RunInDirWithTimeout(opt.Timeout, r.path) + RunInDir(r.path) return err } diff --git a/repo_tag_test.go b/repo_tag_test.go index bd5bc79b..263f0be2 100644 --- a/repo_tag_test.go +++ b/repo_tag_test.go @@ -5,12 +5,14 @@ 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 +38,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 +52,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 +62,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 +94,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 +132,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 +145,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 42ae99c9..1a3f17de 100644 --- a/repo_test.go +++ b/repo_test.go @@ -5,6 +5,7 @@ package git import ( + "context" "os" "path/filepath" "testing" @@ -22,6 +23,7 @@ func TestRepository(t *testing.T) { } func TestInit(t *testing.T) { + ctx := context.Background() tests := []struct { opt InitOptions }{ @@ -42,7 +44,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) } }) @@ -58,6 +60,7 @@ func TestOpen(t *testing.T) { } func TestClone(t *testing.T) { + ctx := context.Background() tests := []struct { opt CloneOptions }{ @@ -96,7 +99,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) } }) @@ -104,6 +107,7 @@ func TestClone(t *testing.T) { } func setupTempRepo() (_ *Repository, cleanup func(), err error) { + ctx := context.Background() path := tempPath() cleanup = func() { _ = os.RemoveAll(path) @@ -114,7 +118,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 } @@ -126,6 +130,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) @@ -147,7 +152,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) } }) @@ -155,6 +160,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) @@ -187,7 +193,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) } }) @@ -195,6 +201,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) @@ -215,7 +222,7 @@ func TestRepository_Push(t *testing.T) { 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) } }) @@ -223,6 +230,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) @@ -247,7 +255,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) } }) @@ -255,6 +263,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) @@ -275,7 +284,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) } }) @@ -283,6 +292,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) @@ -303,7 +313,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) } }) @@ -311,6 +321,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) @@ -325,7 +336,7 @@ func TestRepository_Add(t *testing.T) { } // 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 { @@ -334,6 +345,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) @@ -351,7 +363,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) @@ -366,19 +378,19 @@ func TestRepository_Commit(t *testing.T) { 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) } @@ -398,19 +410,19 @@ func TestRepository_Commit(t *testing.T) { 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) } @@ -424,6 +436,7 @@ func TestRepository_Commit(t *testing.T) { } func TestRepository_RevParse(t *testing.T) { + ctx := context.Background() tests := []struct { rev string expID string @@ -463,7 +476,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) }) @@ -471,16 +484,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..8fb2e663 100644 --- a/repo_tree.go +++ b/repo_tree.go @@ -6,8 +6,8 @@ package git import ( "bytes" + "context" "fmt" - "time" ) // UnescapeChars reverses escaped characters in quoted output from Git. @@ -104,17 +104,12 @@ 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] @@ -127,7 +122,7 @@ func (r *Repository) LsTree(treeID string, opts ...LsTreeOptions) (*Tree, error) } 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 +131,14 @@ func (r *Repository) LsTree(treeID string, opts ...LsTreeOptions) (*Tree, error) repo: r, } - cmd := NewCommand("ls-tree") + cmd := NewCommand(ctx, "ls-tree") if opt.Verbatim { cmd.AddArgs("-z") } stdout, err := cmd. AddOptions(opt.CommandOptions). AddArgs(treeID). - RunInDirWithTimeout(opt.Timeout, r.path) + RunInDir(r.path) if err != nil { return nil, err } diff --git a/repo_tree_test.go b/repo_tree_test.go index 51965f6c..1c361f2a 100644 --- a/repo_tree_test.go +++ b/repo_tree_test.go @@ -5,6 +5,7 @@ package git import ( + "context" "os" "path/filepath" "runtime" @@ -55,6 +56,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,7 +64,7 @@ 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` @@ -72,23 +74,23 @@ func TestRepository_LsTree(t *testing.T) { repo, err := Open(path) require.NoError(t, err) - err = repo.Add(AddOptions{All: true}) + err = repo.Add(ctx, AddOptions{All: true}) require.NoError(t, err) - err = repo.Commit(&Signature{Name: "test", Email: "test@test.com"}, "initial commit") + 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/server.go b/server.go index 0c2d275e..1b4d2356 100755 --- a/server.go +++ b/server.go @@ -5,6 +5,7 @@ package git import ( + "context" "time" ) @@ -15,25 +16,22 @@ 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) + cmd := NewCommand(ctx, "update-server-info").AddOptions(opt.CommandOptions) if opt.Force { cmd.AddArgs("--force") } - _, err := cmd.RunInDirWithTimeout(opt.Timeout, path) + _, err := cmd.RunInDir(path) return err } @@ -46,20 +44,17 @@ 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) + cmd := NewCommand(ctx, "receive-pack").AddOptions(opt.CommandOptions) if opt.Quiet { cmd.AddArgs("--quiet") } @@ -67,7 +62,7 @@ func ReceivePack(path string, opts ...ReceivePackOptions) ([]byte, error) { cmd.AddArgs("--http-backend-info-refs") } cmd.AddArgs(".") - return cmd.RunInDirWithTimeout(opt.Timeout, path) + return cmd.RunInDir(path) } // UploadPackOptions contains optional arguments for sending the packfile to the @@ -82,32 +77,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 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 // The additional options to be passed to the underlying git. 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) + cmd := NewCommand(ctx, "upload-pack").AddOptions(opt.CommandOptions) if opt.StatelessRPC { cmd.AddArgs("--stateless-rpc") } if opt.Strict { cmd.AddArgs("--strict") } - if opt.Timeout > 0 { - cmd.AddArgs("--timeout", opt.Timeout.String()) + if opt.InactivityTimeout > 0 { + cmd.AddArgs("--timeout", opt.InactivityTimeout.String()) } if opt.HTTPBackendInfoRefs { cmd.AddArgs("--http-backend-info-refs") } cmd.AddArgs(".") - return cmd.RunInDirWithTimeout(opt.Timeout, path) + return cmd.RunInDir(path) } diff --git a/server_test.go b/server_test.go index b8d95dc2..2d343dd5 100755 --- a/server_test.go +++ b/server_test.go @@ -5,6 +5,7 @@ package git import ( + "context" "os" "path/filepath" "testing" @@ -14,22 +15,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/tag.go b/tag.go index e26cb1b3..137eb039 100644 --- a/tag.go +++ b/tag.go @@ -4,6 +4,8 @@ package git +import "context" + // Tag contains information of a Git tag. type Tag struct { typ ObjectType @@ -47,6 +49,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..83341e75 100644 --- a/tag_test.go +++ b/tag_test.go @@ -5,13 +5,15 @@ 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 +33,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..998b65f3 100644 --- a/tree.go +++ b/tree.go @@ -5,6 +5,7 @@ package git import ( + "context" "strings" "sync" ) @@ -16,13 +17,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 +36,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 +51,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..020ee3c5 100644 --- a/tree_blob.go +++ b/tree_blob.go @@ -5,12 +5,13 @@ 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 +27,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 +38,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 +48,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 +62,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 +72,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..3af1fc59 100644 --- a/tree_blob_test.go +++ b/tree_blob_test.go @@ -5,18 +5,20 @@ 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 +29,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 +50,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..30f99641 100644 --- a/tree_entry.go +++ b/tree_entry.go @@ -5,6 +5,7 @@ package git import ( + "context" "fmt" "path" "runtime" @@ -13,7 +14,6 @@ import ( "strings" "sync" "sync/atomic" - "time" ) // EntryMode is the unix file mode of a tree entry. @@ -37,8 +37,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 +87,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 := NewCommand(ctx, "cat-file", "-s", e.id.String()).RunInDir(e.parent.repo.path) + if err != nil { + return 0 + } + e.size, _ = strconv.ParseInt(strings.TrimSpace(string(stdout)), 10, 64) + e.sizeSet = true return e.size } @@ -159,9 +163,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 +171,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 +233,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 +244,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..0d305b21 100644 --- a/tree_entry_test.go +++ b/tree_entry_test.go @@ -5,6 +5,7 @@ package git import ( + "context" "testing" "github.com/stretchr/testify/assert" @@ -31,12 +32,13 @@ func TestTreeEntry(t *testing.T) { } 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 +122,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 +252,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 +285,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 { From 78fda405b969a61015c81d0cbac5757b63484497 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=B4=8A=E1=B4=8F=E1=B4=87=20=E1=B4=84=CA=9C=E1=B4=87?= =?UTF-8?q?=C9=B4?= Date: Sun, 15 Feb 2026 14:06:32 -0500 Subject: [PATCH 4/7] refactor: use sourcegraph/run for command execution (#127) Co-authored-by: Claude Opus 4.6 (1M context) --- blob.go | 16 +-- blob_test.go | 4 +- command.go | 311 ++++++++++++++++++----------------------- command_test.go | 103 ++++---------- commit.go | 2 +- commit_archive.go | 22 +-- commit_archive_test.go | 4 +- git.go | 4 +- git_test.go | 2 +- go.mod | 15 +- go.sum | 60 ++++++++ repo.go | 125 +++++++++-------- repo_blame.go | 6 +- repo_commit.go | 87 +++++------- repo_diff.go | 55 +++----- repo_grep.go | 24 ++-- repo_pull.go | 11 +- repo_pull_test.go | 34 ++--- repo_reference.go | 41 +++--- repo_remote.go | 96 ++++++------- repo_tag.go | 42 +++--- repo_tree.go | 13 +- server.go | 34 +++-- tree_entry.go | 2 +- tree_entry_test.go | 34 +++++ utils.go | 8 -- 26 files changed, 556 insertions(+), 599 deletions(-) diff --git a/blob.go b/blob.go index 399171b4..843e1dbd 100644 --- a/blob.go +++ b/blob.go @@ -15,25 +15,23 @@ type Blob struct { *TreeEntry } -// Bytes reads and returns the content of the blob all at once in bytes. This -// can be very slow and memory consuming for huge content. +// Bytes reads and returns the content of the blob all at once in bytes. This can +// be very slow and memory consuming for huge content. func (b *Blob) Bytes(ctx context.Context) ([]byte, error) { stdout := new(bytes.Buffer) - stderr := new(bytes.Buffer) // Preallocate memory to save ~50% memory usage on big files. if size := b.Size(ctx); size > 0 && size < int64(^uint(0)>>1) { stdout.Grow(int(size)) } - if err := b.Pipeline(ctx, stdout, stderr); err != nil { - return nil, concatenateError(err, stderr.String()) + if err := b.Pipe(ctx, stdout); err != nil { + return nil, err } return stdout.Bytes(), nil } -// Pipeline reads the content of the blob and pipes stdout and stderr to -// supplied io.Writer. -func (b *Blob) Pipeline(ctx context.Context, stdout, stderr io.Writer) error { - return NewCommand(ctx, "show", b.id.String()).RunInDirPipeline(stdout, stderr, b.parent.repo.path) +// Pipe reads the content of the blob and pipes stdout to the supplied io.Writer. +func (b *Blob) Pipe(ctx context.Context, stdout io.Writer) error { + return pipe(ctx, b.parent.repo.path, []string{"show", "--end-of-options", b.id.String()}, nil, stdout) } diff --git a/blob_test.go b/blob_test.go index 6ccc46ee..743c3ed0 100644 --- a/blob_test.go +++ b/blob_test.go @@ -48,9 +48,9 @@ This demo also includes an image with changes on a branch for examination of ima assert.Equal(t, expOutput, string(p)) }) - t.Run("get data with pipeline", func(t *testing.T) { + t.Run("get data with pipe", func(t *testing.T) { stdout := new(bytes.Buffer) - err := blob.Pipeline(ctx, stdout, nil) + err := blob.Pipe(ctx, stdout) assert.Nil(t, err) assert.Equal(t, expOutput, stdout.String()) }) diff --git a/command.go b/command.go index 54e0094f..88a5538b 100644 --- a/command.go +++ b/command.go @@ -7,81 +7,155 @@ package git import ( "bytes" "context" - "fmt" + "errors" "io" "os" - "os/exec" + "strconv" "strings" "time" -) -// Command contains the name, arguments and environment variables of a command. -type Command struct { - name string - args []string - envs []string - ctx context.Context -} + "github.com/sourcegraph/run" +) -// CommandOptions contains options for running a command. +// CommandOptions contains additional options for running a Git command. type CommandOptions struct { - Args []string Envs []string } -// String returns the string representation of the command. -func (c *Command) String() string { - if len(c.args) == 0 { - return c.name +// DefaultTimeout is the default timeout duration for all commands. It is +// applied when the context does not already have a deadline. +const DefaultTimeout = time.Minute + +// cmd builds a *run.Command for git with the given arguments, environment +// variables and working directory. DefaultTimeout will be applied if the context +// does not already have a deadline. +func cmd(ctx context.Context, dir string, args []string, envs []string) (*run.Command, context.CancelFunc) { + cancel := func() {} + if _, ok := ctx.Deadline(); !ok { + var timeoutCancel context.CancelFunc + ctx, timeoutCancel = context.WithTimeout(ctx, DefaultTimeout) + cancel = timeoutCancel } - return fmt.Sprintf("%s %s", c.name, strings.Join(c.args, " ")) -} -// NewCommand creates and returns a new Command with given arguments for "git". -func NewCommand(ctx context.Context, args ...string) *Command { - return &Command{ - name: "git", - args: args, - ctx: ctx, + // run.Cmd joins all parts into a single string and then shell-parses it. We must + // quote each argument so that special characters (spaces, quotes, angle + // brackets, etc.) are preserved correctly. + parts := make([]string, 0, 1+len(args)) + parts = append(parts, "git") + for _, arg := range args { + parts = append(parts, run.Arg(arg)) } -} -// AddArgs appends given arguments to the command. -func (c *Command) AddArgs(args ...string) *Command { - c.args = append(c.args, args...) - return c + c := run.Cmd(ctx, parts...) + if dir != "" { + c = c.Dir(dir) + } + if len(envs) > 0 { + c = c.Environ(append(os.Environ(), envs...)) + } + return c, cancel } -// AddEnvs appends given environment variables to the command. -func (c *Command) AddEnvs(envs ...string) *Command { - c.envs = append(c.envs, envs...) - return c -} +// exec executes a git command in the given directory and returns stdout as +// bytes. Stderr is included in the error message on failure. DefaultTimeout will +// be applied if the context does not already have a deadline. It returns +// ErrExecTimeout if the execution was timed out. +func exec(ctx context.Context, dir string, args []string, envs []string) ([]byte, error) { + c, cancel := cmd(ctx, dir, args, envs) + defer cancel() + + var logBuf *bytes.Buffer + if logOutput != nil { + logBuf = new(bytes.Buffer) + logBuf.Grow(512) + defer func() { + log(dir, args, logBuf.Bytes()) + }() + } + + // Use Stream to a buffer to preserve raw bytes (including NUL bytes from + // commands like "ls-tree -z"). The String/Lines methods process output + // line-by-line which corrupts binary-ish output. + stdout := new(bytes.Buffer) + err := c.StdOut().Run().Stream(stdout) + + // Capture (partial) stdout for logging even on error, so failed commands produce + // a useful log entry rather than an empty one. + if logOutput != nil { + data := stdout.Bytes() + limit := len(data) + if limit > 512 { + limit = 512 + } + logBuf.Write(data[:limit]) + if len(data) > 512 { + logBuf.WriteString("... (more omitted)") + } + } -// WithContext returns a new Command with the given context. -func (c Command) WithContext(ctx context.Context) *Command { - c.ctx = ctx - return &c + if err != nil { + return nil, mapContextError(err, ctx) + } + return stdout.Bytes(), nil } -// AddOptions adds options to the command. -func (c *Command) AddOptions(opts ...CommandOptions) *Command { - for _, opt := range opts { - c.AddArgs(opt.Args...) - c.AddEnvs(opt.Envs...) +// pipe executes a git command in the given directory, streaming stdout to the +// given io.Writer. +func pipe(ctx context.Context, dir string, args []string, envs []string, stdout io.Writer) error { + c, cancel := cmd(ctx, dir, args, envs) + defer cancel() + + var buf *bytes.Buffer + w := stdout + if logOutput != nil { + buf = new(bytes.Buffer) + buf.Grow(512) + w = &limitDualWriter{ + W: buf, + N: int64(buf.Cap()), + w: stdout, + } + + defer func() { + log(dir, args, buf.Bytes()) + }() } - return c + + streamErr := c.StdOut().Run().Stream(w) + if streamErr != nil { + return mapContextError(streamErr, ctx) + } + return nil } -// AddCommitter appends given committer to the command. -func (c *Command) AddCommitter(committer *Signature) *Command { - c.AddEnvs("GIT_COMMITTER_NAME="+committer.Name, "GIT_COMMITTER_EMAIL="+committer.Email) - return c +// committerEnvs returns environment variables for setting the Git committer. +func committerEnvs(committer *Signature) []string { + return []string{ + "GIT_COMMITTER_NAME=" + committer.Name, + "GIT_COMMITTER_EMAIL=" + committer.Email, + } } -// DefaultTimeout is the default timeout duration for all commands. It is -// applied when the context does not already have a deadline. -const DefaultTimeout = time.Minute +// log logs a git command execution with its output. +func log(dir string, args []string, output []byte) { + cmdStr := "git" + if len(args) > 0 { + quoted := make([]string, len(args)) + for i, a := range args { + if strings.ContainsAny(a, " \t\n\"'\\<>") { + quoted[i] = strconv.Quote(a) + } else { + quoted[i] = a + } + } + cmdStr = "git " + strings.Join(quoted, " ") + } + if len(dir) == 0 { + logf("%s\n%s", cmdStr, output) + } else { + logf("%s: %s\n%s", dir, cmdStr, output) + } +} // A limitDualWriter writes to W but limits the amount of data written to just N // bytes. On the other hand, it passes everything to w. @@ -111,134 +185,25 @@ func (w *limitDualWriter) Write(p []byte) (int, error) { return w.w.Write(p) } -// RunInDirOptions contains options for running a command in a directory. -type RunInDirOptions struct { - // Stdin is the input to the command. - Stdin io.Reader - // Stdout is the outputs from the command. - Stdout io.Writer - // Stderr is the error output from the command. - Stderr io.Writer -} - -// RunInDirWithOptions executes the command in given directory and options. It -// pipes stdin from supplied io.Reader, and pipes stdout and stderr to supplied -// io.Writer. If the command's context does not have a deadline, DefaultTimeout -// will be applied automatically. It returns an ErrExecTimeout if the execution -// was timed out. -func (c *Command) RunInDirWithOptions(dir string, opts ...RunInDirOptions) (err error) { - var opt RunInDirOptions - if len(opts) > 0 { - opt = opts[0] - } - - buf := new(bytes.Buffer) - w := opt.Stdout - if logOutput != nil { - buf.Grow(512) - w = &limitDualWriter{ - W: buf, - N: int64(buf.Cap()), - w: opt.Stdout, - } - } - - defer func() { - if len(dir) == 0 { - log("%s\n%s", c, buf.Bytes()) - } else { - log("%s: %s\n%s", dir, c, buf.Bytes()) - } - }() - - ctx := c.ctx +// mapContextError maps context errors to the appropriate sentinel errors used +// by this package. +func mapContextError(err error, ctx context.Context) error { if ctx == nil { - ctx = context.Background() - } - - // Apply default timeout if the context doesn't already have a deadline. - if _, ok := ctx.Deadline(); !ok { - var cancel context.CancelFunc - ctx, cancel = context.WithTimeout(ctx, DefaultTimeout) - defer cancel() - } - - cmd := exec.CommandContext(ctx, c.name, c.args...) - if len(c.envs) > 0 { - cmd.Env = append(os.Environ(), c.envs...) - } - cmd.Dir = dir - cmd.Stdin = opt.Stdin - cmd.Stdout = w - cmd.Stderr = opt.Stderr - if err = cmd.Start(); err != nil { - if ctx.Err() == context.DeadlineExceeded { - return ErrExecTimeout - } else if ctx.Err() != nil { - return ctx.Err() - } return err } - - result := make(chan error) - go func() { - result <- cmd.Wait() - }() - - select { - case <-ctx.Done(): - // Kill the process before waiting so cancellation is enforced promptly. - if cmd.Process != nil { - _ = cmd.Process.Kill() - } - <-result - - if ctx.Err() == context.DeadlineExceeded { + if ctxErr := ctx.Err(); ctxErr != nil { + if errors.Is(ctxErr, context.DeadlineExceeded) { return ErrExecTimeout } - return ctx.Err() - case err = <-result: - // Normalize errors when the context may have expired around the same time. - if err != nil { - if ctxErr := ctx.Err(); ctxErr != nil { - if ctxErr == context.DeadlineExceeded { - return ErrExecTimeout - } - return ctxErr - } - } - return err + return ctxErr } - -} - -// RunInDirPipeline executes the command in given directory. It pipes stdout and -// stderr to supplied io.Writer. -func (c *Command) RunInDirPipeline(stdout, stderr io.Writer, dir string) error { - return c.RunInDirWithOptions(dir, RunInDirOptions{ - Stdin: nil, - Stdout: stdout, - Stderr: stderr, - }) -} - -// RunInDir executes the command in given directory. It returns stdout and error -// (combined with stderr). -func (c *Command) RunInDir(dir string) ([]byte, error) { - stdout := new(bytes.Buffer) - stderr := new(bytes.Buffer) - if err := c.RunInDirPipeline(stdout, stderr, dir); err != nil { - return nil, concatenateError(err, stderr.String()) - } - return stdout.Bytes(), nil + return err } -// Run executes the command in working directory. It returns stdout and -// error (combined with stderr). -func (c *Command) Run() ([]byte, error) { - stdout, err := c.RunInDir("") - if err != nil { - return nil, err - } - return stdout, nil +// isExitStatus reports whether err represents a specific process exit status +// code, using the run.ExitCoder interface provided by sourcegraph/run. +func isExitStatus(err error, code int) bool { + var exitCoder run.ExitCoder + ok := errors.As(err, &exitCoder) + return ok && exitCoder.ExitCode() == code } diff --git a/command_test.go b/command_test.go index e2fb26f5..4c02835d 100644 --- a/command_test.go +++ b/command_test.go @@ -13,63 +13,12 @@ import ( "github.com/stretchr/testify/assert" ) -func TestCommand_String(t *testing.T) { - ctx := context.Background() - tests := []struct { - name string - args []string - expStr string - }{ - { - name: "no args", - args: nil, - expStr: "git", - }, - { - name: "has one arg", - args: []string{"version"}, - expStr: "git version", - }, - { - name: "has more args", - args: []string{"config", "--global", "http.proxy", "http://localhost:8080"}, - expStr: "git config --global http.proxy http://localhost:8080", - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - cmd := NewCommand(ctx, test.args...) - assert.Equal(t, test.expStr, cmd.String()) - }) - } -} - -func TestCommand_AddArgs(t *testing.T) { - ctx := context.Background() - cmd := NewCommand(ctx) - assert.Equal(t, []string(nil), cmd.args) - - cmd.AddArgs("push") - cmd.AddArgs("origin", "master") - assert.Equal(t, []string{"push", "origin", "master"}, cmd.args) -} - -func TestCommand_AddEnvs(t *testing.T) { - ctx := context.Background() - cmd := NewCommand(ctx) - assert.Equal(t, []string(nil), cmd.envs) - - cmd.AddEnvs("GIT_DIR=/tmp") - cmd.AddEnvs("HOME=/Users/unknwon", "GIT_EDITOR=code") - assert.Equal(t, []string{"GIT_DIR=/tmp", "HOME=/Users/unknwon", "GIT_EDITOR=code"}, cmd.envs) -} - -func TestCommand_RunWithContextTimeout(t *testing.T) { +func TestExec_ContextTimeout(t *testing.T) { t.Run("context already expired before start", func(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Nanosecond) defer cancel() time.Sleep(time.Millisecond) // ensure deadline has passed - _, err := NewCommand(ctx, "version").Run() + _, err := exec(ctx, "", []string{"version"}, nil) assert.Equal(t, ErrExecTimeout, err) }) @@ -77,21 +26,21 @@ func TestCommand_RunWithContextTimeout(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) defer cancel() - // Use a blocking reader so the command starts successfully and blocks - // reading stdin until the context deadline fires. - err := NewCommand(ctx, "hash-object", "--stdin").RunInDirWithOptions("", RunInDirOptions{ - Stdin: blockingReader{cancel: ctx.Done()}, - Stdout: io.Discard, - Stderr: io.Discard, - }) + // Use cmd directly with a blocking stdin so the command starts successfully and + // blocks reading until the context deadline fires. + c, timeoutCancel := cmd(ctx, "", []string{"hash-object", "--stdin"}, nil) + defer timeoutCancel() + + err := c.Input(blockingReader{cancel: ctx.Done()}).StdOut().Run().Stream(io.Discard) + err = mapContextError(err, ctx) assert.Equal(t, ErrExecTimeout, err) }) } -// blockingReader is an io.Reader that blocks until its cancel channel is -// closed, simulating a stdin that never provides data. When cancelled it -// returns io.EOF so that exec's stdin copy goroutine can exit cleanly, -// allowing cmd.Wait() to return. +// blockingReader is an io.Reader that blocks until its cancel channel is closed, +// simulating a stdin that never provides data. When canceled it returns io.EOF +// so that the stdin copy goroutine can exit cleanly, allowing cmd.Wait() to +// return. type blockingReader struct { cancel <-chan struct{} } @@ -101,11 +50,11 @@ func (r blockingReader) Read(p []byte) (int, error) { return 0, io.EOF } -func TestCommand_RunWithContextCancellation(t *testing.T) { +func TestCmd_ContextCancellation(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) - // Cancel in the background after a short delay so the command is already - // running when cancellation arrives. Close done to unblock the reader. + // Cancel in the background after a short delay so the command is already running + // when cancellation arrives. Close done to unblock the reader. done := make(chan struct{}) go func() { time.Sleep(50 * time.Millisecond) @@ -113,22 +62,22 @@ func TestCommand_RunWithContextCancellation(t *testing.T) { close(done) }() - err := NewCommand(ctx, "hash-object", "--stdin").RunInDirWithOptions("", RunInDirOptions{ - Stdin: blockingReader{cancel: done}, - Stdout: io.Discard, - Stderr: io.Discard, - }) + c, timeoutCancel := cmd(ctx, "", []string{"hash-object", "--stdin"}, nil) + defer timeoutCancel() + + err := c.Input(blockingReader{cancel: done}).StdOut().Run().Stream(io.Discard) + err = mapContextError(err, ctx) assert.ErrorIs(t, err, context.Canceled) // Must NOT be ErrExecTimeout — cancellation is distinct from deadline. assert.NotEqual(t, ErrExecTimeout, err) } -func TestCommand_DefaultTimeoutApplied(t *testing.T) { - // A plain context.Background() has no deadline. The command should still - // succeed because DefaultTimeout (1 min) is applied automatically and - // "git version" completes well within that. +func TestExec_DefaultTimeoutApplied(t *testing.T) { + // A plain context.Background() has no deadline. The command should still succeed + // because DefaultTimeout is applied automatically and "git version" completes + // well within that. ctx := context.Background() - stdout, err := NewCommand(ctx, "version").Run() + stdout, err := exec(ctx, "", []string{"version"}, nil) assert.NoError(t, err) assert.Contains(t, string(stdout), "git version") } diff --git a/commit.go b/commit.go index 381734e2..134f39fb 100644 --- a/commit.go +++ b/commit.go @@ -154,7 +154,7 @@ func (c *Commit) isImageFile(ctx context.Context, blob *Blob, err error) (bool, N: int64(buf.Cap()), } - err = blob.Pipeline(ctx, stdout, io.Discard) + err = blob.Pipe(ctx, stdout) if err != nil { return false, err } diff --git a/commit_archive.go b/commit_archive.go index 535f6f82..5daba32b 100644 --- a/commit_archive.go +++ b/commit_archive.go @@ -19,14 +19,20 @@ const ( ArchiveTarGz ArchiveFormat = "tar.gz" ) -// CreateArchive creates given format of archive to the destination. -func (c *Commit) CreateArchive(ctx context.Context, format ArchiveFormat, dst string) error { +// Archive creates given format of archive to the destination. +func (c *Commit) Archive(ctx context.Context, format ArchiveFormat, dst string) error { prefix := filepath.Base(strings.TrimSuffix(c.repo.path, ".git")) + "/" - _, err := NewCommand(ctx, "archive", - "--prefix="+prefix, - "--format="+string(format), - "-o", dst, - c.ID.String(), - ).RunInDir(c.repo.path) + _, err := exec(ctx, + c.repo.path, + []string{ + "archive", + "--prefix=" + prefix, + "--format=" + string(format), + "-o", dst, + "--end-of-options", + c.ID.String(), + }, + nil, + ) return err } diff --git a/commit_archive_test.go b/commit_archive_test.go index 2523c73c..892859c8 100644 --- a/commit_archive_test.go +++ b/commit_archive_test.go @@ -19,7 +19,7 @@ func tempPath() string { return filepath.Join(os.TempDir(), strconv.Itoa(int(time.Now().UnixNano()))) } -func TestCommit_CreateArchive(t *testing.T) { +func TestCommit_Archive(t *testing.T) { ctx := context.Background() for _, format := range []ArchiveFormat{ ArchiveZip, @@ -36,7 +36,7 @@ func TestCommit_CreateArchive(t *testing.T) { _ = os.Remove(dst) }() - assert.Nil(t, c.CreateArchive(ctx, format, dst)) + assert.Nil(t, c.Archive(ctx, format, dst)) }) } } diff --git a/git.go b/git.go index ebbfdfd7..e96ab1e9 100644 --- a/git.go +++ b/git.go @@ -29,7 +29,7 @@ func SetPrefix(prefix string) { logPrefix = prefix } -func log(format string, args ...interface{}) { +func logf(format string, args ...interface{}) { if logOutput == nil { return } @@ -58,7 +58,7 @@ func BinVersion(ctx context.Context) (string, error) { return gitVersion, nil } - stdout, err := NewCommand(ctx, "version").Run() + stdout, err := exec(ctx, "", []string{"version"}, nil) if err != nil { return "", err } diff --git a/git_test.go b/git_test.go index 5c0e0df9..c4af3655 100644 --- a/git_test.go +++ b/git_test.go @@ -83,7 +83,7 @@ func Test_log(t *testing.T) { var buf bytes.Buffer SetOutput(&buf) - log(test.format, test.args...) + logf(test.format, test.args...) assert.Equal(t, test.expOutput, buf.String()) }) } diff --git a/go.mod b/go.mod index 0753589a..52cd9060 100644 --- a/go.mod +++ b/go.mod @@ -2,10 +2,23 @@ module github.com/gogs/git-module/v2 go 1.26.0 -require github.com/stretchr/testify v1.11.1 +require ( + github.com/sourcegraph/run v0.12.0 + github.com/stretchr/testify v1.11.1 +) require ( + bitbucket.org/creachadair/shell v0.0.7 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/djherbis/buffer v1.2.0 // indirect + github.com/djherbis/nio/v3 v3.0.1 // indirect + github.com/go-logr/logr v1.2.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/itchyny/gojq v0.12.11 // indirect + github.com/itchyny/timefmt-go v0.1.5 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + go.bobheadxi.dev/streamline v1.2.1 // indirect + go.opentelemetry.io/otel v1.11.0 // indirect + go.opentelemetry.io/otel/trace v1.11.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index c4c1710c..2957c960 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,70 @@ +bitbucket.org/creachadair/shell v0.0.7 h1:Z96pB6DkSb7F3Y3BBnJeOZH2gazyMTWlvecSD4vDqfk= +bitbucket.org/creachadair/shell v0.0.7/go.mod h1:oqtXSSvSYr4624lnnabXHaBsYW6RD80caLi2b3hJk0U= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/djherbis/buffer v1.1.0/go.mod h1:VwN8VdFkMY0DCALdY8o00d3IZ6Amz/UNVMWcSaJT44o= +github.com/djherbis/buffer v1.2.0 h1:PH5Dd2ss0C7CRRhQCZ2u7MssF+No9ide8Ye71nPHcrQ= +github.com/djherbis/buffer v1.2.0/go.mod h1:fjnebbZjCUpPinBRD+TDwXSOeNQ7fPQWLfGQqiAiUyE= +github.com/djherbis/nio/v3 v3.0.1 h1:6wxhnuppteMa6RHA4L81Dq7ThkZH8SwnDzXDYy95vB4= +github.com/djherbis/nio/v3 v3.0.1/go.mod h1:Ng4h80pbZFMla1yKzm61cF0tqqilXZYrogmWgZxOcmg= +github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= +github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/hexops/autogold v1.3.1 h1:YgxF9OHWbEIUjhDbpnLhgVsjUDsiHDTyDfy2lrfdlzo= +github.com/hexops/autogold v1.3.1/go.mod h1:sQO+mQUCVfxOKPht+ipDSkJ2SCJ7BNJVHZexsXqWMx4= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/hexops/valast v1.4.3 h1:oBoGERMJh6UZdRc6cduE1CTPK+VAdXA59Y1HFgu3sm0= +github.com/hexops/valast v1.4.3/go.mod h1:Iqx2kLj3Jn47wuXpj3wX40xn6F93QNFBHuiKBerkTGA= +github.com/itchyny/gojq v0.12.11 h1:YhLueoHhHiN4mkfM+3AyJV6EPcCxKZsOnYf+aVSwaQw= +github.com/itchyny/gojq v0.12.11/go.mod h1:o3FT8Gkbg/geT4pLI0tF3hvip5F3Y/uskjRz9OYa38g= +github.com/itchyny/timefmt-go v0.1.5 h1:G0INE2la8S6ru/ZI5JecgyzbbJNs5lG1RcBqa7Jm6GE= +github.com/itchyny/timefmt-go v0.1.5/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/nightlyone/lockfile v1.0.0 h1:RHep2cFKK4PonZJDdEl4GmkabuhbsRMgk/k3uAmxBiA= +github.com/nightlyone/lockfile v1.0.0/go.mod h1:rywoIealpdNse2r832aiD9jRk8ErCatROs6LzC841CI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/sourcegraph/run v0.12.0 h1:3A8w5e8HIYPfafHekvmdmmh42RHKGVhmiTZAPJclg7I= +github.com/sourcegraph/run v0.12.0/go.mod h1:PwaP936BTnAJC1cqR5rSbG5kOs/EWStTK3lqvMX5GUA= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.bobheadxi.dev/streamline v1.2.1 h1:IqKSA1TbeuDqCzYNAwtlh8sqf3tsQus8XgJdkCWFT8c= +go.bobheadxi.dev/streamline v1.2.1/go.mod h1:yJsVXOSBFLgAKvsnf6WmIzmB2A65nWqkR/sRNxJPa74= +go.opentelemetry.io/otel v1.11.0 h1:kfToEGMDq6TrVrJ9Vht84Y8y9enykSZzDDZglV0kIEk= +go.opentelemetry.io/otel v1.11.0/go.mod h1:H2KtuEphyMvlhZ+F7tg9GRhAOe60moNx61Ex+WmiKkk= +go.opentelemetry.io/otel/sdk v1.11.0 h1:ZnKIL9V9Ztaq+ME43IUi/eo22mNsb6a7tGfzaOWB5fo= +go.opentelemetry.io/otel/sdk v1.11.0/go.mod h1:REusa8RsyKaq0OlyangWXaw97t2VogoO4SSEeKkSTAk= +go.opentelemetry.io/otel/trace v1.11.0 h1:20U/Vj42SX+mASlXLmSGBg6jpI1jQtv682lZtTAOVFI= +go.opentelemetry.io/otel/trace v1.11.0/go.mod h1:nyYjis9jy0gytE9LXGU+/m1sHTKbRY0fX0hulNNDP1U= +golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= +golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/tools v0.4.0 h1:7mTAgkunk3fr4GAloyyCasadO6h9zSsQZbwvcaIciV4= +golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +mvdan.cc/gofumpt v0.4.0 h1:JVf4NN1mIpHogBj7ABpgOyZc65/UUOkKQFkoURsz4MM= +mvdan.cc/gofumpt v0.4.0/go.mod h1:PljLOHDeZqgS8opHRKLzp2It2VBuSdteAgqUfzMTxlQ= diff --git a/repo.go b/repo.go index 4b82c477..7acfeb5a 100644 --- a/repo.go +++ b/repo.go @@ -74,12 +74,12 @@ func Init(ctx context.Context, path string, opts ...InitOptions) error { return err } - cmd := NewCommand(ctx, "init").AddOptions(opt.CommandOptions) + args := []string{"init"} if opt.Bare { - cmd.AddArgs("--bare") + args = append(args, "--bare") } - cmd.AddArgs("--end-of-options") - _, err = cmd.RunInDir(path) + args = append(args, "--end-of-options") + _, err = exec(ctx, path, args, opt.Envs) return err } @@ -131,25 +131,25 @@ func Clone(ctx context.Context, url, dst string, opts ...CloneOptions) error { return err } - cmd := NewCommand(ctx, "clone").AddOptions(opt.CommandOptions) + args := []string{"clone"} if opt.Mirror { - cmd.AddArgs("--mirror") + args = append(args, "--mirror") } if opt.Bare { - cmd.AddArgs("--bare") + args = append(args, "--bare") } if opt.Quiet { - cmd.AddArgs("--quiet") + args = append(args, "--quiet") } if !opt.Bare && opt.Branch != "" { - cmd.AddArgs("-b", opt.Branch) + args = append(args, "-b", opt.Branch) } if opt.Depth > 0 { - cmd.AddArgs("--depth", strconv.FormatUint(opt.Depth, 10)) + args = append(args, "--depth", strconv.FormatUint(opt.Depth, 10)) } - cmd.AddArgs("--end-of-options") - _, err = cmd.AddArgs(url, dst).Run() + args = append(args, "--end-of-options", url, dst) + _, err = exec(ctx, "", args, opt.Envs) return err } @@ -170,12 +170,13 @@ func (r *Repository) Fetch(ctx context.Context, opts ...FetchOptions) error { opt = opts[0] } - cmd := NewCommand(ctx, "fetch").AddOptions(opt.CommandOptions) + args := []string{"fetch"} if opt.Prune { - cmd.AddArgs("--prune") + args = append(args, "--prune") } + args = append(args, "--end-of-options") - _, err := cmd.RunInDir(r.path) + _, err := exec(ctx, r.path, args, opt.Envs) return err } @@ -202,21 +203,22 @@ func (r *Repository) Pull(ctx context.Context, opts ...PullOptions) error { opt = opts[0] } - cmd := NewCommand(ctx, "pull").AddOptions(opt.CommandOptions) + args := []string{"pull"} if opt.Rebase { - cmd.AddArgs("--rebase") + args = append(args, "--rebase") } if opt.All { - cmd.AddArgs("--all") + args = append(args, "--all") } + args = append(args, "--end-of-options") if !opt.All && opt.Remote != "" { - cmd.AddArgs(opt.Remote) + args = append(args, opt.Remote) if opt.Branch != "" { - cmd.AddArgs(opt.Branch) + args = append(args, opt.Branch) } } - _, err := cmd.RunInDir(r.path) + _, err := exec(ctx, r.path, args, opt.Envs) return err } @@ -235,8 +237,8 @@ func (r *Repository) Push(ctx context.Context, remote, branch string, opts ...Pu opt = opts[0] } - cmd := NewCommand(ctx, "push").AddOptions(opt.CommandOptions).AddArgs("--end-of-options", remote, branch) - _, err := cmd.RunInDir(r.path) + args := []string{"push", "--end-of-options", remote, branch} + _, err := exec(ctx, r.path, args, opt.Envs) return err } @@ -257,16 +259,14 @@ func (r *Repository) Checkout(ctx context.Context, branch string, opts ...Checko opt = opts[0] } - cmd := NewCommand(ctx, "checkout").AddOptions(opt.CommandOptions) + args := []string{"checkout"} if opt.BaseBranch != "" { - cmd.AddArgs("-b") - } - cmd.AddArgs(branch) - if opt.BaseBranch != "" { - cmd.AddArgs(opt.BaseBranch) + args = append(args, "-b", branch, "--end-of-options", opt.BaseBranch) + } else { + args = append(args, "--end-of-options", branch) } - _, err := cmd.RunInDir(r.path) + _, err := exec(ctx, r.path, args, opt.Envs) return err } @@ -287,12 +287,13 @@ func (r *Repository) Reset(ctx context.Context, rev string, opts ...ResetOptions opt = opts[0] } - cmd := NewCommand(ctx, "reset") + args := []string{"reset"} if opt.Hard { - cmd.AddArgs("--hard") + args = append(args, "--hard") } + args = append(args, "--end-of-options", rev) - _, err := cmd.AddOptions(opt.CommandOptions).AddArgs("--end-of-options", rev).RunInDir(r.path) + _, err := exec(ctx, r.path, args, opt.Envs) return err } @@ -313,7 +314,8 @@ func (r *Repository) Move(ctx context.Context, src, dst string, opts ...MoveOpti opt = opts[0] } - _, err := NewCommand(ctx, "mv").AddOptions(opt.CommandOptions).AddArgs("--end-of-options", src, dst).RunInDir(r.path) + args := []string{"mv", "--end-of-options", src, dst} + _, err := exec(ctx, r.path, args, opt.Envs) return err } @@ -336,15 +338,15 @@ func (r *Repository) Add(ctx context.Context, opts ...AddOptions) error { opt = opts[0] } - cmd := NewCommand(ctx, "add").AddOptions(opt.CommandOptions) + args := []string{"add"} if opt.All { - cmd.AddArgs("--all") + args = append(args, "--all") } if len(opt.Pathspecs) > 0 { - cmd.AddArgs("--") - cmd.AddArgs(opt.Pathspecs...) + args = append(args, "--") + args = append(args, opt.Pathspecs...) } - _, err := cmd.RunInDir(r.path) + _, err := exec(ctx, r.path, args, opt.Envs) return err } @@ -366,19 +368,21 @@ func (r *Repository) Commit(ctx context.Context, committer *Signature, message s opt = opts[0] } - cmd := NewCommand(ctx, "commit") - cmd.AddCommitter(committer) + envs := committerEnvs(committer) + envs = append(envs, opt.Envs...) if opt.Author == nil { opt.Author = committer } - cmd = cmd.AddArgs(fmt.Sprintf("--author='%s <%s>'", opt.Author.Name, opt.Author.Email)). - AddArgs("-m", message). - AddOptions(opt.CommandOptions) - _, err := cmd.RunInDir(r.path) + args := []string{"commit"} + args = append(args, fmt.Sprintf("--author=%s <%s>", opt.Author.Name, opt.Author.Email)) + args = append(args, "-m", message) + args = append(args, "--end-of-options") + + _, err := exec(ctx, r.path, args, envs) // No stderr but exit status 1 means nothing to commit. - if err != nil && err.Error() == "exit status 1" { + if isExitStatus(err, 1) { return nil } return err @@ -429,14 +433,12 @@ func (r *Repository) ShowNameStatus(ctx context.Context, rev string, opts ...Sho done <- struct{}{} }() - stderr := new(bytes.Buffer) - cmd := NewCommand(ctx, "show", "--name-status", "--pretty=format:''"). - AddOptions(opt.CommandOptions). - AddArgs("--end-of-options", rev) - err := cmd.RunInDirPipeline(w, stderr, r.path) + args := []string{"show", "--name-status", "--pretty=format:''", "--end-of-options", rev} + + err := pipe(ctx, r.path, args, opt.Envs, w) _ = w.Close() // Close writer to exit parsing goroutine if err != nil { - return nil, concatenateError(err, stderr.String()) + return nil, err } <-done @@ -459,12 +461,11 @@ func (r *Repository) RevParse(ctx context.Context, rev string, opts ...RevParseO opt = opts[0] } - commitID, err := NewCommand(ctx, "rev-parse"). - AddOptions(opt.CommandOptions). - AddArgs(rev). - RunInDir(r.path) + args := []string{"rev-parse", rev} + + commitID, err := exec(ctx, r.path, args, opt.Envs) if err != nil { - if strings.Contains(err.Error(), "exit status 128") { + if isExitStatus(err, 128) { return "", ErrRevisionNotExist } return "", err @@ -499,9 +500,9 @@ func (r *Repository) CountObjects(ctx context.Context, opts ...CountObjectsOptio opt = opts[0] } - stdout, err := NewCommand(ctx, "count-objects", "-v"). - AddOptions(opt.CommandOptions). - RunInDir(r.path) + args := []string{"count-objects", "-v", "--end-of-options"} + + stdout, err := exec(ctx, r.path, args, opt.Envs) if err != nil { return nil, err } @@ -552,7 +553,7 @@ func (r *Repository) Fsck(ctx context.Context, opts ...FsckOptions) error { opt = opts[0] } - cmd := NewCommand(ctx, "fsck").AddOptions(opt.CommandOptions) - _, err := cmd.RunInDir(r.path) + args := []string{"fsck", "--end-of-options"} + _, err := exec(ctx, r.path, args, opt.Envs) return err } diff --git a/repo_blame.go b/repo_blame.go index d2ffc062..b10a0d44 100644 --- a/repo_blame.go +++ b/repo_blame.go @@ -24,10 +24,8 @@ func (r *Repository) Blame(ctx context.Context, rev, file string, opts ...BlameO opt = opts[0] } - stdout, err := NewCommand(ctx, "blame"). - AddOptions(opt.CommandOptions). - AddArgs("-l", "-s", rev, "--", file). - RunInDir(r.path) + args := []string{"blame", "-l", "-s", rev, "--", file} + stdout, err := exec(ctx, r.path, args, opt.Envs) if err != nil { return nil, err } diff --git a/repo_commit.go b/repo_commit.go index b2425200..4ffced0f 100644 --- a/repo_commit.go +++ b/repo_commit.go @@ -85,7 +85,7 @@ func (r *Repository) CatFileCommit(ctx context.Context, rev string, opts ...CatF cache, ok := r.cachedCommits.Get(rev) if ok { - log("Cached commit hit: %s", rev) + logf("Cached commit hit: %s", rev) return cache.(*Commit), nil } @@ -94,10 +94,8 @@ func (r *Repository) CatFileCommit(ctx context.Context, rev string, opts ...CatF return nil, err } - stdout, err := NewCommand(ctx, "cat-file"). - AddOptions(opt.CommandOptions). - AddArgs("commit", commitID). - RunInDir(r.path) + args := []string{"cat-file", "commit", commitID} + stdout, err := exec(ctx, r.path, args, opt.Envs) if err != nil { return nil, err } @@ -128,10 +126,8 @@ func (r *Repository) CatFileType(ctx context.Context, rev string, opts ...CatFil opt = opts[0] } - typ, err := NewCommand(ctx, "cat-file"). - AddOptions(opt.CommandOptions). - AddArgs("-t", rev). - RunInDir(r.path) + args := []string{"cat-file", "-t", rev} + typ, err := exec(ctx, r.path, args, opt.Envs) if err != nil { return "", err } @@ -191,30 +187,28 @@ func (r *Repository) Log(ctx context.Context, rev string, opts ...LogOptions) ([ opt = opts[0] } - cmd := NewCommand(ctx, "log"). - AddOptions(opt.CommandOptions). - AddArgs("--pretty=" + LogFormatHashOnly) + args := []string{"log", "--pretty=" + LogFormatHashOnly} if opt.MaxCount > 0 { - cmd.AddArgs("--max-count=" + strconv.Itoa(opt.MaxCount)) + args = append(args, "--max-count="+strconv.Itoa(opt.MaxCount)) } if opt.Skip > 0 { - cmd.AddArgs("--skip=" + strconv.Itoa(opt.Skip)) + args = append(args, "--skip="+strconv.Itoa(opt.Skip)) } if !opt.Since.IsZero() { - cmd.AddArgs("--since=" + opt.Since.Format(time.RFC3339)) + args = append(args, "--since="+opt.Since.Format(time.RFC3339)) } if opt.GrepPattern != "" { - cmd.AddArgs("--grep=" + opt.GrepPattern) + args = append(args, "--grep="+opt.GrepPattern) } if opt.RegexpIgnoreCase { - cmd.AddArgs("--regexp-ignore-case") + args = append(args, "--regexp-ignore-case") } - cmd.AddArgs("--end-of-options", rev, "--") + args = append(args, "--end-of-options", rev, "--") if opt.Path != "" { - cmd.AddArgs(escapePath(opt.Path)) + args = append(args, escapePath(opt.Path)) } - stdout, err := cmd.RunInDir(r.path) + stdout, err := exec(ctx, r.path, args, opt.Envs) if err != nil { return nil, err } @@ -356,21 +350,18 @@ func (r *Repository) DiffNameOnly(ctx context.Context, base, head string, opts . opt = opts[0] } - cmd := NewCommand(ctx, "diff"). - AddOptions(opt.CommandOptions). - AddArgs("--name-only") - cmd.AddArgs("--end-of-options") + args := []string{"diff", "--name-only", "--end-of-options"} if opt.NeedsMergeBase { - cmd.AddArgs(base + "..." + head) + args = append(args, base+"..."+head) } else { - cmd.AddArgs(base, head) + args = append(args, base, head) } - cmd.AddArgs("--") + args = append(args, "--") if opt.Path != "" { - cmd.AddArgs(escapePath(opt.Path)) + args = append(args, escapePath(opt.Path)) } - stdout, err := cmd.RunInDir(r.path) + stdout, err := exec(ctx, r.path, args, opt.Envs) if err != nil { return nil, err } @@ -409,19 +400,14 @@ func (r *Repository) RevListCount(ctx context.Context, refspecs []string, opts . return 0, errors.New("must have at least one refspec") } - cmd := NewCommand(ctx, "rev-list"). - AddOptions(opt.CommandOptions). - AddArgs( - "--count", - "--end-of-options", - ) - cmd.AddArgs(refspecs...) - cmd.AddArgs("--") + args := []string{"rev-list", "--count", "--end-of-options"} + args = append(args, refspecs...) + args = append(args, "--") if opt.Path != "" { - cmd.AddArgs(escapePath(opt.Path)) + args = append(args, escapePath(opt.Path)) } - stdout, err := cmd.RunInDir(r.path) + stdout, err := exec(ctx, r.path, args, opt.Envs) if err != nil { return 0, err } @@ -451,15 +437,14 @@ func (r *Repository) RevList(ctx context.Context, refspecs []string, opts ...Rev return nil, errors.New("must have at least one refspec") } - cmd := NewCommand(ctx, "rev-list").AddOptions(opt.CommandOptions) - cmd.AddArgs("--end-of-options") - cmd.AddArgs(refspecs...) - cmd.AddArgs("--") + args := []string{"rev-list", "--end-of-options"} + args = append(args, refspecs...) + args = append(args, "--") if opt.Path != "" { - cmd.AddArgs(escapePath(opt.Path)) + args = append(args, escapePath(opt.Path)) } - stdout, err := cmd.RunInDir(r.path) + stdout, err := exec(ctx, r.path, args, opt.Envs) if err != nil { return nil, err } @@ -482,18 +467,12 @@ func (r *Repository) LatestCommitTime(ctx context.Context, opts ...LatestCommitT opt = opts[0] } - cmd := NewCommand(ctx, "for-each-ref"). - AddOptions(opt.CommandOptions). - AddArgs( - "--count=1", - "--sort=-committerdate", - "--format=%(committerdate:iso8601)", - ) + args := []string{"for-each-ref", "--count=1", "--sort=-committerdate", "--format=%(committerdate:iso8601)", "--end-of-options"} if opt.Branch != "" { - cmd.AddArgs(RefsHeads + opt.Branch) + args = append(args, RefsHeads+opt.Branch) } - stdout, err := cmd.RunInDir(r.path) + stdout, err := exec(ctx, r.path, args, opt.Envs) if err != nil { return time.Time{}, err } diff --git a/repo_diff.go b/repo_diff.go index 0ea4fdf5..388391aa 100644 --- a/repo_diff.go +++ b/repo_diff.go @@ -5,7 +5,6 @@ package git import ( - "bytes" "context" "fmt" "io" @@ -18,7 +17,7 @@ type DiffOptions struct { // The commit ID to used for computing diff between a range of commits (base, // revision]. When not set, only computes diff for a single commit at revision. Base string - // The additional options to be passed to the underlying git. + // The additional options to be passed to the underlying Git. CommandOptions } @@ -34,37 +33,30 @@ func (r *Repository) Diff(ctx context.Context, rev string, maxFiles, maxFileLine return nil, err } - cmd := NewCommand(ctx) + var args []string if opt.Base == "" { // First commit of repository if commit.ParentsCount() == 0 { - cmd = cmd.AddArgs("show"). - AddOptions(opt.CommandOptions). - AddArgs("--full-index", "--end-of-options", rev) + args = []string{"show", "--full-index", "--end-of-options", rev} } else { c, err := commit.Parent(ctx, 0) if err != nil { return nil, err } - cmd = cmd.AddArgs("diff"). - AddOptions(opt.CommandOptions). - AddArgs("--full-index", "-M", c.ID.String(), "--end-of-options", rev) + args = []string{"diff", "--full-index", "-M", c.ID.String(), "--end-of-options", rev} } } else { - cmd = cmd.AddArgs("diff"). - AddOptions(opt.CommandOptions). - AddArgs("--full-index", "-M", opt.Base, "--end-of-options", rev) + args = []string{"diff", "--full-index", "-M", opt.Base, "--end-of-options", rev} } stdout, w := io.Pipe() done := make(chan SteamParseDiffResult) go StreamParseDiff(stdout, done, maxFiles, maxFileLines, maxLineChars) - stderr := new(bytes.Buffer) - err = cmd.RunInDirPipeline(w, stderr, r.path) + err = pipe(ctx, r.path, args, opt.Envs, w) _ = w.Close() // Close writer to exit parsing goroutine if err != nil { - return nil, concatenateError(err, stderr.String()) + return nil, err } result := <-done @@ -83,7 +75,7 @@ const ( // // Docs: https://git-scm.com/docs/git-format-patch type RawDiffOptions struct { - // The additional options to be passed to the underlying git. + // The additional options to be passed to the underlying Git. CommandOptions } @@ -100,50 +92,41 @@ func (r *Repository) RawDiff(ctx context.Context, rev string, diffType RawDiffFo return err } - cmd := NewCommand(ctx) + var args []string switch diffType { case RawDiffNormal: if commit.ParentsCount() == 0 { - cmd = cmd.AddArgs("show"). - AddOptions(opt.CommandOptions). - AddArgs("--full-index", "--end-of-options", rev) + args = []string{"show", "--full-index", "--end-of-options", rev} } else { c, err := commit.Parent(ctx, 0) if err != nil { return err } - cmd = cmd.AddArgs("diff"). - AddOptions(opt.CommandOptions). - AddArgs("--full-index", "-M", c.ID.String(), "--end-of-options", rev) + args = []string{"diff", "--full-index", "-M", c.ID.String(), "--end-of-options", rev} } case RawDiffPatch: if commit.ParentsCount() == 0 { - cmd = cmd.AddArgs("format-patch"). - AddOptions(opt.CommandOptions). - AddArgs("--full-index", "--no-signoff", "--no-signature", "--stdout", "--root", "--end-of-options", rev) + args = []string{"format-patch", "--full-index", "--no-signoff", "--no-signature", "--stdout", "--root", "--end-of-options", rev} } else { c, err := commit.Parent(ctx, 0) if err != nil { return err } - cmd = cmd.AddArgs("format-patch"). - AddOptions(opt.CommandOptions). - AddArgs("--full-index", "--no-signoff", "--no-signature", "--stdout", "--end-of-options", rev+"..."+c.ID.String()) + args = []string{"format-patch", "--full-index", "--no-signoff", "--no-signature", "--stdout", "--end-of-options", rev + "..." + c.ID.String()} } default: return fmt.Errorf("invalid diffType: %s", diffType) } - stderr := new(bytes.Buffer) - if err = cmd.RunInDirPipeline(w, stderr, r.path); err != nil { - return concatenateError(err, stderr.String()) + if err = pipe(ctx, r.path, args, opt.Envs, w); err != nil { + return err } return nil } // DiffBinaryOptions contains optional arguments for producing binary patch. type DiffBinaryOptions struct { - // The additional options to be passed to the underlying git. + // The additional options to be passed to the underlying Git. CommandOptions } @@ -155,8 +138,6 @@ func (r *Repository) DiffBinary(ctx context.Context, base, head string, opts ... opt = opts[0] } - return NewCommand(ctx, "diff"). - AddOptions(opt.CommandOptions). - AddArgs("--full-index", "--binary", base, head). - RunInDir(r.path) + args := []string{"diff", "--full-index", "--binary", "--end-of-options", base, head} + return exec(ctx, r.path, args, opt.Envs) } diff --git a/repo_grep.go b/repo_grep.go index dd05a64f..3da197c5 100644 --- a/repo_grep.go +++ b/repo_grep.go @@ -25,7 +25,7 @@ type GrepOptions struct { WordRegexp bool // Whether use extended regular expressions. ExtendedRegexp bool - // The additional options to be passed to the underlying git. + // The additional options to be passed to the underlying Git. CommandOptions } @@ -78,29 +78,23 @@ func (r *Repository) Grep(ctx context.Context, pattern string, opts ...GrepOptio opt.Tree = "HEAD" } - cmd := NewCommand(ctx, "grep"). - AddOptions(opt.CommandOptions). - // Display full-name, line number and column number - AddArgs("--full-name", "--line-number", "--column") + args := []string{"grep"} + args = append(args, "--full-name", "--line-number", "--column") if opt.IgnoreCase { - cmd.AddArgs("--ignore-case") + args = append(args, "--ignore-case") } if opt.WordRegexp { - cmd.AddArgs("--word-regexp") + args = append(args, "--word-regexp") } if opt.ExtendedRegexp { - cmd.AddArgs("--extended-regexp") + args = append(args, "--extended-regexp") } - cmd.AddArgs( - "--end-of-options", - pattern, - opt.Tree, - ) + args = append(args, "--end-of-options", pattern, opt.Tree) if opt.Pathspec != "" { - cmd.AddArgs("--", opt.Pathspec) + args = append(args, "--", opt.Pathspec) } - stdout, err := cmd.RunInDir(r.path) + stdout, err := exec(ctx, r.path, args, opt.Envs) if err != nil { return nil } diff --git a/repo_pull.go b/repo_pull.go index 0c9e7775..b83d7083 100644 --- a/repo_pull.go +++ b/repo_pull.go @@ -25,15 +25,10 @@ func (r *Repository) MergeBase(ctx context.Context, base, head string, opts ...M opt = opts[0] } - stdout, err := NewCommand(ctx, "merge-base"). - AddOptions(opt.CommandOptions). - AddArgs( - "--end-of-options", - base, - head, - ).RunInDir(r.path) + args := []string{"merge-base", "--end-of-options", base, head} + stdout, err := exec(ctx, r.path, args, opt.Envs) if err != nil { - if strings.Contains(err.Error(), "exit status 1") { + if isExitStatus(err, 1) { return "", ErrNoMergeBase } return "", err diff --git a/repo_pull_test.go b/repo_pull_test.go index ff231863..737c6c8e 100644 --- a/repo_pull_test.go +++ b/repo_pull_test.go @@ -9,42 +9,42 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestRepository_MergeBase(t *testing.T) { ctx := context.Background() - t.Run("no merge base", func(t *testing.T) { + t.Run("bad revision", func(t *testing.T) { + // "bad_revision" doesn't exist, so git fails with exit status 128 (fatal), + // not exit status 1 (no merge base). mb, err := testrepo.MergeBase(ctx, "0eedd79eba4394bbef888c804e899731644367fe", "bad_revision") - assert.Equal(t, ErrNoMergeBase, err) + assert.Error(t, err) assert.Empty(t, mb) }) tests := []struct { - base string - head string - opt MergeBaseOptions - expMergeBase string + base string + head string + opt MergeBaseOptions + wantMergeBase string }{ { - base: "4e59b72440188e7c2578299fc28ea425fbe9aece", - head: "0eedd79eba4394bbef888c804e899731644367fe", - expMergeBase: "4e59b72440188e7c2578299fc28ea425fbe9aece", + base: "4e59b72440188e7c2578299fc28ea425fbe9aece", + head: "0eedd79eba4394bbef888c804e899731644367fe", + wantMergeBase: "4e59b72440188e7c2578299fc28ea425fbe9aece", }, { - base: "master", - head: "release-1.0", - expMergeBase: "0eedd79eba4394bbef888c804e899731644367fe", + base: "master", + head: "release-1.0", + wantMergeBase: "0eedd79eba4394bbef888c804e899731644367fe", }, } for _, test := range tests { t.Run("", func(t *testing.T) { mb, err := testrepo.MergeBase(ctx, test.base, test.head, test.opt) - if err != nil { - t.Fatal(err) - } - - assert.Equal(t, test.expMergeBase, mb) + require.NoError(t, err) + assert.Equal(t, test.wantMergeBase, mb) }) } } diff --git a/repo_reference.go b/repo_reference.go index eec27159..a3ad2777 100644 --- a/repo_reference.go +++ b/repo_reference.go @@ -37,7 +37,7 @@ type Reference struct { // // Docs: https://git-scm.com/docs/git-show-ref#Documentation/git-show-ref.txt---verify type ShowRefVerifyOptions struct { - // The additional options to be passed to the underlying git. + // The additional options to be passed to the underlying Git. CommandOptions } @@ -51,8 +51,8 @@ func (r *Repository) ShowRefVerify(ctx context.Context, ref string, opts ...Show opt = opts[0] } - cmd := NewCommand(ctx, "show-ref", "--verify", "--end-of-options", ref).AddOptions(opt.CommandOptions) - stdout, err := cmd.RunInDir(r.path) + args := []string{"show-ref", "--verify", "--end-of-options", ref} + stdout, err := exec(ctx, r.path, args, opt.Envs) if err != nil { if strings.Contains(err.Error(), "not a valid ref") { return "", ErrReferenceNotExist @@ -100,7 +100,7 @@ type SymbolicRefOptions struct { // The name of the reference, e.g. "refs/heads/master". When set, it will be // used to update the symbolic ref. Ref string - // The additional options to be passed to the underlying git. + // The additional options to be passed to the underlying Git. CommandOptions } @@ -113,16 +113,16 @@ func (r *Repository) SymbolicRef(ctx context.Context, opts ...SymbolicRefOptions opt = opts[0] } - cmd := NewCommand(ctx, "symbolic-ref").AddOptions(opt.CommandOptions) + args := []string{"symbolic-ref"} if opt.Name == "" { opt.Name = "HEAD" } - cmd.AddArgs("--end-of-options", opt.Name) + args = append(args, "--end-of-options", opt.Name) if opt.Ref != "" { - cmd.AddArgs(opt.Ref) + args = append(args, opt.Ref) } - stdout, err := cmd.RunInDir(r.path) + stdout, err := exec(ctx, r.path, args, opt.Envs) if err != nil { return "", err } @@ -139,7 +139,7 @@ type ShowRefOptions struct { Tags bool // The list of patterns to filter results. Patterns []string - // The additional options to be passed to the underlying git. + // The additional options to be passed to the underlying Git. CommandOptions } @@ -150,19 +150,19 @@ func (r *Repository) ShowRef(ctx context.Context, opts ...ShowRefOptions) ([]*Re opt = opts[0] } - cmd := NewCommand(ctx, "show-ref").AddOptions(opt.CommandOptions) + args := []string{"show-ref"} if opt.Heads { - cmd.AddArgs("--heads") + args = append(args, "--heads") } if opt.Tags { - cmd.AddArgs("--tags") + args = append(args, "--tags") } - cmd.AddArgs("--") + args = append(args, "--") if len(opt.Patterns) > 0 { - cmd.AddArgs(opt.Patterns...) + args = append(args, opt.Patterns...) } - stdout, err := cmd.RunInDir(r.path) + stdout, err := exec(ctx, r.path, args, opt.Envs) if err != nil { return nil, err } @@ -202,7 +202,7 @@ func (r *Repository) Branches(ctx context.Context) ([]string, error) { type DeleteBranchOptions struct { // Indicates whether to force delete the branch. Force bool - // The additional options to be passed to the underlying git. + // The additional options to be passed to the underlying Git. CommandOptions } @@ -213,12 +213,13 @@ func (r *Repository) DeleteBranch(ctx context.Context, name string, opts ...Dele opt = opts[0] } - cmd := NewCommand(ctx, "branch").AddOptions(opt.CommandOptions) + args := []string{"branch"} if opt.Force { - cmd.AddArgs("-D") + args = append(args, "-D") } else { - cmd.AddArgs("-d") + args = append(args, "-d") } - _, err := cmd.AddArgs("--end-of-options", name).RunInDir(r.path) + args = append(args, "--end-of-options", name) + _, err := exec(ctx, r.path, args, opt.Envs) return err } diff --git a/repo_remote.go b/repo_remote.go index 3f13755d..e5438d09 100644 --- a/repo_remote.go +++ b/repo_remote.go @@ -23,33 +23,33 @@ type LsRemoteOptions struct { Refs bool // The list of patterns to filter results. Patterns []string - // The additional options to be passed to the underlying git. + // The additional options to be passed to the underlying Git. CommandOptions } -// LsRemote returns a list references in the remote repository. +// LsRemote returns a list of references in the remote repository. func LsRemote(ctx context.Context, url string, opts ...LsRemoteOptions) ([]*Reference, error) { var opt LsRemoteOptions if len(opts) > 0 { opt = opts[0] } - cmd := NewCommand(ctx, "ls-remote", "--quiet").AddOptions(opt.CommandOptions) + args := []string{"ls-remote", "--quiet"} if opt.Heads { - cmd.AddArgs("--heads") + args = append(args, "--heads") } if opt.Tags { - cmd.AddArgs("--tags") + args = append(args, "--tags") } if opt.Refs { - cmd.AddArgs("--refs") + args = append(args, "--refs") } - cmd.AddArgs("--end-of-options", url) + args = append(args, "--end-of-options", url) if len(opt.Patterns) > 0 { - cmd.AddArgs(opt.Patterns...) + args = append(args, opt.Patterns...) } - stdout, err := cmd.Run() + stdout, err := exec(ctx, "", args, opt.Envs) if err != nil { return nil, err } @@ -88,7 +88,7 @@ type RemoteAddOptions struct { Fetch bool // Indicates whether to add remote as mirror with --mirror=fetch. MirrorFetch bool - // The additional options to be passed to the underlying git. + // The additional options to be passed to the underlying Git. CommandOptions } @@ -99,15 +99,16 @@ func (r *Repository) RemoteAdd(ctx context.Context, name, url string, opts ...Re opt = opts[0] } - cmd := NewCommand(ctx, "remote", "add").AddOptions(opt.CommandOptions) + args := []string{"remote", "add"} if opt.Fetch { - cmd.AddArgs("-f") + args = append(args, "-f") } if opt.MirrorFetch { - cmd.AddArgs("--mirror=fetch") + args = append(args, "--mirror=fetch") } + args = append(args, "--end-of-options", name, url) - _, err := cmd.AddArgs("--end-of-options", name, url).RunInDir(r.path) + _, err := exec(ctx, r.path, args, opt.Envs) return err } @@ -116,7 +117,7 @@ func (r *Repository) RemoteAdd(ctx context.Context, name, url string, opts ...Re // // Docs: https://git-scm.com/docs/git-remote#Documentation/git-remote.txt-emremoveem type RemoteRemoveOptions struct { - // The additional options to be passed to the underlying git. + // The additional options to be passed to the underlying Git. CommandOptions } @@ -127,12 +128,9 @@ func (r *Repository) RemoteRemove(ctx context.Context, name string, opts ...Remo opt = opts[0] } - _, err := NewCommand(ctx, "remote", "remove"). - AddOptions(opt.CommandOptions). - AddArgs("--end-of-options", name). - RunInDir(r.path) + args := []string{"remote", "remove", "--end-of-options", name} + _, err := exec(ctx, r.path, args, opt.Envs) if err != nil { - // the error status may differ from git clients if strings.Contains(err.Error(), "error: No such remote") || strings.Contains(err.Error(), "fatal: No such remote") { return ErrRemoteNotExist @@ -146,7 +144,7 @@ func (r *Repository) RemoteRemove(ctx context.Context, name string, opts ...Remo // / // Docs: https://git-scm.com/docs/git-remote#_commands type RemotesOptions struct { - // The additional options to be passed to the underlying git. + // The additional options to be passed to the underlying Git. CommandOptions } @@ -157,9 +155,8 @@ func (r *Repository) Remotes(ctx context.Context, opts ...RemotesOptions) ([]str opt = opts[0] } - stdout, err := NewCommand(ctx, "remote"). - AddOptions(opt.CommandOptions). - RunInDir(r.path) + args := []string{"remote", "--end-of-options"} + stdout, err := exec(ctx, r.path, args, opt.Envs) if err != nil { return nil, err } @@ -177,7 +174,7 @@ type RemoteGetURLOptions struct { // Indicates whether to get all URLs, including lists that are not part of main // URLs. This option is independent of the Push option. All bool - // The additional options to be passed to the underlying git. + // The additional options to be passed to the underlying Git. CommandOptions } @@ -188,15 +185,16 @@ func (r *Repository) RemoteGetURL(ctx context.Context, name string, opts ...Remo opt = opts[0] } - cmd := NewCommand(ctx, "remote", "get-url").AddOptions(opt.CommandOptions) + args := []string{"remote", "get-url"} if opt.Push { - cmd.AddArgs("--push") + args = append(args, "--push") } if opt.All { - cmd.AddArgs("--all") + args = append(args, "--all") } + args = append(args, "--end-of-options", name) - stdout, err := cmd.AddArgs("--end-of-options", name).RunInDir(r.path) + stdout, err := exec(ctx, r.path, args, opt.Envs) if err != nil { return nil, err } @@ -212,7 +210,7 @@ type RemoteSetURLOptions struct { Push bool // The regex to match existing URLs to replace (instead of first). Regex string - // The additional options to be passed to the underlying git. + // The additional options to be passed to the underlying Git. CommandOptions } @@ -224,18 +222,16 @@ func (r *Repository) RemoteSetURL(ctx context.Context, name, newurl string, opts opt = opts[0] } - cmd := NewCommand(ctx, "remote", "set-url").AddOptions(opt.CommandOptions) + args := []string{"remote", "set-url"} if opt.Push { - cmd.AddArgs("--push") + args = append(args, "--push") } - - cmd.AddArgs("--end-of-options", name, newurl) - + args = append(args, "--end-of-options", name, newurl) if opt.Regex != "" { - cmd.AddArgs(opt.Regex) + args = append(args, opt.Regex) } - _, err := cmd.RunInDir(r.path) + _, err := exec(ctx, r.path, args, opt.Envs) if err != nil { if strings.Contains(err.Error(), "No such URL found") { return ErrURLNotExist @@ -254,7 +250,7 @@ func (r *Repository) RemoteSetURL(ctx context.Context, name, newurl string, opts type RemoteSetURLAddOptions struct { // Indicates whether to get push URLs instead of fetch URLs. Push bool - // The additional options to be passed to the underlying git. + // The additional options to be passed to the underlying Git. CommandOptions } @@ -266,16 +262,13 @@ func (r *Repository) RemoteSetURLAdd(ctx context.Context, name, newurl string, o opt = opts[0] } - cmd := NewCommand(ctx, "remote", "set-url"). - AddOptions(opt.CommandOptions). - AddArgs("--add") + args := []string{"remote", "set-url", "--add"} if opt.Push { - cmd.AddArgs("--push") + args = append(args, "--push") } + args = append(args, "--end-of-options", name, newurl) - cmd.AddArgs("--end-of-options", name, newurl) - - _, err := cmd.RunInDir(r.path) + _, err := exec(ctx, r.path, args, opt.Envs) if err != nil && strings.Contains(err.Error(), "Will not delete all non-push URLs") { return ErrNotDeleteNonPushURLs } @@ -289,7 +282,7 @@ func (r *Repository) RemoteSetURLAdd(ctx context.Context, name, newurl string, o type RemoteSetURLDeleteOptions struct { // Indicates whether to get push URLs instead of fetch URLs. Push bool - // The additional options to be passed to the underlying git. + // The additional options to be passed to the underlying Git. CommandOptions } @@ -301,16 +294,13 @@ func (r *Repository) RemoteSetURLDelete(ctx context.Context, name, regex string, opt = opts[0] } - cmd := NewCommand(ctx, "remote", "set-url"). - AddOptions(opt.CommandOptions). - AddArgs("--delete") + args := []string{"remote", "set-url", "--delete"} if opt.Push { - cmd.AddArgs("--push") + args = append(args, "--push") } + args = append(args, "--end-of-options", name, regex) - cmd.AddArgs("--end-of-options", name, regex) - - _, err := cmd.RunInDir(r.path) + _, err := exec(ctx, r.path, args, opt.Envs) if err != nil && strings.Contains(err.Error(), "Will not delete all non-push URLs") { return ErrNotDeleteNonPushURLs } diff --git a/repo_tag.go b/repo_tag.go index df3b883a..9a57fb3e 100644 --- a/repo_tag.go +++ b/repo_tag.go @@ -55,7 +55,7 @@ l: func (r *Repository) getTag(ctx context.Context, id *SHA1) (*Tag, error) { t, ok := r.cachedTags.Get(id.String()) if ok { - log("Cached tag hit: %s", id) + logf("Cached tag hit: %s", id) return t.(*Tag), nil } @@ -76,7 +76,7 @@ func (r *Repository) getTag(ctx context.Context, id *SHA1) (*Tag, error) { } case ObjectTag: // Tag is an annotation - data, err := NewCommand(ctx, "cat-file", "-p", id.String()).RunInDir(r.path) + data, err := exec(ctx, r.path, []string{"cat-file", "-p", id.String()}, nil) if err != nil { return nil, err } @@ -155,19 +155,18 @@ func (r *Repository) Tags(ctx context.Context, opts ...TagsOptions) ([]string, e opt = opts[0] } - cmd := NewCommand(ctx, "tag", "--list").AddOptions(opt.CommandOptions) - + args := []string{"tag", "--list"} if opt.SortKey != "" { - cmd.AddArgs("--sort=" + opt.SortKey) + args = append(args, "--sort="+opt.SortKey) } else { - cmd.AddArgs("--sort=-creatordate") + args = append(args, "--sort=-creatordate") } - + args = append(args, "--end-of-options") if opt.Pattern != "" { - cmd.AddArgs(opt.Pattern) + args = append(args, opt.Pattern) } - stdout, err := cmd.RunInDir(r.path) + stdout, err := exec(ctx, r.path, args, opt.Envs) if err != nil { return nil, err } @@ -199,21 +198,21 @@ func (r *Repository) CreateTag(ctx context.Context, name, rev string, opts ...Cr opt = opts[0] } - cmd := NewCommand(ctx, "tag").AddOptions(opt.CommandOptions) + args := []string{"tag"} + var envs []string if opt.Annotated { - cmd.AddArgs("-a", name) - cmd.AddArgs("--message", opt.Message) + args = append(args, "-a", name) + args = append(args, "--message", opt.Message) if opt.Author != nil { - cmd.AddCommitter(opt.Author) + envs = committerEnvs(opt.Author) } - cmd.AddArgs("--end-of-options") + args = append(args, "--end-of-options") } else { - cmd.AddArgs("--end-of-options", name) + args = append(args, "--end-of-options", name) } - - cmd.AddArgs(rev) - - _, err := cmd.RunInDir(r.path) + args = append(args, rev) + envs = append(envs, opt.Envs...) + _, err := exec(ctx, r.path, args, envs) return err } @@ -232,8 +231,7 @@ func (r *Repository) DeleteTag(ctx context.Context, name string, opts ...DeleteT opt = opts[0] } - _, err := NewCommand(ctx, "tag", "--delete", "--end-of-options", name). - AddOptions(opt.CommandOptions). - RunInDir(r.path) + args := []string{"tag", "--delete", "--end-of-options", name} + _, err := exec(ctx, r.path, args, opt.Envs) return err } diff --git a/repo_tree.go b/repo_tree.go index 8fb2e663..2c77a6e8 100644 --- a/repo_tree.go +++ b/repo_tree.go @@ -117,7 +117,7 @@ func (r *Repository) LsTree(ctx context.Context, treeID string, opts ...LsTreeOp cache, ok := r.cachedTrees.Get(treeID) if ok { - log("Cached tree hit: %s", treeID) + logf("Cached tree hit: %s", treeID) return cache.(*Tree), nil } @@ -131,14 +131,13 @@ func (r *Repository) LsTree(ctx context.Context, treeID string, opts ...LsTreeOp repo: r, } - cmd := NewCommand(ctx, "ls-tree") + args := []string{"ls-tree"} if opt.Verbatim { - cmd.AddArgs("-z") + args = append(args, "-z") } - stdout, err := cmd. - AddOptions(opt.CommandOptions). - AddArgs(treeID). - RunInDir(r.path) + args = append(args, "--end-of-options", treeID) + + stdout, err := exec(ctx, r.path, args, opt.Envs) if err != nil { return nil, err } diff --git a/server.go b/server.go index 1b4d2356..901f966d 100755 --- a/server.go +++ b/server.go @@ -27,11 +27,13 @@ func UpdateServerInfo(ctx context.Context, path string, opts ...UpdateServerInfo if len(opts) > 0 { opt = opts[0] } - cmd := NewCommand(ctx, "update-server-info").AddOptions(opt.CommandOptions) + + args := []string{"update-server-info"} if opt.Force { - cmd.AddArgs("--force") + args = append(args, "--force") } - _, err := cmd.RunInDir(path) + args = append(args, "--end-of-options") + _, err := exec(ctx, path, args, opt.Envs) return err } @@ -54,15 +56,16 @@ func ReceivePack(ctx context.Context, path string, opts ...ReceivePackOptions) ( if len(opts) > 0 { opt = opts[0] } - cmd := NewCommand(ctx, "receive-pack").AddOptions(opt.CommandOptions) + + args := []string{"receive-pack"} if opt.Quiet { - cmd.AddArgs("--quiet") + args = append(args, "--quiet") } if opt.HTTPBackendInfoRefs { - cmd.AddArgs("--http-backend-info-refs") + args = append(args, "--http-backend-info-refs") } - cmd.AddArgs(".") - return cmd.RunInDir(path) + args = append(args, "--end-of-options", ".") + return exec(ctx, path, args, opt.Envs) } // UploadPackOptions contains optional arguments for sending the packfile to the @@ -91,19 +94,20 @@ func UploadPack(ctx context.Context, path string, opts ...UploadPackOptions) ([] if len(opts) > 0 { opt = opts[0] } - cmd := NewCommand(ctx, "upload-pack").AddOptions(opt.CommandOptions) + + args := []string{"upload-pack"} if opt.StatelessRPC { - cmd.AddArgs("--stateless-rpc") + args = append(args, "--stateless-rpc") } if opt.Strict { - cmd.AddArgs("--strict") + args = append(args, "--strict") } if opt.InactivityTimeout > 0 { - cmd.AddArgs("--timeout", opt.InactivityTimeout.String()) + args = append(args, "--timeout", opt.InactivityTimeout.String()) } if opt.HTTPBackendInfoRefs { - cmd.AddArgs("--http-backend-info-refs") + args = append(args, "--http-backend-info-refs") } - cmd.AddArgs(".") - return cmd.RunInDir(path) + args = append(args, "--end-of-options", ".") + return exec(ctx, path, args, opt.Envs) } diff --git a/tree_entry.go b/tree_entry.go index 30f99641..c854cfc7 100644 --- a/tree_entry.go +++ b/tree_entry.go @@ -98,7 +98,7 @@ func (e *TreeEntry) Size(ctx context.Context) int64 { return e.size } - stdout, err := NewCommand(ctx, "cat-file", "-s", e.id.String()).RunInDir(e.parent.repo.path) + stdout, err := exec(ctx, e.parent.repo.path, []string{"cat-file", "-s", e.id.String()}, nil) if err != nil { return 0 } diff --git a/tree_entry_test.go b/tree_entry_test.go index 0d305b21..3f1ff9fd 100644 --- a/tree_entry_test.go +++ b/tree_entry_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestTreeEntry(t *testing.T) { @@ -31,6 +32,39 @@ func TestTreeEntry(t *testing.T) { assert.Equal(t, "go.mod", e.Name()) } +func TestTreeEntry_Size(t *testing.T) { + ctx := context.Background() + tree, err := testrepo.LsTree(ctx, "0eedd79eba4394bbef888c804e899731644367fe") + require.NoError(t, err) + + es, err := tree.Entries(ctx) + require.NoError(t, err) + + t.Run("blob", func(t *testing.T) { + var entry *TreeEntry + for _, e := range es { + if e.Name() == "README.txt" { + entry = e + break + } + } + require.NotNil(t, entry, "entry README.txt not found") + assert.Equal(t, int64(795), entry.Size(ctx)) + }) + + t.Run("tree returns zero", func(t *testing.T) { + var entry *TreeEntry + for _, e := range es { + if e.IsTree() { + entry = e + break + } + } + require.NotNil(t, entry, "tree entry not found") + assert.Equal(t, int64(0), entry.Size(ctx)) + }) +} + func TestEntries_Sort(t *testing.T) { ctx := context.Background() tree, err := testrepo.LsTree(ctx, "0eedd79eba4394bbef888c804e899731644367fe") diff --git a/utils.go b/utils.go index 79c0e278..305dfc10 100644 --- a/utils.go +++ b/utils.go @@ -5,7 +5,6 @@ package git import ( - "fmt" "os" "strings" "sync" @@ -66,13 +65,6 @@ func isExist(path string) bool { return err == nil || os.IsExist(err) } -func concatenateError(err error, stderr string) error { - if len(stderr) == 0 { - return err - } - return fmt.Errorf("%v - %s", err, stderr) -} - // bytesToStrings splits given bytes into strings by line separator ("\n"). It // returns empty slice if the given bytes only contains line separators. func bytesToStrings(in []byte) []string { From 9f4d314fc3ac0b693dc2d386ed8ec21ce02b6d8f Mon Sep 17 00:00:00 2001 From: Joe Chen Date: Sun, 15 Feb 2026 14:23:47 -0500 Subject: [PATCH 5/7] chore: remove copyright headers --- blame.go | 4 ---- blob.go | 4 ---- blob_test.go | 4 ---- command.go | 4 ---- command_test.go | 4 ---- commit.go | 4 ---- commit_archive.go | 4 ---- commit_archive_test.go | 4 ---- commit_submodule.go | 4 ---- commit_submodule_test.go | 4 ---- commit_test.go | 4 ---- diff.go | 4 ---- diff_test.go | 4 ---- error.go | 4 ---- git.go | 4 ---- git_test.go | 4 ---- hook.go | 4 ---- hook_test.go | 4 ---- object.go | 4 ---- repo.go | 4 ---- repo_blame.go | 4 ---- repo_blame_test.go | 4 ---- repo_blob.go | 4 ---- repo_blob_test.go | 4 ---- repo_commit.go | 4 ---- repo_commit_test.go | 4 ---- repo_diff.go | 4 ---- repo_diff_test.go | 4 ---- repo_grep.go | 4 ---- repo_grep_test.go | 4 ---- repo_hook.go | 4 ---- repo_hook_test.go | 4 ---- repo_pull.go | 4 ---- repo_pull_test.go | 4 ---- repo_reference.go | 4 ---- repo_reference_test.go | 4 ---- repo_remote.go | 4 ---- repo_remote_test.go | 4 ---- repo_tag.go | 4 ---- repo_tag_test.go | 4 ---- repo_test.go | 4 ---- repo_tree.go | 4 ---- repo_tree_test.go | 4 ---- server.go | 4 ---- server_test.go | 4 ---- sha1.go | 4 ---- sha1_test.go | 4 ---- signature.go | 4 ---- signature_test.go | 4 ---- tag.go | 4 ---- tag_test.go | 4 ---- tree.go | 4 ---- tree_blob.go | 4 ---- tree_blob_test.go | 4 ---- tree_entry.go | 4 ---- tree_entry_test.go | 4 ---- utils.go | 4 ---- 57 files changed, 228 deletions(-) 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 843e1dbd..8c8535f1 100644 --- a/blob.go +++ b/blob.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/blob_test.go b/blob_test.go index 743c3ed0..cc02c0ba 100644 --- a/blob_test.go +++ b/blob_test.go @@ -1,7 +1,3 @@ -// 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 ( diff --git a/command.go b/command.go index 88a5538b..5ff78fb2 100644 --- a/command.go +++ b/command.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/command_test.go b/command_test.go index 4c02835d..f38b0c22 100644 --- a/command_test.go +++ b/command_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/commit.go b/commit.go index 134f39fb..0edda64a 100644 --- a/commit.go +++ b/commit.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/commit_archive.go b/commit_archive.go index 5daba32b..9bb34bfe 100644 --- a/commit_archive.go +++ b/commit_archive.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/commit_archive_test.go b/commit_archive_test.go index 892859c8..8e29580f 100644 --- a/commit_archive_test.go +++ b/commit_archive_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/commit_submodule.go b/commit_submodule.go index 3c4da05c..59e85777 100644 --- a/commit_submodule.go +++ b/commit_submodule.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/commit_submodule_test.go b/commit_submodule_test.go index 2861fc0f..b9516796 100644 --- a/commit_submodule_test.go +++ b/commit_submodule_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/commit_test.go b/commit_test.go index ca256804..3a752451 100644 --- a/commit_test.go +++ b/commit_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/diff.go b/diff.go index e70932e6..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 ( 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 e96ab1e9..5de2d7a1 100644 --- a/git.go +++ b/git.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_test.go b/git_test.go index c4af3655..165f64e8 100644 --- a/git_test.go +++ b/git_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/hook.go b/hook.go index 7083c1ad..0c602f8b 100644 --- a/hook.go +++ b/hook.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/hook_test.go b/hook_test.go index 92cd4866..83596a3e 100644 --- a/hook_test.go +++ b/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/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 7acfeb5a..e206b62e 100644 --- a/repo.go +++ b/repo.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/repo_blame.go b/repo_blame.go index b10a0d44..401975ba 100644 --- a/repo_blame.go +++ b/repo_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 import ( diff --git a/repo_blame_test.go b/repo_blame_test.go index 85fe38b7..133ea4c5 100644 --- a/repo_blame_test.go +++ b/repo_blame_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_blob.go b/repo_blob.go index 906d18d5..6d3e1c03 100644 --- a/repo_blob.go +++ b/repo_blob.go @@ -1,7 +1,3 @@ -// 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" diff --git a/repo_blob_test.go b/repo_blob_test.go index 434a1969..4818145e 100644 --- a/repo_blob_test.go +++ b/repo_blob_test.go @@ -1,7 +1,3 @@ -// 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 ( diff --git a/repo_commit.go b/repo_commit.go index 4ffced0f..4ab634f0 100644 --- a/repo_commit.go +++ b/repo_commit.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/repo_commit_test.go b/repo_commit_test.go index c7108993..b4ccac77 100644 --- a/repo_commit_test.go +++ b/repo_commit_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_diff.go b/repo_diff.go index 388391aa..0c0398f8 100644 --- a/repo_diff.go +++ b/repo_diff.go @@ -1,7 +1,3 @@ -// 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 ( diff --git a/repo_diff_test.go b/repo_diff_test.go index 96a619e9..4fcbd57e 100644 --- a/repo_diff_test.go +++ b/repo_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/repo_grep.go b/repo_grep.go index 3da197c5..3a5f5ede 100644 --- a/repo_grep.go +++ b/repo_grep.go @@ -1,7 +1,3 @@ -// 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 ( diff --git a/repo_grep_test.go b/repo_grep_test.go index 1402c71d..c5e954cf 100644 --- a/repo_grep_test.go +++ b/repo_grep_test.go @@ -1,7 +1,3 @@ -// 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 ( diff --git a/repo_hook.go b/repo_hook.go index 32fffc85..8592d4c5 100644 --- a/repo_hook.go +++ b/repo_hook.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/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 b83d7083..df7697c8 100644 --- a/repo_pull.go +++ b/repo_pull.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/repo_pull_test.go b/repo_pull_test.go index 737c6c8e..082e987b 100644 --- a/repo_pull_test.go +++ b/repo_pull_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_reference.go b/repo_reference.go index a3ad2777..08ac9dd9 100644 --- a/repo_reference.go +++ b/repo_reference.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/repo_reference_test.go b/repo_reference_test.go index 3299ddf2..05a243c0 100644 --- a/repo_reference_test.go +++ b/repo_reference_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_remote.go b/repo_remote.go index e5438d09..2aeb4b7f 100644 --- a/repo_remote.go +++ b/repo_remote.go @@ -1,7 +1,3 @@ -// 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 ( diff --git a/repo_remote_test.go b/repo_remote_test.go index b75e9a58..d4e0962a 100644 --- a/repo_remote_test.go +++ b/repo_remote_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_tag.go b/repo_tag.go index 9a57fb3e..751fc904 100644 --- a/repo_tag.go +++ b/repo_tag.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/repo_tag_test.go b/repo_tag_test.go index 263f0be2..9905c927 100644 --- a/repo_tag_test.go +++ b/repo_tag_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_test.go b/repo_test.go index 1a3f17de..2553e0fe 100644 --- a/repo_test.go +++ b/repo_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_tree.go b/repo_tree.go index 2c77a6e8..02b10f7d 100644 --- a/repo_tree.go +++ b/repo_tree.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/repo_tree_test.go b/repo_tree_test.go index 1c361f2a..bcc708c3 100644 --- a/repo_tree_test.go +++ b/repo_tree_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/server.go b/server.go index 901f966d..08f0c52d 100755 --- a/server.go +++ b/server.go @@ -1,7 +1,3 @@ -// 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 ( diff --git a/server_test.go b/server_test.go index 2d343dd5..32baf1e3 100755 --- a/server_test.go +++ b/server_test.go @@ -1,7 +1,3 @@ -// 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 ( 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 137eb039..40efd5a0 100644 --- a/tag.go +++ b/tag.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 "context" diff --git a/tag_test.go b/tag_test.go index 83341e75..c43c1510 100644 --- a/tag_test.go +++ b/tag_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/tree.go b/tree.go index 998b65f3..539027b4 100644 --- a/tree.go +++ b/tree.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/tree_blob.go b/tree_blob.go index 020ee3c5..a50b08fa 100644 --- a/tree_blob.go +++ b/tree_blob.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/tree_blob_test.go b/tree_blob_test.go index 3af1fc59..ed9d509e 100644 --- a/tree_blob_test.go +++ b/tree_blob_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/tree_entry.go b/tree_entry.go index c854cfc7..071d9abf 100644 --- a/tree_entry.go +++ b/tree_entry.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/tree_entry_test.go b/tree_entry_test.go index 3f1ff9fd..e8c3707d 100644 --- a/tree_entry_test.go +++ b/tree_entry_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/utils.go b/utils.go index 305dfc10..29aade21 100644 --- a/utils.go +++ b/utils.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 ( From d5f694fafabb48a5c235dc5bcdd0b0548b7e42d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=B4=8A=E1=B4=8F=E1=B4=87=20=E1=B4=84=CA=9C=E1=B4=87?= =?UTF-8?q?=C9=B4?= Date: Sun, 15 Feb 2026 14:33:42 -0500 Subject: [PATCH 6/7] Add Push.SetUpstream option and worktree support (#132) Co-authored-by: Claude Opus 4.6 --- repo.go | 22 +++++-------- repo_blame.go | 1 - repo_blob.go | 1 - repo_commit.go | 11 ------- repo_diff.go | 3 -- repo_grep.go | 1 - repo_pull.go | 1 - repo_reference.go | 4 --- repo_remote.go | 8 ----- repo_tag.go | 4 --- repo_test.go | 5 +++ repo_tree.go | 1 - repo_worktree.go | 56 +++++++++++++++++++++++++++++++++ repo_worktree_test.go | 73 +++++++++++++++++++++++++++++++++++++++++++ server.go | 3 -- 15 files changed, 141 insertions(+), 53 deletions(-) create mode 100644 repo_worktree.go create mode 100644 repo_worktree_test.go diff --git a/repo.go b/repo.go index e206b62e..140ece35 100644 --- a/repo.go +++ b/repo.go @@ -54,7 +54,6 @@ func (r *Repository) parsePrettyFormatLogToList(ctx context.Context, logs []byte type InitOptions struct { // Indicates whether the repository should be initialized in bare format. Bare bool - // The additional options to be passed to the underlying git. CommandOptions } @@ -111,7 +110,6 @@ type CloneOptions struct { Branch string // The number of revisions to clone. Depth uint64 - // The additional options to be passed to the underlying git. CommandOptions } @@ -155,7 +153,6 @@ func Clone(ctx context.Context, url, dst string, opts ...CloneOptions) error { type FetchOptions struct { // Indicates whether to prune during fetching. Prune bool - // The additional options to be passed to the underlying git. CommandOptions } @@ -188,7 +185,6 @@ type PullOptions struct { Remote string // The branch to pull updates from when All=false and Remote is supplied. Branch string - // The additional options to be passed to the underlying git. CommandOptions } @@ -222,7 +218,8 @@ func (r *Repository) Pull(ctx context.Context, opts ...PullOptions) error { // // Docs: https://git-scm.com/docs/git-push type PushOptions struct { - // The additional options to be passed to the underlying git. + // Indicates whether to set upstream tracking for the branch. + SetUpstream bool CommandOptions } @@ -233,7 +230,11 @@ func (r *Repository) Push(ctx context.Context, remote, branch string, opts ...Pu opt = opts[0] } - args := []string{"push", "--end-of-options", remote, branch} + 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 } @@ -244,7 +245,6 @@ func (r *Repository) Push(ctx context.Context, remote, branch string, opts ...Pu type CheckoutOptions struct { // The base branch if checks out to a new branch. BaseBranch string - // The additional options to be passed to the underlying git. CommandOptions } @@ -272,7 +272,6 @@ func (r *Repository) Checkout(ctx context.Context, branch string, opts ...Checko type ResetOptions struct { // Indicates whether to perform a hard reset. Hard bool - // The additional options to be passed to the underlying git. CommandOptions } @@ -298,7 +297,6 @@ func (r *Repository) Reset(ctx context.Context, rev string, opts ...ResetOptions // // Docs: https://git-scm.com/docs/git-mv type MoveOptions struct { - // The additional options to be passed to the underlying git. CommandOptions } @@ -323,7 +321,6 @@ type AddOptions struct { All bool // The specific pathspecs to be added to index. Pathspecs []string - // The additional options to be passed to the underlying git. CommandOptions } @@ -352,7 +349,6 @@ func (r *Repository) Add(ctx context.Context, opts ...AddOptions) error { type CommitOptions struct { // Author is the author of the changes if that's not the same as committer. Author *Signature - // The additional options to be passed to the underlying git. CommandOptions } @@ -395,7 +391,6 @@ type NameStatus struct { // // Docs: https://git-scm.com/docs/git-show#Documentation/git-show.txt---name-status type ShowNameStatusOptions struct { - // The additional options to be passed to the underlying git. CommandOptions } @@ -445,7 +440,6 @@ func (r *Repository) ShowNameStatus(ctx context.Context, rev string, opts ...Sho // // Docs: https://git-scm.com/docs/git-rev-parse type RevParseOptions struct { - // The additional options to be passed to the underlying git. CommandOptions } @@ -485,7 +479,6 @@ type CountObject struct { // // Docs: https://git-scm.com/docs/git-count-objects type CountObjectsOptions struct { - // The additional options to be passed to the underlying git. CommandOptions } @@ -537,7 +530,6 @@ func (r *Repository) CountObjects(ctx context.Context, opts ...CountObjectsOptio // // Docs: https://git-scm.com/docs/git-fsck type FsckOptions struct { - // The additional options to be passed to the underlying git. CommandOptions } diff --git a/repo_blame.go b/repo_blame.go index 401975ba..efdd368c 100644 --- a/repo_blame.go +++ b/repo_blame.go @@ -8,7 +8,6 @@ import ( // BlameOptions contains optional arguments for blaming a file. // Docs: https://git-scm.com/docs/git-blame type BlameOptions struct { - // The additional options to be passed to the underlying git. CommandOptions } diff --git a/repo_blob.go b/repo_blob.go index 6d3e1c03..ce521e5c 100644 --- a/repo_blob.go +++ b/repo_blob.go @@ -6,7 +6,6 @@ import "context" // // Docs: https://git-scm.com/docs/git-cat-file#Documentation/git-cat-file.txt type CatFileBlobOptions struct { - // The additional options to be passed to the underlying git. CommandOptions } diff --git a/repo_commit.go b/repo_commit.go index 4ab634f0..04d1ce38 100644 --- a/repo_commit.go +++ b/repo_commit.go @@ -66,7 +66,6 @@ loop: // // Docs: https://git-scm.com/docs/git-cat-file#Documentation/git-cat-file.txt-lttypegt type CatFileCommitOptions struct { - // The additional options to be passed to the underlying git. CommandOptions } @@ -111,7 +110,6 @@ func (r *Repository) CatFileCommit(ctx context.Context, rev string, opts ...CatF // // Docs: https://git-scm.com/docs/git-cat-file#Documentation/git-cat-file.txt--t type CatFileTypeOptions struct { - // The additional options to be passed to the underlying git. CommandOptions } @@ -159,7 +157,6 @@ type LogOptions struct { RegexpIgnoreCase bool // The relative path of the repository. Path string - // The additional options to be passed to the underlying git. CommandOptions } @@ -217,7 +214,6 @@ func (r *Repository) Log(ctx context.Context, rev string, opts ...LogOptions) ([ type CommitByRevisionOptions struct { // The relative path of the repository. Path string - // The additional options to be passed to the underlying git. CommandOptions } @@ -251,7 +247,6 @@ func (r *Repository) CommitByRevision(ctx context.Context, rev string, opts ...C type CommitsByPageOptions struct { // The relative path of the repository. Path string - // The additional options to be passed to the underlying git. CommandOptions } @@ -279,7 +274,6 @@ type SearchCommitsOptions struct { MaxCount int // The relative path of the repository. Path string - // The additional options to be passed to the underlying git. CommandOptions } @@ -307,7 +301,6 @@ func (r *Repository) SearchCommits(ctx context.Context, rev, pattern string, opt type CommitsSinceOptions struct { // The relative path of the repository. Path string - // The additional options to be passed to the underlying git. CommandOptions } @@ -334,7 +327,6 @@ type DiffNameOnlyOptions struct { NeedsMergeBase bool // The relative path of the repository. Path string - // The additional options to be passed to the underlying git. CommandOptions } @@ -380,7 +372,6 @@ func (r *Repository) DiffNameOnly(ctx context.Context, base, head string, opts . type RevListCountOptions struct { // The relative path of the repository. Path string - // The additional options to be passed to the underlying git. CommandOptions } @@ -417,7 +408,6 @@ func (r *Repository) RevListCount(ctx context.Context, refspecs []string, opts . type RevListOptions struct { // The relative path of the repository. Path string - // The additional options to be passed to the underlying git. CommandOptions } @@ -452,7 +442,6 @@ func (r *Repository) RevList(ctx context.Context, refspecs []string, opts ...Rev type LatestCommitTimeOptions struct { // To get the latest commit time of the branch. When not set, it checks all branches. Branch string - // The additional options to be passed to the underlying git. CommandOptions } diff --git a/repo_diff.go b/repo_diff.go index 0c0398f8..d65bda22 100644 --- a/repo_diff.go +++ b/repo_diff.go @@ -13,7 +13,6 @@ type DiffOptions struct { // The commit ID to used for computing diff between a range of commits (base, // revision]. When not set, only computes diff for a single commit at revision. Base string - // The additional options to be passed to the underlying Git. CommandOptions } @@ -71,7 +70,6 @@ const ( // // Docs: https://git-scm.com/docs/git-format-patch type RawDiffOptions struct { - // The additional options to be passed to the underlying Git. CommandOptions } @@ -122,7 +120,6 @@ func (r *Repository) RawDiff(ctx context.Context, rev string, diffType RawDiffFo // DiffBinaryOptions contains optional arguments for producing binary patch. type DiffBinaryOptions struct { - // The additional options to be passed to the underlying Git. CommandOptions } diff --git a/repo_grep.go b/repo_grep.go index 3a5f5ede..3e83e3f7 100644 --- a/repo_grep.go +++ b/repo_grep.go @@ -21,7 +21,6 @@ type GrepOptions struct { WordRegexp bool // Whether use extended regular expressions. ExtendedRegexp bool - // The additional options to be passed to the underlying Git. CommandOptions } diff --git a/repo_pull.go b/repo_pull.go index df7697c8..d5eebaeb 100644 --- a/repo_pull.go +++ b/repo_pull.go @@ -9,7 +9,6 @@ import ( // // Docs: https://git-scm.com/docs/git-merge-base type MergeBaseOptions struct { - // The additional options to be passed to the underlying git. CommandOptions } diff --git a/repo_reference.go b/repo_reference.go index 08ac9dd9..b525ae9b 100644 --- a/repo_reference.go +++ b/repo_reference.go @@ -33,7 +33,6 @@ type Reference struct { // // Docs: https://git-scm.com/docs/git-show-ref#Documentation/git-show-ref.txt---verify type ShowRefVerifyOptions struct { - // The additional options to be passed to the underlying Git. CommandOptions } @@ -96,7 +95,6 @@ type SymbolicRefOptions struct { // The name of the reference, e.g. "refs/heads/master". When set, it will be // used to update the symbolic ref. Ref string - // The additional options to be passed to the underlying Git. CommandOptions } @@ -135,7 +133,6 @@ type ShowRefOptions struct { Tags bool // The list of patterns to filter results. Patterns []string - // The additional options to be passed to the underlying Git. CommandOptions } @@ -198,7 +195,6 @@ func (r *Repository) Branches(ctx context.Context) ([]string, error) { type DeleteBranchOptions struct { // Indicates whether to force delete the branch. Force bool - // The additional options to be passed to the underlying Git. CommandOptions } diff --git a/repo_remote.go b/repo_remote.go index 2aeb4b7f..5b0379a9 100644 --- a/repo_remote.go +++ b/repo_remote.go @@ -19,7 +19,6 @@ type LsRemoteOptions struct { Refs bool // The list of patterns to filter results. Patterns []string - // The additional options to be passed to the underlying Git. CommandOptions } @@ -84,7 +83,6 @@ type RemoteAddOptions struct { Fetch bool // Indicates whether to add remote as mirror with --mirror=fetch. MirrorFetch bool - // The additional options to be passed to the underlying Git. CommandOptions } @@ -113,7 +111,6 @@ func (r *Repository) RemoteAdd(ctx context.Context, name, url string, opts ...Re // // Docs: https://git-scm.com/docs/git-remote#Documentation/git-remote.txt-emremoveem type RemoteRemoveOptions struct { - // The additional options to be passed to the underlying Git. CommandOptions } @@ -140,7 +137,6 @@ func (r *Repository) RemoteRemove(ctx context.Context, name string, opts ...Remo // / // Docs: https://git-scm.com/docs/git-remote#_commands type RemotesOptions struct { - // The additional options to be passed to the underlying Git. CommandOptions } @@ -170,7 +166,6 @@ type RemoteGetURLOptions struct { // Indicates whether to get all URLs, including lists that are not part of main // URLs. This option is independent of the Push option. All bool - // The additional options to be passed to the underlying Git. CommandOptions } @@ -206,7 +201,6 @@ type RemoteSetURLOptions struct { Push bool // The regex to match existing URLs to replace (instead of first). Regex string - // The additional options to be passed to the underlying Git. CommandOptions } @@ -246,7 +240,6 @@ func (r *Repository) RemoteSetURL(ctx context.Context, name, newurl string, opts type RemoteSetURLAddOptions struct { // Indicates whether to get push URLs instead of fetch URLs. Push bool - // The additional options to be passed to the underlying Git. CommandOptions } @@ -278,7 +271,6 @@ func (r *Repository) RemoteSetURLAdd(ctx context.Context, name, newurl string, o type RemoteSetURLDeleteOptions struct { // Indicates whether to get push URLs instead of fetch URLs. Push bool - // The additional options to be passed to the underlying Git. CommandOptions } diff --git a/repo_tag.go b/repo_tag.go index 751fc904..3d309897 100644 --- a/repo_tag.go +++ b/repo_tag.go @@ -96,7 +96,6 @@ func (r *Repository) getTag(ctx context.Context, id *SHA1) (*Tag, error) { // // Docs: https://git-scm.com/docs/git-cat-file type TagOptions struct { - // The additional options to be passed to the underlying git. CommandOptions } @@ -140,7 +139,6 @@ type TagsOptions struct { SortKey string // Pattern filters tags matching the specified pattern. Pattern string - // The additional options to be passed to the underlying git. CommandOptions } @@ -183,7 +181,6 @@ type CreateTagOptions struct { Message string // Author is the author of the tag. It is ignored when tag is not annotated. Author *Signature - // The additional options to be passed to the underlying git. CommandOptions } @@ -216,7 +213,6 @@ func (r *Repository) CreateTag(ctx context.Context, name, rev string, opts ...Cr // // Docs: https://git-scm.com/docs/git-tag#Documentation/git-tag.txt---delete type DeleteTagOptions struct { - // The additional options to be passed to the underlying git. CommandOptions } diff --git a/repo_test.go b/repo_test.go index 2553e0fe..6f7e9bd8 100644 --- a/repo_test.go +++ b/repo_test.go @@ -214,6 +214,11 @@ 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) { diff --git a/repo_tree.go b/repo_tree.go index 02b10f7d..98badf2e 100644 --- a/repo_tree.go +++ b/repo_tree.go @@ -100,7 +100,6 @@ 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 additional options to be passed to the underlying Git. CommandOptions } 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 08f0c52d..92d9d25d 100755 --- a/server.go +++ b/server.go @@ -12,7 +12,6 @@ import ( type UpdateServerInfoOptions struct { // Indicates whether to overwrite the existing server info. Force bool - // The additional options to be passed to the underlying git. CommandOptions } @@ -42,7 +41,6 @@ type ReceivePackOptions struct { Quiet bool // Indicates whether to generate the "info/refs" used by the "git http-backend". HTTPBackendInfoRefs bool - // The additional options to be passed to the underlying git. CommandOptions } @@ -80,7 +78,6 @@ type UploadPackOptions struct { // This is separate from the command execution timeout which is controlled via // context.Context. InactivityTimeout time.Duration - // The additional options to be passed to the underlying git. CommandOptions } From eb099a7d0a344fb22ae7eb03dca1fde8c5c3f19f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=B4=8A=E1=B4=8F=E1=B4=87=20=E1=B4=84=CA=9C=E1=B4=87?= =?UTF-8?q?=C9=B4?= Date: Fri, 5 Jun 2026 21:43:46 -0400 Subject: [PATCH 7/7] Add installation section to README Added installation instructions for the project. --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 22d84576..7af17359 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,12 @@ Package git-module is a Go module for Git access through shell commands. - Git version must be no less than **2.4.9**. +## Installation + +```zsh +go get github.com/gogs/git-module/v2 +``` + ## License This project is under the MIT License. See the [LICENSE](LICENSE) file for the full license text.