diff options
55 files changed, 1406 insertions, 223 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/stale-issues-bot.yaml b/.github/workflows/stale-issues-bot.yaml index 11b86ae..fe40db3 100644 --- a/.github/workflows/stale-issues-bot.yaml +++ b/.github/workflows/stale-issues-bot.yaml @@ -15,7 +15,7 @@ jobs: with: ascending: true operations-per-run: 30 - days-before-stale: 90 + days-before-stale: 180 days-before-close: 30 stale-issue-label: stale stale-pr-label: stale 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 ff0c22c..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 @@ -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) @@ -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.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.2 + 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.21.0 - golang.org/x/net v0.22.0 - golang.org/x/sys v0.18.0 - golang.org/x/text v0.14.0 + golang.org/x/crypto v0.26.0 + golang.org/x/net v0.28.0 + golang.org/x/sys v0.24.0 + golang.org/x/text v0.17.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.8.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 ) @@ -19,8 +19,8 @@ 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= @@ -29,8 +29,8 @@ 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,8 +64,8 @@ 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.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= -github.com/skeema/knownhosts v1.2.2/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= @@ -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.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= 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.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= -golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= 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.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.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.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.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.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= +golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= 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.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 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/plumbing/format/gitattributes/dir.go b/plumbing/format/gitattributes/dir.go index d3b0dbe..4238196 100644 --- a/plumbing/format/gitattributes/dir.go +++ b/plumbing/format/gitattributes/dir.go @@ -2,6 +2,8 @@ package gitattributes import ( "os" + "path/filepath" + "strings" "github.com/go-git/go-billy/v5" @@ -59,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 aca5d0d..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 { 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..fc25d37 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,75 @@ 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 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 +297,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 +482,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/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/file/client.go b/plumbing/transport/file/client.go index 38714e2..d921d0a 100644 --- a/plumbing/transport/file/client.go +++ b/plumbing/transport/file/client.go @@ -7,6 +7,7 @@ import ( "io" "os" "path/filepath" + "runtime" "strings" "github.com/go-git/go-git/v5/plumbing/transport" @@ -95,7 +96,23 @@ func (r *runner) Command(cmd string, ep *transport.Endpoint, auth transport.Auth } } - return &command{cmd: execabs.Command(cmd, ep.Path)}, nil + return &command{cmd: execabs.Command(cmd, adjustPathForWindows(ep.Path))}, nil +} + +func isDriveLetter(c byte) bool { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') +} + +// On Windows, the path that results from a file: URL has a leading slash. This +// has to be removed if there's a drive letter +func adjustPathForWindows(p string) string { + if runtime.GOOS != "windows" { + return p + } + if len(p) >= 3 && p[0] == '/' && isDriveLetter(p[1]) && p[2] == ':' { + return p[1:] + } + return p } type command struct { diff --git a/plumbing/transport/http/common.go b/plumbing/transport/http/common.go index 1c4ceee..120008d 100644 --- a/plumbing/transport/http/common.go +++ b/plumbing/transport/http/common.go @@ -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 c831ac3..f0eb68d 100644 --- a/plumbing/transport/http/common_test.go +++ b/plumbing/transport/http/common_test.go @@ -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/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) } @@ -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" @@ -491,7 +492,18 @@ func (r *Remote) fetch(ctx context.Context, o *FetchOptions) (sto storer.Referen } if !updated && !updatedPrune { - return remoteRefs, NoErrAlreadyUpToDate + // 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 @@ -878,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 } diff --git a/remote_test.go b/remote_test.go index 1ffedd4..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", ), } @@ -1489,6 +1527,7 @@ func (s *RemoteSuite) TestFetchPrune(c *C) { 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(), @@ -1546,6 +1585,7 @@ func (s *RemoteSuite) TestFetchPruneTags(c *C) { 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(), diff --git a/repository.go b/repository.go index a57c714..6d7e196 100644 --- a/repository.go +++ b/repository.go @@ -1037,7 +1037,7 @@ func (r *Repository) setIsBare(isBare bool) error { return r.Storer.SetConfig(cfg) } -func (r *Repository) updateRemoteConfigIfNeeded(o *CloneOptions, c *config.RemoteConfig, head *plumbing.Reference) error { +func (r *Repository) updateRemoteConfigIfNeeded(o *CloneOptions, c *config.RemoteConfig, _ *plumbing.Reference) error { if !o.SingleBranch { return nil } diff --git a/repository_test.go b/repository_test.go index b211f8c..0b77c5a 100644 --- a/repository_test.go +++ b/repository_test.go @@ -103,9 +103,10 @@ func createCommit(c *C, r *Repository) plumbing.Hash { } h, err := wt.Commit("test commit message", &CommitOptions{ - All: true, - Author: &author, - Committer: &author, + All: true, + Author: &author, + Committer: &author, + AllowEmptyCommits: true, }) c.Assert(err, IsNil) return h diff --git a/repository_windows_test.go b/repository_windows_test.go index bec0acd..e7c1ac7 100644 --- a/repository_windows_test.go +++ b/repository_windows_test.go @@ -1,9 +1,47 @@ package git -import "fmt" +import ( + "fmt" + "strings" + + "github.com/go-git/go-billy/v5/util" + "github.com/go-git/go-git/v5/storage/memory" + . "gopkg.in/check.v1" +) // preReceiveHook returns the bytes of a pre-receive hook script // that prints m before exiting successfully func preReceiveHook(m string) []byte { return []byte(fmt.Sprintf("#!C:/Program\\ Files/Git/usr/bin/sh.exe\nprintf '%s'\n", m)) } + +func (s *RepositorySuite) TestCloneFileUrlWindows(c *C) { + dir, clean := s.TemporalDir() + defer clean() + + r, err := PlainInit(dir, false) + c.Assert(err, IsNil) + + err = util.WriteFile(r.wt, "foo", nil, 0755) + c.Assert(err, IsNil) + + w, err := r.Worktree() + c.Assert(err, IsNil) + + _, err = w.Add("foo") + c.Assert(err, IsNil) + + _, err = w.Commit("foo", &CommitOptions{ + Author: defaultSignature(), + Committer: defaultSignature(), + }) + c.Assert(err, IsNil) + + url := "file:///" + strings.ReplaceAll(dir, "\\", "/") + c.Assert(url, Matches, "file:///[A-Za-z]:/.*") + _, err = Clone(memory.NewStorage(), nil, &CloneOptions{ + URL: url, + }) + + c.Assert(err, IsNil) +} @@ -4,6 +4,9 @@ import ( "bytes" "fmt" "path/filepath" + + mindex "github.com/go-git/go-git/v5/utils/merkletrie/index" + "github.com/go-git/go-git/v5/utils/merkletrie/noder" ) // Status represents the current status of a Worktree. @@ -77,3 +80,69 @@ const ( Copied StatusCode = 'C' UpdatedButUnmerged StatusCode = 'U' ) + +// StatusStrategy defines the different types of strategies when processing +// the worktree status. +type StatusStrategy int + +const ( + // TODO: (V6) Review the default status strategy. + // TODO: (V6) Review the type used to represent Status, to enable lazy + // processing of statuses going direct to the backing filesystem. + defaultStatusStrategy = Empty + + // Empty starts its status map from empty. Missing entries for a given + // path means that the file is untracked. This causes a known issue (#119) + // whereby unmodified files can be incorrectly reported as untracked. + // + // This can be used when returning the changed state within a modified Worktree. + // For example, to check whether the current worktree is clean. + Empty StatusStrategy = 0 + // Preload goes through all existing nodes from the index and add them to the + // status map as unmodified. This is currently the most reliable strategy + // although it comes at a performance cost in large repositories. + // + // This method is recommended when fetching the status of unmodified files. + // For example, to confirm the status of a specific file that is either + // untracked or unmodified. + Preload StatusStrategy = 1 +) + +func (s StatusStrategy) new(w *Worktree) (Status, error) { + switch s { + case Preload: + return preloadStatus(w) + case Empty: + return make(Status), nil + } + return nil, fmt.Errorf("%w: %+v", ErrUnsupportedStatusStrategy, s) +} + +func preloadStatus(w *Worktree) (Status, error) { + idx, err := w.r.Storer.Index() + if err != nil { + return nil, err + } + + idxRoot := mindex.NewRootNode(idx) + nodes := []noder.Noder{idxRoot} + + status := make(Status) + for len(nodes) > 0 { + var node noder.Noder + node, nodes = nodes[0], nodes[1:] + if node.IsDir() { + children, err := node.Children() + if err != nil { + return nil, err + } + nodes = append(nodes, children...) + continue + } + fs := status.File(node.Name()) + fs.Worktree = Unmodified + fs.Staging = Unmodified + } + + return status, nil +} 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..fdb8a57 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) { @@ -605,7 +612,7 @@ func (s *SuiteDotGit) TestObjectsExclusive(c *C) { testObjectsWithPrefix(c, fs, dir) } -func testObjects(c *C, fs billy.Filesystem, dir *DotGit) { +func testObjects(c *C, _ billy.Filesystem, dir *DotGit) { hashes, err := dir.Objects() c.Assert(err, IsNil) c.Assert(hashes, HasLen, 187) @@ -614,7 +621,7 @@ func testObjects(c *C, fs billy.Filesystem, dir *DotGit) { c.Assert(hashes[2].String(), Equals, "03db8e1fbe133a480f2867aac478fd866686d69e") } -func testObjectsWithPrefix(c *C, fs billy.Filesystem, dir *DotGit) { +func testObjectsWithPrefix(c *C, _ billy.Filesystem, dir *DotGit) { prefix, _ := hex.DecodeString("01d5") hashes, err := dir.ObjectsWithPrefix(prefix) c.Assert(err, IsNil) @@ -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_commit.go b/worktree_commit.go index f62054b..2faf6f0 100644 --- a/worktree_commit.go +++ b/worktree_commit.go @@ -38,8 +38,6 @@ 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 { @@ -61,16 +59,34 @@ func (w *Worktree) Commit(msg string, opts *CommitOptions) (plumbing.Hash, error return plumbing.ZeroHash, err } + // 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) + 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) if err != nil { return plumbing.ZeroHash, err @@ -175,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 fee8b15..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, @@ -164,6 +250,34 @@ func (s *WorktreeSuite) TestCommitAmend(c *C) { 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) { expected := plumbing.NewHash("375a3808ffde7f129cdd3c8c252fd0fe37cfd13b") @@ -233,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) @@ -256,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_linux.go b/worktree_linux.go index 6fcace2..f6b85fe 100644 --- a/worktree_linux.go +++ b/worktree_linux.go @@ -1,3 +1,4 @@ +//go:build linux // +build linux package git @@ -21,6 +22,6 @@ func init() { } } -func isSymlinkWindowsNonAdmin(err error) bool { +func isSymlinkWindowsNonAdmin(_ error) bool { return false } diff --git a/worktree_status.go b/worktree_status.go index dd9b243..6e72db9 100644 --- a/worktree_status.go +++ b/worktree_status.go @@ -29,10 +29,23 @@ var ( // ErrGlobNoMatches in an AddGlob if the glob pattern does not match any // files in the worktree. ErrGlobNoMatches = errors.New("glob pattern did not match any files") + // ErrUnsupportedStatusStrategy occurs when an invalid StatusStrategy is used + // when processing the Worktree status. + ErrUnsupportedStatusStrategy = errors.New("unsupported status strategy") ) // Status returns the working tree status. func (w *Worktree) Status() (Status, error) { + return w.StatusWithOptions(StatusOptions{Strategy: defaultStatusStrategy}) +} + +// StatusOptions defines the options for Worktree.StatusWithOptions(). +type StatusOptions struct { + Strategy StatusStrategy +} + +// StatusWithOptions returns the working tree status. +func (w *Worktree) StatusWithOptions(o StatusOptions) (Status, error) { var hash plumbing.Hash ref, err := w.r.Head() @@ -44,11 +57,14 @@ func (w *Worktree) Status() (Status, error) { hash = ref.Hash() } - return w.status(hash) + return w.status(o.Strategy, hash) } -func (w *Worktree) status(commit plumbing.Hash) (Status, error) { - s := make(Status) +func (w *Worktree) status(ss StatusStrategy, commit plumbing.Hash) (Status, error) { + s, err := ss.new(w) + if err != nil { + return nil, err + } left, err := w.diffCommitWithStaging(commit, false) if err != nil { @@ -488,7 +504,7 @@ func (w *Worktree) copyFileToStorage(path string) (hash plumbing.Hash, err error return w.r.Storer.SetEncodedObject(obj) } -func (w *Worktree) fillEncodedObjectFromFile(dst io.Writer, path string, fi os.FileInfo) (err error) { +func (w *Worktree) fillEncodedObjectFromFile(dst io.Writer, path string, _ os.FileInfo) (err error) { src, err := w.Filesystem.Open(path) if err != nil { return err @@ -503,7 +519,7 @@ func (w *Worktree) fillEncodedObjectFromFile(dst io.Writer, path string, fi os.F return err } -func (w *Worktree) fillEncodedObjectFromSymlink(dst io.Writer, path string, fi os.FileInfo) error { +func (w *Worktree) fillEncodedObjectFromSymlink(dst io.Writer, path string, _ os.FileInfo) error { target, err := w.Filesystem.Readlink(path) if err != nil { return err @@ -543,9 +559,11 @@ func (w *Worktree) doUpdateFileToIndex(e *index.Entry, filename string, h plumbi return err } - if e.Mode.IsRegular() { - e.Size = uint32(info.Size()) - } + // The entry size must always reflect the current state, otherwise + // it will cause go-git's Worktree.Status() to divert from "git status". + // The size of a symlink is the length of the path to the target. + // The size of Regular and Executable files is the size of the files. + e.Size = uint32(info.Size()) fillSystemInfo(e, info.Sys()) return nil diff --git a/worktree_status_test.go b/worktree_status_test.go new file mode 100644 index 0000000..629ebd5 --- /dev/null +++ b/worktree_status_test.go @@ -0,0 +1,89 @@ +package git + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/go-git/go-billy/v5/osfs" + "github.com/go-git/go-git/v5/plumbing/cache" + "github.com/go-git/go-git/v5/storage/filesystem" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// For additional context: #1159. +func TestIndexEntrySizeUpdatedForNonRegularFiles(t *testing.T) { + w := osfs.New(t.TempDir(), osfs.WithBoundOS()) + dot, err := w.Chroot(GitDirName) + require.NoError(t, err) + + s := filesystem.NewStorage(dot, cache.NewObjectLRUDefault()) + r, err := Init(s, w) + require.NoError(t, err) + require.NotNil(t, r) + + wt, err := r.Worktree() + require.NoError(t, err) + require.NotNil(t, wt) + + file := "LICENSE" + f, err := w.OpenFile(file, os.O_CREATE|os.O_WRONLY, 0o666) + require.NoError(t, err) + require.NotNil(t, f) + + content := []byte(strings.Repeat("a\n", 1000)) + _, err = f.Write(content) + require.NoError(t, err) + err = f.Close() + require.NoError(t, err) + + _, err = wt.Add(file) + require.NoError(t, err) + + _, err = wt.Commit("add file", &CommitOptions{}) + require.NoError(t, err) + + st, err := wt.StatusWithOptions(StatusOptions{Strategy: Preload}) + require.NoError(t, err) + assert.Equal(t, + &FileStatus{Worktree: Unmodified, Staging: Unmodified}, + st.File(file)) + + // Make the file not regular. The same would apply to a transition + // from regular file to symlink. + err = os.Chmod(filepath.Join(w.Root(), file), 0o777) + require.NoError(t, err) + + f, err = w.OpenFile(file, os.O_APPEND|os.O_RDWR, 0o777) + require.NoError(t, err) + require.NotNil(t, f) + + _, err = f.Write([]byte("\n\n")) + require.NoError(t, err) + err = f.Close() + require.NoError(t, err) + + _, err = wt.Add(file) + assert.NoError(t, err) + + // go-git's Status diverges from "git status", so this check does not + // fail, even when the issue is present. As at this point "git status" + // reports the unstaged file was modified while "git diff" would return + // empty, as the files are the same but the index has the incorrect file + // size. + st, err = wt.StatusWithOptions(StatusOptions{Strategy: Preload}) + assert.NoError(t, err) + assert.Equal(t, + &FileStatus{Worktree: Unmodified, Staging: Modified}, + st.File(file)) + + idx, err := wt.r.Storer.Index() + assert.NoError(t, err) + require.NotNil(t, idx) + require.Len(t, idx.Entries, 1) + + // Check whether the index was updated with the two new line breaks. + assert.Equal(t, uint32(len(content)+2), idx.Entries[0].Size) +} diff --git a/worktree_test.go b/worktree_test.go index e84f3b1..e1a6c0a 100644 --- a/worktree_test.go +++ b/worktree_test.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "errors" - "fmt" "io" "os" "path/filepath" @@ -34,9 +33,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 @@ -89,8 +90,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) @@ -126,15 +128,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) @@ -287,7 +291,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"), }) @@ -295,8 +300,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) @@ -1003,14 +1009,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) @@ -1053,6 +1059,33 @@ func (s *WorktreeSuite) TestStatusEmptyDirty(c *C) { c.Assert(status, HasLen, 1) } +func (s *WorktreeSuite) TestStatusUnmodified(c *C) { + fs := memfs.New() + w := &Worktree{ + r: s.Repository, + Filesystem: fs, + } + + err := w.Checkout(&CheckoutOptions{Force: true}) + c.Assert(err, IsNil) + + status, err := w.StatusWithOptions(StatusOptions{Strategy: Preload}) + c.Assert(err, IsNil) + c.Assert(status.IsClean(), Equals, true) + c.Assert(status.IsUntracked("LICENSE"), Equals, false) + + c.Assert(status.File("LICENSE").Staging, Equals, Unmodified) + c.Assert(status.File("LICENSE").Worktree, Equals, Unmodified) + + status, err = w.StatusWithOptions(StatusOptions{Strategy: Empty}) + c.Assert(err, IsNil) + c.Assert(status.IsClean(), Equals, true) + c.Assert(status.IsUntracked("LICENSE"), Equals, false) + + c.Assert(status.File("LICENSE").Staging, Equals, Untracked) + c.Assert(status.File("LICENSE").Worktree, Equals, Untracked) +} + func (s *WorktreeSuite) TestReset(c *C) { fs := memfs.New() w := &Worktree{ @@ -1242,6 +1275,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() @@ -2057,7 +2091,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) @@ -2983,7 +3017,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) @@ -3014,7 +3048,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) }) |