aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/git.yml2
-rw-r--r--.github/workflows/test.yml5
-rw-r--r--COMPATIBILITY.md18
-rw-r--r--Makefile1
-rw-r--r--_examples/README.md5
-rw-r--r--_examples/checkout-branch/main.go85
-rw-r--r--_examples/common_test.go73
-rw-r--r--blame.go5
-rw-r--r--cli/go-git/go.mod18
-rw-r--r--cli/go-git/go.sum41
-rw-r--r--example_test.go15
-rw-r--r--go.mod25
-rw-r--r--go.sum49
-rw-r--r--internal/test/checkers.go43
-rw-r--r--options.go32
-rw-r--r--options_test.go6
-rw-r--r--plumbing/format/gitattributes/attributes.go14
-rw-r--r--plumbing/format/gitattributes/dir.go14
-rw-r--r--plumbing/format/gitignore/dir.go8
-rw-r--r--plumbing/format/gitignore/dir_test.go16
-rw-r--r--plumbing/format/index/decoder.go104
-rw-r--r--plumbing/format/index/decoder_test.go102
-rw-r--r--plumbing/format/index/encoder.go34
-rw-r--r--plumbing/format/packfile/delta_index.go20
-rw-r--r--plumbing/object/commit_test.go2
-rw-r--r--plumbing/object/commit_walker_path.go19
-rw-r--r--plumbing/object/commit_walker_test.go26
-rw-r--r--plumbing/object/patch.go95
-rw-r--r--plumbing/object/patch_test.go110
-rw-r--r--plumbing/object/tree.go32
-rw-r--r--plumbing/object/tree_test.go26
-rw-r--r--plumbing/object/treenoder.go4
-rw-r--r--plumbing/protocol/packp/filter.go76
-rw-r--r--plumbing/protocol/packp/filter_test.go58
-rw-r--r--plumbing/protocol/packp/sideband/demux.go2
-rw-r--r--plumbing/protocol/packp/sideband/demux_test.go29
-rw-r--r--plumbing/protocol/packp/ulreq.go1
-rw-r--r--plumbing/protocol/packp/ulreq_encode.go11
-rw-r--r--plumbing/protocol/packp/ulreq_encode_test.go14
-rw-r--r--plumbing/serverinfo/serverinfo_test.go1
-rw-r--r--plumbing/transport/common.go7
-rw-r--r--plumbing/transport/common_test.go35
-rw-r--r--plumbing/transport/http/common.go26
-rw-r--r--plumbing/transport/http/common_test.go8
-rw-r--r--plumbing/transport/http/transport.go12
-rw-r--r--plumbing/transport/http/upload_pack_test.go3
-rw-r--r--plumbing/transport/ssh/auth_method.go13
-rw-r--r--plumbing/transport/ssh/auth_method_test.go106
-rw-r--r--plumbing/transport/ssh/common.go17
-rw-r--r--plumbing/transport/test/receive_pack.go5
-rw-r--r--remote.go60
-rw-r--r--remote_test.go160
-rw-r--r--repository.go65
-rw-r--r--repository_test.go120
-rw-r--r--signer.go33
-rw-r--r--signer_test.go56
-rw-r--r--storage/filesystem/dotgit/dotgit.go33
-rw-r--r--storage/filesystem/dotgit/dotgit_test.go81
-rw-r--r--storage/filesystem/index.go2
-rw-r--r--storage/filesystem/object.go4
-rw-r--r--storage/filesystem/object_test.go61
-rw-r--r--utils/merkletrie/change.go9
-rw-r--r--utils/merkletrie/change_test.go11
-rw-r--r--worktree.go4
-rw-r--r--worktree_commit.go88
-rw-r--r--worktree_commit_test.go148
-rw-r--r--worktree_test.go37
67 files changed, 2031 insertions, 414 deletions
diff --git a/.github/workflows/git.yml b/.github/workflows/git.yml
index 6e0ebb6..c7ae9ee 100644
--- a/.github/workflows/git.yml
+++ b/.github/workflows/git.yml
@@ -22,7 +22,7 @@ jobs:
- name: Install Go
uses: actions/setup-go@v5
with:
- go-version: 1.21.x
+ go-version: 1.22.x
- name: Install build dependencies
run: sudo apt-get update && sudo apt-get install gettext libcurl4-openssl-dev
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index f94d3e7..a04763d 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -8,7 +8,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- go-version: [1.19.x, 1.20.x, 1.21.x]
+ go-version: [1.20.x, 1.21.x, 1.22.x]
platform: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.platform }}
@@ -34,3 +34,6 @@ jobs:
- name: Test
run: make test-coverage
+
+ - name: Test Examples
+ run: go test -timeout 30s -v -run '^TestExamples$' github.com/go-git/go-git/v5/_examples --examples
diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md
index c1f280d..0e1b696 100644
--- a/COMPATIBILITY.md
+++ b/COMPATIBILITY.md
@@ -11,7 +11,7 @@ compatibility status with go-git.
| `init` | `--bare` | ✅ | | |
| `init` | `--template` <br/> `--separate-git-dir` <br/> `--shared` | ❌ | | |
| `clone` | | ✅ | | - [PlainClone](_examples/clone/main.go) |
-| `clone` | Authentication: <br/> - none <br/> - access token <br/> - username + password <br/> - ssh | ✅ | | - [clone ssh](_examples/clone/auth/ssh/main.go) <br/> - [clone access token](_examples/clone/auth/basic/access_token/main.go) <br/> - [clone user + password](_examples/clone/auth/basic/username_password/main.go) |
+| `clone` | Authentication: <br/> - none <br/> - access token <br/> - username + password <br/> - ssh | ✅ | | - [clone ssh (private_key)](_examples/clone/auth/ssh/private_key/main.go) <br/> - [clone ssh (ssh_agent)](_examples/clone/auth/ssh/ssh_agent/main.go) <br/> - [clone access token](_examples/clone/auth/basic/access_token/main.go) <br/> - [clone user + password](_examples/clone/auth/basic/username_password/main.go) |
| `clone` | `--progress` <br/> `--single-branch` <br/> `--depth` <br/> `--origin` <br/> `--recurse-submodules` <br/>`--shared` | ✅ | | - [recurse submodules](_examples/clone/main.go) <br/> - [progress](_examples/progress/main.go) |
## Basic snapshotting
@@ -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) <br/> - [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) <br/> - [tag create and push](_examples/tag-create-push/main.go) |
## Sharing and updating projects
diff --git a/Makefile b/Makefile
index 1e10396..3d5b54f 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 46f1fb0..1e9ea6a 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 0000000..59dfdfc
--- /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>")
+ 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 <branch>` defaulting to `git checkout -b <branch> --track <remote>/<branch>`")
+
+ 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/common_test.go b/_examples/common_test.go
index 6630f15..5e3f753 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/blame.go b/blame.go
index 2a877dc..e83caf3 100644
--- a/blame.go
+++ b/blame.go
@@ -97,13 +97,10 @@ func Blame(c *object.Commit, path string) (*BlameResult, error) {
if err != nil {
return nil, err
}
- if finished == true {
+ if finished {
break
}
}
- if err != nil {
- return nil, err
- }
b.lineToCommit = make([]*object.Commit, finalLength)
for i := range needsMap {
diff --git a/cli/go-git/go.mod b/cli/go-git/go.mod
index 33f5f24..56c2850 100644
--- a/cli/go-git/go.mod
+++ b/cli/go-git/go.mod
@@ -1,16 +1,16 @@
module github.com/go-git/go-git/cli/go-git
-go 1.19
+go 1.20
require (
- github.com/go-git/go-git/v5 v5.11.0
- github.com/jessevdk/go-flags v1.5.0
+ github.com/go-git/go-git/v5 v5.12.0
+ github.com/jessevdk/go-flags v1.6.1
)
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/ProtonMail/go-crypto v1.0.0 // 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
@@ -20,13 +20,13 @@ require (
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect
- github.com/sergi/go-diff v1.1.0 // indirect
- github.com/skeema/knownhosts v1.2.1 // indirect
+ github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
+ github.com/skeema/knownhosts v1.2.2 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
- golang.org/x/crypto v0.17.0 // indirect
+ golang.org/x/crypto v0.21.0 // indirect
golang.org/x/mod v0.12.0 // indirect
- golang.org/x/net v0.19.0 // indirect
- golang.org/x/sys v0.15.0 // indirect
+ golang.org/x/net v0.23.0 // indirect
+ golang.org/x/sys v0.21.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 42324f5..b140b0e 100644
--- a/cli/go-git/go.sum
+++ b/cli/go-git/go.sum
@@ -3,8 +3,8 @@ 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/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
@@ -19,21 +19,21 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU=
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.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE=
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=
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.11.0 h1:XIZc1p+8YzypNr34itUfSvYJcv+eYdTnTvOZ2vD3cA4=
-github.com/go-git/go-git/v5 v5.11.0/go.mod h1:6GFcX2P3NM7FPBfpePbpLd21XxsgdAt+lKqXmCUiUCY=
+github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys=
+github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY=
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=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
-github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc=
-github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
+github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4=
+github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@@ -49,15 +49,15 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
-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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
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=
@@ -66,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.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
-golang.org/x/crypto v0.17.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=
@@ -79,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.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
-golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
+golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
+golang.org/x/net v0.23.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=
@@ -89,7 +89,6 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -99,14 +98,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.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
-golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
+golang.org/x/sys v0.21.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.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=
@@ -128,6 +127,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/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/example_test.go b/example_test.go
index 27ea4a2..7b6adc5 100644
--- a/example_test.go
+++ b/example_test.go
@@ -137,6 +137,21 @@ func ExampleRepository_References() {
}
+func ExampleRepository_Branches() {
+ r, _ := git.Clone(memory.NewStorage(), nil, &git.CloneOptions{
+ URL: "https://github.com/git-fixtures/basic.git",
+ })
+
+ branches, _ := r.Branches()
+ branches.ForEach(func(branch *plumbing.Reference) error {
+ fmt.Println(branch.Hash().String(), branch.Name())
+ return nil
+ })
+
+ // Example Output:
+ // 6ecf0ef2c2dffb796033e5a02219af86ec6584e5 refs/heads/master
+}
+
func ExampleRepository_CreateRemote() {
r, _ := git.Init(memory.NewStorage(), nil)
diff --git a/go.mod b/go.mod
index c49b3bb..de11ca9 100644
--- a/go.mod
+++ b/go.mod
@@ -1,17 +1,17 @@
module github.com/go-git/go-git/v5
// go-git supports the last 3 stable Go versions.
-go 1.19
+go 1.20
require (
dario.cat/mergo v1.0.0
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/elazarl/goproxy v0.0.0-20240618083138-03be62527ccb
github.com/emirpasic/gods v1.18.1
- github.com/gliderlabs/ssh v0.3.6
+ 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-billy/v5 v5.5.1-0.20240427054813-8453aa90c6ec
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da
github.com/google/go-cmp v0.6.0
@@ -19,13 +19,13 @@ require (
github.com/kevinburke/ssh_config v1.2.0
github.com/pjbgf/sha1cd v0.3.0
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3
- github.com/skeema/knownhosts v1.2.1
- github.com/stretchr/testify v1.8.4
+ github.com/skeema/knownhosts v1.3.0
+ github.com/stretchr/testify v1.9.0
github.com/xanzy/ssh-agent v0.3.3
- golang.org/x/crypto v0.18.0
- golang.org/x/net v0.20.0
- golang.org/x/sys v0.16.0
- golang.org/x/text v0.14.0
+ golang.org/x/crypto v0.25.0
+ golang.org/x/net v0.27.0
+ golang.org/x/sys v0.22.0
+ golang.org/x/text v0.16.0
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c
)
@@ -39,8 +39,9 @@ require (
github.com/kr/text v0.2.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.11.0 // indirect
- golang.org/x/mod v0.12.0 // indirect
- golang.org/x/tools v0.13.0 // indirect
+ golang.org/x/mod v0.17.0 // indirect
+ golang.org/x/sync v0.7.0 // indirect
+ golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
diff --git a/go.sum b/go.sum
index 055de85..1e54947 100644
--- a/go.sum
+++ b/go.sum
@@ -19,18 +19,18 @@ github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxG
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU=
-github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
+github.com/elazarl/goproxy v0.0.0-20240618083138-03be62527ccb h1:2SoxRauy2IqekRMggrQk3yNI5X6omSnk6ugVbFywwXs=
+github.com/elazarl/goproxy v0.0.0-20240618083138-03be62527ccb/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2 h1:dWB6v3RcOy03t/bUadywsbyrQwCqZeNIEX6M1OtSZOM=
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.6 h1:ZzjlDa05TcFRICb3anf/dSPN3ewz1Zx6CMLPWgkm3b8=
-github.com/gliderlabs/ssh v0.3.6/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8=
+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=
-github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow=
+github.com/go-git/go-billy/v5 v5.5.1-0.20240427054813-8453aa90c6ec h1:JtjPVUU/+C1OaEXG+ojNfspw7t7Y30jiyr6zsXA8Eco=
+github.com/go-git/go-billy/v5 v5.5.1-0.20240427054813-8453aa90c6ec/go.mod h1:bmsuIkj+yaSISZdLRNCLRaSiWnwDatBN1b62vLkXn24=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
@@ -64,13 +64,13 @@ github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUz
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.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY=
+github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M=
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=
@@ -79,12 +79,12 @@ 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.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
-golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
+golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
+golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
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=
-golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
+golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
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=
@@ -92,12 +92,13 @@ 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.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
-golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
+golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
+golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
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=
-golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
+golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
+golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -111,14 +112,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.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
-golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
+golang.org/x/sys v0.22.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.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
+golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
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=
@@ -126,14 +127,14 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
-golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
-golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
+golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
-golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
-golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
+golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
+golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/internal/test/checkers.go b/internal/test/checkers.go
new file mode 100644
index 0000000..257d93d
--- /dev/null
+++ b/internal/test/checkers.go
@@ -0,0 +1,43 @@
+package test
+
+import (
+ "errors"
+ "fmt"
+
+ check "gopkg.in/check.v1"
+)
+
+// This check.Checker implementation exists because there's no implementation
+// in the library that compares errors using `errors.Is`. If / when the check
+// library fixes https://github.com/go-check/check/issues/139, this code can
+// likely be removed and replaced with the library implementation.
+//
+// Added in Go 1.13 [https://go.dev/blog/go1.13-errors] `errors.Is` is the
+// best mechanism to use to compare errors that might be wrapped in other
+// errors.
+type errorIsChecker struct {
+ *check.CheckerInfo
+}
+
+var ErrorIs check.Checker = errorIsChecker{
+ &check.CheckerInfo{
+ Name: "ErrorIs",
+ Params: []string{"obtained", "expected"},
+ },
+}
+
+func (e errorIsChecker) Check(params []interface{}, names []string) (bool, string) {
+ obtained, ok := params[0].(error)
+ if !ok {
+ return false, "obtained is not an error"
+ }
+ expected, ok := params[1].(error)
+ if !ok {
+ return false, "expected is not an error"
+ }
+
+ if !errors.Is(obtained, expected) {
+ return false, fmt.Sprintf("obtained: %+v expected: %+v", obtained, expected)
+ }
+ return true, ""
+}
diff --git a/options.go b/options.go
index 0a6eb94..d7776da 100644
--- a/options.go
+++ b/options.go
@@ -1,7 +1,6 @@
package git
import (
- "crypto"
"errors"
"fmt"
"regexp"
@@ -90,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 == "" {
@@ -167,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
)
@@ -199,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.
@@ -406,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
@@ -516,7 +542,7 @@ type CommitOptions struct {
// 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 crypto.Signer
+ 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 171222c..677c317 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 d36ec1b..026d221 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 123fe25..4238196 100644
--- a/plumbing/format/gitattributes/dir.go
+++ b/plumbing/format/gitattributes/dir.go
@@ -2,8 +2,11 @@ package gitattributes
import (
"os"
+ "path/filepath"
+ "strings"
"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 +29,8 @@ func ReadAttributesFile(fs billy.Filesystem, path []string, attributesFile strin
return nil, err
}
+ defer gioutil.CheckClose(f, &err)
+
return ReadAttributes(f, path, allowMacro)
}
@@ -56,7 +61,14 @@ func walkDirectory(fs billy.Filesystem, root []string) (attributes []MatchAttrib
continue
}
- path := append(root, fi.Name())
+ p := fi.Name()
+
+ // Handles the case whereby just the volume name ("C:") is appended,
+ // to root. Change it to "C:\", which is better handled by fs.Join().
+ if filepath.VolumeName(p) != "" && !strings.HasSuffix(p, string(filepath.Separator)) {
+ p = p + string(filepath.Separator)
+ }
+ path := append(root, p)
dirAttributes, err := ReadAttributesFile(fs, path, gitattributesFile, false)
if err != nil {
diff --git a/plumbing/format/gitignore/dir.go b/plumbing/format/gitignore/dir.go
index d8fb30c..92df5a3 100644
--- a/plumbing/format/gitignore/dir.go
+++ b/plumbing/format/gitignore/dir.go
@@ -64,6 +64,10 @@ func ReadPatterns(fs billy.Filesystem, path []string) (ps []Pattern, err error)
for _, fi := range fis {
if fi.IsDir() && fi.Name() != gitDir {
+ if NewMatcher(ps).Match(append(path, fi.Name()), true) {
+ continue
+ }
+
var subps []Pattern
subps, err = ReadPatterns(fs, append(path, fi.Name()))
if err != nil {
@@ -116,7 +120,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 +136,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/format/gitignore/dir_test.go b/plumbing/format/gitignore/dir_test.go
index 465c571..ba8ad80 100644
--- a/plumbing/format/gitignore/dir_test.go
+++ b/plumbing/format/gitignore/dir_test.go
@@ -44,6 +44,8 @@ func (s *MatcherSuite) SetUpTest(c *C) {
c.Assert(err, IsNil)
_, err = f.Write([]byte("ignore.crlf\r\n"))
c.Assert(err, IsNil)
+ _, err = f.Write([]byte("ignore_dir\n"))
+ c.Assert(err, IsNil)
err = f.Close()
c.Assert(err, IsNil)
@@ -56,6 +58,17 @@ func (s *MatcherSuite) SetUpTest(c *C) {
err = f.Close()
c.Assert(err, IsNil)
+ err = fs.MkdirAll("ignore_dir", os.ModePerm)
+ c.Assert(err, IsNil)
+ f, err = fs.Create("ignore_dir/.gitignore")
+ c.Assert(err, IsNil)
+ _, err = f.Write([]byte("!file\n"))
+ c.Assert(err, IsNil)
+ _, err = fs.Create("ignore_dir/file")
+ c.Assert(err, IsNil)
+ err = f.Close()
+ c.Assert(err, IsNil)
+
err = fs.MkdirAll("another", os.ModePerm)
c.Assert(err, IsNil)
err = fs.MkdirAll("exclude.crlf", os.ModePerm)
@@ -267,12 +280,13 @@ func (s *MatcherSuite) SetUpTest(c *C) {
func (s *MatcherSuite) TestDir_ReadPatterns(c *C) {
checkPatterns := func(ps []Pattern) {
- c.Assert(ps, HasLen, 6)
+ c.Assert(ps, HasLen, 7)
m := NewMatcher(ps)
c.Assert(m.Match([]string{"exclude.crlf"}, true), Equals, true)
c.Assert(m.Match([]string{"ignore.crlf"}, true), Equals, true)
c.Assert(m.Match([]string{"vendor", "gopkg.in"}, true), Equals, true)
+ c.Assert(m.Match([]string{"ignore_dir", "file"}, false), Equals, true)
c.Assert(m.Match([]string{"vendor", "github.com"}, true), Equals, false)
c.Assert(m.Match([]string{"multiple", "sub", "ignores", "first", "ignore_dir"}, true), Equals, true)
c.Assert(m.Match([]string{"multiple", "sub", "ignores", "second", "ignore_dir"}, true), Equals, true)
diff --git a/plumbing/format/index/decoder.go b/plumbing/format/index/decoder.go
index 6778cf7..f43b1c5 100644
--- a/plumbing/format/index/decoder.go
+++ b/plumbing/format/index/decoder.go
@@ -24,8 +24,8 @@ var (
// ErrInvalidChecksum is returned by Decode if the SHA1 hash mismatch with
// the read content
ErrInvalidChecksum = errors.New("invalid checksum")
-
- errUnknownExtension = errors.New("unknown extension")
+ // ErrUnknownExtension is returned when an index extension is encountered that is considered mandatory
+ ErrUnknownExtension = errors.New("unknown extension")
)
const (
@@ -39,6 +39,7 @@ const (
// A Decoder reads and decodes index files from an input stream.
type Decoder struct {
+ buf *bufio.Reader
r io.Reader
hash hash.Hash
lastEntry *Entry
@@ -49,8 +50,10 @@ type Decoder struct {
// NewDecoder returns a new decoder that reads from r.
func NewDecoder(r io.Reader) *Decoder {
h := hash.New(hash.CryptoType)
+ buf := bufio.NewReader(r)
return &Decoder{
- r: io.TeeReader(r, h),
+ buf: buf,
+ r: io.TeeReader(buf, h),
hash: h,
extReader: bufio.NewReader(nil),
}
@@ -210,71 +213,76 @@ func (d *Decoder) readExtensions(idx *Index) error {
// count that they are not supported by jgit or libgit
var expected []byte
+ var peeked []byte
var err error
- var header [4]byte
+ // we should always be able to peek for 4 bytes (header) + 4 bytes (extlen) + final hash
+ // if this fails, we know that we're at the end of the index
+ peekLen := 4 + 4 + d.hash.Size()
+
for {
expected = d.hash.Sum(nil)
-
- var n int
- if n, err = io.ReadFull(d.r, header[:]); err != nil {
- if n == 0 {
- err = io.EOF
- }
-
+ peeked, err = d.buf.Peek(peekLen)
+ if len(peeked) < peekLen {
+ // there can't be an extension at this point, so let's bail out
+ err = nil
break
}
+ if err != nil {
+ return err
+ }
- err = d.readExtension(idx, header[:])
+ err = d.readExtension(idx)
if err != nil {
- break
+ return err
}
}
- if err != errUnknownExtension {
+ return d.readChecksum(expected)
+}
+
+func (d *Decoder) readExtension(idx *Index) error {
+ var header [4]byte
+
+ if _, err := io.ReadFull(d.r, header[:]); err != nil {
return err
}
- return d.readChecksum(expected, header)
-}
+ r, err := d.getExtensionReader()
+ if err != nil {
+ return err
+ }
-func (d *Decoder) readExtension(idx *Index, header []byte) error {
switch {
- case bytes.Equal(header, treeExtSignature):
- r, err := d.getExtensionReader()
- if err != nil {
- return err
- }
-
+ case bytes.Equal(header[:], treeExtSignature):
idx.Cache = &Tree{}
d := &treeExtensionDecoder{r}
if err := d.Decode(idx.Cache); err != nil {
return err
}
- case bytes.Equal(header, resolveUndoExtSignature):
- r, err := d.getExtensionReader()
- if err != nil {
- return err
- }
-
+ case bytes.Equal(header[:], resolveUndoExtSignature):
idx.ResolveUndo = &ResolveUndo{}
d := &resolveUndoDecoder{r}
if err := d.Decode(idx.ResolveUndo); err != nil {
return err
}
- case bytes.Equal(header, endOfIndexEntryExtSignature):
- r, err := d.getExtensionReader()
- if err != nil {
- return err
- }
-
+ case bytes.Equal(header[:], endOfIndexEntryExtSignature):
idx.EndOfIndexEntry = &EndOfIndexEntry{}
d := &endOfIndexEntryDecoder{r}
if err := d.Decode(idx.EndOfIndexEntry); err != nil {
return err
}
default:
- return errUnknownExtension
+ // See https://git-scm.com/docs/index-format, which says:
+ // If the first byte is 'A'..'Z' the extension is optional and can be ignored.
+ if header[0] < 'A' || header[0] > 'Z' {
+ return ErrUnknownExtension
+ }
+
+ d := &unknownExtensionDecoder{r}
+ if err := d.Decode(); err != nil {
+ return err
+ }
}
return nil
@@ -290,11 +298,10 @@ func (d *Decoder) getExtensionReader() (*bufio.Reader, error) {
return d.extReader, nil
}
-func (d *Decoder) readChecksum(expected []byte, alreadyRead [4]byte) error {
+func (d *Decoder) readChecksum(expected []byte) error {
var h plumbing.Hash
- copy(h[:4], alreadyRead[:])
- if _, err := io.ReadFull(d.r, h[4:]); err != nil {
+ if _, err := io.ReadFull(d.r, h[:]); err != nil {
return err
}
@@ -476,3 +483,22 @@ func (d *endOfIndexEntryDecoder) Decode(e *EndOfIndexEntry) error {
_, err = io.ReadFull(d.r, e.Hash[:])
return err
}
+
+type unknownExtensionDecoder struct {
+ r *bufio.Reader
+}
+
+func (d *unknownExtensionDecoder) Decode() error {
+ var buf [1024]byte
+
+ for {
+ _, err := d.r.Read(buf[:])
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
diff --git a/plumbing/format/index/decoder_test.go b/plumbing/format/index/decoder_test.go
index 39ab336..4adddda 100644
--- a/plumbing/format/index/decoder_test.go
+++ b/plumbing/format/index/decoder_test.go
@@ -1,6 +1,11 @@
package index
import (
+ "bytes"
+ "crypto"
+ "github.com/go-git/go-git/v5/plumbing/hash"
+ "github.com/go-git/go-git/v5/utils/binary"
+ "io"
"testing"
"github.com/go-git/go-git/v5/plumbing"
@@ -218,3 +223,100 @@ func (s *IndexSuite) TestDecodeEndOfIndexEntry(c *C) {
c.Assert(idx.EndOfIndexEntry.Offset, Equals, uint32(716))
c.Assert(idx.EndOfIndexEntry.Hash.String(), Equals, "922e89d9ffd7cefce93a211615b2053c0f42bd78")
}
+
+func (s *IndexSuite) readSimpleIndex(c *C) *Index {
+ f, err := fixtures.Basic().One().DotGit().Open("index")
+ c.Assert(err, IsNil)
+ defer func() { c.Assert(f.Close(), IsNil) }()
+
+ idx := &Index{}
+ d := NewDecoder(f)
+ err = d.Decode(idx)
+ c.Assert(err, IsNil)
+
+ return idx
+}
+
+func (s *IndexSuite) buildIndexWithExtension(c *C, signature string, data string) []byte {
+ idx := s.readSimpleIndex(c)
+
+ buf := bytes.NewBuffer(nil)
+ e := NewEncoder(buf)
+
+ err := e.encode(idx, false)
+ c.Assert(err, IsNil)
+ err = e.encodeRawExtension(signature, []byte(data))
+ c.Assert(err, IsNil)
+
+ err = e.encodeFooter()
+ c.Assert(err, IsNil)
+
+ return buf.Bytes()
+}
+
+func (s *IndexSuite) TestDecodeUnknownOptionalExt(c *C) {
+ f := bytes.NewReader(s.buildIndexWithExtension(c, "TEST", "testdata"))
+
+ idx := &Index{}
+ d := NewDecoder(f)
+ err := d.Decode(idx)
+ c.Assert(err, IsNil)
+}
+
+func (s *IndexSuite) TestDecodeUnknownMandatoryExt(c *C) {
+ f := bytes.NewReader(s.buildIndexWithExtension(c, "test", "testdata"))
+
+ idx := &Index{}
+ d := NewDecoder(f)
+ err := d.Decode(idx)
+ c.Assert(err, ErrorMatches, ErrUnknownExtension.Error())
+}
+
+func (s *IndexSuite) TestDecodeTruncatedExt(c *C) {
+ idx := s.readSimpleIndex(c)
+
+ buf := bytes.NewBuffer(nil)
+ e := NewEncoder(buf)
+
+ err := e.encode(idx, false)
+ c.Assert(err, IsNil)
+
+ _, err = e.w.Write([]byte("TEST"))
+ c.Assert(err, IsNil)
+
+ err = binary.WriteUint32(e.w, uint32(100))
+ c.Assert(err, IsNil)
+
+ _, err = e.w.Write([]byte("truncated"))
+ c.Assert(err, IsNil)
+
+ err = e.encodeFooter()
+ c.Assert(err, IsNil)
+
+ idx = &Index{}
+ d := NewDecoder(buf)
+ err = d.Decode(idx)
+ c.Assert(err, ErrorMatches, io.EOF.Error())
+}
+
+func (s *IndexSuite) TestDecodeInvalidHash(c *C) {
+ idx := s.readSimpleIndex(c)
+
+ buf := bytes.NewBuffer(nil)
+ e := NewEncoder(buf)
+
+ err := e.encode(idx, false)
+ c.Assert(err, IsNil)
+
+ err = e.encodeRawExtension("TEST", []byte("testdata"))
+ c.Assert(err, IsNil)
+
+ h := hash.New(crypto.SHA1)
+ err = binary.Write(e.w, h.Sum(nil))
+ c.Assert(err, IsNil)
+
+ idx = &Index{}
+ d := NewDecoder(buf)
+ err = d.Decode(idx)
+ c.Assert(err, ErrorMatches, ErrInvalidChecksum.Error())
+}
diff --git a/plumbing/format/index/encoder.go b/plumbing/format/index/encoder.go
index fa2d814..c292c2c 100644
--- a/plumbing/format/index/encoder.go
+++ b/plumbing/format/index/encoder.go
@@ -3,6 +3,7 @@ package index
import (
"bytes"
"errors"
+ "fmt"
"io"
"sort"
"time"
@@ -35,6 +36,11 @@ func NewEncoder(w io.Writer) *Encoder {
// Encode writes the Index to the stream of the encoder.
func (e *Encoder) Encode(idx *Index) error {
+ return e.encode(idx, true)
+}
+
+func (e *Encoder) encode(idx *Index, footer bool) error {
+
// TODO: support v4
// TODO: support extensions
if idx.Version > EncodeVersionSupported {
@@ -49,7 +55,10 @@ func (e *Encoder) Encode(idx *Index) error {
return err
}
- return e.encodeFooter()
+ if footer {
+ return e.encodeFooter()
+ }
+ return nil
}
func (e *Encoder) encodeHeader(idx *Index) error {
@@ -135,6 +144,29 @@ func (e *Encoder) encodeEntry(entry *Entry) error {
return binary.Write(e.w, []byte(entry.Name))
}
+func (e *Encoder) encodeRawExtension(signature string, data []byte) error {
+ if len(signature) != 4 {
+ return fmt.Errorf("invalid signature length")
+ }
+
+ _, err := e.w.Write([]byte(signature))
+ if err != nil {
+ return err
+ }
+
+ err = binary.WriteUint32(e.w, uint32(len(data)))
+ if err != nil {
+ return err
+ }
+
+ _, err = e.w.Write(data)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
func (e *Encoder) timeToUint32(t *time.Time) (uint32, uint32, error) {
if t.IsZero() {
return 0, 0, nil
diff --git a/plumbing/format/packfile/delta_index.go b/plumbing/format/packfile/delta_index.go
index 07a6112..a60ec0b 100644
--- a/plumbing/format/packfile/delta_index.go
+++ b/plumbing/format/packfile/delta_index.go
@@ -32,19 +32,17 @@ func (idx *deltaIndex) findMatch(src, tgt []byte, tgtOffset int) (srcOffset, l i
return 0, -1
}
- if len(tgt) >= tgtOffset+s && len(src) >= blksz {
- h := hashBlock(tgt, tgtOffset)
- tIdx := h & idx.mask
- eIdx := idx.table[tIdx]
- if eIdx != 0 {
- srcOffset = idx.entries[eIdx]
- } else {
- return
- }
-
- l = matchLength(src, tgt, tgtOffset, srcOffset)
+ h := hashBlock(tgt, tgtOffset)
+ tIdx := h & idx.mask
+ eIdx := idx.table[tIdx]
+ if eIdx == 0 {
+ return
}
+ srcOffset = idx.entries[eIdx]
+
+ l = matchLength(src, tgt, tgtOffset, srcOffset)
+
return
}
diff --git a/plumbing/object/commit_test.go b/plumbing/object/commit_test.go
index 6651ef8..a048926 100644
--- a/plumbing/object/commit_test.go
+++ b/plumbing/object/commit_test.go
@@ -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 aa0ca15..c1ec8ba 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 c47d68b..fa0ca7d 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 dd8fef4..3c61f62 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:
+// <pad><filename><pad>|<pad><changeNumber><pad><+++/---><newline>
+// 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:
- // <pad><filename><pad>|<pad><changeNumber><pad><+++/---><newline>
- // example: " main.go | 10 +++++++--- "
-
- // <pad><filename><pad>
- leftTextLength := padLength + longestLength + padLength
-
- // <pad><number><pad><+++++/-----><newline>
- // 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 2cff795..e0e63a5 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 e9f7666..0fd0e51 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 bb5fc7a..feb058a 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 6e7b334..2adb645 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/protocol/packp/filter.go b/plumbing/protocol/packp/filter.go
new file mode 100644
index 0000000..145fc71
--- /dev/null
+++ b/plumbing/protocol/packp/filter.go
@@ -0,0 +1,76 @@
+package packp
+
+import (
+ "errors"
+ "fmt"
+ "github.com/go-git/go-git/v5/plumbing"
+ "net/url"
+ "strings"
+)
+
+var ErrUnsupportedObjectFilterType = errors.New("unsupported object filter type")
+
+// Filter values enable the partial clone capability which causes
+// the server to omit objects that match the filter.
+//
+// See [Git's documentation] for more details.
+//
+// [Git's documentation]: https://github.com/git/git/blob/e02ecfcc534e2021aae29077a958dd11c3897e4c/Documentation/rev-list-options.txt#L948
+type Filter string
+
+type BlobLimitPrefix string
+
+const (
+ BlobLimitPrefixNone BlobLimitPrefix = ""
+ BlobLimitPrefixKibi BlobLimitPrefix = "k"
+ BlobLimitPrefixMebi BlobLimitPrefix = "m"
+ BlobLimitPrefixGibi BlobLimitPrefix = "g"
+)
+
+// FilterBlobNone omits all blobs.
+func FilterBlobNone() Filter {
+ return "blob:none"
+}
+
+// FilterBlobLimit omits blobs of size at least n bytes (when prefix is
+// BlobLimitPrefixNone), n kibibytes (when prefix is BlobLimitPrefixKibi),
+// n mebibytes (when prefix is BlobLimitPrefixMebi) or n gibibytes (when
+// prefix is BlobLimitPrefixGibi). n can be zero, in which case all blobs
+// will be omitted.
+func FilterBlobLimit(n uint64, prefix BlobLimitPrefix) Filter {
+ return Filter(fmt.Sprintf("blob:limit=%d%s", n, prefix))
+}
+
+// FilterTreeDepth omits all blobs and trees whose depth from the root tree
+// is larger or equal to depth.
+func FilterTreeDepth(depth uint64) Filter {
+ return Filter(fmt.Sprintf("tree:%d", depth))
+}
+
+// FilterObjectType omits all objects which are not of the requested type t.
+// Supported types are TagObject, CommitObject, TreeObject and BlobObject.
+func FilterObjectType(t plumbing.ObjectType) (Filter, error) {
+ switch t {
+ case plumbing.TagObject:
+ fallthrough
+ case plumbing.CommitObject:
+ fallthrough
+ case plumbing.TreeObject:
+ fallthrough
+ case plumbing.BlobObject:
+ return Filter(fmt.Sprintf("object:type=%s", t.String())), nil
+ default:
+ return "", fmt.Errorf("%w: %s", ErrUnsupportedObjectFilterType, t.String())
+ }
+}
+
+// FilterCombine combines multiple Filter values together.
+func FilterCombine(filters ...Filter) Filter {
+ var escapedFilters []string
+
+ for _, filter := range filters {
+ escapedFilters = append(escapedFilters, url.QueryEscape(string(filter)))
+ }
+
+ return Filter(fmt.Sprintf("combine:%s", strings.Join(escapedFilters, "+")))
+}
diff --git a/plumbing/protocol/packp/filter_test.go b/plumbing/protocol/packp/filter_test.go
new file mode 100644
index 0000000..266670f
--- /dev/null
+++ b/plumbing/protocol/packp/filter_test.go
@@ -0,0 +1,58 @@
+package packp
+
+import (
+ "github.com/go-git/go-git/v5/plumbing"
+ "github.com/stretchr/testify/require"
+ "testing"
+)
+
+func TestFilterBlobNone(t *testing.T) {
+ require.EqualValues(t, "blob:none", FilterBlobNone())
+}
+
+func TestFilterBlobLimit(t *testing.T) {
+ require.EqualValues(t, "blob:limit=0", FilterBlobLimit(0, BlobLimitPrefixNone))
+ require.EqualValues(t, "blob:limit=1000", FilterBlobLimit(1000, BlobLimitPrefixNone))
+ require.EqualValues(t, "blob:limit=4k", FilterBlobLimit(4, BlobLimitPrefixKibi))
+ require.EqualValues(t, "blob:limit=4m", FilterBlobLimit(4, BlobLimitPrefixMebi))
+ require.EqualValues(t, "blob:limit=4g", FilterBlobLimit(4, BlobLimitPrefixGibi))
+}
+
+func TestFilterTreeDepth(t *testing.T) {
+ require.EqualValues(t, "tree:0", FilterTreeDepth(0))
+ require.EqualValues(t, "tree:1", FilterTreeDepth(1))
+ require.EqualValues(t, "tree:2", FilterTreeDepth(2))
+}
+
+func TestFilterObjectType(t *testing.T) {
+ filter, err := FilterObjectType(plumbing.TagObject)
+ require.NoError(t, err)
+ require.EqualValues(t, "object:type=tag", filter)
+
+ filter, err = FilterObjectType(plumbing.CommitObject)
+ require.NoError(t, err)
+ require.EqualValues(t, "object:type=commit", filter)
+
+ filter, err = FilterObjectType(plumbing.TreeObject)
+ require.NoError(t, err)
+ require.EqualValues(t, "object:type=tree", filter)
+
+ filter, err = FilterObjectType(plumbing.BlobObject)
+ require.NoError(t, err)
+ require.EqualValues(t, "object:type=blob", filter)
+
+ _, err = FilterObjectType(plumbing.InvalidObject)
+ require.Error(t, err)
+
+ _, err = FilterObjectType(plumbing.OFSDeltaObject)
+ require.Error(t, err)
+}
+
+func TestFilterCombine(t *testing.T) {
+ require.EqualValues(t, "combine:tree%3A2+blob%3Anone",
+ FilterCombine(
+ FilterTreeDepth(2),
+ FilterBlobNone(),
+ ),
+ )
+}
diff --git a/plumbing/protocol/packp/sideband/demux.go b/plumbing/protocol/packp/sideband/demux.go
index 0116f96..01d95a3 100644
--- a/plumbing/protocol/packp/sideband/demux.go
+++ b/plumbing/protocol/packp/sideband/demux.go
@@ -114,7 +114,7 @@ func (d *Demuxer) nextPackData() ([]byte, error) {
size := len(content)
if size == 0 {
- return nil, nil
+ return nil, io.EOF
} else if size > d.max {
return nil, ErrMaxPackedExceeded
}
diff --git a/plumbing/protocol/packp/sideband/demux_test.go b/plumbing/protocol/packp/sideband/demux_test.go
index 8f23353..1ba3ad9 100644
--- a/plumbing/protocol/packp/sideband/demux_test.go
+++ b/plumbing/protocol/packp/sideband/demux_test.go
@@ -105,8 +105,34 @@ func (s *SidebandSuite) TestDecodeWithProgress(c *C) {
c.Assert(progress, DeepEquals, []byte{'F', 'O', 'O', '\n'})
}
-func (s *SidebandSuite) TestDecodeWithUnknownChannel(c *C) {
+func (s *SidebandSuite) TestDecodeFlushEOF(c *C) {
+ expected := []byte("abcdefghijklmnopqrstuvwxyz")
+
+ input := bytes.NewBuffer(nil)
+ e := pktline.NewEncoder(input)
+ e.Encode(PackData.WithPayload(expected[0:8]))
+ e.Encode(ProgressMessage.WithPayload([]byte{'F', 'O', 'O', '\n'}))
+ e.Encode(PackData.WithPayload(expected[8:16]))
+ e.Encode(PackData.WithPayload(expected[16:26]))
+ e.Flush()
+ e.Encode(PackData.WithPayload([]byte("bar\n")))
+
+ output := bytes.NewBuffer(nil)
+ content := bytes.NewBuffer(nil)
+ d := NewDemuxer(Sideband64k, input)
+ d.Progress = output
+
+ n, err := content.ReadFrom(d)
+ c.Assert(err, IsNil)
+ c.Assert(n, Equals, int64(26))
+ c.Assert(content.Bytes(), DeepEquals, expected)
+ progress, err := io.ReadAll(output)
+ c.Assert(err, IsNil)
+ c.Assert(progress, DeepEquals, []byte{'F', 'O', 'O', '\n'})
+}
+
+func (s *SidebandSuite) TestDecodeWithUnknownChannel(c *C) {
buf := bytes.NewBuffer(nil)
e := pktline.NewEncoder(buf)
e.Encode([]byte{'4', 'F', 'O', 'O', '\n'})
@@ -150,5 +176,4 @@ func (s *SidebandSuite) TestDecodeErrMaxPacked(c *C) {
n, err := io.ReadFull(d, content)
c.Assert(err, Equals, ErrMaxPackedExceeded)
c.Assert(n, Equals, 0)
-
}
diff --git a/plumbing/protocol/packp/ulreq.go b/plumbing/protocol/packp/ulreq.go
index 344f8c7..ef4e08a 100644
--- a/plumbing/protocol/packp/ulreq.go
+++ b/plumbing/protocol/packp/ulreq.go
@@ -17,6 +17,7 @@ type UploadRequest struct {
Wants []plumbing.Hash
Shallows []plumbing.Hash
Depth Depth
+ Filter Filter
}
// Depth values stores the desired depth of the requested packfile: see
diff --git a/plumbing/protocol/packp/ulreq_encode.go b/plumbing/protocol/packp/ulreq_encode.go
index c451e23..8b19c0f 100644
--- a/plumbing/protocol/packp/ulreq_encode.go
+++ b/plumbing/protocol/packp/ulreq_encode.go
@@ -132,6 +132,17 @@ func (e *ulReqEncoder) encodeDepth() stateFn {
return nil
}
+ return e.encodeFilter
+}
+
+func (e *ulReqEncoder) encodeFilter() stateFn {
+ if filter := e.data.Filter; filter != "" {
+ if err := e.pe.Encodef("filter %s\n", filter); err != nil {
+ e.err = fmt.Errorf("encoding filter %s: %s", filter, err)
+ return nil
+ }
+ }
+
return e.encodeFlush
}
diff --git a/plumbing/protocol/packp/ulreq_encode_test.go b/plumbing/protocol/packp/ulreq_encode_test.go
index ba6df1a..247de27 100644
--- a/plumbing/protocol/packp/ulreq_encode_test.go
+++ b/plumbing/protocol/packp/ulreq_encode_test.go
@@ -273,6 +273,20 @@ func (s *UlReqEncodeSuite) TestDepthReference(c *C) {
testUlReqEncode(c, ur, expected)
}
+func (s *UlReqEncodeSuite) TestFilter(c *C) {
+ ur := NewUploadRequest()
+ ur.Wants = append(ur.Wants, plumbing.NewHash("1111111111111111111111111111111111111111"))
+ ur.Filter = FilterTreeDepth(0)
+
+ expected := []string{
+ "want 1111111111111111111111111111111111111111\n",
+ "filter tree:0\n",
+ pktline.FlushString,
+ }
+
+ testUlReqEncode(c, ur, expected)
+}
+
func (s *UlReqEncodeSuite) TestAll(c *C) {
ur := NewUploadRequest()
ur.Wants = append(ur.Wants,
diff --git a/plumbing/serverinfo/serverinfo_test.go b/plumbing/serverinfo/serverinfo_test.go
index 0a52ea2..251746b 100644
--- a/plumbing/serverinfo/serverinfo_test.go
+++ b/plumbing/serverinfo/serverinfo_test.go
@@ -179,6 +179,7 @@ func (s *ServerInfoSuite) TestUpdateServerInfoBasicChange(c *C) {
c.Assert(err, IsNil)
err = UpdateServerInfo(st, fs)
+ c.Assert(err, IsNil)
assertInfoRefs(c, st, fs)
assertObjectPacks(c, st, fs)
diff --git a/plumbing/transport/common.go b/plumbing/transport/common.go
index b05437f..fae1aa9 100644
--- a/plumbing/transport/common.go
+++ b/plumbing/transport/common.go
@@ -19,6 +19,7 @@ import (
"fmt"
"io"
"net/url"
+ "path/filepath"
"strconv"
"strings"
@@ -295,7 +296,11 @@ func parseFile(endpoint string) (*Endpoint, bool) {
return nil, false
}
- path := endpoint
+ path, err := filepath.Abs(endpoint)
+ if err != nil {
+ return nil, false
+ }
+
return &Endpoint{
Protocol: "file",
Path: path,
diff --git a/plumbing/transport/common_test.go b/plumbing/transport/common_test.go
index 3efc555..1501f73 100644
--- a/plumbing/transport/common_test.go
+++ b/plumbing/transport/common_test.go
@@ -3,6 +3,9 @@ package transport
import (
"fmt"
"net/url"
+ "os"
+ "path/filepath"
+ "runtime"
"testing"
"github.com/go-git/go-git/v5/plumbing/protocol/packp/capability"
@@ -120,6 +123,14 @@ func (s *SuiteCommon) TestNewEndpointSCPLikeWithPort(c *C) {
}
func (s *SuiteCommon) TestNewEndpointFileAbs(c *C) {
+ var err error
+ abs := "/foo.git"
+
+ if runtime.GOOS == "windows" {
+ abs, err = filepath.Abs(abs)
+ c.Assert(err, IsNil)
+ }
+
e, err := NewEndpoint("/foo.git")
c.Assert(err, IsNil)
c.Assert(e.Protocol, Equals, "file")
@@ -127,11 +138,14 @@ func (s *SuiteCommon) TestNewEndpointFileAbs(c *C) {
c.Assert(e.Password, Equals, "")
c.Assert(e.Host, Equals, "")
c.Assert(e.Port, Equals, 0)
- c.Assert(e.Path, Equals, "/foo.git")
- c.Assert(e.String(), Equals, "file:///foo.git")
+ c.Assert(e.Path, Equals, abs)
+ c.Assert(e.String(), Equals, "file://"+abs)
}
func (s *SuiteCommon) TestNewEndpointFileRel(c *C) {
+ abs, err := filepath.Abs("foo.git")
+ c.Assert(err, IsNil)
+
e, err := NewEndpoint("foo.git")
c.Assert(err, IsNil)
c.Assert(e.Protocol, Equals, "file")
@@ -139,11 +153,20 @@ func (s *SuiteCommon) TestNewEndpointFileRel(c *C) {
c.Assert(e.Password, Equals, "")
c.Assert(e.Host, Equals, "")
c.Assert(e.Port, Equals, 0)
- c.Assert(e.Path, Equals, "foo.git")
- c.Assert(e.String(), Equals, "file://foo.git")
+ c.Assert(e.Path, Equals, abs)
+ c.Assert(e.String(), Equals, "file://"+abs)
}
func (s *SuiteCommon) TestNewEndpointFileWindows(c *C) {
+ abs := "C:\\foo.git"
+
+ if runtime.GOOS != "windows" {
+ cwd, err := os.Getwd()
+ c.Assert(err, IsNil)
+
+ abs = filepath.Join(cwd, "C:\\foo.git")
+ }
+
e, err := NewEndpoint("C:\\foo.git")
c.Assert(err, IsNil)
c.Assert(e.Protocol, Equals, "file")
@@ -151,8 +174,8 @@ func (s *SuiteCommon) TestNewEndpointFileWindows(c *C) {
c.Assert(e.Password, Equals, "")
c.Assert(e.Host, Equals, "")
c.Assert(e.Port, Equals, 0)
- c.Assert(e.Path, Equals, "C:\\foo.git")
- c.Assert(e.String(), Equals, "file://C:\\foo.git")
+ c.Assert(e.Path, Equals, abs)
+ c.Assert(e.String(), Equals, "file://"+abs)
}
func (s *SuiteCommon) TestNewEndpointFileURL(c *C) {
diff --git a/plumbing/transport/http/common.go b/plumbing/transport/http/common.go
index 54126fe..120008d 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{
@@ -430,11 +430,11 @@ func NewErr(r *http.Response) error {
switch r.StatusCode {
case http.StatusUnauthorized:
- return transport.ErrAuthenticationRequired
+ return fmt.Errorf("%w: %s", transport.ErrAuthenticationRequired, reason)
case http.StatusForbidden:
- return transport.ErrAuthorizationFailed
+ return fmt.Errorf("%w: %s", transport.ErrAuthorizationFailed, reason)
case http.StatusNotFound:
- return transport.ErrRepositoryNotFound
+ return fmt.Errorf("%w: %s", transport.ErrRepositoryNotFound, reason)
}
return plumbing.NewUnexpectedError(&Err{r, reason})
diff --git a/plumbing/transport/http/common_test.go b/plumbing/transport/http/common_test.go
index 6bd018b..f0eb68d 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) {
@@ -76,15 +76,15 @@ func (s *ClientSuite) TestNewErrOK(c *C) {
}
func (s *ClientSuite) TestNewErrUnauthorized(c *C) {
- s.testNewHTTPError(c, http.StatusUnauthorized, "authentication required")
+ s.testNewHTTPError(c, http.StatusUnauthorized, ".*authentication required.*")
}
func (s *ClientSuite) TestNewErrForbidden(c *C) {
- s.testNewHTTPError(c, http.StatusForbidden, "authorization failed")
+ s.testNewHTTPError(c, http.StatusForbidden, ".*authorization failed.*")
}
func (s *ClientSuite) TestNewErrNotFound(c *C) {
- s.testNewHTTPError(c, http.StatusNotFound, "repository not found")
+ s.testNewHTTPError(c, http.StatusNotFound, ".*repository not found.*")
}
func (s *ClientSuite) TestNewHTTPError40x(c *C) {
diff --git a/plumbing/transport/http/transport.go b/plumbing/transport/http/transport.go
index 052f3c8..c8db389 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/http/upload_pack_test.go b/plumbing/transport/http/upload_pack_test.go
index abb7adf..3a1610a 100644
--- a/plumbing/transport/http/upload_pack_test.go
+++ b/plumbing/transport/http/upload_pack_test.go
@@ -8,6 +8,7 @@ import (
"os"
"path/filepath"
+ . "github.com/go-git/go-git/v5/internal/test"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/protocol/packp"
"github.com/go-git/go-git/v5/plumbing/transport"
@@ -37,7 +38,7 @@ func (s *UploadPackSuite) TestAdvertisedReferencesNotExists(c *C) {
r, err := s.Client.NewUploadPackSession(s.NonExistentEndpoint, s.EmptyAuth)
c.Assert(err, IsNil)
info, err := r.AdvertisedReferences()
- c.Assert(err, Equals, transport.ErrRepositoryNotFound)
+ c.Assert(err, ErrorIs, transport.ErrRepositoryNotFound)
c.Assert(info, IsNil)
}
diff --git a/plumbing/transport/ssh/auth_method.go b/plumbing/transport/ssh/auth_method.go
index ac4e358..f9c598e 100644
--- a/plumbing/transport/ssh/auth_method.go
+++ b/plumbing/transport/ssh/auth_method.go
@@ -230,11 +230,11 @@ func (a *PublicKeysCallback) ClientConfig() (*ssh.ClientConfig, error) {
// ~/.ssh/known_hosts
// /etc/ssh/ssh_known_hosts
func NewKnownHostsCallback(files ...string) (ssh.HostKeyCallback, error) {
- kh, err := newKnownHosts(files...)
- return ssh.HostKeyCallback(kh), err
+ db, err := newKnownHostsDb(files...)
+ return db.HostKeyCallback(), err
}
-func newKnownHosts(files ...string) (knownhosts.HostKeyCallback, error) {
+func newKnownHostsDb(files ...string) (*knownhosts.HostKeyDB, error) {
var err error
if len(files) == 0 {
@@ -247,7 +247,7 @@ func newKnownHosts(files ...string) (knownhosts.HostKeyCallback, error) {
return nil, err
}
- return knownhosts.New(files...)
+ return knownhosts.NewDB(files...)
}
func getDefaultKnownHostsFiles() ([]string, error) {
@@ -301,11 +301,12 @@ type HostKeyCallbackHelper struct {
// HostKeyCallback is empty a default callback is created using
// NewKnownHostsCallback.
func (m *HostKeyCallbackHelper) SetHostKeyCallback(cfg *ssh.ClientConfig) (*ssh.ClientConfig, error) {
- var err error
if m.HostKeyCallback == nil {
- if m.HostKeyCallback, err = NewKnownHostsCallback(); err != nil {
+ db, err := newKnownHostsDb()
+ if err != nil {
return cfg, err
}
+ m.HostKeyCallback = db.HostKeyCallback()
}
cfg.HostKeyCallback = m.HostKeyCallback
diff --git a/plumbing/transport/ssh/auth_method_test.go b/plumbing/transport/ssh/auth_method_test.go
index b275018..e3f652e 100644
--- a/plumbing/transport/ssh/auth_method_test.go
+++ b/plumbing/transport/ssh/auth_method_test.go
@@ -18,7 +18,8 @@ import (
type (
SuiteCommon struct{}
- mockKnownHosts struct{}
+ mockKnownHosts struct{}
+ mockKnownHostsWithCert struct{}
)
func (mockKnownHosts) host() string { return "github.com" }
@@ -27,6 +28,19 @@ func (mockKnownHosts) knownHosts() []byte {
}
func (mockKnownHosts) Network() string { return "tcp" }
func (mockKnownHosts) String() string { return "github.com:22" }
+func (mockKnownHosts) Algorithms() []string {
+ return []string{ssh.KeyAlgoRSA, ssh.KeyAlgoRSASHA256, ssh.KeyAlgoRSASHA512}
+}
+
+func (mockKnownHostsWithCert) host() string { return "github.com" }
+func (mockKnownHostsWithCert) knownHosts() []byte {
+ return []byte(`@cert-authority github.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==`)
+}
+func (mockKnownHostsWithCert) Network() string { return "tcp" }
+func (mockKnownHostsWithCert) String() string { return "github.com:22" }
+func (mockKnownHostsWithCert) Algorithms() []string {
+ return []string{ssh.CertAlgoRSASHA512v01, ssh.CertAlgoRSASHA256v01, ssh.CertAlgoRSAv01}
+}
var _ = Suite(&SuiteCommon{})
@@ -230,3 +244,93 @@ func (*SuiteCommon) TestNewKnownHostsCallback(c *C) {
err = clb(mock.String(), mock, hostKey)
c.Assert(err, IsNil)
}
+
+func (*SuiteCommon) TestNewKnownHostsDbWithoutCert(c *C) {
+ if runtime.GOOS == "js" {
+ c.Skip("not available in wasm")
+ }
+
+ var mock = mockKnownHosts{}
+
+ f, err := util.TempFile(osfs.Default, "", "known-hosts")
+ c.Assert(err, IsNil)
+
+ _, err = f.Write(mock.knownHosts())
+ c.Assert(err, IsNil)
+
+ err = f.Close()
+ c.Assert(err, IsNil)
+
+ defer util.RemoveAll(osfs.Default, f.Name())
+
+ f, err = osfs.Default.Open(f.Name())
+ c.Assert(err, IsNil)
+
+ defer f.Close()
+
+ db, err := newKnownHostsDb(f.Name())
+ c.Assert(err, IsNil)
+
+ algos := db.HostKeyAlgorithms(mock.String())
+ c.Assert(algos, HasLen, len(mock.Algorithms()))
+
+ contains := func(container []string, value string) bool {
+ for _, inner := range container {
+ if inner == value {
+ return true
+ }
+ }
+ return false
+ }
+
+ for _, algorithm := range mock.Algorithms() {
+ if !contains(algos, algorithm) {
+ c.Error("algos does not contain ", algorithm)
+ }
+ }
+}
+
+func (*SuiteCommon) TestNewKnownHostsDbWithCert(c *C) {
+ if runtime.GOOS == "js" {
+ c.Skip("not available in wasm")
+ }
+
+ var mock = mockKnownHostsWithCert{}
+
+ f, err := util.TempFile(osfs.Default, "", "known-hosts")
+ c.Assert(err, IsNil)
+
+ _, err = f.Write(mock.knownHosts())
+ c.Assert(err, IsNil)
+
+ err = f.Close()
+ c.Assert(err, IsNil)
+
+ defer util.RemoveAll(osfs.Default, f.Name())
+
+ f, err = osfs.Default.Open(f.Name())
+ c.Assert(err, IsNil)
+
+ defer f.Close()
+
+ db, err := newKnownHostsDb(f.Name())
+ c.Assert(err, IsNil)
+
+ algos := db.HostKeyAlgorithms(mock.String())
+ c.Assert(algos, HasLen, len(mock.Algorithms()))
+
+ contains := func(container []string, value string) bool {
+ for _, inner := range container {
+ if inner == value {
+ return true
+ }
+ }
+ return false
+ }
+
+ for _, algorithm := range mock.Algorithms() {
+ if !contains(algos, algorithm) {
+ c.Error("algos does not contain ", algorithm)
+ }
+ }
+}
diff --git a/plumbing/transport/ssh/common.go b/plumbing/transport/ssh/common.go
index 05dea44..a37024f 100644
--- a/plumbing/transport/ssh/common.go
+++ b/plumbing/transport/ssh/common.go
@@ -11,7 +11,6 @@ import (
"github.com/go-git/go-git/v5/plumbing/transport"
"github.com/go-git/go-git/v5/plumbing/transport/internal/common"
- "github.com/skeema/knownhosts"
"github.com/kevinburke/ssh_config"
"golang.org/x/crypto/ssh"
@@ -127,17 +126,25 @@ func (c *command) connect() error {
}
hostWithPort := c.getHostWithPort()
if config.HostKeyCallback == nil {
- kh, err := newKnownHosts()
+ db, err := newKnownHostsDb()
if err != nil {
return err
}
- config.HostKeyCallback = kh.HostKeyCallback()
- config.HostKeyAlgorithms = kh.HostKeyAlgorithms(hostWithPort)
+
+ config.HostKeyCallback = db.HostKeyCallback()
+ config.HostKeyAlgorithms = db.HostKeyAlgorithms(hostWithPort)
} else if len(config.HostKeyAlgorithms) == 0 {
// Set the HostKeyAlgorithms based on HostKeyCallback.
// For background see https://github.com/go-git/go-git/issues/411 as well as
// https://github.com/golang/go/issues/29286 for root cause.
- config.HostKeyAlgorithms = knownhosts.HostKeyAlgorithms(config.HostKeyCallback, hostWithPort)
+ db, err := newKnownHostsDb()
+ if err != nil {
+ return err
+ }
+
+ // Note that the knownhost database is used, as it provides additional functionality
+ // to handle ssh cert-authorities.
+ config.HostKeyAlgorithms = db.HostKeyAlgorithms(hostWithPort)
}
overrideConfig(c.config, config)
diff --git a/plumbing/transport/test/receive_pack.go b/plumbing/transport/test/receive_pack.go
index 9414fba..d4d2b10 100644
--- a/plumbing/transport/test/receive_pack.go
+++ b/plumbing/transport/test/receive_pack.go
@@ -9,6 +9,7 @@ import (
"os"
"path/filepath"
+ . "github.com/go-git/go-git/v5/internal/test"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/format/packfile"
"github.com/go-git/go-git/v5/plumbing/protocol/packp"
@@ -42,7 +43,7 @@ func (s *ReceivePackSuite) TestAdvertisedReferencesNotExists(c *C) {
r, err := s.Client.NewReceivePackSession(s.NonExistentEndpoint, s.EmptyAuth)
c.Assert(err, IsNil)
ar, err := r.AdvertisedReferences()
- c.Assert(err, Equals, transport.ErrRepositoryNotFound)
+ c.Assert(err, ErrorIs, transport.ErrRepositoryNotFound)
c.Assert(ar, IsNil)
c.Assert(r.Close(), IsNil)
@@ -54,7 +55,7 @@ func (s *ReceivePackSuite) TestAdvertisedReferencesNotExists(c *C) {
}
writer, err := r.ReceivePack(context.Background(), req)
- c.Assert(err, Equals, transport.ErrRepositoryNotFound)
+ c.Assert(err, ErrorIs, transport.ErrRepositoryNotFound)
c.Assert(writer, IsNil)
c.Assert(r.Close(), IsNil)
}
diff --git a/remote.go b/remote.go
index 0cb70bc..170883a 100644
--- a/remote.go
+++ b/remote.go
@@ -9,6 +9,7 @@ import (
"time"
"github.com/go-git/go-billy/v5/osfs"
+
"github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/internal/url"
"github.com/go-git/go-git/v5/plumbing"
@@ -470,6 +471,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,8 +491,19 @@ func (r *Remote) fetch(ctx context.Context, o *FetchOptions) (sto storer.Referen
}
}
- if !updated {
- return remoteRefs, NoErrAlreadyUpToDate
+ if !updated && !updatedPrune {
+ // No references updated, but may have fetched new objects, check if we now have any of our wants
+ for _, hash := range req.Wants {
+ exists, _ := objectExists(r.s, hash)
+ if exists {
+ updated = true
+ break
+ }
+ }
+
+ if !updated {
+ return remoteRefs, NoErrAlreadyUpToDate
+ }
}
return remoteRefs, nil
@@ -574,6 +594,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,
@@ -849,17 +890,12 @@ func getHavesFromRef(
return nil
}
- // No need to load the commit if we know the remote already
- // has this hash.
- if remoteRefs[h] {
- haves[h] = true
- return nil
- }
-
commit, err := object.GetCommit(s, h)
if err != nil {
- // Ignore the error if this isn't a commit.
- haves[ref.Hash()] = true
+ if !errors.Is(err, plumbing.ErrObjectNotFound) {
+ // Ignore the error if this isn't a commit.
+ haves[ref.Hash()] = true
+ }
return nil
}
@@ -1099,7 +1135,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 81c60bc..c816cc5 100644
--- a/remote_test.go
+++ b/remote_test.go
@@ -14,6 +14,9 @@ import (
"time"
"github.com/go-git/go-billy/v5/memfs"
+ "github.com/go-git/go-billy/v5/osfs"
+ "github.com/go-git/go-billy/v5/util"
+
"github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/cache"
@@ -346,6 +349,38 @@ func (s *RemoteSuite) testFetch(c *C, r *Remote, o *FetchOptions, expected []*pl
}
}
+func (s *RemoteSuite) TestFetchOfMissingObjects(c *C) {
+ tmp, clean := s.TemporalDir()
+ defer clean()
+
+ // clone to a local temp folder
+ _, err := PlainClone(tmp, true, &CloneOptions{
+ URL: fixtures.Basic().One().DotGit().Root(),
+ })
+ c.Assert(err, IsNil)
+
+ // Delete the pack files
+ fsTmp := osfs.New(tmp)
+ err = util.RemoveAll(fsTmp, "objects/pack")
+ c.Assert(err, IsNil)
+
+ // Reopen the repo from the filesystem (with missing objects)
+ r, err := Open(filesystem.NewStorage(fsTmp, cache.NewObjectLRUDefault()), nil)
+ c.Assert(err, IsNil)
+
+ // Confirm we are missing a commit
+ _, err = r.CommitObject(plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e5"))
+ c.Assert(err, Equals, plumbing.ErrObjectNotFound)
+
+ // Refetch to get all the missing objects
+ err = r.Fetch(&FetchOptions{})
+ c.Assert(err, IsNil)
+
+ // Confirm we now have the commit
+ _, err = r.CommitObject(plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e5"))
+ c.Assert(err, IsNil)
+}
+
func (s *RemoteSuite) TestFetchWithProgress(c *C) {
url := s.GetBasicLocalRepositoryURL()
sto := memory.NewStorage()
@@ -1220,17 +1255,20 @@ func (s *RemoteSuite) TestGetHaves(c *C) {
sto := filesystem.NewStorage(f.DotGit(), cache.NewObjectLRUDefault())
var localRefs = []*plumbing.Reference{
+ // Exists
plumbing.NewReferenceFromStrings(
"foo",
- "f7b877701fbf855b44c0a9e86f3fdce2c298b07f",
+ "b029517f6300c2da0f4b651b8642506cd6aaf45d",
),
+ // Exists
plumbing.NewReferenceFromStrings(
"bar",
- "fe6cb94756faa81e5ed9240f9191b833db5f40ae",
+ "b8e471f58bcbca63b07bda20e428190409c2db47",
),
+ // Doesn't Exist
plumbing.NewReferenceFromStrings(
"qux",
- "f7b877701fbf855b44c0a9e86f3fdce2c298b07f",
+ "0000000",
),
}
@@ -1444,6 +1482,122 @@ 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",
+ }})
+ c.Assert(err, IsNil)
+
+ 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",
+ }})
+ c.Assert(err, IsNil)
+
+ 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 1524a69..a57c714 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 51df845..0b77c5a 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,15 @@ func createCommit(c *C, r *Repository) {
Email: "go-git@fake.local",
When: time.Now(),
}
- _, err = wt.Commit("test commit message", &CommitOptions{
- All: true,
- Author: &author,
- Committer: &author,
+
+ h, err := wt.Commit("test commit message", &CommitOptions{
+ All: true,
+ Author: &author,
+ Committer: &author,
+ AllowEmptyCommits: true,
})
c.Assert(err, IsNil)
-
+ return h
}
func (s *RepositorySuite) TestInitNonStandardDotGit(c *C) {
@@ -439,6 +441,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 0000000..e3ef7eb
--- /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 0000000..eba0922
--- /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/storage/filesystem/dotgit/dotgit.go b/storage/filesystem/dotgit/dotgit.go
index 31c4694..72c9ccf 100644
--- a/storage/filesystem/dotgit/dotgit.go
+++ b/storage/filesystem/dotgit/dotgit.go
@@ -72,6 +72,9 @@ var (
// ErrIsDir is returned when a reference file is attempting to be read,
// but the path specified is a directory.
ErrIsDir = errors.New("reference path is a directory")
+ // ErrEmptyRefFile is returned when a reference file is attempted to be read,
+ // but the file is empty
+ ErrEmptyRefFile = errors.New("ref file is empty")
)
// Options holds configuration for the storage.
@@ -249,7 +252,7 @@ func (d *DotGit) objectPacks() ([]plumbing.Hash, error) {
continue
}
- h := plumbing.NewHash(n[5 : len(n)-5]) //pack-(hash).pack
+ h := plumbing.NewHash(n[5 : len(n)-5]) // pack-(hash).pack
if h.IsZero() {
// Ignore files with badly-formatted names.
continue
@@ -661,18 +664,33 @@ func (d *DotGit) readReferenceFrom(rd io.Reader, name string) (ref *plumbing.Ref
return nil, err
}
+ if len(b) == 0 {
+ return nil, ErrEmptyRefFile
+ }
+
line := strings.TrimSpace(string(b))
return plumbing.NewReferenceFromStrings(name, line), nil
}
+// checkReferenceAndTruncate reads the reference from the given file, or the `pack-refs` file if
+// the file was empty. Then it checks that the old reference matches the stored reference and
+// truncates the file.
func (d *DotGit) checkReferenceAndTruncate(f billy.File, old *plumbing.Reference) error {
if old == nil {
return nil
}
+
ref, err := d.readReferenceFrom(f, old.Name().String())
+ if errors.Is(err, ErrEmptyRefFile) {
+ // This may happen if the reference is being read from a newly created file.
+ // In that case, try getting the reference from the packed refs file.
+ ref, err = d.packedRef(old.Name())
+ }
+
if err != nil {
return err
}
+
if ref.Hash() != old.Hash() {
return storage.ErrReferenceHasChanged
}
@@ -701,16 +719,16 @@ func (d *DotGit) SetRef(r, old *plumbing.Reference) error {
// Symbolic references are resolved and included in the output.
func (d *DotGit) Refs() ([]*plumbing.Reference, error) {
var refs []*plumbing.Reference
- var seen = make(map[plumbing.ReferenceName]bool)
- if err := d.addRefsFromRefDir(&refs, seen); err != nil {
+ seen := make(map[plumbing.ReferenceName]bool)
+ if err := d.addRefFromHEAD(&refs); err != nil {
return nil, err
}
- if err := d.addRefsFromPackedRefs(&refs, seen); err != nil {
+ if err := d.addRefsFromRefDir(&refs, seen); err != nil {
return nil, err
}
- if err := d.addRefFromHEAD(&refs); err != nil {
+ if err := d.addRefsFromPackedRefs(&refs, seen); err != nil {
return nil, err
}
@@ -815,7 +833,8 @@ func (d *DotGit) addRefsFromPackedRefsFile(refs *[]*plumbing.Reference, f billy.
}
func (d *DotGit) openAndLockPackedRefs(doCreate bool) (
- pr billy.File, err error) {
+ pr billy.File, err error,
+) {
var f billy.File
defer func() {
if err != nil && f != nil {
@@ -1020,7 +1039,7 @@ func (d *DotGit) readReferenceFile(path, name string) (ref *plumbing.Reference,
func (d *DotGit) CountLooseRefs() (int, error) {
var refs []*plumbing.Reference
- var seen = make(map[plumbing.ReferenceName]bool)
+ seen := make(map[plumbing.ReferenceName]bool)
if err := d.addRefsFromRefDir(&refs, seen); err != nil {
return 0, err
}
diff --git a/storage/filesystem/dotgit/dotgit_test.go b/storage/filesystem/dotgit/dotgit_test.go
index 2cbdb0c..be66fee 100644
--- a/storage/filesystem/dotgit/dotgit_test.go
+++ b/storage/filesystem/dotgit/dotgit_test.go
@@ -16,6 +16,7 @@ import (
"github.com/go-git/go-billy/v5/util"
fixtures "github.com/go-git/go-git-fixtures/v4"
"github.com/go-git/go-git/v5/plumbing"
+ "github.com/go-git/go-git/v5/storage"
"github.com/stretchr/testify/assert"
. "gopkg.in/check.v1"
)
@@ -85,6 +86,15 @@ func (s *SuiteDotGit) TestSetRefsNorwfs(c *C) {
testSetRefs(c, dir)
}
+func (s *SuiteDotGit) TestRefsHeadFirst(c *C) {
+ fs := fixtures.Basic().ByTag(".git").One().DotGit()
+ dir := New(fs)
+ refs, err := dir.Refs()
+ c.Assert(err, IsNil)
+ c.Assert(len(refs), Not(Equals), 0)
+ c.Assert(refs[0].Name().String(), Equals, "HEAD")
+}
+
func testSetRefs(c *C, dir *DotGit) {
firstFoo := plumbing.NewReferenceFromStrings(
"refs/heads/foo",
@@ -175,7 +185,6 @@ func (s *SuiteDotGit) TestRefsFromPackedRefs(c *C) {
ref := findReference(refs, "refs/remotes/origin/branch")
c.Assert(ref, NotNil)
c.Assert(ref.Hash().String(), Equals, "e8d3ffab552895c19b9fcf7aa264d277cde33881")
-
}
func (s *SuiteDotGit) TestRefsFromReferenceFile(c *C) {
@@ -189,7 +198,6 @@ func (s *SuiteDotGit) TestRefsFromReferenceFile(c *C) {
c.Assert(ref, NotNil)
c.Assert(ref.Type(), Equals, plumbing.SymbolicReference)
c.Assert(string(ref.Target()), Equals, "refs/remotes/origin/master")
-
}
func BenchmarkRefMultipleTimes(b *testing.B) {
@@ -538,7 +546,6 @@ func (s *SuiteDotGit) TestObjectPackWithKeepDescriptors(c *C) {
err = dir.Close()
c.Assert(err, NotNil)
-
}
func (s *SuiteDotGit) TestObjectPackIdx(c *C) {
@@ -649,7 +656,7 @@ func (s *SuiteDotGit) TestObject(c *C) {
file.Name(), fs.Join("objects", "03", "db8e1fbe133a480f2867aac478fd866686d69e")),
Equals, true,
)
- incomingHash := "9d25e0f9bde9f82882b49fe29117b9411cb157b7" //made up hash
+ incomingHash := "9d25e0f9bde9f82882b49fe29117b9411cb157b7" // made up hash
incomingDirPath := fs.Join("objects", "tmp_objdir-incoming-123456")
incomingFilePath := fs.Join(incomingDirPath, incomingHash[0:2], incomingHash[2:40])
fs.MkdirAll(incomingDirPath, os.FileMode(0755))
@@ -670,7 +677,7 @@ func (s *SuiteDotGit) TestPreGit235Object(c *C) {
file.Name(), fs.Join("objects", "03", "db8e1fbe133a480f2867aac478fd866686d69e")),
Equals, true,
)
- incomingHash := "9d25e0f9bde9f82882b49fe29117b9411cb157b7" //made up hash
+ incomingHash := "9d25e0f9bde9f82882b49fe29117b9411cb157b7" // made up hash
incomingDirPath := fs.Join("objects", "incoming-123456")
incomingFilePath := fs.Join(incomingDirPath, incomingHash[0:2], incomingHash[2:40])
fs.MkdirAll(incomingDirPath, os.FileMode(0755))
@@ -687,7 +694,7 @@ func (s *SuiteDotGit) TestObjectStat(c *C) {
hash := plumbing.NewHash("03db8e1fbe133a480f2867aac478fd866686d69e")
_, err := dir.ObjectStat(hash)
c.Assert(err, IsNil)
- incomingHash := "9d25e0f9bde9f82882b49fe29117b9411cb157b7" //made up hash
+ incomingHash := "9d25e0f9bde9f82882b49fe29117b9411cb157b7" // made up hash
incomingDirPath := fs.Join("objects", "tmp_objdir-incoming-123456")
incomingFilePath := fs.Join(incomingDirPath, incomingHash[0:2], incomingHash[2:40])
fs.MkdirAll(incomingDirPath, os.FileMode(0755))
@@ -705,7 +712,7 @@ func (s *SuiteDotGit) TestObjectDelete(c *C) {
err := dir.ObjectDelete(hash)
c.Assert(err, IsNil)
- incomingHash := "9d25e0f9bde9f82882b49fe29117b9411cb157b7" //made up hash
+ incomingHash := "9d25e0f9bde9f82882b49fe29117b9411cb157b7" // made up hash
incomingDirPath := fs.Join("objects", "tmp_objdir-incoming-123456")
incomingSubDirPath := fs.Join(incomingDirPath, incomingHash[0:2])
incomingFilePath := fs.Join(incomingSubDirPath, incomingHash[2:40])
@@ -1040,3 +1047,63 @@ func (s *SuiteDotGit) TestDeletedRefs(c *C) {
c.Assert(refs, HasLen, 1)
c.Assert(refs[0].Name(), Equals, plumbing.ReferenceName("refs/heads/foo"))
}
+
+// Checks that seting a reference that has been packed and checking its old value is successful
+func (s *SuiteDotGit) TestSetPackedRef(c *C) {
+ fs, clean := s.TemporalFilesystem()
+ defer clean()
+
+ dir := New(fs)
+
+ err := dir.SetRef(plumbing.NewReferenceFromStrings(
+ "refs/heads/foo",
+ "e8d3ffab552895c19b9fcf7aa264d277cde33881",
+ ), nil)
+ c.Assert(err, IsNil)
+
+ refs, err := dir.Refs()
+ c.Assert(err, IsNil)
+ c.Assert(refs, HasLen, 1)
+ looseCount, err := dir.CountLooseRefs()
+ c.Assert(err, IsNil)
+ c.Assert(looseCount, Equals, 1)
+
+ err = dir.PackRefs()
+ c.Assert(err, IsNil)
+
+ // Make sure the refs are still there, but no longer loose.
+ refs, err = dir.Refs()
+ c.Assert(err, IsNil)
+ c.Assert(refs, HasLen, 1)
+ looseCount, err = dir.CountLooseRefs()
+ c.Assert(err, IsNil)
+ c.Assert(looseCount, Equals, 0)
+
+ ref, err := dir.Ref("refs/heads/foo")
+ c.Assert(err, IsNil)
+ c.Assert(ref, NotNil)
+ c.Assert(ref.Hash().String(), Equals, "e8d3ffab552895c19b9fcf7aa264d277cde33881")
+
+ // Attempt to update the reference using an invalid old reference value
+ err = dir.SetRef(plumbing.NewReferenceFromStrings(
+ "refs/heads/foo",
+ "b8d3ffab552895c19b9fcf7aa264d277cde33881",
+ ), plumbing.NewReferenceFromStrings(
+ "refs/heads/foo",
+ "e8d3ffab552895c19b9fcf7aa264d277cde33882",
+ ))
+ c.Assert(err, Equals, storage.ErrReferenceHasChanged)
+
+ // Now update the reference and it should pass
+ err = dir.SetRef(plumbing.NewReferenceFromStrings(
+ "refs/heads/foo",
+ "b8d3ffab552895c19b9fcf7aa264d277cde33881",
+ ), plumbing.NewReferenceFromStrings(
+ "refs/heads/foo",
+ "e8d3ffab552895c19b9fcf7aa264d277cde33881",
+ ))
+ c.Assert(err, IsNil)
+ looseCount, err = dir.CountLooseRefs()
+ c.Assert(err, IsNil)
+ c.Assert(looseCount, Equals, 1)
+}
diff --git a/storage/filesystem/index.go b/storage/filesystem/index.go
index a19176f..a86ef3e 100644
--- a/storage/filesystem/index.go
+++ b/storage/filesystem/index.go
@@ -48,7 +48,7 @@ func (s *IndexStorage) Index() (i *index.Index, err error) {
defer ioutil.CheckClose(f, &err)
- d := index.NewDecoder(bufio.NewReader(f))
+ d := index.NewDecoder(f)
err = d.Decode(idx)
return idx, err
}
diff --git a/storage/filesystem/object.go b/storage/filesystem/object.go
index e812fe9..91b4ace 100644
--- a/storage/filesystem/object.go
+++ b/storage/filesystem/object.go
@@ -431,13 +431,13 @@ func (s *ObjectStorage) getFromUnpacked(h plumbing.Hash) (obj plumbing.EncodedOb
defer ioutil.CheckClose(w, &err)
- s.objectCache.Put(obj)
-
bufp := copyBufferPool.Get().(*[]byte)
buf := *bufp
_, err = io.CopyBuffer(w, r, buf)
copyBufferPool.Put(bufp)
+ s.objectCache.Put(obj)
+
return obj, err
}
diff --git a/storage/filesystem/object_test.go b/storage/filesystem/object_test.go
index 251077a..4f98458 100644
--- a/storage/filesystem/object_test.go
+++ b/storage/filesystem/object_test.go
@@ -547,3 +547,64 @@ func BenchmarkGetObjectFromPackfile(b *testing.B) {
})
}
}
+
+func (s *FsSuite) TestGetFromUnpackedCachesObjects(c *C) {
+ fs := fixtures.ByTag(".git").ByTag("unpacked").One().DotGit()
+ objectCache := cache.NewObjectLRUDefault()
+ objectStorage := NewObjectStorage(dotgit.New(fs), objectCache)
+ hash := plumbing.NewHash("f3dfe29d268303fc6e1bbce268605fc99573406e")
+
+ // Assert the cache is empty initially
+ _, ok := objectCache.Get(hash)
+ c.Assert(ok, Equals, false)
+
+ // Load the object
+ obj, err := objectStorage.EncodedObject(plumbing.AnyObject, hash)
+ c.Assert(err, IsNil)
+ c.Assert(obj.Hash(), Equals, hash)
+
+ // The object should've been cached during the load
+ cachedObj, ok := objectCache.Get(hash)
+ c.Assert(ok, Equals, true)
+ c.Assert(cachedObj, DeepEquals, obj)
+
+ // Assert that both objects can be read and that they both produce the same bytes
+
+ objReader, err := obj.Reader()
+ c.Assert(err, IsNil)
+ objBytes, err := io.ReadAll(objReader)
+ c.Assert(err, IsNil)
+ c.Assert(len(objBytes), Not(Equals), 0)
+ err = objReader.Close()
+ c.Assert(err, IsNil)
+
+ cachedObjReader, err := cachedObj.Reader()
+ c.Assert(err, IsNil)
+ cachedObjBytes, err := io.ReadAll(cachedObjReader)
+ c.Assert(len(cachedObjBytes), Not(Equals), 0)
+ c.Assert(err, IsNil)
+ err = cachedObjReader.Close()
+ c.Assert(err, IsNil)
+
+ c.Assert(cachedObjBytes, DeepEquals, objBytes)
+}
+
+func (s *FsSuite) TestGetFromUnpackedDoesNotCacheLargeObjects(c *C) {
+ fs := fixtures.ByTag(".git").ByTag("unpacked").One().DotGit()
+ objectCache := cache.NewObjectLRUDefault()
+ objectStorage := NewObjectStorageWithOptions(dotgit.New(fs), objectCache, Options{LargeObjectThreshold: 1})
+ hash := plumbing.NewHash("f3dfe29d268303fc6e1bbce268605fc99573406e")
+
+ // Assert the cache is empty initially
+ _, ok := objectCache.Get(hash)
+ c.Assert(ok, Equals, false)
+
+ // Load the object
+ obj, err := objectStorage.EncodedObject(plumbing.AnyObject, hash)
+ c.Assert(err, IsNil)
+ c.Assert(obj.Hash(), Equals, hash)
+
+ // The object should not have been cached during the load
+ _, ok = objectCache.Get(hash)
+ c.Assert(ok, Equals, false)
+}
diff --git a/utils/merkletrie/change.go b/utils/merkletrie/change.go
index cc6dc89..450feb4 100644
--- a/utils/merkletrie/change.go
+++ b/utils/merkletrie/change.go
@@ -1,12 +1,17 @@
package merkletrie
import (
+ "errors"
"fmt"
"io"
"github.com/go-git/go-git/v5/utils/merkletrie/noder"
)
+var (
+ ErrEmptyFileName = errors.New("empty filename in tree entry")
+)
+
// Action values represent the kind of things a Change can represent:
// insertion, deletions or modifications of files.
type Action int
@@ -121,6 +126,10 @@ func (l *Changes) AddRecursiveDelete(root noder.Path) error {
type noderToChangeFn func(noder.Path) Change // NewInsert or NewDelete
func (l *Changes) addRecursive(root noder.Path, ctor noderToChangeFn) error {
+ if root.String() == "" {
+ return ErrEmptyFileName
+ }
+
if !root.IsDir() {
l.Add(ctor(root))
return nil
diff --git a/utils/merkletrie/change_test.go b/utils/merkletrie/change_test.go
index f73eb86..cd28bfe 100644
--- a/utils/merkletrie/change_test.go
+++ b/utils/merkletrie/change_test.go
@@ -28,6 +28,17 @@ func (s *ChangeSuite) TestUnsupportedAction(c *C) {
c.Assert(a.String, PanicMatches, "unsupported action.*")
}
+func (s ChangeSuite) TestEmptyChanges(c *C) {
+ ret := merkletrie.NewChanges()
+ p := noder.Path{}
+
+ err := ret.AddRecursiveInsert(p)
+ c.Assert(err, Equals, merkletrie.ErrEmptyFileName)
+
+ err = ret.AddRecursiveDelete(p)
+ c.Assert(err, Equals, merkletrie.ErrEmptyFileName)
+}
+
func (s ChangeSuite) TestNewInsert(c *C) {
tree, err := fsnoder.New("(a(b(z<>)))")
c.Assert(err, IsNil)
diff --git a/worktree.go b/worktree.go
index 4dfe036..ab11d42 100644
--- a/worktree.go
+++ b/worktree.go
@@ -428,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 18002f2..2faf6f0 100644
--- a/worktree_commit.go
+++ b/worktree_commit.go
@@ -2,8 +2,6 @@ package git
import (
"bytes"
- "crypto"
- "crypto/rand"
"errors"
"io"
"path"
@@ -40,36 +38,53 @@ func (w *Worktree) Commit(msg string, opts *CommitOptions) (plumbing.Hash, error
}
}
- var treeHash plumbing.Hash
-
if opts.Amend {
head, err := w.r.Head()
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)
+ // First handle the case of the first commit in the repository being empty.
+ if len(opts.Parents) == 0 && len(idx.Entries) == 0 && !opts.AllowEmptyCommits {
+ return plumbing.ZeroHash, ErrEmptyCommit
+ }
+
+ h := &buildTreeHelper{
+ fs: w.Filesystem,
+ s: w.r.Storer,
+ }
+
+ treeHash, err := h.BuildTree(idx, opts)
+ if err != nil {
+ return plumbing.ZeroHash, err
+ }
+
+ previousTree := plumbing.ZeroHash
+ if len(opts.Parents) > 0 {
+ parentCommit, err := w.r.CommitObject(opts.Parents[0])
if err != nil {
return plumbing.ZeroHash, err
}
+ previousTree = parentCommit.TreeHash
+ }
+
+ if treeHash == previousTree && !opts.AllowEmptyCommits {
+ return plumbing.ZeroHash, ErrEmptyCommit
}
commit, err := w.buildCommitObject(msg, opts, treeHash)
@@ -135,7 +150,7 @@ func (w *Worktree) buildCommitObject(msg string, opts *CommitOptions, tree plumb
signer = &gpgSigner{key: opts.SignKey}
}
if signer != nil {
- sig, err := w.buildCommitSignature(commit, signer)
+ sig, err := signObject(signer, commit)
if err != nil {
return plumbing.ZeroHash, err
}
@@ -151,44 +166,17 @@ func (w *Worktree) buildCommitObject(msg string, opts *CommitOptions, tree plumb
type gpgSigner struct {
key *openpgp.Entity
+ cfg *packet.Config
}
-func (s *gpgSigner) Public() crypto.PublicKey {
- return s.key.PrimaryKey
-}
-
-func (s *gpgSigner) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) {
- var cfg *packet.Config
- if opts != nil {
- cfg = &packet.Config{
- DefaultHash: opts.HashFunc(),
- }
- }
-
+func (s *gpgSigner) Sign(message io.Reader) ([]byte, error) {
var b bytes.Buffer
- if err := openpgp.ArmoredDetachSign(&b, s.key, bytes.NewReader(digest), cfg); err != nil {
+ if err := openpgp.ArmoredDetachSign(&b, s.key, message, s.cfg); err != nil {
return nil, err
}
return b.Bytes(), nil
}
-func (w *Worktree) buildCommitSignature(commit *object.Commit, signer crypto.Signer) ([]byte, error) {
- encoded := &plumbing.MemoryObject{}
- if err := commit.Encode(encoded); err != nil {
- return nil, err
- }
- r, err := encoded.Reader()
- if err != nil {
- return nil, err
- }
- b, err := io.ReadAll(r)
- if err != nil {
- return nil, err
- }
-
- return signer.Sign(rand.Reader, b, nil)
-}
-
// buildTreeHelper converts a given index.Index file into multiple git objects
// reading the blobs from the given filesystem and creating the trees from the
// index structure. The created objects are pushed to a given Storer.
@@ -203,10 +191,6 @@ type buildTreeHelper struct {
// BuildTree builds the tree objects and push its to the storer, the hash
// of the root tree is returned.
func (h *buildTreeHelper) BuildTree(idx *index.Index, opts *CommitOptions) (plumbing.Hash, error) {
- if len(idx.Entries) == 0 && (opts == nil || !opts.AllowEmptyCommits) {
- return plumbing.ZeroHash, ErrEmptyCommit
- }
-
const rootNode = ""
h.trees = map[string]*object.Tree{rootNode: {}}
h.entries = map[string]*object.TreeEntry{}
diff --git a/worktree_commit_test.go b/worktree_commit_test.go
index a3103b7..e028fac 100644
--- a/worktree_commit_test.go
+++ b/worktree_commit_test.go
@@ -89,6 +89,56 @@ func (s *WorktreeSuite) TestNothingToCommit(c *C) {
c.Assert(err, IsNil)
}
+func (s *WorktreeSuite) TestNothingToCommitNonEmptyRepo(c *C) {
+ fs := memfs.New()
+ r, err := Init(memory.NewStorage(), fs)
+ c.Assert(err, IsNil)
+
+ w, err := r.Worktree()
+ c.Assert(err, IsNil)
+
+ err = util.WriteFile(fs, "foo", []byte("foo"), 0644)
+ c.Assert(err, IsNil)
+
+ w.Add("foo")
+ _, err = w.Commit("previous commit\n", &CommitOptions{Author: defaultSignature()})
+ c.Assert(err, IsNil)
+
+ hash, err := w.Commit("failed empty commit\n", &CommitOptions{Author: defaultSignature()})
+ c.Assert(hash, Equals, plumbing.ZeroHash)
+ c.Assert(err, Equals, ErrEmptyCommit)
+
+ _, err = w.Commit("enable empty commits\n", &CommitOptions{Author: defaultSignature(), AllowEmptyCommits: true})
+ c.Assert(err, IsNil)
+}
+
+func (s *WorktreeSuite) TestRemoveAndCommitToMakeEmptyRepo(c *C) {
+ fs := memfs.New()
+ r, err := Init(memory.NewStorage(), fs)
+ c.Assert(err, IsNil)
+
+ w, err := r.Worktree()
+ c.Assert(err, IsNil)
+
+ err = util.WriteFile(fs, "foo", []byte("foo"), 0644)
+ c.Assert(err, IsNil)
+
+ _, err = w.Add("foo")
+ c.Assert(err, IsNil)
+
+ _, err = w.Commit("Add in Repo\n", &CommitOptions{Author: defaultSignature()})
+ c.Assert(err, IsNil)
+
+ err = fs.Remove("foo")
+ c.Assert(err, IsNil)
+
+ _, err = w.Add("foo")
+ c.Assert(err, IsNil)
+
+ _, err = w.Commit("Remove foo\n", &CommitOptions{Author: defaultSignature()})
+ c.Assert(err, IsNil)
+}
+
func (s *WorktreeSuite) TestCommitParent(c *C) {
expected := plumbing.NewHash("ef3ca05477530b37f48564be33ddd48063fc7a22")
@@ -101,7 +151,8 @@ func (s *WorktreeSuite) TestCommitParent(c *C) {
err := w.Checkout(&CheckoutOptions{})
c.Assert(err, IsNil)
- util.WriteFile(fs, "foo", []byte("foo"), 0644)
+ err = util.WriteFile(fs, "foo", []byte("foo"), 0644)
+ c.Assert(err, IsNil)
_, err = w.Add("foo")
c.Assert(err, IsNil)
@@ -113,7 +164,42 @@ func (s *WorktreeSuite) TestCommitParent(c *C) {
assertStorageStatus(c, s.Repository, 13, 11, 10, expected)
}
-func (s *WorktreeSuite) TestCommitAmend(c *C) {
+func (s *WorktreeSuite) TestCommitAmendWithoutChanges(c *C) {
+ fs := memfs.New()
+ w := &Worktree{
+ r: s.Repository,
+ Filesystem: fs,
+ }
+
+ err := w.Checkout(&CheckoutOptions{})
+ c.Assert(err, IsNil)
+
+ err = util.WriteFile(fs, "foo", []byte("foo"), 0644)
+ c.Assert(err, IsNil)
+
+ _, err = w.Add("foo")
+ c.Assert(err, IsNil)
+
+ prevHash, err := w.Commit("foo\n", &CommitOptions{Author: defaultSignature()})
+ c.Assert(err, IsNil)
+
+ amendedHash, err := w.Commit("foo\n", &CommitOptions{Author: defaultSignature(), Amend: true})
+ c.Assert(err, IsNil)
+
+ headRef, err := w.r.Head()
+ c.Assert(err, IsNil)
+
+ c.Assert(amendedHash, Equals, headRef.Hash())
+ c.Assert(amendedHash, Equals, prevHash)
+
+ commit, err := w.r.CommitObject(headRef.Hash())
+ c.Assert(err, IsNil)
+ c.Assert(commit.Message, Equals, "foo\n")
+
+ assertStorageStatus(c, s.Repository, 13, 11, 10, amendedHash)
+}
+
+func (s *WorktreeSuite) TestCommitAmendWithChanges(c *C) {
fs := memfs.New()
w := &Worktree{
r: s.Repository,
@@ -131,16 +217,65 @@ 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, 13, 11, 11, amendedHash)
+ assertStorageStatus(c, s.Repository, 14, 12, 11, amendedHash)
+}
+
+func (s *WorktreeSuite) TestCommitAmendNothingToCommit(c *C) {
+ fs := memfs.New()
+ w := &Worktree{
+ r: s.Repository,
+ Filesystem: fs,
+ }
+
+ err := w.Checkout(&CheckoutOptions{})
+ c.Assert(err, IsNil)
+
+ err = util.WriteFile(fs, "foo", []byte("foo"), 0644)
+ c.Assert(err, IsNil)
+
+ _, err = w.Add("foo")
+ c.Assert(err, IsNil)
+
+ prevHash, err := w.Commit("foo\n", &CommitOptions{Author: defaultSignature()})
+ c.Assert(err, IsNil)
+
+ _, err = w.Commit("bar\n", &CommitOptions{Author: defaultSignature(), AllowEmptyCommits: true})
+ c.Assert(err, IsNil)
+
+ amendedHash, err := w.Commit("foo\n", &CommitOptions{Author: defaultSignature(), Amend: true})
+ c.Log(prevHash, amendedHash)
+ c.Assert(err, Equals, ErrEmptyCommit)
+ c.Assert(amendedHash, Equals, plumbing.ZeroHash)
}
func (s *WorktreeSuite) TestAddAndCommitWithSkipStatus(c *C) {
@@ -212,7 +347,9 @@ func (s *WorktreeSuite) TestAddAndCommitWithSkipStatusPathNotModified(c *C) {
})
c.Assert(hash, Equals, expected)
c.Assert(err, IsNil)
+
commit1, err := w.r.CommitObject(hash)
+ c.Assert(err, IsNil)
status, err = w.Status()
c.Assert(err, IsNil)
@@ -235,11 +372,14 @@ func (s *WorktreeSuite) TestAddAndCommitWithSkipStatusPathNotModified(c *C) {
c.Assert(foo.Worktree, Equals, Untracked)
hash, err = w.Commit("commit with no changes\n", &CommitOptions{
- Author: defaultSignature(),
+ Author: defaultSignature(),
+ AllowEmptyCommits: true,
})
c.Assert(hash, Equals, expected2)
c.Assert(err, IsNil)
+
commit2, err := w.r.CommitObject(hash)
+ c.Assert(err, IsNil)
status, err = w.Status()
c.Assert(err, IsNil)
diff --git a/worktree_test.go b/worktree_test.go
index 7ecd818..636ccbe 100644
--- a/worktree_test.go
+++ b/worktree_test.go
@@ -4,7 +4,6 @@ import (
"bytes"
"context"
"errors"
- "fmt"
"io"
"os"
"path/filepath"
@@ -33,9 +32,11 @@ import (
. "gopkg.in/check.v1"
)
-var (
- defaultTestCommitOptions = &CommitOptions{Author: &object.Signature{Name: "testuser", Email: "testemail"}}
-)
+func defaultTestCommitOptions() *CommitOptions {
+ return &CommitOptions{
+ Author: &object.Signature{Name: "testuser", Email: "testemail"},
+ }
+}
type WorktreeSuite struct {
BaseSuite
@@ -88,8 +89,9 @@ func (s *WorktreeSuite) TestPullFastForward(c *C) {
w, err := server.Worktree()
c.Assert(err, IsNil)
- err = os.WriteFile(filepath.Join(path, "foo"), []byte("foo"), 0755)
+ err = os.WriteFile(filepath.Join(url, "foo"), []byte("foo"), 0755)
c.Assert(err, IsNil)
+ w.Add("foo")
hash, err := w.Commit("foo", &CommitOptions{Author: defaultSignature()})
c.Assert(err, IsNil)
@@ -125,15 +127,17 @@ func (s *WorktreeSuite) TestPullNonFastForward(c *C) {
w, err := server.Worktree()
c.Assert(err, IsNil)
- err = os.WriteFile(filepath.Join(path, "foo"), []byte("foo"), 0755)
+ err = os.WriteFile(filepath.Join(url, "foo"), []byte("foo"), 0755)
c.Assert(err, IsNil)
+ w.Add("foo")
_, err = w.Commit("foo", &CommitOptions{Author: defaultSignature()})
c.Assert(err, IsNil)
w, err = r.Worktree()
c.Assert(err, IsNil)
- err = os.WriteFile(filepath.Join(path, "bar"), []byte("bar"), 0755)
+ err = os.WriteFile(filepath.Join(dir, "bar"), []byte("bar"), 0755)
c.Assert(err, IsNil)
+ w.Add("bar")
_, err = w.Commit("bar", &CommitOptions{Author: defaultSignature()})
c.Assert(err, IsNil)
@@ -286,7 +290,8 @@ func (s *RepositorySuite) TestPullAdd(c *C) {
func (s *WorktreeSuite) TestPullAlreadyUptodate(c *C) {
path := fixtures.Basic().ByTag("worktree").One().Worktree().Root()
- r, err := Clone(memory.NewStorage(), memfs.New(), &CloneOptions{
+ fs := memfs.New()
+ r, err := Clone(memory.NewStorage(), fs, &CloneOptions{
URL: filepath.Join(path, ".git"),
})
@@ -294,8 +299,9 @@ func (s *WorktreeSuite) TestPullAlreadyUptodate(c *C) {
w, err := r.Worktree()
c.Assert(err, IsNil)
- err = os.WriteFile(filepath.Join(path, "bar"), []byte("bar"), 0755)
+ err = util.WriteFile(fs, "bar", []byte("bar"), 0755)
c.Assert(err, IsNil)
+ w.Add("bar")
_, err = w.Commit("bar", &CommitOptions{Author: defaultSignature()})
c.Assert(err, IsNil)
@@ -1002,14 +1008,14 @@ func (s *WorktreeSuite) TestStatusCheckedInBeforeIgnored(c *C) {
_, err = w.Add("fileToIgnore")
c.Assert(err, IsNil)
- _, err = w.Commit("Added file that will be ignored later", defaultTestCommitOptions)
+ _, err = w.Commit("Added file that will be ignored later", defaultTestCommitOptions())
c.Assert(err, IsNil)
err = util.WriteFile(fs, ".gitignore", []byte("fileToIgnore\nsecondIgnoredFile"), 0755)
c.Assert(err, IsNil)
_, err = w.Add(".gitignore")
c.Assert(err, IsNil)
- _, err = w.Commit("Added .gitignore", defaultTestCommitOptions)
+ _, err = w.Commit("Added .gitignore", defaultTestCommitOptions())
c.Assert(err, IsNil)
status, err := w.Status()
c.Assert(err, IsNil)
@@ -1268,6 +1274,7 @@ func (s *WorktreeSuite) TestResetHardWithGitIgnore(c *C) {
f, err := fs.Create(".gitignore")
c.Assert(err, IsNil)
_, err = f.Write([]byte("foo\n"))
+ c.Assert(err, IsNil)
_, err = f.Write([]byte("newTestFile.txt\n"))
c.Assert(err, IsNil)
err = f.Close()
@@ -2083,7 +2090,7 @@ func (s *WorktreeSuite) TestAddSkipStatusWithIgnoredPath(c *C) {
c.Assert(err, IsNil)
_, err = w.Add(".gitignore")
c.Assert(err, IsNil)
- _, err = w.Commit("Added .gitignore", defaultTestCommitOptions)
+ _, err = w.Commit("Added .gitignore", defaultTestCommitOptions())
c.Assert(err, IsNil)
err = util.WriteFile(fs, "fileToIgnore", []byte("file to ignore"), 0644)
@@ -2984,6 +2991,8 @@ func TestValidPath(t *testing.T) {
{"git~1", true},
{"a/../b", true},
{"a\\..\\b", true},
+ {"/", true},
+ {"", true},
{".gitmodules", false},
{".gitignore", false},
{"a..b", false},
@@ -3007,7 +3016,7 @@ func TestValidPath(t *testing.T) {
}
for _, tc := range tests {
- t.Run(fmt.Sprintf("%s", tc.path), func(t *testing.T) {
+ t.Run(tc.path, func(t *testing.T) {
err := validPath(tc.path)
if tc.wantErr {
assert.Error(t, err)
@@ -3038,7 +3047,7 @@ func TestWindowsValidPath(t *testing.T) {
}
for _, tc := range tests {
- t.Run(fmt.Sprintf("%s", tc.path), func(t *testing.T) {
+ t.Run(tc.path, func(t *testing.T) {
got := windowsValidPath(tc.path)
assert.Equal(t, tc.want, got)
})