diff --git a/.github/workflows/cifuzz.yml b/.github/workflows/cifuzz.yml index 2b17ac18b..a93087634 100644 --- a/.github/workflows/cifuzz.yml +++ b/.github/workflows/cifuzz.yml @@ -21,14 +21,14 @@ jobs: fuzz-seconds: 300 output-sarif: true - name: Upload Crash - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: failure() && steps.build.outcome == 'success' with: name: artifacts path: ./out/artifacts - name: Upload Sarif if: always() && steps.build.outcome == 'success' - uses: github/codeql-action/upload-sarif@v2 + uses: github/codeql-action/upload-sarif@v3 with: # Path to SARIF file relative to the root of the repository sarif_file: cifuzz-sarif/results.sarif diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index bfe9879af..920fc3e58 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -28,7 +28,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@29b1f65c5e92e24fe6b6647da1eaabe529cec70f # v2.3.3 + uses: github/codeql-action/init@03e7845b7bfcd5e7fb63d1ae8c61b0e791134fab # v2.22.11 with: languages: ${{ matrix.language }} # xref: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs @@ -39,6 +39,6 @@ jobs: run: go build ./... - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@29b1f65c5e92e24fe6b6647da1eaabe529cec70f # v2.3.3 + uses: github/codeql-action/analyze@03e7845b7bfcd5e7fb63d1ae8c61b0e791134fab # v2.22.11 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/git.yml b/.github/workflows/git.yml index d46cd3b0e..6e0ebb651 100644 --- a/.github/workflows/git.yml +++ b/.github/workflows/git.yml @@ -20,7 +20,7 @@ jobs: uses: actions/checkout@v4 - name: Install Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: go-version: 1.21.x diff --git a/.github/workflows/stale-issues-bot.yaml b/.github/workflows/stale-issues-bot.yaml index 69a0d3558..11b86ae27 100644 --- a/.github/workflows/stale-issues-bot.yaml +++ b/.github/workflows/stale-issues-bot.yaml @@ -11,7 +11,7 @@ jobs: stale-bot: runs-on: ubuntu-latest steps: - - uses: actions/stale@v8 + - uses: actions/stale@v9 with: ascending: true operations-per-run: 30 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 378000871..f94d3e738 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,7 +17,7 @@ jobs: uses: actions/checkout@v4 - name: Install Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index c1f280d4d..ff0c22c89 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -27,14 +27,14 @@ compatibility status with go-git. ## Branching and merging -| Feature | Sub-feature | Status | Notes | Examples | -| ----------- | ----------- | ------ | --------------------------------------- | ----------------------------------------------------------------------------------------------- | -| `branch` | | ✅ | | - [branch](_examples/branch/main.go) | -| `checkout` | | ✅ | Basic usages of checkout are supported. | - [checkout](_examples/checkout/main.go) | -| `merge` | | ❌ | | | -| `mergetool` | | ❌ | | | -| `stash` | | ❌ | | | -| `tag` | | ✅ | | - [tag](_examples/tag/main.go)
- [tag create and push](_examples/tag-create-push/main.go) | +| Feature | Sub-feature | Status | Notes | Examples | +| ----------- | ----------- | ------------ | --------------------------------------- | ----------------------------------------------------------------------------------------------- | +| `branch` | | ✅ | | - [branch](_examples/branch/main.go) | +| `checkout` | | ✅ | Basic usages of checkout are supported. | - [checkout](_examples/checkout/main.go) | +| `merge` | | ⚠️ (partial) | Fast-forward only | | +| `mergetool` | | ❌ | | | +| `stash` | | ❌ | | | +| `tag` | | ✅ | | - [tag](_examples/tag/main.go)
- [tag create and push](_examples/tag-create-push/main.go) | ## Sharing and updating projects diff --git a/Makefile b/Makefile index 1e1039674..3d5b54f7e 100644 --- a/Makefile +++ b/Makefile @@ -28,6 +28,7 @@ build-git: test: @echo "running against `git version`"; \ $(GOTEST) -race ./... + $(GOTEST) -v _examples/common_test.go _examples/common.go --examples TEMP_REPO := $(shell mktemp) test-sha256: diff --git a/_examples/README.md b/_examples/README.md index 46f1fb0fc..1e9ea6ae6 100644 --- a/_examples/README.md +++ b/_examples/README.md @@ -10,7 +10,8 @@ Here you can find a list of annotated _go-git_ examples: using a username and password. - [personal access token](clone/auth/basic/access_token/main.go) - Cloning a repository using a GitHub personal access token. - - [ssh private key](clone/auth/ssh/main.go) - Cloning a repository using a ssh private key. + - [ssh private key](clone/auth/ssh/private_key/main.go) - Cloning a repository using a ssh private key. + - [ssh agent](clone/auth/ssh/ssh_agent/main.go) - Cloning a repository using ssh-agent. - [commit](commit/main.go) - Commit changes to the current branch to an existent repository. - [push](push/main.go) - Push repository to default remote (origin). - [pull](pull/main.go) - Pull changes from a remote repository. @@ -32,4 +33,4 @@ Here you can find a list of annotated _go-git_ examples: - [custom_http](custom_http/main.go) - Replacing the HTTP client using a custom one. - [clone with context](context/main.go) - Cloning a repository with graceful cancellation. - [storage](storage/README.md) - Implementing a custom storage system. -- [sha256](sha256/main.go) - Init and commiting repositories that use sha256 as object format. +- [sha256](sha256/main.go) - Init and committing repositories that use sha256 as object format. diff --git a/_examples/checkout-branch/main.go b/_examples/checkout-branch/main.go new file mode 100644 index 000000000..59dfdfc3d --- /dev/null +++ b/_examples/checkout-branch/main.go @@ -0,0 +1,85 @@ +package main + +import ( + "fmt" + "os" + + "github.com/go-git/go-git/v5" + . "github.com/go-git/go-git/v5/_examples" + "github.com/go-git/go-git/v5/config" + "github.com/go-git/go-git/v5/plumbing" +) + +// Checkout a branch +func main() { + CheckArgs("", "", "") + url, directory, branch := os.Args[1], os.Args[2], os.Args[3] + + // Clone the given repository to the given directory + Info("git clone %s %s", url, directory) + r, err := git.PlainClone(directory, false, &git.CloneOptions{ + URL: url, + }) + CheckIfError(err) + + // ... retrieving the commit being pointed by HEAD + Info("git show-ref --head HEAD") + ref, err := r.Head() + CheckIfError(err) + + fmt.Println(ref.Hash()) + + w, err := r.Worktree() + CheckIfError(err) + + // ... checking out branch + Info("git checkout %s", branch) + + branchRefName := plumbing.NewBranchReferenceName(branch) + branchCoOpts := git.CheckoutOptions{ + Branch: plumbing.ReferenceName(branchRefName), + Force: true, + } + if err := w.Checkout(&branchCoOpts); err != nil { + Warning("local checkout of branch '%s' failed, will attempt to fetch remote branch of same name.", branch) + Warning("like `git checkout ` defaulting to `git checkout -b --track /`") + + mirrorRemoteBranchRefSpec := fmt.Sprintf("refs/heads/%s:refs/heads/%s", branch, branch) + err = fetchOrigin(r, mirrorRemoteBranchRefSpec) + CheckIfError(err) + + err = w.Checkout(&branchCoOpts) + CheckIfError(err) + } + CheckIfError(err) + + Info("checked out branch: %s", branch) + + // ... retrieving the commit being pointed by HEAD (branch now) + Info("git show-ref --head HEAD") + ref, err = r.Head() + CheckIfError(err) + fmt.Println(ref.Hash()) +} + +func fetchOrigin(repo *git.Repository, refSpecStr string) error { + remote, err := repo.Remote("origin") + CheckIfError(err) + + var refSpecs []config.RefSpec + if refSpecStr != "" { + refSpecs = []config.RefSpec{config.RefSpec(refSpecStr)} + } + + if err = remote.Fetch(&git.FetchOptions{ + RefSpecs: refSpecs, + }); err != nil { + if err == git.NoErrAlreadyUpToDate { + fmt.Print("refs already up to date") + } else { + return fmt.Errorf("fetch origin failed: %v", err) + } + } + + return nil +} diff --git a/_examples/clone/auth/ssh/main.go b/_examples/clone/auth/ssh/private_key/main.go similarity index 100% rename from _examples/clone/auth/ssh/main.go rename to _examples/clone/auth/ssh/private_key/main.go diff --git a/_examples/clone/auth/ssh/ssh_agent/main.go b/_examples/clone/auth/ssh/ssh_agent/main.go new file mode 100644 index 000000000..7a2ebd367 --- /dev/null +++ b/_examples/clone/auth/ssh/ssh_agent/main.go @@ -0,0 +1,37 @@ +package main + +import ( + "fmt" + "os" + + git "github.com/go-git/go-git/v5" + . "github.com/go-git/go-git/v5/_examples" + "github.com/go-git/go-git/v5/plumbing/transport/ssh" +) + +func main() { + CheckArgs("", "") + url, directory := os.Args[1], os.Args[2] + + authMethod, err := ssh.NewSSHAgentAuth("git") + CheckIfError(err) + + // Clone the given repository to the given directory + Info("git clone %s ", url) + + r, err := git.PlainClone(directory, false, &git.CloneOptions{ + Auth: authMethod, + URL: url, + Progress: os.Stdout, + }) + CheckIfError(err) + + // ... retrieving the branch being pointed by HEAD + ref, err := r.Head() + CheckIfError(err) + // ... retrieving the commit object + commit, err := r.CommitObject(ref.Hash()) + CheckIfError(err) + + fmt.Println(commit) +} diff --git a/_examples/common_test.go b/_examples/common_test.go index 6630f15ee..5e3f75381 100644 --- a/_examples/common_test.go +++ b/_examples/common_test.go @@ -2,10 +2,10 @@ package examples import ( "flag" - "go/build" "os" "os/exec" "path/filepath" + "runtime" "testing" ) @@ -14,26 +14,43 @@ var examplesTest = flag.Bool("examples", false, "run the examples tests") var defaultURL = "https://github.com/git-fixtures/basic.git" var args = map[string][]string{ - "branch": {defaultURL, tempFolder()}, - "checkout": {defaultURL, tempFolder(), "35e85108805c84807bc66a02d91535e1e24b38b9"}, - "clone": {defaultURL, tempFolder()}, - "context": {defaultURL, tempFolder()}, - "commit": {cloneRepository(defaultURL, tempFolder())}, - "custom_http": {defaultURL}, - "open": {cloneRepository(defaultURL, tempFolder())}, - "progress": {defaultURL, tempFolder()}, - "push": {setEmptyRemote(cloneRepository(defaultURL, tempFolder()))}, - "revision": {cloneRepository(defaultURL, tempFolder()), "master~2^"}, - "showcase": {defaultURL, tempFolder()}, - "tag": {cloneRepository(defaultURL, tempFolder())}, - "pull": {createRepositoryWithRemote(tempFolder(), defaultURL)}, - "ls": {cloneRepository(defaultURL, tempFolder()), "HEAD", "vendor"}, - "merge_base": {cloneRepository(defaultURL, tempFolder()), "--is-ancestor", "HEAD~3", "HEAD^"}, + "blame": {defaultURL, "CHANGELOG"}, + "branch": {defaultURL, tempFolder()}, + "checkout": {defaultURL, tempFolder(), "35e85108805c84807bc66a02d91535e1e24b38b9"}, + "checkout-branch": {defaultURL, tempFolder(), "branch"}, + "clone": {defaultURL, tempFolder()}, + "commit": {cloneRepository(defaultURL, tempFolder())}, + "context": {defaultURL, tempFolder()}, + "custom_http": {defaultURL}, + "find-if-any-tag-point-head": {cloneRepository(defaultURL, tempFolder())}, + "ls": {cloneRepository(defaultURL, tempFolder()), "HEAD", "vendor"}, + "ls-remote": {defaultURL}, + "merge_base": {cloneRepository(defaultURL, tempFolder()), "--is-ancestor", "HEAD~3", "HEAD^"}, + "open": {cloneRepository(defaultURL, tempFolder())}, + "progress": {defaultURL, tempFolder()}, + "pull": {createRepositoryWithRemote(tempFolder(), defaultURL)}, + "push": {setEmptyRemote(cloneRepository(defaultURL, tempFolder()))}, + "revision": {cloneRepository(defaultURL, tempFolder()), "master~2^"}, + "sha256": {tempFolder()}, + "showcase": {defaultURL, tempFolder()}, + "tag": {cloneRepository(defaultURL, tempFolder())}, } -var ignored = map[string]bool{} +// tests not working / set-up +var ignored = map[string]bool{ + "azure_devops": true, + "ls": true, + "sha256": true, + "submodule": true, + "tag-create-push": true, +} + +var ( + tempFolders = []string{} -var tempFolders = []string{} + _, callingFile, _, _ = runtime.Caller(0) + basepath = filepath.Dir(callingFile) +) func TestExamples(t *testing.T) { flag.Parse() @@ -44,13 +61,13 @@ func TestExamples(t *testing.T) { defer deleteTempFolders() - examples, err := filepath.Glob(examplesFolder()) + exampleMains, err := filepath.Glob(filepath.Join(basepath, "*", "main.go")) if err != nil { t.Errorf("error finding tests: %s", err) } - for _, example := range examples { - dir := filepath.Dir(example) + for _, main := range exampleMains { + dir := filepath.Dir(main) _, name := filepath.Split(dir) if ignored[name] { @@ -71,20 +88,6 @@ func tempFolder() string { return path } -func packageFolder() string { - return filepath.Join( - build.Default.GOPATH, - "src", "github.com/go-git/go-git/v5", - ) -} - -func examplesFolder() string { - return filepath.Join( - packageFolder(), - "_examples", "*", "main.go", - ) -} - func cloneRepository(url, folder string) string { cmd := exec.Command("git", "clone", url, folder) err := cmd.Run() diff --git a/cli/go-git/go.mod b/cli/go-git/go.mod index 5cc0d18ad..33f5f2447 100644 --- a/cli/go-git/go.mod +++ b/cli/go-git/go.mod @@ -1,9 +1,9 @@ -module github.com/go-git/v5/go-git/cli/go-git +module github.com/go-git/go-git/cli/go-git go 1.19 require ( - github.com/go-git/go-git/v5 v5.10.1 + github.com/go-git/go-git/v5 v5.11.0 github.com/jessevdk/go-flags v1.5.0 ) @@ -11,7 +11,7 @@ require ( dario.cat/mergo v1.0.0 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect - github.com/cloudflare/circl v1.3.3 // indirect + github.com/cloudflare/circl v1.3.7 // indirect github.com/cyphar/filepath-securejoin v0.2.4 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect @@ -23,10 +23,10 @@ require ( github.com/sergi/go-diff v1.1.0 // indirect github.com/skeema/knownhosts v1.2.1 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect - golang.org/x/crypto v0.15.0 // indirect + golang.org/x/crypto v0.17.0 // indirect golang.org/x/mod v0.12.0 // indirect - golang.org/x/net v0.18.0 // indirect - golang.org/x/sys v0.14.0 // indirect + golang.org/x/net v0.19.0 // indirect + golang.org/x/sys v0.15.0 // indirect golang.org/x/tools v0.13.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect ) diff --git a/cli/go-git/go.sum b/cli/go-git/go.sum index 45c055264..42324f59a 100644 --- a/cli/go-git/go.sum +++ b/cli/go-git/go.sum @@ -8,8 +8,9 @@ github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371/go.mod h1:EjA github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= -github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs= github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= +github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= +github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -24,8 +25,8 @@ github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmS github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= -github.com/go-git/go-git/v5 v5.10.1 h1:tu8/D8i+TWxgKpzQ3Vc43e+kkhXqtsZCKI/egajKnxk= -github.com/go-git/go-git/v5 v5.10.1/go.mod h1:uEuHjxkHap8kAl//V5F/nNWwqIYtP/402ddd05mp0wg= +github.com/go-git/go-git/v5 v5.11.0 h1:XIZc1p+8YzypNr34itUfSvYJcv+eYdTnTvOZ2vD3cA4= +github.com/go-git/go-git/v5 v5.11.0/go.mod h1:6GFcX2P3NM7FPBfpePbpLd21XxsgdAt+lKqXmCUiUCY= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= @@ -55,8 +56,8 @@ github.com/skeema/knownhosts v1.2.1 h1:SHWdIUa82uGZz+F+47k8SY4QhhI291cXCpopT1lK2 github.com/skeema/knownhosts v1.2.1/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= @@ -65,8 +66,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA= -golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= +golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= @@ -78,8 +79,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= -golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -98,14 +99,14 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= -golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.14.0 h1:LGK9IlZ8T9jvdy6cTdfKUCltatMFOehAQo9SRC46UQ8= +golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -127,7 +128,6 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/go.mod b/go.mod index 5247d063c..3270b9279 100644 --- a/go.mod +++ b/go.mod @@ -5,11 +5,11 @@ go 1.19 require ( dario.cat/mergo v1.0.0 - github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 + github.com/ProtonMail/go-crypto v1.0.0 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a github.com/emirpasic/gods v1.18.1 - github.com/gliderlabs/ssh v0.3.5 + github.com/gliderlabs/ssh v0.3.7 github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 github.com/go-git/go-billy/v5 v5.5.0 github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 @@ -18,13 +18,13 @@ require ( github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 github.com/kevinburke/ssh_config v1.2.0 github.com/pjbgf/sha1cd v0.3.0 - github.com/sergi/go-diff v1.1.0 - github.com/skeema/knownhosts v1.2.1 - github.com/stretchr/testify v1.8.4 + github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 + github.com/skeema/knownhosts v1.2.2 + github.com/stretchr/testify v1.9.0 github.com/xanzy/ssh-agent v0.3.3 - golang.org/x/crypto v0.16.0 - golang.org/x/net v0.19.0 - golang.org/x/sys v0.15.0 + golang.org/x/crypto v0.21.0 + golang.org/x/net v0.22.0 + golang.org/x/sys v0.18.0 golang.org/x/text v0.14.0 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c ) @@ -32,7 +32,7 @@ require ( require ( github.com/Microsoft/go-winio v0.6.1 // indirect github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect - github.com/cloudflare/circl v1.3.3 // indirect + github.com/cloudflare/circl v1.3.7 // indirect github.com/cyphar/filepath-securejoin v0.2.4 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/kr/pretty v0.3.1 // indirect diff --git a/go.sum b/go.sum index c0593765b..0a2b9d442 100644 --- a/go.sum +++ b/go.sum @@ -3,15 +3,16 @@ dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= -github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 h1:kkhsdkhsCvIsutKu5zLMgWtgh9YxGCNAw8Ad8hjwfYg= -github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= +github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= +github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= -github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs= github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= +github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= +github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= @@ -24,8 +25,8 @@ github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2 h1:dWB6v3RcOy0 github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= -github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY= -github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4= +github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= +github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= @@ -60,27 +61,26 @@ github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYe github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= -github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= -github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/skeema/knownhosts v1.2.1 h1:SHWdIUa82uGZz+F+47k8SY4QhhI291cXCpopT1lK2AQ= -github.com/skeema/knownhosts v1.2.1/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= +github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= +github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= -golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= @@ -89,12 +89,11 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= -golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -108,21 +107,18 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= +golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -146,6 +142,6 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/options.go b/options.go index 8902b7e3e..d7776dad5 100644 --- a/options.go +++ b/options.go @@ -89,6 +89,25 @@ type CloneOptions struct { Shared bool } +// MergeOptions describes how a merge should be performed. +type MergeOptions struct { + // Strategy defines the merge strategy to be used. + Strategy MergeStrategy +} + +// MergeStrategy represents the different types of merge strategies. +type MergeStrategy int8 + +const ( + // FastForwardMerge represents a Git merge strategy where the current + // branch can be simply updated to point to the HEAD of the branch being + // merged. This is only possible if the history of the branch being merged + // is a linear descendant of the current branch, with no conflicting commits. + // + // This is the default option. + FastForwardMerge MergeStrategy = iota +) + // Validate validates the fields and sets the default values. func (o *CloneOptions) Validate() error { if o.URL == "" { @@ -166,7 +185,7 @@ const ( // AllTags fetch all tags from the remote (i.e., fetch remote tags // refs/tags/* into local tags with the same name) AllTags - //NoTags fetch no tags from the remote at all + // NoTags fetch no tags from the remote at all NoTags ) @@ -198,6 +217,9 @@ type FetchOptions struct { CABundle []byte // ProxyOptions provides info required for connecting to a proxy. ProxyOptions transport.ProxyOptions + // Prune specify that local refs that match given RefSpecs and that do + // not exist remotely will be removed. + Prune bool } // Validate validates the fields and sets the default values. @@ -324,9 +346,9 @@ var ( // CheckoutOptions describes how a checkout operation should be performed. type CheckoutOptions struct { - // Hash is the hash of the commit to be checked out. If used, HEAD will be - // in detached mode. If Create is not used, Branch and Hash are mutually - // exclusive. + // Hash is the hash of a commit or tag to be checked out. If used, HEAD + // will be in detached mode. If Create is not used, Branch and Hash are + // mutually exclusive. Hash plumbing.Hash // Branch to be checked out, if Branch and Hash are empty is set to `master`. Branch plumbing.ReferenceName @@ -405,6 +427,11 @@ func (o *ResetOptions) Validate(r *Repository) error { } o.Commit = ref.Hash() + } else { + _, err := r.CommitObject(o.Commit) + if err != nil { + return fmt.Errorf("invalid reset option: %w", err) + } } return nil @@ -474,6 +501,11 @@ type AddOptions struct { // Glob adds all paths, matching pattern, to the index. If pattern matches a // directory path, all directory contents are added to the index recursively. Glob string + // SkipStatus adds the path with no status check. This option is relevant only + // when the `Path` option is specified and does not apply when the `All` option is used. + // Notice that when passing an ignored path it will be added anyway. + // When true it can speed up adding files to the worktree in very large repositories. + SkipStatus bool } // Validate validates the fields and sets the default values. @@ -507,6 +539,10 @@ type CommitOptions struct { // commit will not be signed. The private key must be present and already // decrypted. SignKey *openpgp.Entity + // Signer denotes a cryptographic signer to sign the commit with. + // A nil value here means the commit will not be signed. + // Takes precedence over SignKey. + Signer Signer // Amend will create a new commit object and replace the commit that HEAD currently // points to. Cannot be used with All nor Parents. Amend bool diff --git a/options_test.go b/options_test.go index 171222c29..677c31719 100644 --- a/options_test.go +++ b/options_test.go @@ -23,6 +23,12 @@ func (s *OptionsSuite) TestCommitOptionsParentsFromHEAD(c *C) { c.Assert(o.Parents, HasLen, 1) } +func (s *OptionsSuite) TestResetOptionsCommitNotFound(c *C) { + o := ResetOptions{Commit: plumbing.NewHash("ab1b15c6f6487b4db16f10d8ec69bb8bf91dcabd")} + err := o.Validate(s.Repository) + c.Assert(err, NotNil) +} + func (s *OptionsSuite) TestCommitOptionsCommitter(c *C) { sig := &object.Signature{} diff --git a/plumbing/format/gitattributes/attributes.go b/plumbing/format/gitattributes/attributes.go index d36ec1b53..026d221b0 100644 --- a/plumbing/format/gitattributes/attributes.go +++ b/plumbing/format/gitattributes/attributes.go @@ -1,6 +1,7 @@ package gitattributes import ( + "bufio" "errors" "io" "strings" @@ -88,13 +89,10 @@ func (a attribute) String() string { // ReadAttributes reads patterns and attributes from the gitattributes format. func ReadAttributes(r io.Reader, domain []string, allowMacro bool) (attributes []MatchAttribute, err error) { - data, err := io.ReadAll(r) - if err != nil { - return nil, err - } + scanner := bufio.NewScanner(r) - for _, line := range strings.Split(string(data), eol) { - attribute, err := ParseAttributesLine(line, domain, allowMacro) + for scanner.Scan() { + attribute, err := ParseAttributesLine(scanner.Text(), domain, allowMacro) if err != nil { return attributes, err } @@ -105,6 +103,10 @@ func ReadAttributes(r io.Reader, domain []string, allowMacro bool) (attributes [ attributes = append(attributes, attribute) } + if err := scanner.Err(); err != nil { + return attributes, err + } + return attributes, nil } diff --git a/plumbing/format/gitattributes/dir.go b/plumbing/format/gitattributes/dir.go index 123fe2546..d3b0dbefd 100644 --- a/plumbing/format/gitattributes/dir.go +++ b/plumbing/format/gitattributes/dir.go @@ -4,6 +4,7 @@ import ( "os" "github.com/go-git/go-billy/v5" + "github.com/go-git/go-git/v5/plumbing/format/config" gioutil "github.com/go-git/go-git/v5/utils/ioutil" ) @@ -26,6 +27,8 @@ func ReadAttributesFile(fs billy.Filesystem, path []string, attributesFile strin return nil, err } + defer gioutil.CheckClose(f, &err) + return ReadAttributes(f, path, allowMacro) } diff --git a/plumbing/format/gitignore/dir.go b/plumbing/format/gitignore/dir.go index d8fb30c16..aca5d0dbd 100644 --- a/plumbing/format/gitignore/dir.go +++ b/plumbing/format/gitignore/dir.go @@ -116,7 +116,7 @@ func loadPatterns(fs billy.Filesystem, path string) (ps []Pattern, err error) { return } -// LoadGlobalPatterns loads gitignore patterns from from the gitignore file +// LoadGlobalPatterns loads gitignore patterns from the gitignore file // declared in a user's ~/.gitconfig file. If the ~/.gitconfig file does not // exist the function will return nil. If the core.excludesfile property // is not declared, the function will return nil. If the file pointed to by @@ -132,7 +132,7 @@ func LoadGlobalPatterns(fs billy.Filesystem) (ps []Pattern, err error) { return loadPatterns(fs, fs.Join(home, gitconfigFile)) } -// LoadSystemPatterns loads gitignore patterns from from the gitignore file +// LoadSystemPatterns loads gitignore patterns from the gitignore file // declared in a system's /etc/gitconfig file. If the /etc/gitconfig file does // not exist the function will return nil. If the core.excludesfile property // is not declared, the function will return nil. If the file pointed to by diff --git a/plumbing/object/commit.go b/plumbing/object/commit.go index ceed5d01e..3d096e18b 100644 --- a/plumbing/object/commit.go +++ b/plumbing/object/commit.go @@ -27,7 +27,7 @@ const ( // the commit with the "mergetag" header. headermergetag string = "mergetag" - defaultUtf8CommitMesageEncoding MessageEncoding = "UTF-8" + defaultUtf8CommitMessageEncoding MessageEncoding = "UTF-8" ) // Hash represents the hash of an object @@ -189,7 +189,7 @@ func (c *Commit) Decode(o plumbing.EncodedObject) (err error) { } c.Hash = o.Hash() - c.Encoding = defaultUtf8CommitMesageEncoding + c.Encoding = defaultUtf8CommitMessageEncoding reader, err := o.Reader() if err != nil { @@ -335,7 +335,7 @@ func (c *Commit) encode(o plumbing.EncodedObject, includeSig bool) (err error) { } } - if string(c.Encoding) != "" && c.Encoding != defaultUtf8CommitMesageEncoding { + if string(c.Encoding) != "" && c.Encoding != defaultUtf8CommitMessageEncoding { if _, err = fmt.Fprintf(w, "\n%s %s", headerencoding, c.Encoding); err != nil { return err } diff --git a/plumbing/object/commit_test.go b/plumbing/object/commit_test.go index 3e1fe1b90..a0489269a 100644 --- a/plumbing/object/commit_test.go +++ b/plumbing/object/commit_test.go @@ -228,7 +228,7 @@ change Message: "Message\n\nFoo\nBar\nWith trailing blank lines\n\n", TreeHash: plumbing.NewHash("f000000000000000000000000000000000000001"), ParentHashes: []plumbing.Hash{plumbing.NewHash("f000000000000000000000000000000000000002")}, - Encoding: defaultUtf8CommitMesageEncoding, + Encoding: defaultUtf8CommitMessageEncoding, }, { Author: Signature{Name: "Foo", Email: "foo@example.local", When: ts}, @@ -253,7 +253,7 @@ change plumbing.NewHash("f000000000000000000000000000000000000003"), }, MergeTag: tag, - Encoding: defaultUtf8CommitMesageEncoding, + Encoding: defaultUtf8CommitMessageEncoding, }, { Author: Signature{Name: "Foo", Email: "foo@example.local", When: ts}, @@ -266,7 +266,7 @@ change }, MergeTag: tag, PGPSignature: pgpsignature, - Encoding: defaultUtf8CommitMesageEncoding, + Encoding: defaultUtf8CommitMessageEncoding, }, } for _, commit := range commits { @@ -455,7 +455,7 @@ func (s *SuiteCommit) TestStat(c *C) { c.Assert(fileStats[1].Name, Equals, "php/crappy.php") c.Assert(fileStats[1].Addition, Equals, 259) c.Assert(fileStats[1].Deletion, Equals, 0) - c.Assert(fileStats[1].String(), Equals, " php/crappy.php | 259 ++++++++++++++++++++++++++++++++++++++++++++++++++++\n") + c.Assert(fileStats[1].String(), Equals, " php/crappy.php | 259 +++++++++++++++++++++++++++++++++++++++++++++++++++++\n") } func (s *SuiteCommit) TestVerify(c *C) { diff --git a/plumbing/object/commit_walker_path.go b/plumbing/object/commit_walker_path.go index aa0ca15fd..c1ec8ba7a 100644 --- a/plumbing/object/commit_walker_path.go +++ b/plumbing/object/commit_walker_path.go @@ -57,6 +57,8 @@ func (c *commitPathIter) Next() (*Commit, error) { } func (c *commitPathIter) getNextFileCommit() (*Commit, error) { + var parentTree, currentTree *Tree + for { // Parent-commit can be nil if the current-commit is the initial commit parentCommit, parentCommitErr := c.sourceIter.Next() @@ -68,13 +70,17 @@ func (c *commitPathIter) getNextFileCommit() (*Commit, error) { parentCommit = nil } - // Fetch the trees of the current and parent commits - currentTree, currTreeErr := c.currentCommit.Tree() - if currTreeErr != nil { - return nil, currTreeErr + if parentTree == nil { + var currTreeErr error + currentTree, currTreeErr = c.currentCommit.Tree() + if currTreeErr != nil { + return nil, currTreeErr + } + } else { + currentTree = parentTree + parentTree = nil } - var parentTree *Tree if parentCommit != nil { var parentTreeErr error parentTree, parentTreeErr = parentCommit.Tree() @@ -115,7 +121,8 @@ func (c *commitPathIter) hasFileChange(changes Changes, parent *Commit) bool { // filename matches, now check if source iterator contains all commits (from all refs) if c.checkParent { - if parent != nil && isParentHash(parent.Hash, c.currentCommit) { + // Check if parent is beyond the initial commit + if parent == nil || isParentHash(parent.Hash, c.currentCommit) { return true } continue diff --git a/plumbing/object/commit_walker_test.go b/plumbing/object/commit_walker_test.go index c47d68b76..fa0ca7d32 100644 --- a/plumbing/object/commit_walker_test.go +++ b/plumbing/object/commit_walker_test.go @@ -228,3 +228,29 @@ func (s *CommitWalkerSuite) TestCommitBSFIteratorWithIgnore(c *C) { c.Assert(commit.Hash.String(), Equals, expected[i]) } } + +func (s *CommitWalkerSuite) TestCommitPathIteratorInitialCommit(c *C) { + commit := s.commit(c, plumbing.NewHash(s.Fixture.Head)) + + fileName := "LICENSE" + + var commits []*Commit + NewCommitPathIterFromIter( + func(path string) bool { return path == fileName }, + NewCommitIterCTime(commit, nil, nil), + true, + ).ForEach(func(c *Commit) error { + commits = append(commits, c) + return nil + }) + + expected := []string{ + "b029517f6300c2da0f4b651b8642506cd6aaf45d", + } + + c.Assert(commits, HasLen, len(expected)) + + for i, commit := range commits { + c.Assert(commit.Hash.String(), Equals, expected[i]) + } +} diff --git a/plumbing/object/patch.go b/plumbing/object/patch.go index dd8fef447..3c61f626a 100644 --- a/plumbing/object/patch.go +++ b/plumbing/object/patch.go @@ -6,7 +6,7 @@ import ( "errors" "fmt" "io" - "math" + "strconv" "strings" "github.com/go-git/go-git/v5/plumbing" @@ -234,69 +234,56 @@ func (fileStats FileStats) String() string { return printStat(fileStats) } +// printStat prints the stats of changes in content of files. +// Original implementation: https://github.com/git/git/blob/1a87c842ece327d03d08096395969aca5e0a6996/diff.c#L2615 +// Parts of the output: +// |<+++/---> +// example: " main.go | 10 +++++++--- " func printStat(fileStats []FileStat) string { - padLength := float64(len(" ")) - newlineLength := float64(len("\n")) - separatorLength := float64(len("|")) - // Soft line length limit. The text length calculation below excludes - // length of the change number. Adding that would take it closer to 80, - // but probably not more than 80, until it's a huge number. - lineLength := 72.0 - - // Get the longest filename and longest total change. - var longestLength float64 - var longestTotalChange float64 - for _, fs := range fileStats { - if int(longestLength) < len(fs.Name) { - longestLength = float64(len(fs.Name)) - } - totalChange := fs.Addition + fs.Deletion - if int(longestTotalChange) < totalChange { - longestTotalChange = float64(totalChange) - } - } - - // Parts of the output: - // |<+++/---> - // example: " main.go | 10 +++++++--- " - - // - leftTextLength := padLength + longestLength + padLength - - // <+++++/-----> - // Excluding number length here. - rightTextLength := padLength + padLength + newlineLength + maxGraphWidth := uint(53) + maxNameLen := 0 + maxChangeLen := 0 - totalTextArea := leftTextLength + separatorLength + rightTextLength - heightOfHistogram := lineLength - totalTextArea + scaleLinear := func(it, width, max uint) uint { + if it == 0 || max == 0 { + return 0 + } - // Scale the histogram. - var scaleFactor float64 - if longestTotalChange > heightOfHistogram { - // Scale down to heightOfHistogram. - scaleFactor = longestTotalChange / heightOfHistogram - } else { - scaleFactor = 1.0 + return 1 + (it * (width - 1) / max) } - finalOutput := "" for _, fs := range fileStats { - addn := float64(fs.Addition) - deln := float64(fs.Deletion) - addc := int(math.Floor(addn/scaleFactor)) - delc := int(math.Floor(deln/scaleFactor)) - if addc < 0 { - addc = 0 + if len(fs.Name) > maxNameLen { + maxNameLen = len(fs.Name) } - if delc < 0 { - delc = 0 + + changes := strconv.Itoa(fs.Addition + fs.Deletion) + if len(changes) > maxChangeLen { + maxChangeLen = len(changes) } - adds := strings.Repeat("+", addc) - dels := strings.Repeat("-", delc) - finalOutput += fmt.Sprintf(" %s | %d %s%s\n", fs.Name, (fs.Addition + fs.Deletion), adds, dels) } - return finalOutput + result := "" + for _, fs := range fileStats { + add := uint(fs.Addition) + del := uint(fs.Deletion) + np := maxNameLen - len(fs.Name) + cp := maxChangeLen - len(strconv.Itoa(fs.Addition+fs.Deletion)) + + total := add + del + if total > maxGraphWidth { + add = scaleLinear(add, maxGraphWidth, total) + del = scaleLinear(del, maxGraphWidth, total) + } + + adds := strings.Repeat("+", int(add)) + dels := strings.Repeat("-", int(del)) + namePad := strings.Repeat(" ", np) + changePad := strings.Repeat(" ", cp) + + result += fmt.Sprintf(" %s%s | %s%d %s%s\n", fs.Name, namePad, changePad, total, adds, dels) + } + return result } func getFileStatsFromFilePatches(filePatches []fdiff.FilePatch) FileStats { diff --git a/plumbing/object/patch_test.go b/plumbing/object/patch_test.go index 2cff795ed..e0e63a507 100644 --- a/plumbing/object/patch_test.go +++ b/plumbing/object/patch_test.go @@ -45,3 +45,113 @@ func (s *PatchSuite) TestStatsWithSubmodules(c *C) { c.Assert(err, IsNil) c.Assert(p, NotNil) } + +func (s *PatchSuite) TestFileStatsString(c *C) { + testCases := []struct { + description string + input FileStats + expected string + }{ + + { + description: "no files changed", + input: []FileStat{}, + expected: "", + }, + { + description: "one file touched - no changes", + input: []FileStat{ + { + Name: "file1", + }, + }, + expected: " file1 | 0 \n", + }, + { + description: "one file changed", + input: []FileStat{ + { + Name: "file1", + Addition: 1, + }, + }, + expected: " file1 | 1 +\n", + }, + { + description: "one file changed with one addition and one deletion", + input: []FileStat{ + { + Name: ".github/workflows/git.yml", + Addition: 1, + Deletion: 1, + }, + }, + expected: " .github/workflows/git.yml | 2 +-\n", + }, + { + description: "two files changed", + input: []FileStat{ + { + Name: ".github/workflows/git.yml", + Addition: 1, + Deletion: 1, + }, + { + Name: "cli/go-git/go.mod", + Addition: 4, + Deletion: 4, + }, + }, + expected: " .github/workflows/git.yml | 2 +-\n cli/go-git/go.mod | 8 ++++----\n", + }, + { + description: "three files changed", + input: []FileStat{ + { + Name: ".github/workflows/git.yml", + Addition: 3, + Deletion: 3, + }, + { + Name: "worktree.go", + Addition: 107, + }, + { + Name: "worktree_test.go", + Addition: 75, + }, + }, + expected: " .github/workflows/git.yml | 6 +++---\n" + + " worktree.go | 107 +++++++++++++++++++++++++++++++++++++++++++++++++++++\n" + + " worktree_test.go | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++++\n", + }, + { + description: "three files changed with deletions and additions", + input: []FileStat{ + { + Name: ".github/workflows/git.yml", + Addition: 3, + Deletion: 3, + }, + { + Name: "worktree.go", + Addition: 107, + Deletion: 217, + }, + { + Name: "worktree_test.go", + Addition: 75, + Deletion: 275, + }, + }, + expected: " .github/workflows/git.yml | 6 +++---\n" + + " worktree.go | 324 ++++++++++++++++++-----------------------------------\n" + + " worktree_test.go | 350 ++++++++++++-----------------------------------------\n", + }, + } + + for _, tc := range testCases { + c.Log("Executing test cases:", tc.description) + c.Assert(printStat(tc.input), Equals, tc.expected) + } +} diff --git a/plumbing/object/tree.go b/plumbing/object/tree.go index e9f7666b8..0fd0e5139 100644 --- a/plumbing/object/tree.go +++ b/plumbing/object/tree.go @@ -7,6 +7,7 @@ import ( "io" "path" "path/filepath" + "sort" "strings" "github.com/go-git/go-git/v5/plumbing" @@ -27,6 +28,7 @@ var ( ErrFileNotFound = errors.New("file not found") ErrDirectoryNotFound = errors.New("directory not found") ErrEntryNotFound = errors.New("entry not found") + ErrEntriesNotSorted = errors.New("entries in tree are not sorted") ) // Tree is basically like a directory - it references a bunch of other trees @@ -270,6 +272,28 @@ func (t *Tree) Decode(o plumbing.EncodedObject) (err error) { return nil } +type TreeEntrySorter []TreeEntry + +func (s TreeEntrySorter) Len() int { + return len(s) +} + +func (s TreeEntrySorter) Less(i, j int) bool { + name1 := s[i].Name + name2 := s[j].Name + if s[i].Mode == filemode.Dir { + name1 += "/" + } + if s[j].Mode == filemode.Dir { + name2 += "/" + } + return name1 < name2 +} + +func (s TreeEntrySorter) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + // Encode transforms a Tree into a plumbing.EncodedObject. func (t *Tree) Encode(o plumbing.EncodedObject) (err error) { o.SetType(plumbing.TreeObject) @@ -279,7 +303,15 @@ func (t *Tree) Encode(o plumbing.EncodedObject) (err error) { } defer ioutil.CheckClose(w, &err) + + if !sort.IsSorted(TreeEntrySorter(t.Entries)) { + return ErrEntriesNotSorted + } + for _, entry := range t.Entries { + if strings.IndexByte(entry.Name, 0) != -1 { + return fmt.Errorf("malformed filename %q", entry.Name) + } if _, err = fmt.Fprintf(w, "%o %s", entry.Mode, entry.Name); err != nil { return err } diff --git a/plumbing/object/tree_test.go b/plumbing/object/tree_test.go index bb5fc7a09..feb058a68 100644 --- a/plumbing/object/tree_test.go +++ b/plumbing/object/tree_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "io" + "sort" "testing" fixtures "github.com/go-git/go-git-fixtures/v4" @@ -220,6 +221,30 @@ func (o *SortReadCloser) Read(p []byte) (int, error) { return nw, nil } +func (s *TreeSuite) TestTreeEntriesSorted(c *C) { + tree := &Tree{ + Entries: []TreeEntry{ + {"foo", filemode.Empty, plumbing.NewHash("b029517f6300c2da0f4b651b8642506cd6aaf45d")}, + {"bar", filemode.Empty, plumbing.NewHash("c029517f6300c2da0f4b651b8642506cd6aaf45d")}, + {"baz", filemode.Empty, plumbing.NewHash("d029517f6300c2da0f4b651b8642506cd6aaf45d")}, + }, + } + + { + c.Assert(sort.IsSorted(TreeEntrySorter(tree.Entries)), Equals, false) + obj := &plumbing.MemoryObject{} + err := tree.Encode(obj) + c.Assert(err, Equals, ErrEntriesNotSorted) + } + + { + sort.Sort(TreeEntrySorter(tree.Entries)) + obj := &plumbing.MemoryObject{} + err := tree.Encode(obj) + c.Assert(err, IsNil) + } +} + func (s *TreeSuite) TestTreeDecodeEncodeIdempotent(c *C) { trees := []*Tree{ { @@ -231,6 +256,7 @@ func (s *TreeSuite) TestTreeDecodeEncodeIdempotent(c *C) { }, } for _, tree := range trees { + sort.Sort(TreeEntrySorter(tree.Entries)) obj := &plumbing.MemoryObject{} err := tree.Encode(obj) c.Assert(err, IsNil) diff --git a/plumbing/object/treenoder.go b/plumbing/object/treenoder.go index 6e7b334cb..2adb64528 100644 --- a/plumbing/object/treenoder.go +++ b/plumbing/object/treenoder.go @@ -88,7 +88,9 @@ func (t *treeNoder) Children() ([]noder.Noder, error) { } } - return transformChildren(parent) + var err error + t.children, err = transformChildren(parent) + return t.children, err } // Returns the children of a tree as treenoders. diff --git a/plumbing/transport/http/common.go b/plumbing/transport/http/common.go index 54126febf..1c4ceee68 100644 --- a/plumbing/transport/http/common.go +++ b/plumbing/transport/http/common.go @@ -91,9 +91,9 @@ func advertisedReferences(ctx context.Context, s *session, serviceName string) ( } type client struct { - c *http.Client + client *http.Client transports *lru.Cache - m sync.RWMutex + mutex sync.RWMutex } // ClientOptions holds user configurable options for the client. @@ -147,7 +147,7 @@ func NewClientWithOptions(c *http.Client, opts *ClientOptions) transport.Transpo } } cl := &client{ - c: c, + client: c, } if opts != nil { @@ -234,10 +234,10 @@ func newSession(c *client, ep *transport.Endpoint, auth transport.AuthMethod) (* // if the client wasn't configured to have a cache for transports then just configure // the transport and use it directly, otherwise try to use the cache. if c.transports == nil { - tr, ok := c.c.Transport.(*http.Transport) + tr, ok := c.client.Transport.(*http.Transport) if !ok { return nil, fmt.Errorf("expected underlying client transport to be of type: %s; got: %s", - reflect.TypeOf(transport), reflect.TypeOf(c.c.Transport)) + reflect.TypeOf(transport), reflect.TypeOf(c.client.Transport)) } transport = tr.Clone() @@ -258,7 +258,7 @@ func newSession(c *client, ep *transport.Endpoint, auth transport.AuthMethod) (* transport, found = c.fetchTransport(transportOpts) if !found { - transport = c.c.Transport.(*http.Transport).Clone() + transport = c.client.Transport.(*http.Transport).Clone() configureTransport(transport, ep) c.addTransport(transportOpts, transport) } @@ -266,12 +266,12 @@ func newSession(c *client, ep *transport.Endpoint, auth transport.AuthMethod) (* httpClient = &http.Client{ Transport: transport, - CheckRedirect: c.c.CheckRedirect, - Jar: c.c.Jar, - Timeout: c.c.Timeout, + CheckRedirect: c.client.CheckRedirect, + Jar: c.client.Jar, + Timeout: c.client.Timeout, } } else { - httpClient = c.c + httpClient = c.client } s := &session{ diff --git a/plumbing/transport/http/common_test.go b/plumbing/transport/http/common_test.go index 6bd018bb4..c831ac3c8 100644 --- a/plumbing/transport/http/common_test.go +++ b/plumbing/transport/http/common_test.go @@ -46,7 +46,7 @@ func (s *UploadPackSuite) TestNewClient(c *C) { cl := &http.Client{Transport: roundTripper} r, ok := NewClient(cl).(*client) c.Assert(ok, Equals, true) - c.Assert(r.c, Equals, cl) + c.Assert(r.client, Equals, cl) } func (s *ClientSuite) TestNewBasicAuth(c *C) { diff --git a/plumbing/transport/http/transport.go b/plumbing/transport/http/transport.go index 052f3c8e2..c8db38920 100644 --- a/plumbing/transport/http/transport.go +++ b/plumbing/transport/http/transport.go @@ -14,21 +14,21 @@ type transportOptions struct { } func (c *client) addTransport(opts transportOptions, transport *http.Transport) { - c.m.Lock() + c.mutex.Lock() c.transports.Add(opts, transport) - c.m.Unlock() + c.mutex.Unlock() } func (c *client) removeTransport(opts transportOptions) { - c.m.Lock() + c.mutex.Lock() c.transports.Remove(opts) - c.m.Unlock() + c.mutex.Unlock() } func (c *client) fetchTransport(opts transportOptions) (*http.Transport, bool) { - c.m.RLock() + c.mutex.RLock() t, ok := c.transports.Get(opts) - c.m.RUnlock() + c.mutex.RUnlock() if !ok { return nil, false } diff --git a/plumbing/transport/ssh/common.go b/plumbing/transport/ssh/common.go index 46fda73fa..05dea448f 100644 --- a/plumbing/transport/ssh/common.go +++ b/plumbing/transport/ssh/common.go @@ -49,7 +49,9 @@ type runner struct { func (r *runner) Command(cmd string, ep *transport.Endpoint, auth transport.AuthMethod) (common.Command, error) { c := &command{command: cmd, endpoint: ep, config: r.config} if auth != nil { - c.setAuth(auth) + if err := c.setAuth(auth); err != nil { + return nil, err + } } if err := c.connect(); err != nil { diff --git a/plumbing/transport/ssh/common_test.go b/plumbing/transport/ssh/common_test.go index 4cc2a0693..a72493686 100644 --- a/plumbing/transport/ssh/common_test.go +++ b/plumbing/transport/ssh/common_test.go @@ -206,3 +206,26 @@ func (c *mockSSHConfig) Get(alias, key string) string { return a[key] } + +type invalidAuthMethod struct { +} + +func (a *invalidAuthMethod) Name() string { + return "invalid" +} + +func (a *invalidAuthMethod) String() string { + return "invalid" +} + +func (s *SuiteCommon) TestCommandWithInvalidAuthMethod(c *C) { + uploadPack := &UploadPackSuite{} + uploadPack.SetUpSuite(c) + r := &runner{} + auth := &invalidAuthMethod{} + + _, err := r.Command("command", uploadPack.newEndpoint(c, "endpoint"), auth) + + c.Assert(err, NotNil) + c.Assert(err, ErrorMatches, "invalid auth method") +} diff --git a/remote.go b/remote.go index 0cb70bc00..7cc0db9b7 100644 --- a/remote.go +++ b/remote.go @@ -470,6 +470,14 @@ func (r *Remote) fetch(ctx context.Context, o *FetchOptions) (sto storer.Referen } } + var updatedPrune bool + if o.Prune { + updatedPrune, err = r.pruneRemotes(o.RefSpecs, localRefs, remoteRefs) + if err != nil { + return nil, err + } + } + updated, err := r.updateLocalReferenceStorage(o.RefSpecs, refs, remoteRefs, specToRefs, o.Tags, o.Force) if err != nil { return nil, err @@ -482,7 +490,7 @@ func (r *Remote) fetch(ctx context.Context, o *FetchOptions) (sto storer.Referen } } - if !updated { + if !updated && !updatedPrune { return remoteRefs, NoErrAlreadyUpToDate } @@ -574,6 +582,27 @@ func (r *Remote) fetchPack(ctx context.Context, o *FetchOptions, s transport.Upl return err } +func (r *Remote) pruneRemotes(specs []config.RefSpec, localRefs []*plumbing.Reference, remoteRefs memory.ReferenceStorage) (bool, error) { + var updatedPrune bool + for _, spec := range specs { + rev := spec.Reverse() + for _, ref := range localRefs { + if !rev.Match(ref.Name()) { + continue + } + _, err := remoteRefs.Reference(rev.Dst(ref.Name())) + if errors.Is(err, plumbing.ErrReferenceNotFound) { + updatedPrune = true + err := r.s.RemoveReference(ref.Name()) + if err != nil { + return false, err + } + } + } + } + return updatedPrune, nil +} + func (r *Remote) addReferencesToUpdate( refspecs []config.RefSpec, localRefs []*plumbing.Reference, @@ -1099,7 +1128,7 @@ func isFastForward(s storer.EncodedObjectStorer, old, new plumbing.Hash, earlies } found := false - // stop iterating at the earlist shallow commit, ignoring its parents + // stop iterating at the earliest shallow commit, ignoring its parents // note: when pull depth is smaller than the number of new changes on the remote, this fails due to missing parents. // as far as i can tell, without the commits in-between the shallow pull and the earliest shallow, there's no // real way of telling whether it will be a fast-forward merge. diff --git a/remote_test.go b/remote_test.go index 81c60bcb8..1ffedd4a9 100644 --- a/remote_test.go +++ b/remote_test.go @@ -1444,6 +1444,120 @@ func (s *RemoteSuite) TestPushRequireRemoteRefs(c *C) { c.Assert(newRef, Not(DeepEquals), oldRef) } +func (s *RemoteSuite) TestFetchPrune(c *C) { + fs := fixtures.Basic().One().DotGit() + + url, clean := s.TemporalDir() + defer clean() + + _, err := PlainClone(url, true, &CloneOptions{ + URL: fs.Root(), + }) + c.Assert(err, IsNil) + + dir, clean := s.TemporalDir() + defer clean() + + r, err := PlainClone(dir, true, &CloneOptions{ + URL: url, + }) + c.Assert(err, IsNil) + + remote, err := r.Remote(DefaultRemoteName) + c.Assert(err, IsNil) + + ref, err := r.Reference(plumbing.ReferenceName("refs/heads/master"), true) + c.Assert(err, IsNil) + + err = remote.Push(&PushOptions{RefSpecs: []config.RefSpec{ + "refs/heads/master:refs/heads/branch", + }}) + c.Assert(err, IsNil) + + dirSave, clean := s.TemporalDir() + defer clean() + + rSave, err := PlainClone(dirSave, true, &CloneOptions{ + URL: url, + }) + c.Assert(err, IsNil) + + AssertReferences(c, rSave, map[string]string{ + "refs/remotes/origin/branch": ref.Hash().String(), + }) + + err = remote.Push(&PushOptions{RefSpecs: []config.RefSpec{ + ":refs/heads/branch", + }}) + + AssertReferences(c, rSave, map[string]string{ + "refs/remotes/origin/branch": ref.Hash().String(), + }) + + err = rSave.Fetch(&FetchOptions{Prune: true}) + c.Assert(err, IsNil) + + _, err = rSave.Reference("refs/remotes/origin/branch", true) + c.Assert(err, ErrorMatches, "reference not found") +} + +func (s *RemoteSuite) TestFetchPruneTags(c *C) { + fs := fixtures.Basic().One().DotGit() + + url, clean := s.TemporalDir() + defer clean() + + _, err := PlainClone(url, true, &CloneOptions{ + URL: fs.Root(), + }) + c.Assert(err, IsNil) + + dir, clean := s.TemporalDir() + defer clean() + + r, err := PlainClone(dir, true, &CloneOptions{ + URL: url, + }) + c.Assert(err, IsNil) + + remote, err := r.Remote(DefaultRemoteName) + c.Assert(err, IsNil) + + ref, err := r.Reference(plumbing.ReferenceName("refs/heads/master"), true) + c.Assert(err, IsNil) + + err = remote.Push(&PushOptions{RefSpecs: []config.RefSpec{ + "refs/heads/master:refs/tags/v1", + }}) + c.Assert(err, IsNil) + + dirSave, clean := s.TemporalDir() + defer clean() + + rSave, err := PlainClone(dirSave, true, &CloneOptions{ + URL: url, + }) + c.Assert(err, IsNil) + + AssertReferences(c, rSave, map[string]string{ + "refs/tags/v1": ref.Hash().String(), + }) + + err = remote.Push(&PushOptions{RefSpecs: []config.RefSpec{ + ":refs/tags/v1", + }}) + + AssertReferences(c, rSave, map[string]string{ + "refs/tags/v1": ref.Hash().String(), + }) + + err = rSave.Fetch(&FetchOptions{Prune: true, RefSpecs: []config.RefSpec{"refs/tags/*:refs/tags/*"}}) + c.Assert(err, IsNil) + + _, err = rSave.Reference("refs/tags/v1", true) + c.Assert(err, ErrorMatches, "reference not found") +} + func (s *RemoteSuite) TestCanPushShasToReference(c *C) { d, err := os.MkdirTemp("", "TestCanPushShasToReference") c.Assert(err, IsNil) diff --git a/repository.go b/repository.go index 1524a6913..a57c7141f 100644 --- a/repository.go +++ b/repository.go @@ -51,19 +51,21 @@ var ( // ErrFetching is returned when the packfile could not be downloaded ErrFetching = errors.New("unable to fetch packfile") - ErrInvalidReference = errors.New("invalid reference, should be a tag or a branch") - ErrRepositoryNotExists = errors.New("repository does not exist") - ErrRepositoryIncomplete = errors.New("repository's commondir path does not exist") - ErrRepositoryAlreadyExists = errors.New("repository already exists") - ErrRemoteNotFound = errors.New("remote not found") - ErrRemoteExists = errors.New("remote already exists") - ErrAnonymousRemoteName = errors.New("anonymous remote name must be 'anonymous'") - ErrWorktreeNotProvided = errors.New("worktree should be provided") - ErrIsBareRepository = errors.New("worktree not available in a bare repository") - ErrUnableToResolveCommit = errors.New("unable to resolve commit") - ErrPackedObjectsNotSupported = errors.New("packed objects not supported") - ErrSHA256NotSupported = errors.New("go-git was not compiled with SHA256 support") - ErrAlternatePathNotSupported = errors.New("alternate path must use the file scheme") + ErrInvalidReference = errors.New("invalid reference, should be a tag or a branch") + ErrRepositoryNotExists = errors.New("repository does not exist") + ErrRepositoryIncomplete = errors.New("repository's commondir path does not exist") + ErrRepositoryAlreadyExists = errors.New("repository already exists") + ErrRemoteNotFound = errors.New("remote not found") + ErrRemoteExists = errors.New("remote already exists") + ErrAnonymousRemoteName = errors.New("anonymous remote name must be 'anonymous'") + ErrWorktreeNotProvided = errors.New("worktree should be provided") + ErrIsBareRepository = errors.New("worktree not available in a bare repository") + ErrUnableToResolveCommit = errors.New("unable to resolve commit") + ErrPackedObjectsNotSupported = errors.New("packed objects not supported") + ErrSHA256NotSupported = errors.New("go-git was not compiled with SHA256 support") + ErrAlternatePathNotSupported = errors.New("alternate path must use the file scheme") + ErrUnsupportedMergeStrategy = errors.New("unsupported merge strategy") + ErrFastForwardMergeNotPossible = errors.New("not possible to fast-forward merge changes") ) // Repository represents a git repository @@ -1769,8 +1771,43 @@ func (r *Repository) RepackObjects(cfg *RepackConfig) (err error) { return nil } +// Merge merges the reference branch into the current branch. +// +// If the merge is not possible (or supported) returns an error without changing +// the HEAD for the current branch. Possible errors include: +// - The merge strategy is not supported. +// - The specific strategy cannot be used (e.g. using FastForwardMerge when one is not possible). +func (r *Repository) Merge(ref plumbing.Reference, opts MergeOptions) error { + if opts.Strategy != FastForwardMerge { + return ErrUnsupportedMergeStrategy + } + + // Ignore error as not having a shallow list is optional here. + shallowList, _ := r.Storer.Shallow() + var earliestShallow *plumbing.Hash + if len(shallowList) > 0 { + earliestShallow = &shallowList[0] + } + + head, err := r.Head() + if err != nil { + return err + } + + ff, err := isFastForward(r.Storer, head.Hash(), ref.Hash(), earliestShallow) + if err != nil { + return err + } + + if !ff { + return ErrFastForwardMergeNotPossible + } + + return r.Storer.SetReference(plumbing.NewHashReference(head.Name(), ref.Hash())) +} + // createNewObjectPack is a helper for RepackObjects taking care -// of creating a new pack. It is used so the the PackfileWriter +// of creating a new pack. It is used so the PackfileWriter // deferred close has the right scope. func (r *Repository) createNewObjectPack(cfg *RepackConfig) (h plumbing.Hash, err error) { ow := newObjectWalker(r.Storer) diff --git a/repository_test.go b/repository_test.go index 51df84512..b211f8cee 100644 --- a/repository_test.go +++ b/repository_test.go @@ -82,7 +82,7 @@ func (s *RepositorySuite) TestInitWithInvalidDefaultBranch(c *C) { c.Assert(err, NotNil) } -func createCommit(c *C, r *Repository) { +func createCommit(c *C, r *Repository) plumbing.Hash { // Create a commit so there is a HEAD to check wt, err := r.Worktree() c.Assert(err, IsNil) @@ -101,13 +101,14 @@ func createCommit(c *C, r *Repository) { Email: "go-git@fake.local", When: time.Now(), } - _, err = wt.Commit("test commit message", &CommitOptions{ + + h, err := wt.Commit("test commit message", &CommitOptions{ All: true, Author: &author, Committer: &author, }) c.Assert(err, IsNil) - + return h } func (s *RepositorySuite) TestInitNonStandardDotGit(c *C) { @@ -439,6 +440,112 @@ func (s *RepositorySuite) TestCreateBranchAndBranch(c *C) { c.Assert(branch.Merge, Equals, testBranch.Merge) } +func (s *RepositorySuite) TestMergeFF(c *C) { + r, err := Init(memory.NewStorage(), memfs.New()) + c.Assert(err, IsNil) + c.Assert(r, NotNil) + + createCommit(c, r) + createCommit(c, r) + createCommit(c, r) + lastCommit := createCommit(c, r) + + wt, err := r.Worktree() + c.Assert(err, IsNil) + + targetBranch := plumbing.NewBranchReferenceName("foo") + err = wt.Checkout(&CheckoutOptions{ + Hash: lastCommit, + Create: true, + Branch: targetBranch, + }) + c.Assert(err, IsNil) + + createCommit(c, r) + fooHash := createCommit(c, r) + + // Checkout the master branch so that we can try to merge foo into it. + err = wt.Checkout(&CheckoutOptions{ + Branch: plumbing.Master, + }) + c.Assert(err, IsNil) + + head, err := r.Head() + c.Assert(err, IsNil) + c.Assert(head.Hash(), Equals, lastCommit) + + targetRef := plumbing.NewHashReference(targetBranch, fooHash) + c.Assert(targetRef, NotNil) + + err = r.Merge(*targetRef, MergeOptions{ + Strategy: FastForwardMerge, + }) + c.Assert(err, IsNil) + + head, err = r.Head() + c.Assert(err, IsNil) + c.Assert(head.Hash(), Equals, fooHash) +} + +func (s *RepositorySuite) TestMergeFF_Invalid(c *C) { + r, err := Init(memory.NewStorage(), memfs.New()) + c.Assert(err, IsNil) + c.Assert(r, NotNil) + + // Keep track of the first commit, which will be the + // reference to create the target branch so that we + // can simulate a non-ff merge. + firstCommit := createCommit(c, r) + createCommit(c, r) + createCommit(c, r) + lastCommit := createCommit(c, r) + + wt, err := r.Worktree() + c.Assert(err, IsNil) + + targetBranch := plumbing.NewBranchReferenceName("foo") + err = wt.Checkout(&CheckoutOptions{ + Hash: firstCommit, + Create: true, + Branch: targetBranch, + }) + + c.Assert(err, IsNil) + + createCommit(c, r) + h := createCommit(c, r) + + // Checkout the master branch so that we can try to merge foo into it. + err = wt.Checkout(&CheckoutOptions{ + Branch: plumbing.Master, + }) + c.Assert(err, IsNil) + + head, err := r.Head() + c.Assert(err, IsNil) + c.Assert(head.Hash(), Equals, lastCommit) + + targetRef := plumbing.NewHashReference(targetBranch, h) + c.Assert(targetRef, NotNil) + + err = r.Merge(*targetRef, MergeOptions{ + Strategy: MergeStrategy(10), + }) + c.Assert(err, Equals, ErrUnsupportedMergeStrategy) + + // Failed merge operations must not change HEAD. + head, err = r.Head() + c.Assert(err, IsNil) + c.Assert(head.Hash(), Equals, lastCommit) + + err = r.Merge(*targetRef, MergeOptions{}) + c.Assert(err, Equals, ErrFastForwardMergeNotPossible) + + head, err = r.Head() + c.Assert(err, IsNil) + c.Assert(head.Hash(), Equals, lastCommit) +} + func (s *RepositorySuite) TestCreateBranchUnmarshal(c *C) { r, _ := Init(memory.NewStorage(), nil) diff --git a/signer.go b/signer.go new file mode 100644 index 000000000..e3ef7ebd3 --- /dev/null +++ b/signer.go @@ -0,0 +1,33 @@ +package git + +import ( + "io" + + "github.com/go-git/go-git/v5/plumbing" +) + +// signableObject is an object which can be signed. +type signableObject interface { + EncodeWithoutSignature(o plumbing.EncodedObject) error +} + +// Signer is an interface for signing git objects. +// message is a reader containing the encoded object to be signed. +// Implementors should return the encoded signature and an error if any. +// See https://git-scm.com/docs/gitformat-signature for more information. +type Signer interface { + Sign(message io.Reader) ([]byte, error) +} + +func signObject(signer Signer, obj signableObject) ([]byte, error) { + encoded := &plumbing.MemoryObject{} + if err := obj.EncodeWithoutSignature(encoded); err != nil { + return nil, err + } + r, err := encoded.Reader() + if err != nil { + return nil, err + } + + return signer.Sign(r) +} diff --git a/signer_test.go b/signer_test.go new file mode 100644 index 000000000..eba0922d7 --- /dev/null +++ b/signer_test.go @@ -0,0 +1,56 @@ +package git + +import ( + "encoding/base64" + "fmt" + "io" + "time" + + "github.com/go-git/go-billy/v5/memfs" + "github.com/go-git/go-git/v5/plumbing/object" + "github.com/go-git/go-git/v5/storage/memory" +) + +type b64signer struct{} + +// This is not secure, and is only used as an example for testing purposes. +// Please don't do this. +func (b64signer) Sign(message io.Reader) ([]byte, error) { + b, err := io.ReadAll(message) + if err != nil { + return nil, err + } + out := make([]byte, base64.StdEncoding.EncodedLen(len(b))) + base64.StdEncoding.Encode(out, b) + return out, nil +} + +func ExampleSigner() { + repo, err := Init(memory.NewStorage(), memfs.New()) + if err != nil { + panic(err) + } + w, err := repo.Worktree() + if err != nil { + panic(err) + } + commit, err := w.Commit("example commit", &CommitOptions{ + Author: &object.Signature{ + Name: "John Doe", + Email: "john@example.com", + When: time.UnixMicro(1234567890).UTC(), + }, + Signer: b64signer{}, + AllowEmptyCommits: true, + }) + if err != nil { + panic(err) + } + + obj, err := repo.CommitObject(commit) + if err != nil { + panic(err) + } + fmt.Println(obj.PGPSignature) + // Output: dHJlZSA0YjgyNWRjNjQyY2I2ZWI5YTA2MGU1NGJmOGQ2OTI4OGZiZWU0OTA0CmF1dGhvciBKb2huIERvZSA8am9obkBleGFtcGxlLmNvbT4gMTIzNCArMDAwMApjb21taXR0ZXIgSm9obiBEb2UgPGpvaG5AZXhhbXBsZS5jb20+IDEyMzQgKzAwMDAKCmV4YW1wbGUgY29tbWl0 +} diff --git a/utils/merkletrie/filesystem/node.go b/utils/merkletrie/filesystem/node.go index 7bba0d03e..33800627d 100644 --- a/utils/merkletrie/filesystem/node.go +++ b/utils/merkletrie/filesystem/node.go @@ -29,6 +29,8 @@ type node struct { hash []byte children []noder.Noder isDir bool + mode os.FileMode + size int64 } // NewRootNode returns the root node based on a given billy.Filesystem. @@ -48,8 +50,15 @@ func NewRootNode( // difftree algorithm will detect changes in the contents of files and also in // their mode. // +// Please note that the hash is calculated on first invocation of Hash(), +// meaning that it will not update when the underlying file changes +// between invocations. +// // The hash of a directory is always a 24-bytes slice of zero values func (n *node) Hash() []byte { + if n.hash == nil { + n.calculateHash() + } return n.hash } @@ -121,81 +130,74 @@ func (n *node) calculateChildren() error { func (n *node) newChildNode(file os.FileInfo) (*node, error) { path := path.Join(n.path, file.Name()) - hash, err := n.calculateHash(path, file) - if err != nil { - return nil, err - } - node := &node{ fs: n.fs, submodules: n.submodules, path: path, - hash: hash, isDir: file.IsDir(), + size: file.Size(), + mode: file.Mode(), } - if hash, isSubmodule := n.submodules[path]; isSubmodule { - node.hash = append(hash[:], filemode.Submodule.Bytes()...) + if _, isSubmodule := n.submodules[path]; isSubmodule { node.isDir = false } return node, nil } -func (n *node) calculateHash(path string, file os.FileInfo) ([]byte, error) { - if file.IsDir() { - return make([]byte, 24), nil - } - - var hash plumbing.Hash - var err error - if file.Mode()&os.ModeSymlink != 0 { - hash, err = n.doCalculateHashForSymlink(path, file) - } else { - hash, err = n.doCalculateHashForRegular(path, file) +func (n *node) calculateHash() { + if n.isDir { + n.hash = make([]byte, 24) + return } - + mode, err := filemode.NewFromOSFileMode(n.mode) if err != nil { - return nil, err + n.hash = plumbing.ZeroHash[:] + return } - - mode, err := filemode.NewFromOSFileMode(file.Mode()) - if err != nil { - return nil, err + if submoduleHash, isSubmodule := n.submodules[n.path]; isSubmodule { + n.hash = append(submoduleHash[:], filemode.Submodule.Bytes()...) + return } - - return append(hash[:], mode.Bytes()...), nil + var hash plumbing.Hash + if n.mode&os.ModeSymlink != 0 { + hash = n.doCalculateHashForSymlink() + } else { + hash = n.doCalculateHashForRegular() + } + n.hash = append(hash[:], mode.Bytes()...) } -func (n *node) doCalculateHashForRegular(path string, file os.FileInfo) (plumbing.Hash, error) { - f, err := n.fs.Open(path) +func (n *node) doCalculateHashForRegular() plumbing.Hash { + f, err := n.fs.Open(n.path) if err != nil { - return plumbing.ZeroHash, err + return plumbing.ZeroHash } defer f.Close() - h := plumbing.NewHasher(plumbing.BlobObject, file.Size()) + h := plumbing.NewHasher(plumbing.BlobObject, n.size) if _, err := io.Copy(h, f); err != nil { - return plumbing.ZeroHash, err + return plumbing.ZeroHash } - return h.Sum(), nil + return h.Sum() } -func (n *node) doCalculateHashForSymlink(path string, file os.FileInfo) (plumbing.Hash, error) { - target, err := n.fs.Readlink(path) +func (n *node) doCalculateHashForSymlink() plumbing.Hash { + target, err := n.fs.Readlink(n.path) if err != nil { - return plumbing.ZeroHash, err + return plumbing.ZeroHash } - h := plumbing.NewHasher(plumbing.BlobObject, file.Size()) + h := plumbing.NewHasher(plumbing.BlobObject, n.size) if _, err := h.Write([]byte(target)); err != nil { - return plumbing.ZeroHash, err + return plumbing.ZeroHash } - return h.Sum(), nil + return h.Sum() } func (n *node) String() string { diff --git a/worktree.go b/worktree.go index ad525c1a4..ab11d42db 100644 --- a/worktree.go +++ b/worktree.go @@ -227,20 +227,17 @@ func (w *Worktree) createBranch(opts *CheckoutOptions) error { } func (w *Worktree) getCommitFromCheckoutOptions(opts *CheckoutOptions) (plumbing.Hash, error) { - if !opts.Hash.IsZero() { - return opts.Hash, nil - } - - b, err := w.r.Reference(opts.Branch, true) - if err != nil { - return plumbing.ZeroHash, err - } + hash := opts.Hash + if hash.IsZero() { + b, err := w.r.Reference(opts.Branch, true) + if err != nil { + return plumbing.ZeroHash, err + } - if !b.Name().IsTag() { - return b.Hash(), nil + hash = b.Hash() } - o, err := w.r.Object(plumbing.AnyObject, b.Hash()) + o, err := w.r.Object(plumbing.AnyObject, hash) if err != nil { return plumbing.ZeroHash, err } @@ -248,7 +245,7 @@ func (w *Worktree) getCommitFromCheckoutOptions(opts *CheckoutOptions) (plumbing switch o := o.(type) { case *object.Tag: if o.TargetType != plumbing.CommitObject { - return plumbing.ZeroHash, fmt.Errorf("unsupported tag object target %q", o.TargetType) + return plumbing.ZeroHash, fmt.Errorf("%w: tag target %q", object.ErrUnsupportedObject, o.TargetType) } return o.Target, nil @@ -256,7 +253,7 @@ func (w *Worktree) getCommitFromCheckoutOptions(opts *CheckoutOptions) (plumbing return o.Hash, nil } - return plumbing.ZeroHash, fmt.Errorf("unsupported tag target %q", o.Type()) + return plumbing.ZeroHash, fmt.Errorf("%w: %q", object.ErrUnsupportedObject, o.Type()) } func (w *Worktree) setHEADToCommit(commit plumbing.Hash) error { @@ -431,6 +428,10 @@ var worktreeDeny = map[string]struct{}{ func validPath(paths ...string) error { for _, p := range paths { parts := strings.FieldsFunc(p, func(r rune) bool { return (r == '\\' || r == '/') }) + if len(parts) == 0 { + return fmt.Errorf("invalid path: %q", p) + } + if _, denied := worktreeDeny[strings.ToLower(parts[0])]; denied { return fmt.Errorf("invalid path prefix: %q", p) } diff --git a/worktree_commit.go b/worktree_commit.go index eaa21c3f1..f62054bcb 100644 --- a/worktree_commit.go +++ b/worktree_commit.go @@ -3,6 +3,7 @@ package git import ( "bytes" "errors" + "io" "path" "sort" "strings" @@ -14,6 +15,7 @@ import ( "github.com/go-git/go-git/v5/storage" "github.com/ProtonMail/go-crypto/openpgp" + "github.com/ProtonMail/go-crypto/openpgp/packet" "github.com/go-git/go-billy/v5" ) @@ -43,29 +45,30 @@ func (w *Worktree) Commit(msg string, opts *CommitOptions) (plumbing.Hash, error if err != nil { return plumbing.ZeroHash, err } - - t, err := w.r.getTreeFromCommitHash(head.Hash()) + headCommit, err := w.r.CommitObject(head.Hash()) if err != nil { return plumbing.ZeroHash, err } - treeHash = t.Hash - opts.Parents = []plumbing.Hash{head.Hash()} - } else { - idx, err := w.r.Storer.Index() - if err != nil { - return plumbing.ZeroHash, err + opts.Parents = nil + if len(headCommit.ParentHashes) != 0 { + opts.Parents = []plumbing.Hash{headCommit.ParentHashes[0]} } + } - h := &buildTreeHelper{ - fs: w.Filesystem, - s: w.r.Storer, - } + idx, err := w.r.Storer.Index() + if err != nil { + return plumbing.ZeroHash, err + } - treeHash, err = h.BuildTree(idx, opts) - if err != nil { - return plumbing.ZeroHash, err - } + h := &buildTreeHelper{ + fs: w.Filesystem, + s: w.r.Storer, + } + + treeHash, err = h.BuildTree(idx, opts) + if err != nil { + return plumbing.ZeroHash, err } commit, err := w.buildCommitObject(msg, opts, treeHash) @@ -125,12 +128,17 @@ func (w *Worktree) buildCommitObject(msg string, opts *CommitOptions, tree plumb ParentHashes: opts.Parents, } - if opts.SignKey != nil { - sig, err := w.buildCommitSignature(commit, opts.SignKey) + // Convert SignKey into a Signer if set. Existing Signer should take priority. + signer := opts.Signer + if signer == nil && opts.SignKey != nil { + signer = &gpgSigner{key: opts.SignKey} + } + if signer != nil { + sig, err := signObject(signer, commit) if err != nil { return plumbing.ZeroHash, err } - commit.PGPSignature = sig + commit.PGPSignature = string(sig) } obj := w.r.Storer.NewEncodedObject() @@ -140,20 +148,17 @@ func (w *Worktree) buildCommitObject(msg string, opts *CommitOptions, tree plumb return w.r.Storer.SetEncodedObject(obj) } -func (w *Worktree) buildCommitSignature(commit *object.Commit, signKey *openpgp.Entity) (string, error) { - encoded := &plumbing.MemoryObject{} - if err := commit.Encode(encoded); err != nil { - return "", err - } - r, err := encoded.Reader() - if err != nil { - return "", err - } +type gpgSigner struct { + key *openpgp.Entity + cfg *packet.Config +} + +func (s *gpgSigner) Sign(message io.Reader) ([]byte, error) { var b bytes.Buffer - if err := openpgp.ArmoredDetachSign(&b, signKey, r, nil); err != nil { - return "", err + if err := openpgp.ArmoredDetachSign(&b, s.key, message, s.cfg); err != nil { + return nil, err } - return b.String(), nil + return b.Bytes(), nil } // buildTreeHelper converts a given index.Index file into multiple git objects @@ -263,4 +268,4 @@ func (h *buildTreeHelper) copyTreeToStorageRecursive(parent string, t *object.Tr return hash, nil } return h.s.SetEncodedObject(o) -} \ No newline at end of file +} diff --git a/worktree_commit_test.go b/worktree_commit_test.go index 1ac1990f4..fee8b1548 100644 --- a/worktree_commit_test.go +++ b/worktree_commit_test.go @@ -131,17 +131,149 @@ func (s *WorktreeSuite) TestCommitAmend(c *C) { _, err = w.Commit("foo\n", &CommitOptions{Author: defaultSignature()}) c.Assert(err, IsNil) + util.WriteFile(fs, "bar", []byte("bar"), 0644) + + _, err = w.Add("bar") + c.Assert(err, IsNil) amendedHash, err := w.Commit("bar\n", &CommitOptions{Amend: true}) c.Assert(err, IsNil) headRef, err := w.r.Head() + c.Assert(err, IsNil) + c.Assert(amendedHash, Equals, headRef.Hash()) + commit, err := w.r.CommitObject(headRef.Hash()) c.Assert(err, IsNil) c.Assert(commit.Message, Equals, "bar\n") + c.Assert(commit.NumParents(), Equals, 1) + + stats, err := commit.Stats() + c.Assert(err, IsNil) + c.Assert(stats, HasLen, 2) + c.Assert(stats[0], Equals, object.FileStat{ + Name: "bar", + Addition: 1, + }) + c.Assert(stats[1], Equals, object.FileStat{ + Name: "foo", + Addition: 1, + }) + + assertStorageStatus(c, s.Repository, 14, 12, 11, amendedHash) +} + +func (s *WorktreeSuite) TestAddAndCommitWithSkipStatus(c *C) { + expected := plumbing.NewHash("375a3808ffde7f129cdd3c8c252fd0fe37cfd13b") + + fs := memfs.New() + w := &Worktree{ + r: s.Repository, + Filesystem: fs, + } + + err := w.Checkout(&CheckoutOptions{}) + c.Assert(err, IsNil) + + util.WriteFile(fs, "LICENSE", []byte("foo"), 0644) + util.WriteFile(fs, "foo", []byte("foo"), 0644) + + err = w.AddWithOptions(&AddOptions{ + Path: "foo", + SkipStatus: true, + }) + c.Assert(err, IsNil) + + hash, err := w.Commit("commit foo only\n", &CommitOptions{ + Author: defaultSignature(), + }) + + c.Assert(hash, Equals, expected) + c.Assert(err, IsNil) + + assertStorageStatus(c, s.Repository, 13, 11, 10, expected) +} + +func (s *WorktreeSuite) TestAddAndCommitWithSkipStatusPathNotModified(c *C) { + expected := plumbing.NewHash("375a3808ffde7f129cdd3c8c252fd0fe37cfd13b") + expected2 := plumbing.NewHash("8691273baf8f6ee2cccfc05e910552c04d02d472") + + fs := memfs.New() + w := &Worktree{ + r: s.Repository, + Filesystem: fs, + } + + err := w.Checkout(&CheckoutOptions{}) + c.Assert(err, IsNil) + + util.WriteFile(fs, "foo", []byte("foo"), 0644) + + status, err := w.Status() + c.Assert(err, IsNil) + foo := status.File("foo") + c.Assert(foo.Staging, Equals, Untracked) + c.Assert(foo.Worktree, Equals, Untracked) + + err = w.AddWithOptions(&AddOptions{ + Path: "foo", + SkipStatus: true, + }) + c.Assert(err, IsNil) + + status, err = w.Status() + c.Assert(err, IsNil) + foo = status.File("foo") + c.Assert(foo.Staging, Equals, Added) + c.Assert(foo.Worktree, Equals, Unmodified) + + hash, err := w.Commit("commit foo only\n", &CommitOptions{All: true, + Author: defaultSignature(), + }) + c.Assert(hash, Equals, expected) + c.Assert(err, IsNil) + commit1, err := w.r.CommitObject(hash) + + status, err = w.Status() + c.Assert(err, IsNil) + foo = status.File("foo") + c.Assert(foo.Staging, Equals, Untracked) + c.Assert(foo.Worktree, Equals, Untracked) + + assertStorageStatus(c, s.Repository, 13, 11, 10, expected) + + err = w.AddWithOptions(&AddOptions{ + Path: "foo", + SkipStatus: true, + }) + c.Assert(err, IsNil) + + status, err = w.Status() + c.Assert(err, IsNil) + foo = status.File("foo") + c.Assert(foo.Staging, Equals, Untracked) + c.Assert(foo.Worktree, Equals, Untracked) + + hash, err = w.Commit("commit with no changes\n", &CommitOptions{ + Author: defaultSignature(), + }) + c.Assert(hash, Equals, expected2) + c.Assert(err, IsNil) + commit2, err := w.r.CommitObject(hash) + + status, err = w.Status() + c.Assert(err, IsNil) + foo = status.File("foo") + c.Assert(foo.Staging, Equals, Untracked) + c.Assert(foo.Worktree, Equals, Untracked) + + patch, err := commit2.Patch(commit1) + c.Assert(err, IsNil) + files := patch.FilePatches() + c.Assert(files, IsNil) - assertStorageStatus(c, s.Repository, 13, 11, 11, amendedHash) + assertStorageStatus(c, s.Repository, 13, 11, 11, expected2) } func (s *WorktreeSuite) TestCommitAll(c *C) { diff --git a/worktree_status.go b/worktree_status.go index 730108754..dd9b2439c 100644 --- a/worktree_status.go +++ b/worktree_status.go @@ -271,7 +271,7 @@ func diffTreeIsEquals(a, b noder.Hasher) bool { // no error is returned. When path is a file, the blob.Hash is returned. func (w *Worktree) Add(path string) (plumbing.Hash, error) { // TODO(mcuadros): deprecate in favor of AddWithOption in v6. - return w.doAdd(path, make([]gitignore.Pattern, 0)) + return w.doAdd(path, make([]gitignore.Pattern, 0), false) } func (w *Worktree) doAddDirectory(idx *index.Index, s Status, directory string, ignorePattern []gitignore.Pattern) (added bool, err error) { @@ -321,7 +321,7 @@ func (w *Worktree) AddWithOptions(opts *AddOptions) error { } if opts.All { - _, err := w.doAdd(".", w.Excludes) + _, err := w.doAdd(".", w.Excludes, false) return err } @@ -329,16 +329,11 @@ func (w *Worktree) AddWithOptions(opts *AddOptions) error { return w.AddGlob(opts.Glob) } - _, err := w.Add(opts.Path) + _, err := w.doAdd(opts.Path, make([]gitignore.Pattern, 0), opts.SkipStatus) return err } -func (w *Worktree) doAdd(path string, ignorePattern []gitignore.Pattern) (plumbing.Hash, error) { - s, err := w.Status() - if err != nil { - return plumbing.ZeroHash, err - } - +func (w *Worktree) doAdd(path string, ignorePattern []gitignore.Pattern, skipStatus bool) (plumbing.Hash, error) { idx, err := w.r.Storer.Index() if err != nil { return plumbing.ZeroHash, err @@ -348,6 +343,17 @@ func (w *Worktree) doAdd(path string, ignorePattern []gitignore.Pattern) (plumbi var added bool fi, err := w.Filesystem.Lstat(path) + + // status is required for doAddDirectory + var s Status + var err2 error + if !skipStatus || fi == nil || fi.IsDir() { + s, err2 = w.Status() + if err2 != nil { + return plumbing.ZeroHash, err2 + } + } + if err != nil || !fi.IsDir() { added, h, err = w.doAddFile(idx, s, path, ignorePattern) } else { @@ -421,8 +427,9 @@ func (w *Worktree) AddGlob(pattern string) error { // doAddFile create a new blob from path and update the index, added is true if // the file added is different from the index. +// if s status is nil will skip the status check and update the index anyway func (w *Worktree) doAddFile(idx *index.Index, s Status, path string, ignorePattern []gitignore.Pattern) (added bool, h plumbing.Hash, err error) { - if s.File(path).Worktree == Unmodified { + if s != nil && s.File(path).Worktree == Unmodified { return false, h, nil } if len(ignorePattern) > 0 { diff --git a/worktree_test.go b/worktree_test.go index 50ff189fa..668c30a70 100644 --- a/worktree_test.go +++ b/worktree_test.go @@ -886,6 +886,41 @@ func (s *WorktreeSuite) TestCheckoutTag(c *C) { c.Assert(head.Name().String(), Equals, "HEAD") } +func (s *WorktreeSuite) TestCheckoutTagHash(c *C) { + f := fixtures.ByTag("tags").One() + r := s.NewRepositoryWithEmptyWorktree(f) + w, err := r.Worktree() + c.Assert(err, IsNil) + + for _, hash := range []string{ + "b742a2a9fa0afcfa9a6fad080980fbc26b007c69", // annotated tag + "ad7897c0fb8e7d9a9ba41fa66072cf06095a6cfc", // commit tag + "f7b877701fbf855b44c0a9e86f3fdce2c298b07f", // lightweight tag + } { + err = w.Checkout(&CheckoutOptions{ + Hash: plumbing.NewHash(hash), + }) + c.Assert(err, IsNil) + head, err := w.r.Head() + c.Assert(err, IsNil) + c.Assert(head.Name().String(), Equals, "HEAD") + + status, err := w.Status() + c.Assert(err, IsNil) + c.Assert(status.IsClean(), Equals, true) + } + + for _, hash := range []string{ + "fe6cb94756faa81e5ed9240f9191b833db5f40ae", // blob tag + "152175bf7e5580299fa1f0ba41ef6474cc043b70", // tree tag + } { + err = w.Checkout(&CheckoutOptions{ + Hash: plumbing.NewHash(hash), + }) + c.Assert(err, NotNil) + } +} + func (s *WorktreeSuite) TestCheckoutBisect(c *C) { if testing.Short() { c.Skip("skipping test in short mode.") @@ -1895,6 +1930,166 @@ func (s *WorktreeSuite) TestAddGlobErrorNoMatches(c *C) { c.Assert(err, Equals, ErrGlobNoMatches) } +func (s *WorktreeSuite) TestAddSkipStatusAddedPath(c *C) { + fs := memfs.New() + w := &Worktree{ + r: s.Repository, + Filesystem: fs, + } + + err := w.Checkout(&CheckoutOptions{Force: true}) + c.Assert(err, IsNil) + + idx, err := w.r.Storer.Index() + c.Assert(err, IsNil) + c.Assert(idx.Entries, HasLen, 9) + + err = util.WriteFile(w.Filesystem, "file1", []byte("file1"), 0644) + c.Assert(err, IsNil) + + err = w.AddWithOptions(&AddOptions{Path: "file1", SkipStatus: true}) + c.Assert(err, IsNil) + + idx, err = w.r.Storer.Index() + c.Assert(err, IsNil) + c.Assert(idx.Entries, HasLen, 10) + + e, err := idx.Entry("file1") + c.Assert(err, IsNil) + c.Assert(e.Mode, Equals, filemode.Regular) + + status, err := w.Status() + c.Assert(err, IsNil) + c.Assert(status, HasLen, 1) + + file := status.File("file1") + c.Assert(file.Staging, Equals, Added) + c.Assert(file.Worktree, Equals, Unmodified) +} + +func (s *WorktreeSuite) TestAddSkipStatusModifiedPath(c *C) { + fs := memfs.New() + w := &Worktree{ + r: s.Repository, + Filesystem: fs, + } + + err := w.Checkout(&CheckoutOptions{Force: true}) + c.Assert(err, IsNil) + + idx, err := w.r.Storer.Index() + c.Assert(err, IsNil) + c.Assert(idx.Entries, HasLen, 9) + + err = util.WriteFile(w.Filesystem, "LICENSE", []byte("file1"), 0644) + c.Assert(err, IsNil) + + err = w.AddWithOptions(&AddOptions{Path: "LICENSE", SkipStatus: true}) + c.Assert(err, IsNil) + + idx, err = w.r.Storer.Index() + c.Assert(err, IsNil) + c.Assert(idx.Entries, HasLen, 9) + + e, err := idx.Entry("LICENSE") + c.Assert(err, IsNil) + c.Assert(e.Mode, Equals, filemode.Regular) + + status, err := w.Status() + c.Assert(err, IsNil) + c.Assert(status, HasLen, 1) + + file := status.File("LICENSE") + c.Assert(file.Staging, Equals, Modified) + c.Assert(file.Worktree, Equals, Unmodified) +} + +func (s *WorktreeSuite) TestAddSkipStatusNonModifiedPath(c *C) { + fs := memfs.New() + w := &Worktree{ + r: s.Repository, + Filesystem: fs, + } + + err := w.Checkout(&CheckoutOptions{Force: true}) + c.Assert(err, IsNil) + + idx, err := w.r.Storer.Index() + c.Assert(err, IsNil) + c.Assert(idx.Entries, HasLen, 9) + + err = w.AddWithOptions(&AddOptions{Path: "LICENSE", SkipStatus: true}) + c.Assert(err, IsNil) + + idx, err = w.r.Storer.Index() + c.Assert(err, IsNil) + c.Assert(idx.Entries, HasLen, 9) + + e, err := idx.Entry("LICENSE") + c.Assert(err, IsNil) + c.Assert(e.Mode, Equals, filemode.Regular) + + status, err := w.Status() + c.Assert(err, IsNil) + c.Assert(status, HasLen, 0) + + file := status.File("LICENSE") + c.Assert(file.Staging, Equals, Untracked) + c.Assert(file.Worktree, Equals, Untracked) +} + +func (s *WorktreeSuite) TestAddSkipStatusWithIgnoredPath(c *C) { + fs := memfs.New() + w := &Worktree{ + r: s.Repository, + Filesystem: fs, + } + + err := w.Checkout(&CheckoutOptions{Force: true}) + c.Assert(err, IsNil) + + idx, err := w.r.Storer.Index() + c.Assert(err, IsNil) + c.Assert(idx.Entries, HasLen, 9) + + err = util.WriteFile(fs, ".gitignore", []byte("fileToIgnore\n"), 0755) + c.Assert(err, IsNil) + _, err = w.Add(".gitignore") + c.Assert(err, IsNil) + _, err = w.Commit("Added .gitignore", defaultTestCommitOptions) + c.Assert(err, IsNil) + + err = util.WriteFile(fs, "fileToIgnore", []byte("file to ignore"), 0644) + c.Assert(err, IsNil) + + status, err := w.Status() + c.Assert(err, IsNil) + c.Assert(status, HasLen, 0) + + file := status.File("fileToIgnore") + c.Assert(file.Staging, Equals, Untracked) + c.Assert(file.Worktree, Equals, Untracked) + + err = w.AddWithOptions(&AddOptions{Path: "fileToIgnore", SkipStatus: true}) + c.Assert(err, IsNil) + + idx, err = w.r.Storer.Index() + c.Assert(err, IsNil) + c.Assert(idx.Entries, HasLen, 10) + + e, err := idx.Entry("fileToIgnore") + c.Assert(err, IsNil) + c.Assert(e.Mode, Equals, filemode.Regular) + + status, err = w.Status() + c.Assert(err, IsNil) + c.Assert(status, HasLen, 1) + + file = status.File("fileToIgnore") + c.Assert(file.Staging, Equals, Added) + c.Assert(file.Worktree, Equals, Unmodified) +} + func (s *WorktreeSuite) TestRemove(c *C) { fs := memfs.New() w := &Worktree{ @@ -2762,6 +2957,8 @@ func TestValidPath(t *testing.T) { {"git~1", true}, {"a/../b", true}, {"a\\..\\b", true}, + {"/", true}, + {"", true}, {".gitmodules", false}, {".gitignore", false}, {"a..b", false},