diff --git a/CHANGELOG.md b/CHANGELOG.md index 68570a0..a758fc1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,12 +9,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### New Features + - Expose native cached read-only and writable fixtures through `gix.testtools`, + with Python closures for fixture creation and post-processing. - Select a local Gitoxide checkout for Python development builds with `GIXPYTHON_GITOXIDE_PATH` or `--gitoxide-path`, using a separate development lockfile. `--packaged` forces release dependencies. Local builds report checkout HEAD. ### Other + - Use public Gitoxide `main` containing all three upstream prerequisites and + remove the vendored crates. `gix 0.89.0` does not yet include these fixes. - Reintegrate the updated Gitoxide `pygix` branch and its security-audit base. Refresh the three bundled prerequisites and propagate native errors from fallible path, configuration, reference, shallow-state, and worktree queries. diff --git a/Cargo.lock b/Cargo.lock index c6ee7bb..1198163 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -172,6 +172,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" + [[package]] name = "crc32fast" version = "1.5.2" @@ -432,6 +447,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "futures-channel" version = "0.3.34" @@ -517,8 +538,8 @@ dependencies = [ [[package]] name = "gix" -version = "0.88.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=1d7bac742f70b72ddda0c7294f4a97566b8db596#1d7bac742f70b72ddda0c7294f4a97566b8db596" +version = "0.89.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d#09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d" dependencies = [ "gix-actor", "gix-archive", @@ -581,8 +602,8 @@ dependencies = [ [[package]] name = "gix-actor" -version = "0.43.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=1d7bac742f70b72ddda0c7294f4a97566b8db596#1d7bac742f70b72ddda0c7294f4a97566b8db596" +version = "0.44.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d#09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d" dependencies = [ "bstr", "gix-date", @@ -591,8 +612,8 @@ dependencies = [ [[package]] name = "gix-archive" -version = "0.37.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=1d7bac742f70b72ddda0c7294f4a97566b8db596#1d7bac742f70b72ddda0c7294f4a97566b8db596" +version = "0.38.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d#09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d" dependencies = [ "bstr", "flate2", @@ -607,8 +628,8 @@ dependencies = [ [[package]] name = "gix-attributes" -version = "0.36.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=1d7bac742f70b72ddda0c7294f4a97566b8db596#1d7bac742f70b72ddda0c7294f4a97566b8db596" +version = "0.37.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d#09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d" dependencies = [ "bstr", "gix-error", @@ -623,16 +644,16 @@ dependencies = [ [[package]] name = "gix-bitmap" -version = "0.5.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=1d7bac742f70b72ddda0c7294f4a97566b8db596#1d7bac742f70b72ddda0c7294f4a97566b8db596" +version = "0.6.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d#09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d" dependencies = [ "gix-error", ] [[package]] name = "gix-blame" -version = "0.18.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=1d7bac742f70b72ddda0c7294f4a97566b8db596#1d7bac742f70b72ddda0c7294f4a97566b8db596" +version = "0.19.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d#09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d" dependencies = [ "gix-commitgraph", "gix-date", @@ -649,16 +670,16 @@ dependencies = [ [[package]] name = "gix-chunk" -version = "0.9.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=1d7bac742f70b72ddda0c7294f4a97566b8db596#1d7bac742f70b72ddda0c7294f4a97566b8db596" +version = "0.10.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d#09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d" dependencies = [ "gix-error", ] [[package]] name = "gix-command" -version = "0.11.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=1d7bac742f70b72ddda0c7294f4a97566b8db596#1d7bac742f70b72ddda0c7294f4a97566b8db596" +version = "0.12.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d#09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d" dependencies = [ "bstr", "gix-error", @@ -669,8 +690,8 @@ dependencies = [ [[package]] name = "gix-commitgraph" -version = "0.40.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=1d7bac742f70b72ddda0c7294f4a97566b8db596#1d7bac742f70b72ddda0c7294f4a97566b8db596" +version = "0.41.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d#09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d" dependencies = [ "bstr", "gix-chunk", @@ -682,8 +703,8 @@ dependencies = [ [[package]] name = "gix-config" -version = "0.61.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=1d7bac742f70b72ddda0c7294f4a97566b8db596#1d7bac742f70b72ddda0c7294f4a97566b8db596" +version = "0.62.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d#09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d" dependencies = [ "bstr", "gix-config-value", @@ -701,8 +722,8 @@ dependencies = [ [[package]] name = "gix-config-value" -version = "0.20.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=1d7bac742f70b72ddda0c7294f4a97566b8db596#1d7bac742f70b72ddda0c7294f4a97566b8db596" +version = "0.21.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d#09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d" dependencies = [ "bitflags 2.13.2", "bstr", @@ -714,8 +735,8 @@ dependencies = [ [[package]] name = "gix-credentials" -version = "0.41.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=1d7bac742f70b72ddda0c7294f4a97566b8db596#1d7bac742f70b72ddda0c7294f4a97566b8db596" +version = "0.42.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d#09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d" dependencies = [ "bstr", "gix-command", @@ -733,8 +754,8 @@ dependencies = [ [[package]] name = "gix-date" -version = "0.17.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=1d7bac742f70b72ddda0c7294f4a97566b8db596#1d7bac742f70b72ddda0c7294f4a97566b8db596" +version = "0.18.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d#09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d" dependencies = [ "bstr", "gix-error", @@ -744,8 +765,8 @@ dependencies = [ [[package]] name = "gix-diff" -version = "0.68.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=1d7bac742f70b72ddda0c7294f4a97566b8db596#1d7bac742f70b72ddda0c7294f4a97566b8db596" +version = "0.69.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d#09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d" dependencies = [ "bstr", "gix-attributes", @@ -767,8 +788,8 @@ dependencies = [ [[package]] name = "gix-dir" -version = "0.30.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=1d7bac742f70b72ddda0c7294f4a97566b8db596#1d7bac742f70b72ddda0c7294f4a97566b8db596" +version = "0.31.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d#09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d" dependencies = [ "bstr", "gix-discover", @@ -786,8 +807,8 @@ dependencies = [ [[package]] name = "gix-discover" -version = "0.56.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=1d7bac742f70b72ddda0c7294f4a97566b8db596#1d7bac742f70b72ddda0c7294f4a97566b8db596" +version = "0.57.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d#09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d" dependencies = [ "bstr", "dunce", @@ -800,16 +821,16 @@ dependencies = [ [[package]] name = "gix-error" -version = "0.4.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=1d7bac742f70b72ddda0c7294f4a97566b8db596#1d7bac742f70b72ddda0c7294f4a97566b8db596" +version = "0.5.1" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d#09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d" dependencies = [ "bstr", ] [[package]] name = "gix-filter" -version = "0.35.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=1d7bac742f70b72ddda0c7294f4a97566b8db596#1d7bac742f70b72ddda0c7294f4a97566b8db596" +version = "0.36.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d#09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d" dependencies = [ "bstr", "encoding_rs", @@ -828,8 +849,8 @@ dependencies = [ [[package]] name = "gix-fs" -version = "0.23.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=1d7bac742f70b72ddda0c7294f4a97566b8db596#1d7bac742f70b72ddda0c7294f4a97566b8db596" +version = "0.24.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d#09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d" dependencies = [ "bstr", "gix-error", @@ -842,8 +863,8 @@ dependencies = [ [[package]] name = "gix-glob" -version = "0.28.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=1d7bac742f70b72ddda0c7294f4a97566b8db596#1d7bac742f70b72ddda0c7294f4a97566b8db596" +version = "0.29.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d#09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d" dependencies = [ "bitflags 2.13.2", "bstr", @@ -855,8 +876,8 @@ dependencies = [ [[package]] name = "gix-hash" -version = "0.27.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=1d7bac742f70b72ddda0c7294f4a97566b8db596#1d7bac742f70b72ddda0c7294f4a97566b8db596" +version = "0.28.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d#09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d" dependencies = [ "faster-hex", "gix-error", @@ -867,8 +888,8 @@ dependencies = [ [[package]] name = "gix-hashtable" -version = "0.17.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=1d7bac742f70b72ddda0c7294f4a97566b8db596#1d7bac742f70b72ddda0c7294f4a97566b8db596" +version = "0.18.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d#09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d" dependencies = [ "gix-hash", "hashbrown 0.17.1", @@ -877,8 +898,8 @@ dependencies = [ [[package]] name = "gix-ignore" -version = "0.23.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=1d7bac742f70b72ddda0c7294f4a97566b8db596#1d7bac742f70b72ddda0c7294f4a97566b8db596" +version = "0.24.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d#09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d" dependencies = [ "bstr", "gix-glob", @@ -889,17 +910,17 @@ dependencies = [ [[package]] name = "gix-imara-diff" -version = "0.3.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=1d7bac742f70b72ddda0c7294f4a97566b8db596#1d7bac742f70b72ddda0c7294f4a97566b8db596" +version = "0.3.1" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d#09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d" dependencies = [ "bstr", - "hashbrown 0.16.1", + "hashbrown 0.17.1", ] [[package]] name = "gix-index" -version = "0.56.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=1d7bac742f70b72ddda0c7294f4a97566b8db596#1d7bac742f70b72ddda0c7294f4a97566b8db596" +version = "0.57.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d#09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d" dependencies = [ "bitflags 2.13.2", "bstr", @@ -926,8 +947,8 @@ dependencies = [ [[package]] name = "gix-lock" -version = "25.0.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=1d7bac742f70b72ddda0c7294f4a97566b8db596#1d7bac742f70b72ddda0c7294f4a97566b8db596" +version = "26.0.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d#09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d" dependencies = [ "gix-error", "gix-fs", @@ -938,7 +959,7 @@ dependencies = [ [[package]] name = "gix-macros" version = "0.2.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=1d7bac742f70b72ddda0c7294f4a97566b8db596#1d7bac742f70b72ddda0c7294f4a97566b8db596" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d#09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d" dependencies = [ "proc-macro2", "quote", @@ -947,8 +968,8 @@ dependencies = [ [[package]] name = "gix-mailmap" -version = "0.35.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=1d7bac742f70b72ddda0c7294f4a97566b8db596#1d7bac742f70b72ddda0c7294f4a97566b8db596" +version = "0.36.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d#09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d" dependencies = [ "bstr", "gix-actor", @@ -958,8 +979,8 @@ dependencies = [ [[package]] name = "gix-merge" -version = "0.21.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=1d7bac742f70b72ddda0c7294f4a97566b8db596#1d7bac742f70b72ddda0c7294f4a97566b8db596" +version = "0.22.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d#09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d" dependencies = [ "bstr", "gix-command", @@ -983,8 +1004,8 @@ dependencies = [ [[package]] name = "gix-negotiate" -version = "0.36.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=1d7bac742f70b72ddda0c7294f4a97566b8db596#1d7bac742f70b72ddda0c7294f4a97566b8db596" +version = "0.37.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d#09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d" dependencies = [ "bitflags 2.13.2", "gix-commitgraph", @@ -997,8 +1018,8 @@ dependencies = [ [[package]] name = "gix-note" -version = "0.2.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=1d7bac742f70b72ddda0c7294f4a97566b8db596#1d7bac742f70b72ddda0c7294f4a97566b8db596" +version = "0.3.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d#09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d" dependencies = [ "gix-error", "gix-hash", @@ -1007,8 +1028,8 @@ dependencies = [ [[package]] name = "gix-object" -version = "0.65.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=1d7bac742f70b72ddda0c7294f4a97566b8db596#1d7bac742f70b72ddda0c7294f4a97566b8db596" +version = "0.66.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d#09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d" dependencies = [ "bstr", "gix-actor", @@ -1027,8 +1048,8 @@ dependencies = [ [[package]] name = "gix-odb" -version = "0.85.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=1d7bac742f70b72ddda0c7294f4a97566b8db596#1d7bac742f70b72ddda0c7294f4a97566b8db596" +version = "0.86.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d#09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d" dependencies = [ "arc-swap", "crc32fast", @@ -1051,8 +1072,8 @@ dependencies = [ [[package]] name = "gix-pack" -version = "0.75.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=1d7bac742f70b72ddda0c7294f4a97566b8db596#1d7bac742f70b72ddda0c7294f4a97566b8db596" +version = "0.76.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d#09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d" dependencies = [ "clru", "crc32fast", @@ -1076,8 +1097,8 @@ dependencies = [ [[package]] name = "gix-packetline" -version = "0.23.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=1d7bac742f70b72ddda0c7294f4a97566b8db596#1d7bac742f70b72ddda0c7294f4a97566b8db596" +version = "0.24.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d#09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d" dependencies = [ "bstr", "faster-hex", @@ -1088,7 +1109,7 @@ dependencies = [ [[package]] name = "gix-parallel" version = "0.50.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=1d7bac742f70b72ddda0c7294f4a97566b8db596#1d7bac742f70b72ddda0c7294f4a97566b8db596" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d#09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d" dependencies = [ "crossbeam-channel", "gix-trace", @@ -1098,8 +1119,8 @@ dependencies = [ [[package]] name = "gix-path" -version = "0.13.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=1d7bac742f70b72ddda0c7294f4a97566b8db596#1d7bac742f70b72ddda0c7294f4a97566b8db596" +version = "0.14.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d#09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d" dependencies = [ "bstr", "gix-error", @@ -1109,8 +1130,8 @@ dependencies = [ [[package]] name = "gix-pathspec" -version = "0.21.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=1d7bac742f70b72ddda0c7294f4a97566b8db596#1d7bac742f70b72ddda0c7294f4a97566b8db596" +version = "0.22.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d#09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d" dependencies = [ "bitflags 2.13.2", "bstr", @@ -1123,8 +1144,8 @@ dependencies = [ [[package]] name = "gix-prompt" -version = "0.18.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=1d7bac742f70b72ddda0c7294f4a97566b8db596#1d7bac742f70b72ddda0c7294f4a97566b8db596" +version = "0.19.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d#09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d" dependencies = [ "gix-command", "gix-config-value", @@ -1135,8 +1156,8 @@ dependencies = [ [[package]] name = "gix-protocol" -version = "0.66.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=1d7bac742f70b72ddda0c7294f4a97566b8db596#1d7bac742f70b72ddda0c7294f4a97566b8db596" +version = "0.67.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d#09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d" dependencies = [ "bstr", "gix-credentials", @@ -1159,8 +1180,8 @@ dependencies = [ [[package]] name = "gix-quote" -version = "0.9.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=1d7bac742f70b72ddda0c7294f4a97566b8db596#1d7bac742f70b72ddda0c7294f4a97566b8db596" +version = "0.10.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d#09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d" dependencies = [ "bstr", "gix-error", @@ -1169,7 +1190,8 @@ dependencies = [ [[package]] name = "gix-ref" -version = "0.68.0" +version = "0.69.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d#09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d" dependencies = [ "gix-actor", "gix-error", @@ -1187,8 +1209,8 @@ dependencies = [ [[package]] name = "gix-refspec" -version = "0.46.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=1d7bac742f70b72ddda0c7294f4a97566b8db596#1d7bac742f70b72ddda0c7294f4a97566b8db596" +version = "0.47.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d#09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d" dependencies = [ "bstr", "gix-error", @@ -1199,8 +1221,8 @@ dependencies = [ [[package]] name = "gix-revision" -version = "0.50.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=1d7bac742f70b72ddda0c7294f4a97566b8db596#1d7bac742f70b72ddda0c7294f4a97566b8db596" +version = "0.51.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d#09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d" dependencies = [ "bitflags 2.13.2", "bstr", @@ -1217,8 +1239,8 @@ dependencies = [ [[package]] name = "gix-revwalk" -version = "0.36.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=1d7bac742f70b72ddda0c7294f4a97566b8db596#1d7bac742f70b72ddda0c7294f4a97566b8db596" +version = "0.37.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d#09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d" dependencies = [ "gix-commitgraph", "gix-date", @@ -1231,8 +1253,8 @@ dependencies = [ [[package]] name = "gix-sec" -version = "0.15.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=1d7bac742f70b72ddda0c7294f4a97566b8db596#1d7bac742f70b72ddda0c7294f4a97566b8db596" +version = "0.16.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d#09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d" dependencies = [ "bitflags 2.13.2", "gix-path", @@ -1242,8 +1264,8 @@ dependencies = [ [[package]] name = "gix-shallow" -version = "0.14.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=1d7bac742f70b72ddda0c7294f4a97566b8db596#1d7bac742f70b72ddda0c7294f4a97566b8db596" +version = "0.15.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d#09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d" dependencies = [ "bstr", "gix-error", @@ -1254,8 +1276,8 @@ dependencies = [ [[package]] name = "gix-status" -version = "0.35.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=1d7bac742f70b72ddda0c7294f4a97566b8db596#1d7bac742f70b72ddda0c7294f4a97566b8db596" +version = "0.36.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d#09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d" dependencies = [ "bstr", "filetime", @@ -1280,7 +1302,8 @@ dependencies = [ [[package]] name = "gix-submodule" -version = "0.35.0" +version = "0.36.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d#09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d" dependencies = [ "bstr", "gix-config", @@ -1293,28 +1316,62 @@ dependencies = [ [[package]] name = "gix-tempfile" -version = "25.0.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=1d7bac742f70b72ddda0c7294f4a97566b8db596#1d7bac742f70b72ddda0c7294f4a97566b8db596" +version = "26.0.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d#09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d" dependencies = [ "dashmap", "gix-fs", "libc", "parking_lot", + "signal-hook", + "signal-hook-registry", "tempfile", "windows-sys 0.61.2", ] +[[package]] +name = "gix-testtools" +version = "0.22.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d#09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d" +dependencies = [ + "bstr", + "crc", + "fastrand", + "fs_extra", + "gix-config", + "gix-discover", + "gix-error", + "gix-hash", + "gix-index", + "gix-lock", + "gix-object", + "gix-odb", + "gix-parallel", + "gix-path", + "gix-ref", + "gix-shallow", + "gix-tempfile", + "gix-worktree", + "io-close", + "is_ci", + "parking_lot", + "serial_test", + "tar", + "tempfile", +] + [[package]] name = "gix-trace" -version = "0.2.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=1d7bac742f70b72ddda0c7294f4a97566b8db596#1d7bac742f70b72ddda0c7294f4a97566b8db596" +version = "0.3.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d#09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d" dependencies = [ "tracing", ] [[package]] name = "gix-transport" -version = "0.60.0" +version = "0.61.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d#09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d" dependencies = [ "base64 0.22.1", "bstr", @@ -1334,8 +1391,8 @@ dependencies = [ [[package]] name = "gix-traverse" -version = "0.62.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=1d7bac742f70b72ddda0c7294f4a97566b8db596#1d7bac742f70b72ddda0c7294f4a97566b8db596" +version = "0.63.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d#09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d" dependencies = [ "bitflags 2.13.2", "gix-commitgraph", @@ -1350,8 +1407,8 @@ dependencies = [ [[package]] name = "gix-url" -version = "0.39.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=1d7bac742f70b72ddda0c7294f4a97566b8db596#1d7bac742f70b72ddda0c7294f4a97566b8db596" +version = "0.40.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d#09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d" dependencies = [ "bstr", "gix-error", @@ -1362,8 +1419,8 @@ dependencies = [ [[package]] name = "gix-utils" -version = "0.4.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=1d7bac742f70b72ddda0c7294f4a97566b8db596#1d7bac742f70b72ddda0c7294f4a97566b8db596" +version = "0.5.1" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d#09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d" dependencies = [ "bstr", "bytes", @@ -1376,8 +1433,8 @@ dependencies = [ [[package]] name = "gix-validate" -version = "0.12.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=1d7bac742f70b72ddda0c7294f4a97566b8db596#1d7bac742f70b72ddda0c7294f4a97566b8db596" +version = "0.13.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d#09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d" dependencies = [ "bstr", "gix-error", @@ -1385,8 +1442,8 @@ dependencies = [ [[package]] name = "gix-worktree" -version = "0.57.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=1d7bac742f70b72ddda0c7294f4a97566b8db596#1d7bac742f70b72ddda0c7294f4a97566b8db596" +version = "0.58.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d#09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d" dependencies = [ "bstr", "dua-core", @@ -1406,8 +1463,8 @@ dependencies = [ [[package]] name = "gix-worktree-state" -version = "0.35.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=1d7bac742f70b72ddda0c7294f4a97566b8db596#1d7bac742f70b72ddda0c7294f4a97566b8db596" +version = "0.36.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d#09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d" dependencies = [ "bstr", "gix-error", @@ -1425,8 +1482,8 @@ dependencies = [ [[package]] name = "gix-worktree-stream" -version = "0.37.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=1d7bac742f70b72ddda0c7294f4a97566b8db596#1d7bac742f70b72ddda0c7294f4a97566b8db596" +version = "0.38.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d#09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d" dependencies = [ "gix-attributes", "gix-error", @@ -1444,8 +1501,8 @@ dependencies = [ [[package]] name = "gix-zlib" -version = "0.2.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=1d7bac742f70b72ddda0c7294f4a97566b8db596#1d7bac742f70b72ddda0c7294f4a97566b8db596" +version = "0.3.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d#09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d" dependencies = [ "gix-error", "zlib-rs", @@ -1780,6 +1837,12 @@ version = "2.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" +[[package]] +name = "is_ci" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" + [[package]] name = "itoa" version = "1.0.18" @@ -2072,6 +2135,7 @@ version = "0.1.0" dependencies = [ "gix", "gix-archive", + "gix-testtools", "graviola", "pyo3", "pyo3-build-config", @@ -2449,6 +2513,28 @@ dependencies = [ "syn 3.0.6", ] +[[package]] +name = "serial_test" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "699f4197115b8a7e7ff19c9a315a4bd6fffec26cc4626ef45ecaea389e081c6d" +dependencies = [ + "once_cell", + "parking_lot", + "serial_test_derive", +] + +[[package]] +name = "serial_test_derive" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94e153fc76e1c6a068703d6d29c508a0b15c061c4b7e43da59cc097bc342673c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "sha1dc" version = "0.1.5" @@ -2472,6 +2558,26 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" +[[package]] +name = "signal-hook" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b29ffa27d3d59a049bb328258de100f8012848d1b08d444e08bbc02ac468435e" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + [[package]] name = "simd_cesu8" version = "1.2.0" @@ -2588,7 +2694,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.3", + "getrandom 0.3.4", "once_cell", "rustix", "windows-sys 0.61.2", diff --git a/Cargo.toml b/Cargo.toml index 44334a1..847af06 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,8 +16,8 @@ crate-type = ["cdylib", "rlib"] [features] default = ["max-pure"] max-pure = ["sha1", "sha256", "max-performance", "revision", "revparse-regex", "index", "attributes", "dirwalk", "status", "blob-diff", "merge", "blame", "notes", "mailmap", "signing", "worktree-mutation", "worktree-stream", "worktree-archive", "https"] -sha1 = ["gix/sha1", "gix-archive?/sha1"] -sha256 = ["gix/sha256", "gix-archive?/sha256"] +sha1 = ["gix/sha1", "gix-archive?/sha1", "gix-testtools/sha1"] +sha256 = ["gix/sha256", "gix-archive?/sha256", "gix-testtools/sha256"] max-performance = ["gix/max-performance"] revision = ["gix/revision", "index"] revparse-regex = ["gix/revparse-regex", "revision"] @@ -46,8 +46,9 @@ abi3 = ["pyo3/abi3-py311"] [dependencies] pyo3 = { version = "0.29.3", features = ["multiple-pymethods"] } -gix = { git = "https://github.com/GitoxideLabs/gitoxide", rev = "1d7bac742f70b72ddda0c7294f4a97566b8db596", default-features = false, features = ["parallel", "auto-chain-error", "progress-tree"] } -gix-archive = { git = "https://github.com/GitoxideLabs/gitoxide", rev = "1d7bac742f70b72ddda0c7294f4a97566b8db596", default-features = false, optional = true } +gix = { git = "https://github.com/GitoxideLabs/gitoxide", rev = "09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d", default-features = false, features = ["parallel", "auto-chain-error", "progress-tree"] } +gix-archive = { git = "https://github.com/GitoxideLabs/gitoxide", rev = "09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d", default-features = false, optional = true } +gix-testtools = { git = "https://github.com/GitoxideLabs/gitoxide", rev = "09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d", default-features = false, features = ["worktree-exclusions"] } reqwest = { version = "0.13.4", default-features = false, features = ["blocking"], optional = true } rustls = { version = "0.23.45", default-features = false, features = ["std", "tls12"], optional = true } rustls-graviola = { version = "=0.4.0", optional = true } @@ -57,11 +58,6 @@ rustls-platform-verifier = { version = "0.7.0", optional = true } [build-dependencies] pyo3-build-config = "0.29.3" -[patch."https://github.com/GitoxideLabs/gitoxide"] -gix-transport = { path = "vendor/gix-transport" } -gix-ref = { path = "vendor/gix-ref" } -gix-submodule = { path = "vendor/gix-submodule" } - [lints.rust] unsafe_code = "forbid" diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 6a095d0..a7c2cf2 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -8,19 +8,23 @@ Git is a test reference implementation and fixture-creation tool, not a runtime GitHub CI setup is authorized for this repository, including pushing the committed project and CI fixes to `origin/main`. Package publication, PyPI interaction, and changes to the upstream Gitoxide remote remain outside the task. Use cached dependencies when practical. For tools whose default cache is outside the writable workspace, choose a task-owned cache under `/private/tmp` or the repository. -## Native upstream prerequisite +## Native upstream integration -The manifest pins Gitoxide revision `1d7bac742f70b72ddda0c7294f4a97566b8db596`. Three small fixes are included as source-preserving Cargo patches under `vendor/`: +The manifest pins public Gitoxide `main` revision +`09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d`, which merged the Python binding +prerequisites in [Gitoxide PR #3054](https://github.com/GitoxideLabs/gitoxide/pull/3054). +No vendored crates or Cargo patches are needed. The integrated fixes provide: -| Crate | Local upstream commit | Reason | -| --- | --- | --- | -| `gix-transport` | `ab1cb834df8f48e907f81e4b7471b06b4c9f1aa0` | Per-client reqwest TLS configuration with cached/lazy initialization and redirect policy. | -| `gix-ref` | `066b042d68e5697b8272b636272c0c4253884882` | Preserve raw reflog message bytes. | -| `gix-submodule` | `e4d2b313b840ea0e00453791882dbc6eb7c636f6` | Query submodule configuration using literal byte names. | +| Crate | Behavior | +| --- | --- | +| `gix-transport` | Per-client reqwest TLS configuration with cached/lazy initialization and redirect policy. | +| `gix-ref` | Preserve raw reflog message bytes. | +| `gix-submodule` | Query submodule configuration using literal byte names. | -Each directory includes its licenses and `README.pygix.md` provenance. Cargo manifests reference the pinned Git source for sibling dependencies. The source distribution includes these patches and does not require the developer's sibling checkout or an unpublished remote commit. Remove each patch after updating to an upstream revision containing its fix. - -The corresponding local upstream commits are in the prepared `gitoxide.pygix` checkout for Sebastian Thiel's review. Nothing was pushed. If later authorized, upstream contributions use `pr-from-session` and draft PRs only. +The latest crates.io release checked on 2026-10-12, `gix 0.89.0`, does not yet +contain these fixes. Package builds therefore use the public Git revision until +a compatible release includes them. The source distribution builds independently +of the developer's sibling checkout. ## Develop against a local Gitoxide checkout @@ -49,14 +53,14 @@ Local `gix.__gix_revision__` reports checkout HEAD. Uncommitted changes are incl in the build but do not change that revision string. Every local invocation reads HEAD again; Cargo tracks the revision environment value to rebuild provenance when switching checkouts or returning to packaged mode. Packaged builds report -the pinned public revision; the vendored fixes retain their separate provenance. +the pinned public revision containing all three fixes. -Continue using the local switch after the first release until the upstream fixes -are merged. Once compatible `gix` and `gix-archive` releases on crates.io contain +Continue using the local switch for upstream development. Once compatible +`gix` and `gix-archive` releases on crates.io contain all three prerequisites, switch the manifest to those published versions, -regenerate the committed lockfile, remove the corresponding vendored patches, -and update the packaged revision in `build.rs`. The local selector derives patch -sources from the manifest, so it continues working with crates.io dependencies. +update the committed lockfile and packaged revision in `build.rs`. The local +selector derives patch sources from the manifest, so it continues working with +crates.io dependencies. Revalidate the supported feature groups and builds without C/C++ compilers at that migration. macOS binary wheels and an independently buildable source distribution remain the release artifacts. @@ -109,6 +113,11 @@ Development builds continue using the local selection described above. ## Integration checks +Use [native cached fixtures](docs/testtools.md) through `gix.testtools` when +tests only inspect stable repositories. Python closures can create fixtures +or post-process scripted fixtures; writable helpers provide disposable copies +for mutations. Do not write into shared read-only fixture directories. + Each new binding should have a focused Python integration check against a disposable repository. Verify errors as well as success, both supported hash kinds, and any meaningful reduced feature configuration. Compare with Git when it provides an appropriate reference result. Threading checks share repository handles between Python threads and verify actual work with the GIL disabled. Lazy-API checks consume only a prefix and confirm that later results have not been eagerly produced. Long-running operations expose pollable `Progress.snapshot()` and `CancellationToken`; they must propagate Python interruption and release resources when stopped. @@ -152,4 +161,4 @@ maturin build --locked --release --out dist --interpreter /path/to/python3.14t maturin sdist --out dist ``` -Install a matching wheel into a disposable environment with `uv pip install --no-index --no-deps`, then run the test suite without `PYTHONPATH=python`. Rebuild the wheel from the unpacked sdist to verify its vendor/support files. The artifact workflow automates these checks. The stable ABI and free-threaded ABI are separate compatibility contracts; a wheel filename and successful import on one interpreter do not validate another ABI. +Install a matching wheel into a disposable environment with `uv pip install --no-index --no-deps`, then run the test suite without `PYTHONPATH=python`. Rebuild the wheel from the unpacked sdist to verify its support files and independent dependency resolution. The artifact workflow automates these checks. The stable ABI and free-threaded ABI are separate compatibility contracts; a wheel filename and successful import on one interpreter do not validate another ABI. diff --git a/README.md b/README.md index 0653f25..3cb10e5 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ python3 etc/build.py --no-default-features --features sha256,revision,index `gix.build_features()` reports the installation's capabilities, and `gix.HashKind.all()` reports its supported hash kinds. Feature-gated Python methods and types are absent when disabled. The included type stubs describe the full build. -Ordinary wheel builds select the `abi3` Cargo feature for CPython 3.11+. Free-threaded builds require CPython 3.14+ and a wheel built for that interpreter. [Development instructions](https://github.com/GitoxideLabs/GixPython/blob/main/DEVELOPMENT.md) describe artifacts, validation, and the vendored upstream prerequisites. GitHub workflows test supported configurations and build macOS release artifacts. See [RELEASING.md](https://github.com/GitoxideLabs/GixPython/blob/main/RELEASING.md) for the manual publishing procedure. +Ordinary wheel builds select the `abi3` Cargo feature for CPython 3.11+. Free-threaded builds require CPython 3.14+ and a wheel built for that interpreter. [Development instructions](https://github.com/GitoxideLabs/GixPython/blob/main/DEVELOPMENT.md) describe artifacts, validation, and upstream integration. GitHub workflows test supported configurations and build macOS release artifacts. See [RELEASING.md](https://github.com/GitoxideLabs/GixPython/blob/main/RELEASING.md) for the manual publishing procedure. ## More examples diff --git a/agents.md b/agents.md index 039cb1a..fa3b50a 100644 --- a/agents.md +++ b/agents.md @@ -28,10 +28,17 @@ - Use `MIT OR Apache-2.0`, with Sebastian Thiel as package author and copyright holder. Include both license texts in distributions. - Divide work into meaningful, independently buildable and testable commits. Include documentation and relevant regression checks with the behavior they describe. -- Follow Gitoxide's purposeful conventional commits: use `feat:`/`fix:` for user-visible changes and plain descriptive subjects for maintenance. Use explicit Codex authorship for agent-created commits; package authorship is separate. +- Follow Gitoxide's purposeful conventional commits: + - Write messages in Markdown, enclosing code, crate names, and shell commands in backticks. Use markup when it helps readers navigate the prose. + - Explain everything known about the motivation in the body, not merely what changed, and record relevant validation. + - Use conventional prefixes only for changelog-worthy changes: `feat:`/`fix:` for user-visible features/fixes; maintenance and refactors use plain descriptive subjects. + - Breaking changes use `!` before the colon, such as `change!:`, `remove!:`, `rename!:`, or `feat(gix-odb)!:`. Scope changelog-worthy changes touching multiple crates to the crate that should receive the entry. + - Every commit must be self-contained and pass the relevant checks independently. Keep breaking changes and all adaptations needed to build and test together. + - Use explicit Codex authorship for agent-created commits; package authorship is separate. - Preserve tagged and published history. Now that `v0.1.0` exists, create new commits for further changes, including CI fixes; do not amend earlier commits unless the user explicitly authorizes that specific amendment. - Keep packaging, type information, examples, contributor guidance, CI, security policy, and artifact-building configuration part of the project. - Tests use disposable repositories and isolated Git configuration/environment. Never mutate the developer's checkout, another worktree, or shared Git metadata as a fixture. +- Test suites may use native cached read-only fixtures through `gix.testtools`; only their initialization/post-processing callback may modify them. Use writable fixtures for mutations. Python fixture callbacks are supported separately from the callback-free progress API, without repository state locks or native signal ownership. - The implementation sequence and persistent design decisions are in [docs/implementation-plan.md](docs/implementation-plan.md). Update its status as work lands; do not present planned coverage as completed. ## Current session boundary diff --git a/build.rs b/build.rs index e2b9fe4..38f8f8b 100644 --- a/build.rs +++ b/build.rs @@ -1,7 +1,7 @@ fn main() { println!("cargo:rerun-if-env-changed=GIXPYTHON_BUILD_GIX_REVISION"); let revision = std::env::var("GIXPYTHON_BUILD_GIX_REVISION") - .unwrap_or_else(|_| "1d7bac742f70b72ddda0c7294f4a97566b8db596".into()); + .unwrap_or_else(|_| "09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d".into()); assert!( revision.len() == 40 && revision.bytes().all(|byte| byte.is_ascii_hexdigit()), "GIXPYTHON_BUILD_GIX_REVISION must be a 40-character Git revision" diff --git a/docs/api-coverage.md b/docs/api-coverage.md index fac2a92..7a5f3e6 100644 --- a/docs/api-coverage.md +++ b/docs/api-coverage.md @@ -1,8 +1,8 @@ # Native API coverage GixPython wraps Gitoxide's repository operations and the objects they return. -The source baseline is [`gix` at `1d7bac742f70b72ddda0c7294f4a97566b8db596`](https://github.com/GitoxideLabs/gitoxide/tree/1d7bac742f70b72ddda0c7294f4a97566b8db596/gix/src). -Small vendored fixes retain their individual provenance under `vendor/`. +The source baseline is [`gix` at `09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d`](https://github.com/GitoxideLabs/gitoxide/tree/09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d/gix/src). +This public `main` revision includes all formerly vendored prerequisites. This is broad coverage of the native engine, not a claim to implement every command available in the Git executable or every generic Rust helper. @@ -120,6 +120,9 @@ policy for unavailable worktree encodings. ## Explicit limits and native behavior +- `gix.testtools` provides native cached fixtures, Python producers and + post-processors, temporary writable copies, and isolated Git test commands. + See [fixture APIs and limits](testtools.md). - Rust callback/delegate interfaces are not Python callbacks. Tree change and directory traversal use native iterators/adapters. Custom per-entry traversal delegates, object-token visitor callbacks and filter callbacks are not exposed. @@ -140,6 +143,8 @@ policy for unavailable worktree encodings. `transport_options()` containers, and trait-only storage helpers do not have separate Python equivalents. Native checkout and transport operations still apply the repository configuration. +- Worktree removal uses the native default retry delay; `retry_delay` is not + configurable through `WorktreeRemoveOptions`. - Native pathspec matching suppresses some attribute-loading failures and may therefore report no match. Direct attribute-stack lookups preserve these I/O errors. `branch_names()` follows native behavior and skips non-UTF-8 names; diff --git a/docs/implementation-plan.md b/docs/implementation-plan.md index cb426e3..538cf9a 100644 --- a/docs/implementation-plan.md +++ b/docs/implementation-plan.md @@ -46,7 +46,7 @@ Local Python build selection is implemented through `GIXPYTHON_GITOXIDE_PATH`, development lockfile; command-local patches build directly from checkout edits without changing packaged inputs. Local provenance reports checkout HEAD. Direct Cargo checks and maturin artifacts continue using release dependencies. -After upstream integration, package builds will use compatible crates.io releases +Once published, package builds will use compatible crates.io releases and retain macOS wheels plus an sdist. See [development instructions](../DEVELOPMENT.md) for the switch and migration requirements. @@ -58,8 +58,21 @@ path, configuration, reference, shallow-state, and worktree results, and use `gix::utils` for interruptible reads following the `gix-parallel` split. The existing release tag and upstream checkout are preserved. +On 2026-10-12, package dependencies moved to public `main` revision +`09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d`, which merged all three prerequisites +in Gitoxide PR #3054. The vendored crates, packaged patches, and vendor-specific +build/archive logic were removed. The latest published `gix 0.89.0` still lacks +these fixes, so migration to crates.io remains pending their next release. +The local checkout selector remains available and packaged provenance uses the +new public revision. + ## Acceptance +Native `gix-testtools` fixtures are available through `gix.testtools`, with +Python callbacks preserving `FixtureState` and return-value semantics. +Read-only caches support inspection suites; writable temporary copies keep +mutation tests isolated. See [the fixture guide](testtools.md). + Every completed capability has a Python integration check and accurate type/documentation coverage. Checks use disposable repositories and isolated Git configuration; both hash kinds and relevant feature selections are exercised. Shared-handle concurrency, GIL-disabled execution, partial iterator consumption, interruption, stale-reference rejection, and artifact installation are project requirements. Gitoxide currently exposes commit merges even where older overview documentation says otherwise. Its push-related configuration does not itself provide native push execution. Verify capabilities against source rather than treating an old feature checklist as authoritative. diff --git a/docs/network.md b/docs/network.md index 4605789..36b939f 100644 --- a/docs/network.md +++ b/docs/network.md @@ -47,8 +47,6 @@ and proxy credential-helper configurations return errors rather than being silently ignored. Custom TLS client identities and Python credential callbacks are not exposed. -The client-builder prerequisite is Gitoxide commit -`ab1cb834df8f48e907f81e4b7471b06b4c9f1aa0`, prepared locally in the sibling checkout. -The reproducible build uses the source-preserving vendored transport patch; -see [its provenance and removal conditions](../vendor/gix-transport/README.pygix.md). -No unpublished remote revision or machine-specific dependency path is required. +The per-client builder is included in the pinned public Gitoxide `main` revision; +see [upstream integration](../DEVELOPMENT.md#native-upstream-integration). +No vendored transport patch or machine-specific dependency path is required. diff --git a/docs/submodules.md b/docs/submodules.md index c0b0795..f1034a5 100644 --- a/docs/submodules.md +++ b/docs/submodules.md @@ -57,5 +57,5 @@ native `SubmoduleStatus`, including recorded and checked-out IDs, state, changes and `is_dirty()`. An unknown dirty state returns `None`. Native status computes its result before returning; it is not a lazy change stream. -The small temporary native raw-name fix is documented in -[`vendor/gix-submodule/README.pygix.md`](../vendor/gix-submodule/README.pygix.md). +Native raw-name handling is included in the pinned public Gitoxide `main` +revision; see [upstream integration](../DEVELOPMENT.md#native-upstream-integration). diff --git a/docs/testtools.md b/docs/testtools.md new file mode 100644 index 0000000..857125d --- /dev/null +++ b/docs/testtools.md @@ -0,0 +1,99 @@ +# Cached fixtures for Python test suites + +`gix.testtools` binds `gix-testtools`' native fixture cache. It is available in +every build, including reduced hash configurations. Paths and return values +follow the Rust API; fixture paths are `pathlib.Path` objects. + +Run tests from the test project's root. Fixtures and scripts live under +`tests/fixtures`, generated directories under +`tests/fixtures/generated-do-not-edit`, and reusable TAR archives under +`tests/fixtures/generated-archives`. Names passed to `rust_fixture_*` must +be a single path component, unique within that project. The Rust function +names are retained even when Python supplies the producer: + +```python +import gix +from gix import testtools + +def make_fixture(state): + path = state.path() + if state.is_uninitialized(): + repo = gix.init(path / "repo", options=gix.OpenOptions.isolated()) + blob_id = repo.write_blob(b"fixture content") + (path / "blob-id").write_text(str(blob_id)) + return (path / "blob-id").read_text() + +fixture, blob_id = testtools.rust_fixture_read_only("objects", 1, make_fixture) +repo = gix.open_opts(fixture / "repo", gix.OpenOptions.isolated()) +assert repo.find_blob(blob_id).data == b"fixture content" +``` + +The callback receives a `FixtureState`. `is_uninitialized()` is true when +it must create or post-process contents; it is false when the fixture comes +from the cache or an archive. The callback runs in either case to compute +its return value. Increment `version` whenever the callback's generation +behavior changes. The native lock coordinates concurrent creators, and a +failed producer leaves a marker so the next call recreates the fixture. +Python exceptions retain their original type and instance. + +Read-only fixtures are a usage contract, not filesystem write protection. +Never mutate the returned directory or run Git operations that could change +its index, references, or configuration. Use isolated native open options +for reading. Post-process only while `is_uninitialized()` is true. + +For a script, use `scripted_fixture_read_only("make.sh")`, or provide a +Python closure to `scripted_fixture_read_only_with_post("make.sh", 1, post)`. +The corresponding `_with_args` functions forward a list of arguments; +`scripted_fixture_read_only_with_args_with_post` accepts both. Scripts run +with native Git environment isolation. Bash fixtures can use +`gix_testtools_require_symlinks` and `fixture_has_symlinks()` for preflight. +Scripts need Git and their own interpreter installed; Python-only producers +can use native `gix` operations. Creating native archives still requires Git +to record its version in the archive metadata; ignoring archive creation +avoids that requirement. + +Tests that mutate a fixture use a writable temporary directory: + +```python +temporary, blob_id = testtools.rust_fixture_writable( + "objects", 1, testtools.Creation.CopyFromReadOnly, make_fixture +) +with temporary: + work = temporary.path() + # Mutate only this disposable copy. +``` + +`TempDir.close()`, leaving its context, or dropping it removes the directory. +The cached source remains available. `Creation.CopyFromReadOnly` may call +the callback twice: once for the cache, then with a fresh state at the copy +to compute a value for its final location. This native copy mode does not +preserve symlinks. Use `Creation.Execute` for symlinks or absolute paths: +it generates directly in the final temporary directory. The scripted +writable helpers provide the same modes and post-processing overloads. + +Archive generation and extraction retain native behavior. Ignore generated +directories with `tests/fixtures/generated-do-not-edit/` and, when archives +are unwanted, ignore `tests/fixtures/generated-archives/*.tar`. Set +`GIX_TEST_IGNORE_ARCHIVES=1` to regenerate locally, or use +`scripted_fixture_read_only_needs_archive()` for native archive preference. +It prefers an existing archive despite that variable and falls back to the +script if no matching archive exists. This build uses TAR, without XZ. +`GIX_TEST_FIXTURE_HASH` selects an enabled fixture hash kind. + +`fixture_path()` resolves a fixture input, `fixture_bytes()` reads its bytes, +and `git(directory, arguments)` runs the native isolated Git helper for a +reference result. The argument string follows its shell-like quote parsing. +Git here is a test oracle, not a repository-operation fallback. + +Native work runs detached from Python. The producer callback attaches only +while it runs; no repository state mutex is held by the binding. Callbacks +must not recursively request the same fixture, whose native file lock is +held until generation finishes. Avoid changing the process working +directory or environment while fixtures run concurrently. Python retains +ownership of signal handlers; importing the module and generating fixtures +does not install native termination handlers. + +The binding covers the fixture APIs above, not every `gix-testtools` utility. +Single-archive argument variants, Git-version predicates, process-environment +guards, repository snapshots, Git daemons, and Cargo example builders are +not currently exposed. diff --git a/docs/validation.md b/docs/validation.md index 39cc905..6c65aac 100644 --- a/docs/validation.md +++ b/docs/validation.md @@ -1,5 +1,80 @@ # Local validation +## Python fixture tools (2026-10-12) + +The pinned `gix-testtools 0.22.0` is available as `gix.testtools` in default +and reduced builds. Python producers and post-processors retain native +`FixtureState`, cache versions, archives, and callback return values. Five +integration checks run in disposable child-process workspaces and cover +concurrent initialization, cross-process cache reuse, archive extraction, +script arguments and Git environment isolation, all bound post-processing +variants, writable copy/direct-generation cleanup, exception identity, +failed-fixture retry, invalid inputs, and Python signal-handler preservation. + +On Apple Silicon macOS with Rust/Cargo 1.99.0, packaged CPython 3.14.7 passes +all 110 tests with two expected skips; free-threaded CPython 3.14.7 with +`PYTHON_GIL=0` passes all 110 with one skip. Both hash-only configurations +pass 110 discovered tests with 75 expected skips, including the fixture +checks. Default and reduced clippy (`sha1,sha256,revision,index`) pass with +warnings denied; seven Rust tests, nine helper checks, formatting, and +whitespace checks pass. Local dependency metadata resolves `gix-testtools` +from the selected checkout's `tests/tools` directory. Dependency metadata +fits Rust 1.89 without an actual MSRV run. Project builds disable C/C++ +compilers; fixture archives use TAR without the C-backed XZ feature. + +The ABI3 macOS wheel rebuilt from its sdist includes `gix.testtools` and +its stubs, and passes all 110 tests on CPython 3.14.7 with two expected +skips in an offline installation. Archive and wheel content checks pass. +Development-profile candidates are in `dist/testtools-validation/`. + +The fixture API is a supported subset, documented in [the guide](testtools.md). +Read-only paths remain a caller contract rather than filesystem protection. +Callbacks must not recursively acquire the same fixture or change process +environment/working directory while fixture calls run concurrently. +No remote operations, publication, Intel/cross-platform builds, or actual +Rust 1.89 execution were performed. + +## Public `main` integration without vendoring (2026-10-12) + +On Apple Silicon macOS with Rust/Cargo 1.99.0, packaged builds resolve public +Gitoxide `main` revision `09f85be4e3fda42f3e8fd19c53a63758fdf6fb3d`, the merge +of Gitoxide PR #3054. Its native transport client configuration, raw reflog +messages, and literal submodule names replace all three vendored crates. +The latest published `gix 0.89.0` lacks those changes: an isolated build against +the registry release fails because `reqwest::Options::configure_client` is +absent, and source comparisons confirm the missing byte-preservation fixes. +The public Git pin remains necessary until a compatible release is published. + +Packaged CPython 3.14.7 passes all 105 integration tests with two expected +skips. Free-threaded CPython 3.14.7 with `PYTHON_GIL=0` passes the same suite +with one expected skip. These exercise the former vendored fixes, concurrent +shared handles, lazy reads and cancellation, and loopback HTTP/TLS fixtures. +SHA-1-only and SHA-256-only builds each pass 105 discovered tests with 75 +expected capability/platform skips. Worktree removal adopts the new native +default retry delay; existing removal lifecycle and safety checks pass. + +Default clippy and reduced clippy for `sha1,sha256,revision,index` and +`sha1,sha256,http` pass with warnings denied. Seven Rust unit tests, nine +build/release/justfile helper checks, Rust formatting, and whitespace checks +pass. All 60 Gitoxide crates resolve to the selected local checkout without +packaged patches, and the local build passes all 105 integration tests on +CPython 3.14.7 with two expected skips. The tracked lockfile retains every unrelated registry +dependency; all dependency metadata fits Rust 1.89, without an actual MSRV run. +Project builds use disabled C/C++ compilers and cached dependencies after +fetching the public revision. A standalone free-threaded interpreter was +downloaded into the ignored worktree cache, without a Python package index. + +An ABI3 macOS wheel rebuilt from the source archive passes all 105 tests +on CPython 3.12.14 with two expected skips, installed offline in a disposable +environment. Archive/wheel content validation and packaged provenance checks +pass. The archive contains no vendored crates, Cargo patches, developer paths, +local lockfiles, or compiled binaries. Development-profile candidates are in +`dist/main-reintegration-validation/`. + +No upstream edits, pushes, publication, remote CI, Intel/cross-platform builds, +or actual Rust 1.89 execution were performed. The existing release tag is +unchanged; broader platform/release-profile checks remain configured in CI. + ## Updated `pygix` integration (2026-10-08) On Apple Silicon macOS with Rust/Cargo 1.99.0, local builds used `pygix` HEAD diff --git a/docs/worktrees.md b/docs/worktrees.md index a1b7d18..da3d04b 100644 --- a/docs/worktrees.md +++ b/docs/worktrees.md @@ -34,6 +34,8 @@ and `OverrideLock` also permits removing a locked worktree. A prepared target retains native selection until removed or closed. Removal consumes the target, including when the native operation fails. Preparation does not bypass checks performed at removal time. +`WorktreeRemoveOptions` retains the native default delay of 10 milliseconds +between retries after directory-not-empty errors. `add_worktree()` accepts progress and cancellation. Removal accepts progress; the native removal API has no interruption flag. `CheckoutOutcome` reports diff --git a/etc/build.py b/etc/build.py index 5d096c1..c2f67a5 100644 --- a/etc/build.py +++ b/etc/build.py @@ -20,10 +20,8 @@ def local_cargo_config(root, checkout): for name, dependency in manifest["dependencies"].items() if name == "gix" or name.startswith("gix-") } - patches.update((source, name) for source, entries in manifest.get("patch", {}).items() - for name in entries if name == "gix" or name.startswith("gix-")) for _, name in sorted(patches): - path = checkout / name / "Cargo.toml" + path = checkout / ("tests/tools" if name == "gix-testtools" else name) / "Cargo.toml" if not path.is_file() or tomllib.loads(path.read_text()).get("package", {}).get("name") != name: raise ValueError(f"Gitoxide checkout must contain {name}/Cargo.toml for package {name}: {checkout}") result = subprocess.run(["cargo", "--version"], check=True, capture_output=True, text=True) @@ -36,7 +34,8 @@ def local_cargo_config(root, checkout): shutil.copy2(root / "Cargo.lock", lock) config = ["--config", "resolver.lockfile-path=" + json.dumps(str(lock))] for source, name in sorted(patches): - config.extend(["--config", f"patch.{json.dumps(source)}.{name}.path=" + json.dumps(str(checkout / name))]) + path = checkout / ("tests/tools" if name == "gix-testtools" else name) + config.extend(["--config", f"patch.{json.dumps(source)}.{name}.path=" + json.dumps(str(path))]) return config diff --git a/etc/check_artifact.py b/etc/check_artifact.py index 5f15b80..04acca9 100644 --- a/etc/check_artifact.py +++ b/etc/check_artifact.py @@ -3,7 +3,6 @@ from email.parser import BytesParser from pathlib import Path import tarfile -import tomllib import zipfile @@ -23,16 +22,6 @@ def check_sdist(path, expected_stubs): "README.md", "CHANGELOG.md", "RELEASING.md", "LICENSE-MIT", "LICENSE-APACHE", "python/gix/__init__.py", "python/gix/py.typed", "etc/build.py", "etc/check_artifact.py", "tests/test_package.py"} required.update("python/gix/" + name for name in expected_stubs) - manifest = tomllib.loads(archive.extractfile(prefix + "Cargo.toml").read().decode()) - for patches in manifest.get("patch", {}).values(): - for dependency in patches.values(): - if "path" not in dependency: - continue - directory = dependency["path"] - require(not Path(directory).is_absolute() and ".." not in Path(directory).parts, - "sdist patch dependencies must live inside the archive") - required.update(directory + "/" + name for name in - ("Cargo.toml", "src/lib.rs", "LICENSE-MIT", "LICENSE-APACHE", "README.pygix.md")) require(required <= names, f"sdist missing: {sorted(required - names)}") require(not any(name.endswith((".so", ".pyd", ".dylib", ".pyc")) for name in names), "sdist contains compiled artifacts") diff --git a/etc/test_build.py b/etc/test_build.py index 802b590..4dba5c3 100644 --- a/etc/test_build.py +++ b/etc/test_build.py @@ -88,13 +88,12 @@ def test_local_configuration_and_lockfile(self): with tempfile.TemporaryDirectory() as directory, patch.object(build.subprocess, "run") as run: root = Path(directory) checkout = root / "checkout with spaces" - for name in ("gix", "gix-archive", "gix-transport", "gix-ref", "gix-submodule"): - path = checkout / name / "Cargo.toml" + for name in ("gix", "gix-archive", "gix-testtools"): + path = checkout / ("tests/tools" if name == "gix-testtools" else name) / "Cargo.toml" path.parent.mkdir(parents=True) path.write_text(f'[package]\nname = "{name}"\n') manifest = '[dependencies]\ngix = { git = "https://example.com/gitoxide" }\ngix-archive = { git = "https://example.com/gitoxide" }\n' - manifest += '[patch."https://example.com/gitoxide"]\n' - manifest += '\n'.join(f'{name} = {{ path = "vendor/{name}" }}' for name in ("gix-transport", "gix-ref", "gix-submodule")) + manifest += 'gix-testtools = { git = "https://example.com/gitoxide" }\n' (root / "Cargo.toml").write_text(manifest) (root / "Cargo.lock").write_text("packaged lockfile\n") run.return_value.stdout = "cargo 1.97.0 (example)\n" @@ -102,10 +101,10 @@ def test_local_configuration_and_lockfile(self): lock = root / ".cache/gitoxide-local/Cargo.lock" self.assertEqual(lock.read_text(), "packaged lockfile\n") self.assertEqual(json.loads(config[1].split("=", 1)[1]), str(lock)) - self.assertEqual(len(config), 12) + self.assertEqual(len(config), 8) self.assertTrue(all('patch."https://example.com/gitoxide".' in value for value in config[3::2])) self.assertEqual({json.loads(value.split("=", 1)[1]) for value in config[3::2]}, - {str(path.parent) for path in checkout.glob("*/Cargo.toml")}) + {str(path.parent) for path in checkout.rglob("Cargo.toml")}) lock.write_text("updated development lockfile\n") build.local_cargo_config(root, checkout) self.assertEqual(lock.read_text(), "updated development lockfile\n") diff --git a/pyproject.toml b/pyproject.toml index c0d6c63..7ebca24 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,6 @@ include = [ "LICENSE-MIT", "LICENSE-APACHE", "python/gix/py.typed", "python/gix/*.pyi", { path = "CHANGELOG.md", format = "sdist" }, { path = "RELEASING.md", format = "sdist" }, - { path = "vendor/**/*", format = "sdist" }, { path = "tests/**/*.py", format = "sdist" }, { path = "etc/*.py", format = "sdist" }, { path = "docs/**/*.md", format = "sdist" }, diff --git a/python/gix/__init__.py b/python/gix/__init__.py index 2037d46..b3a3213 100644 --- a/python/gix/__init__.py +++ b/python/gix/__init__.py @@ -5,3 +5,4 @@ from ._gix import * from ._gix import __gix_revision__, __version__ +from . import testtools as testtools diff --git a/python/gix/__init__.pyi b/python/gix/__init__.pyi index 33456f6..0c14a33 100644 --- a/python/gix/__init__.pyi +++ b/python/gix/__init__.pyi @@ -1,2 +1,3 @@ from ._gix import * from ._gix import __gix_revision__ as __gix_revision__, __version__ as __version__ +from . import testtools as testtools diff --git a/python/gix/testtools.py b/python/gix/testtools.py new file mode 100644 index 0000000..88c8f3c --- /dev/null +++ b/python/gix/testtools.py @@ -0,0 +1,3 @@ +"""Native cached fixtures and isolated Git commands for test suites.""" + +from ._testtools import * diff --git a/python/gix/testtools.pyi b/python/gix/testtools.pyi new file mode 100644 index 0000000..f90a449 --- /dev/null +++ b/python/gix/testtools.pyi @@ -0,0 +1,36 @@ +from os import PathLike +from pathlib import Path +from typing import Callable, ClassVar, Self, TypeVar + +_Path = str | bytes | PathLike[str] | PathLike[bytes] +_T = TypeVar("_T") + +class FixtureState: + def path(self) -> Path: ... + def is_uninitialized(self) -> bool: ... + +class Creation: + CopyFromReadOnly: ClassVar[Creation] + Execute: ClassVar[Creation] + +class TempDir: + def path(self) -> Path: ... + def close(self) -> None: ... + def __enter__(self) -> Self: ... + def __exit__(self, exc_type: object, exc: object, tb: object) -> None: ... + +def fixture_path(path: _Path) -> Path: ... +def fixture_bytes(path: _Path) -> bytes: ... +def fixture_has_symlinks(fixture_root: _Path) -> bool: ... +def scripted_fixture_read_only(script_name: _Path) -> Path: ... +def scripted_fixture_read_only_needs_archive(script_name: _Path) -> Path: ... +def scripted_fixture_read_only_with_args(script_name: _Path, args: list[str]) -> Path: ... +def scripted_fixture_read_only_with_post(script_name: _Path, version: int, post_process: Callable[[FixtureState], _T]) -> tuple[Path, _T]: ... +def scripted_fixture_read_only_with_args_with_post(script_name: _Path, args: list[str], version: int, post_process: Callable[[FixtureState], _T]) -> tuple[Path, _T]: ... +def scripted_fixture_writable(script_name: _Path) -> TempDir: ... +def scripted_fixture_writable_with_args(script_name: _Path, args: list[str], mode: Creation) -> TempDir: ... +def scripted_fixture_writable_with_post(script_name: _Path, version: int, post_process: Callable[[FixtureState], _T]) -> tuple[TempDir, _T]: ... +def scripted_fixture_writable_with_args_with_post(script_name: _Path, args: list[str], mode: Creation, version: int, post_process: Callable[[FixtureState], _T]) -> tuple[TempDir, _T]: ... +def rust_fixture_read_only(name: str, version: int, make_fixture: Callable[[FixtureState], _T]) -> tuple[Path, _T]: ... +def rust_fixture_writable(name: str, version: int, mode: Creation, make_fixture: Callable[[FixtureState], _T]) -> tuple[TempDir, _T]: ... +def git(current_dir: _Path, arguments: str) -> str: ... diff --git a/src/lib.rs b/src/lib.rs index 79fdca4..77c36b5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -41,6 +41,7 @@ mod runtime; mod status; #[cfg(feature = "attributes")] mod submodule; +mod testtools; mod types; mod worktree; @@ -86,6 +87,7 @@ fn build_features() -> Vec<&'static str> { #[pymodule(gil_used = false)] fn _gix(m: &Bound<'_, PyModule>) -> PyResult<()> { + testtools::register(m)?; types::register(m)?; repository::register(m)?; objects::register(m)?; diff --git a/src/testtools.rs b/src/testtools.rs new file mode 100644 index 0000000..633bff2 --- /dev/null +++ b/src/testtools.rs @@ -0,0 +1,337 @@ +use std::path::{Component, Path, PathBuf}; + +use pyo3::{ + exceptions::{PyFileNotFoundError, PyRuntimeError, PyTypeError, PyValueError}, + prelude::*, + types::PyBytes, +}; + +use crate::error::to_py; + +#[pyclass(frozen, module = "gix.testtools")] +pub struct FixtureState { + path: PathBuf, + uninitialized: bool, +} + +#[pymethods] +impl FixtureState { + fn path(&self) -> PathBuf { + self.path.clone() + } + + fn is_uninitialized(&self) -> bool { + self.uninitialized + } +} + +#[pyclass(eq, eq_int, module = "gix.testtools", from_py_object)] +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum Creation { + CopyFromReadOnly, + Execute, +} + +impl From for gix_testtools::Creation { + fn from(value: Creation) -> Self { + match value { + Creation::CopyFromReadOnly => Self::CopyFromReadOnly, + Creation::Execute => Self::Execute, + } + } +} + +#[pyclass(frozen, module = "gix.testtools")] +pub struct TempDir { + inner: std::sync::Mutex>, +} + +impl From for TempDir { + fn from(inner: gix_testtools::tempfile::TempDir) -> Self { + Self { + inner: std::sync::Mutex::new(Some(inner)), + } + } +} + +#[pymethods] +impl TempDir { + fn path(&self) -> PyResult { + self.inner + .lock() + .unwrap_or_else(|e| e.into_inner()) + .as_ref() + .map(|dir| dir.path().to_owned()) + .ok_or_else(|| PyRuntimeError::new_err("temporary fixture is closed")) + } + + fn close(&self, py: Python<'_>) -> PyResult<()> { + let dir = self.inner.lock().unwrap_or_else(|e| e.into_inner()).take(); + py.detach(|| dir.map(|dir| dir.close()).transpose()) + .map(|_| ()) + .map_err(to_py) + } + + fn __enter__(slf: PyRef<'_, Self>) -> PyResult> { + slf.path()?; + Ok(slf) + } + + fn __exit__( + &self, + py: Python<'_>, + _exc_type: &Bound<'_, PyAny>, + _exc: &Bound<'_, PyAny>, + _tb: &Bound<'_, PyAny>, + ) -> PyResult<()> { + self.close(py) + } +} + +fn fixture_error(error: Box) -> PyErr { + match error.downcast::() { + Ok(error) => *error, + Err(error) => to_py(error.as_ref()), + } +} + +fn callback(callback: &Py, state: gix_testtools::FixtureState<'_>) -> gix_testtools::PostResult> { + Python::attach(|py| { + let state = FixtureState { + path: state.path().to_owned(), + uninitialized: state.is_uninitialized(), + }; + callback.call1(py, (state,)) + }) + .map_err(|error| Box::new(error) as _) +} + +fn check_callback(py: Python<'_>, callback: &Py) -> PyResult<()> { + if !callback.bind(py).is_callable() { + return Err(PyTypeError::new_err("fixture post-processing must be callable")); + } + Ok(()) +} + +fn check_script(script_name: &Path) -> PyResult<()> { + let path = gix_testtools::fixture_path(script_name); + std::fs::read(&path).map_err(|error| { + if error.kind() == std::io::ErrorKind::NotFound { + PyFileNotFoundError::new_err(format!("{}: {error}", path.display())) + } else { + to_py(error) + } + })?; + Ok(()) +} + +fn check_name(name: &str) -> PyResult<()> { + let mut components = Path::new(name).components(); + if !matches!(components.next(), Some(Component::Normal(_))) || components.next().is_some() { + return Err(PyValueError::new_err("fixture name must be one normal path component")); + } + Ok(()) +} + +#[pyfunction] +fn fixture_path(path: PathBuf) -> PathBuf { + gix_testtools::fixture_path(path) +} + +#[pyfunction] +fn fixture_bytes(py: Python<'_>, path: PathBuf) -> PyResult> { + let bytes = py + .detach(|| std::fs::read(gix_testtools::fixture_path(path))) + .map_err(to_py)?; + Ok(PyBytes::new(py, &bytes)) +} + +#[pyfunction] +fn fixture_has_symlinks(py: Python<'_>, fixture_root: PathBuf) -> PyResult { + py.detach(|| gix_testtools::fixture_has_symlinks(fixture_root)) + .map_err(to_py) +} + +#[pyfunction] +fn scripted_fixture_read_only(py: Python<'_>, script_name: PathBuf) -> PyResult { + check_script(&script_name)?; + py.detach(|| gix_testtools::scripted_fixture_read_only(script_name)) + .map_err(fixture_error) +} + +#[pyfunction] +fn scripted_fixture_read_only_needs_archive(py: Python<'_>, script_name: PathBuf) -> PyResult { + check_script(&script_name)?; + py.detach(|| gix_testtools::scripted_fixture_read_only_needs_archive(script_name)) + .map_err(fixture_error) +} + +#[pyfunction] +fn scripted_fixture_read_only_with_args(py: Python<'_>, script_name: PathBuf, args: Vec) -> PyResult { + check_script(&script_name)?; + py.detach(|| gix_testtools::scripted_fixture_read_only_with_args(script_name, args)) + .map_err(fixture_error) +} + +#[pyfunction] +fn scripted_fixture_read_only_with_post( + py: Python<'_>, + script_name: PathBuf, + version: u32, + post_process: Py, +) -> PyResult<(PathBuf, Py)> { + check_script(&script_name)?; + check_callback(py, &post_process)?; + py.detach(|| { + gix_testtools::scripted_fixture_read_only_with_post(script_name, version, |state| { + callback(&post_process, state) + }) + }) + .map_err(fixture_error) +} + +#[pyfunction] +fn scripted_fixture_read_only_with_args_with_post( + py: Python<'_>, + script_name: PathBuf, + args: Vec, + version: u32, + post_process: Py, +) -> PyResult<(PathBuf, Py)> { + check_script(&script_name)?; + check_callback(py, &post_process)?; + py.detach(|| { + gix_testtools::scripted_fixture_read_only_with_args_with_post(script_name, args, version, |state| { + callback(&post_process, state) + }) + }) + .map_err(fixture_error) +} + +#[pyfunction] +fn scripted_fixture_writable(py: Python<'_>, script_name: PathBuf) -> PyResult { + check_script(&script_name)?; + py.detach(|| gix_testtools::scripted_fixture_writable(script_name)) + .map(Into::into) + .map_err(fixture_error) +} + +#[pyfunction] +fn scripted_fixture_writable_with_args( + py: Python<'_>, + script_name: PathBuf, + args: Vec, + mode: Creation, +) -> PyResult { + check_script(&script_name)?; + py.detach(|| gix_testtools::scripted_fixture_writable_with_args(script_name, args, mode.into())) + .map(Into::into) + .map_err(fixture_error) +} + +#[pyfunction] +fn scripted_fixture_writable_with_post( + py: Python<'_>, + script_name: PathBuf, + version: u32, + post_process: Py, +) -> PyResult<(TempDir, Py)> { + check_script(&script_name)?; + check_callback(py, &post_process)?; + py.detach(|| { + gix_testtools::scripted_fixture_writable_with_post(script_name, version, |state| callback(&post_process, state)) + }) + .map(|(dir, value)| (dir.into(), value)) + .map_err(fixture_error) +} + +#[pyfunction] +fn scripted_fixture_writable_with_args_with_post( + py: Python<'_>, + script_name: PathBuf, + args: Vec, + mode: Creation, + version: u32, + post_process: Py, +) -> PyResult<(TempDir, Py)> { + check_script(&script_name)?; + check_callback(py, &post_process)?; + py.detach(|| { + gix_testtools::scripted_fixture_writable_with_args_with_post(script_name, args, mode.into(), version, |state| { + callback(&post_process, state) + }) + }) + .map(|(dir, value)| (dir.into(), value)) + .map_err(fixture_error) +} + +#[pyfunction] +fn rust_fixture_read_only( + py: Python<'_>, + name: String, + version: u32, + make_fixture: Py, +) -> PyResult<(PathBuf, Py)> { + check_name(&name)?; + check_callback(py, &make_fixture)?; + py.detach(|| gix_testtools::rust_fixture_read_only(&name, version, |state| callback(&make_fixture, state))) + .map_err(fixture_error) +} + +#[pyfunction] +fn rust_fixture_writable( + py: Python<'_>, + name: String, + version: u32, + mode: Creation, + make_fixture: Py, +) -> PyResult<(TempDir, Py)> { + check_name(&name)?; + check_callback(py, &make_fixture)?; + py.detach(|| { + gix_testtools::rust_fixture_writable(&name, version, mode.into(), |state| callback(&make_fixture, state)) + }) + .map(|(dir, value)| (dir.into(), value)) + .map_err(fixture_error) +} + +#[pyfunction] +fn git(py: Python<'_>, current_dir: PathBuf, arguments: String) -> PyResult { + py.detach(|| gix_testtools::git(current_dir, &arguments)) + .map_err(fixture_error) +} + +pub fn register(parent: &Bound<'_, PyModule>) -> PyResult<()> { + // Freeze the native tempfile registry without handlers before fixture helpers request signal setup. + gix::tempfile::signal::setup(gix::tempfile::signal::handler::Mode::None); + let m = PyModule::new(parent.py(), "gix._testtools")?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + macro_rules! functions { + ($($name:ident),* $(,)?) => { $(m.add_function(wrap_pyfunction!($name, &m)?)?;)* }; + } + functions!( + fixture_path, + fixture_bytes, + fixture_has_symlinks, + scripted_fixture_read_only, + scripted_fixture_read_only_needs_archive, + scripted_fixture_read_only_with_args, + scripted_fixture_read_only_with_post, + scripted_fixture_read_only_with_args_with_post, + scripted_fixture_writable, + scripted_fixture_writable_with_args, + scripted_fixture_writable_with_post, + scripted_fixture_writable_with_args_with_post, + rust_fixture_read_only, + rust_fixture_writable, + git + ); + parent + .py() + .import("sys")? + .getattr("modules")? + .set_item("gix._testtools", &m)?; + Ok(()) +} diff --git a/src/worktree.rs b/src/worktree.rs index a0cc70c..ea06d84 100644 --- a/src/worktree.rs +++ b/src/worktree.rs @@ -437,6 +437,7 @@ mod mutation { inner: gix::worktree::remove::Options { thread_limit, max_retries, + ..Default::default() }, } } diff --git a/tests/test_testtools.py b/tests/test_testtools.py new file mode 100644 index 0000000..0140ed9 --- /dev/null +++ b/tests/test_testtools.py @@ -0,0 +1,215 @@ +"""Exercise native fixture caches in disposable child-process workspaces.""" + +import os +from pathlib import Path +import subprocess +import shutil +import sys +import tempfile +import textwrap +import unittest + +import gix + + +class TesttoolsTests(unittest.TestCase): + def run_fixture_test(self, source, directory): + env = {key: value for key, value in os.environ.items() if not key.startswith("GIT_")} + env.update(PYTHONPATH=str(Path(gix.__file__).parent.parent), GIX_TEST_IGNORE_ARCHIVES="1") + result = subprocess.run([sys.executable, "-c", textwrap.dedent(source)], cwd=directory, + env=env, capture_output=True, text=True, timeout=30) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + + def test_python_fixture_cache_postprocessing_and_writable_copy(self): + with tempfile.TemporaryDirectory() as directory: + self.run_fixture_test(""" + from pathlib import Path + import concurrent.futures + import gix + from gix import testtools as t + + seen = [] + def create(state): + seen.append(state.is_uninitialized()) + if state.is_uninitialized(): + repo = gix.init(state.path() / "repo", options=gix.OpenOptions.isolated()) + (state.path() / "blob-id").write_text(str(repo.write_blob(b"raw \\xff"))) + return (state.path() / "blob-id").read_text() + + # Concurrent first callers share one initialized fixture. + with concurrent.futures.ThreadPoolExecutor(max_workers=4) as pool: + fixtures = list(pool.map(lambda _: t.rust_fixture_read_only("cached", 1, create), range(4))) + assert seen.count(True) == 1 and seen.count(False) == 3, seen + assert len(set(fixtures)) == 1 + path, blob_id = fixtures[0] + assert isinstance(path, Path) + assert gix.open_opts(path / "repo", gix.OpenOptions.isolated()).find_blob(blob_id).data == b"raw \\xff" + + temporary, value = t.rust_fixture_writable("cached", 1, t.Creation.CopyFromReadOnly, create) + with temporary as writable: + copy = writable.path() + assert value == blob_id and copy != path + (copy / "blob-id").write_text("changed") + assert (path / "blob-id").read_text() == blob_id + assert not copy.exists() + temporary.close() + try: + temporary.path() + except RuntimeError: + pass + else: + raise AssertionError("closed fixture still exposes a path") + + _, versioned = t.rust_fixture_read_only("cached", 2, create) + assert seen[-1] is True and versioned == blob_id + # Execute initializes at the final location, including absolute paths. + def absolute(state): + assert state.is_uninitialized() + return state.path() + owned, final_path = t.rust_fixture_writable("absolute", 1, t.Creation.Execute, absolute) + assert owned.path() == final_path + del owned + assert not final_path.exists() + """, directory) + # The generated fixture persists across processes and calls the closure as Fresh. + self.run_fixture_test(""" + from gix import testtools as t + def cached(state): + assert not state.is_uninitialized() + return (state.path() / "blob-id").read_text() + path, value = t.rust_fixture_read_only("cached", 1, cached) + assert value == (path / "blob-id").read_text() + """, directory) + + def test_scripted_fixture_python_post_and_arguments(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + fixtures = root / "tests/fixtures" + fixtures.mkdir(parents=True) + script = fixtures / "make.sh" + script.write_text('set -eu\ngit init repo\nprintf "%s" "$1" > value\n') + self.run_fixture_test(""" + from gix import testtools as t + import os + # Fixture commands must clear inherited redirects to another repository. + os.environ["GIT_DIR"] = str(t.fixture_path("unrelated-repository")) + os.environ["GIT_WORK_TREE"] = str(t.fixture_path("unrelated-worktree")) + states = [] + def post(state): + states.append(state.is_uninitialized()) + if state.is_uninitialized(): + (state.path() / "value").write_bytes((state.path() / "value").read_bytes() + b"-post") + return (state.path() / "value").read_bytes() + + path, value = t.scripted_fixture_read_only_with_args_with_post("make.sh", ["input with spaces"], 1, post) + again, same = t.scripted_fixture_read_only_with_args_with_post("make.sh", ["input with spaces"], 1, post) + assert path == again and value == same == b"input with spaces-post" + assert states == [True, False], states + assert t.git(path / "repo", "rev-parse --is-inside-work-tree").strip() == "true" + with t.scripted_fixture_writable_with_args("make.sh", ["writable"], t.Creation.Execute) as writable: + assert (writable.path() / "value").read_bytes() == b"writable" + assert t.fixture_bytes("make.sh").startswith(b"set -eu") + assert not t.fixture_path("unrelated-repository").exists() + writable, result = t.scripted_fixture_writable_with_args_with_post( + "make.sh", ["copy"], t.Creation.CopyFromReadOnly, 1, post + ) + with writable: + assert result == b"copy-post" + assert (writable.path() / "value").read_bytes() == result + t.fixture_path("plain.sh").write_text('printf original > value\\n') + plain, result = t.scripted_fixture_read_only_with_post("plain.sh", 1, post) + assert result == b"original-post" + temporary, result = t.scripted_fixture_writable_with_post("plain.sh", 1, post) + with temporary: + assert result == b"original-post" and temporary.path() != plain + assert t.scripted_fixture_read_only("plain.sh").is_dir() + assert t.scripted_fixture_read_only_with_args("make.sh", ["plain-args"]).is_dir() + """, directory) + + @unittest.skipIf(sys.platform == "win32", "native archive generation is disabled on Windows") + def test_archive_extraction_skips_initialization(self): + with tempfile.TemporaryDirectory() as directory: + self.run_fixture_test(""" + import os + from pathlib import Path + from gix import testtools as t + os.environ.pop("GIX_TEST_IGNORE_ARCHIVES", None) + os.environ["GIX_TEST_CREATE_ARCHIVES_EVEN_ON_CI"] = "1" + def create(state): + assert state.is_uninitialized() + (state.path() / "value").write_bytes(b"archived") + return "created" + path, value = t.rust_fixture_read_only("archive", 1, create) + assert value == "created" + assert Path("tests/fixtures/generated-archives/rust-archive.tar").is_file() + """, directory) + shutil.rmtree(Path(directory) / "tests/fixtures/generated-do-not-edit") + self.run_fixture_test(""" + import os + from gix import testtools as t + os.environ.pop("GIX_TEST_IGNORE_ARCHIVES", None) + def cached(state): + assert not state.is_uninitialized() + return (state.path() / "value").read_bytes() + assert t.rust_fixture_read_only("archive", 1, cached)[1] == b"archived" + """, directory) + + def test_callback_exception_identity_and_failed_fixture_retry(self): + with tempfile.TemporaryDirectory() as directory: + self.run_fixture_test(""" + from gix import testtools as t + class Failure(Exception): + pass + failure = Failure("post failed") + def fail(state): + (state.path() / "partial").write_text("incomplete") + raise failure + try: + t.rust_fixture_read_only("failure", 1, fail) + except Failure as caught: + assert caught is failure + else: + raise AssertionError("callback exception was suppressed") + def retry(state): + assert state.is_uninitialized() + assert not (state.path() / "partial").exists() + return "retried" + assert t.rust_fixture_read_only("failure", 1, retry)[1] == "retried" + for name in ("", ".", "..", "../escape", "/absolute"): + try: + t.rust_fixture_read_only(name, 1, retry) + except ValueError: + pass + else: + raise AssertionError(name) + try: + t.rust_fixture_read_only("not-callable", 1, 42) + except TypeError: + pass + else: + raise AssertionError("non-callable accepted") + try: + t.scripted_fixture_read_only("missing.sh") + except Exception as error: + assert "missing.sh" in str(error) + else: + raise AssertionError("missing script accepted") + """, directory) + + @unittest.skipIf(sys.platform == "win32", "POSIX termination signals") + def test_fixtures_preserve_python_signal_handlers(self): + with tempfile.TemporaryDirectory() as directory: + self.run_fixture_test(""" + import os + import signal + import gix + received = [] + signal.signal(signal.SIGTERM, lambda signum, frame: received.append(signum)) + gix.testtools.rust_fixture_read_only("signals", 1, lambda state: None) + os.kill(os.getpid(), signal.SIGTERM) + assert received == [signal.SIGTERM] + """, directory) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_typing.py b/tests/test_typing.py index c0e08f1..eaa672e 100644 --- a/tests/test_typing.py +++ b/tests/test_typing.py @@ -25,7 +25,7 @@ def stub_api(): elif isinstance(node, ast.ImportFrom): for alias in node.names: aliases[alias.asname or alias.name] = alias.name - if path.name != "_gix.pyi": + if path.name not in ("_gix.pyi", "testtools.pyi"): continue if isinstance(node, (ast.ClassDef, ast.FunctionDef, ast.AnnAssign)): exports.add(node.target.id if isinstance(node, ast.AnnAssign) else node.name) @@ -108,23 +108,24 @@ class TypingTests(unittest.TestCase): def test_runtime_exports_and_members_have_stubs(self): classes, aliases, functions, exports = stub_api() missing = [] - for name in dir(gix): - if name.startswith("_"): - continue - value = getattr(gix, name) - if not callable(value): - continue - if name not in exports: - missing.append(name) - if not isinstance(value, type) or issubclass(value, BaseException): - continue - members = class_members(name, classes, aliases) - for member in vars(value): - if (not member.startswith("_") or member in PROTOCOL_METHODS) and member not in members: - missing.append(f"{name}.{member}") - elif not member.startswith("_") and member in members: - if callable(getattr(value, member)) != declares_method(members[member]): - missing.append(f"{name}.{member}: method/property mismatch") + for module in (gix, gix.testtools): + for name in dir(module): + if name.startswith("_"): + continue + value = getattr(module, name) + if not callable(value): + continue + if name not in exports: + missing.append(name) + if not isinstance(value, type) or issubclass(value, BaseException): + continue + members = class_members(name, classes, aliases) + for member in vars(value): + if (not member.startswith("_") or member in PROTOCOL_METHODS) and member not in members: + missing.append(f"{name}.{member}") + elif not member.startswith("_") and member in members: + if callable(getattr(value, member)) != declares_method(members[member]): + missing.append(f"{name}.{member}: method/property mismatch") self.assertEqual(missing, [], "Native exports/members missing from shipped stubs") def test_native_parameter_names_kinds_and_defaults_match_stubs(self): @@ -149,10 +150,9 @@ def check(label, value, node): if actual_default != expected_default: mismatches.append(f"{label}.{name}: runtime default={actual_default!r}, stub={expected_default!r}") - for name in dir(gix): + for name, value in ((name, getattr(module, name)) for module in (gix, gix.testtools) for name in dir(module)): if name.startswith("_"): continue - value = getattr(gix, name) if isinstance(value, type): if issubclass(value, BaseException): continue diff --git a/vendor/gix-ref/Cargo.toml b/vendor/gix-ref/Cargo.toml deleted file mode 100644 index 2fc4ed0..0000000 --- a/vendor/gix-ref/Cargo.toml +++ /dev/null @@ -1,45 +0,0 @@ - -[package] -name = "gix-ref" -version = "0.68.0" -repository = "https://github.com/GitoxideLabs/gitoxide" -license = "MIT OR Apache-2.0" -description = "A crate to handle git references" -authors = ["Sebastian Thiel "] -edition = "2024" -include = ["/src/**/*", "/LICENSE-*"] -rust-version = "1.88" - -[lib] -doctest = false -test = true - -[features] -## Enable support for the SHA-1 hash by enabling the respective feature in the `gix-hash` crate. -sha1 = ["gix-hash/sha1"] -## Enable support for the SHA-256 hash by enabling the respective feature in the `gix-hash` crate. -sha256 = ["gix-hash/sha256"] -## Data structures implement `serde::Serialize` and `serde::Deserialize`. -serde = ["dep:serde", "gix-hash/serde", "gix-actor/serde", "gix-object/serde"] - -[dependencies] -gix-parallel = { version = "^0.50.0", git = "https://github.com/GitoxideLabs/gitoxide", rev = "1d7bac742f70b72ddda0c7294f4a97566b8db596" } -gix-error = { version = "^0.4.0", git = "https://github.com/GitoxideLabs/gitoxide", rev = "1d7bac742f70b72ddda0c7294f4a97566b8db596", features = ["bstr"] } -gix-fs = { version = "^0.23.0", git = "https://github.com/GitoxideLabs/gitoxide", rev = "1d7bac742f70b72ddda0c7294f4a97566b8db596", features = ["walkdir"] } -gix-path = { version = "^0.13.0", git = "https://github.com/GitoxideLabs/gitoxide", rev = "1d7bac742f70b72ddda0c7294f4a97566b8db596" } -gix-hash = { version = "^0.27.0", git = "https://github.com/GitoxideLabs/gitoxide", rev = "1d7bac742f70b72ddda0c7294f4a97566b8db596" } -gix-object = { version = "^0.65.0", git = "https://github.com/GitoxideLabs/gitoxide", rev = "1d7bac742f70b72ddda0c7294f4a97566b8db596" } -gix-utils = { version = "^0.4.0", git = "https://github.com/GitoxideLabs/gitoxide", rev = "1d7bac742f70b72ddda0c7294f4a97566b8db596" } -gix-validate = { version = "^0.12.0", git = "https://github.com/GitoxideLabs/gitoxide", rev = "1d7bac742f70b72ddda0c7294f4a97566b8db596" } -gix-actor = { version = "^0.43.0", git = "https://github.com/GitoxideLabs/gitoxide", rev = "1d7bac742f70b72ddda0c7294f4a97566b8db596" } -gix-lock = { version = "^25.0.0", git = "https://github.com/GitoxideLabs/gitoxide", rev = "1d7bac742f70b72ddda0c7294f4a97566b8db596" } -gix-tempfile = { version = "^25.0.0", default-features = false, git = "https://github.com/GitoxideLabs/gitoxide", rev = "1d7bac742f70b72ddda0c7294f4a97566b8db596" } -serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"] } - -# packed refs -memmap2 = "0.9.11" - -document-features = { version = "0.2.1", optional = true } - -[package.metadata.docs.rs] -features = ["sha1", "document-features", "serde"] diff --git a/vendor/gix-ref/LICENSE-APACHE b/vendor/gix-ref/LICENSE-APACHE deleted file mode 100644 index f433b1a..0000000 --- a/vendor/gix-ref/LICENSE-APACHE +++ /dev/null @@ -1,177 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS diff --git a/vendor/gix-ref/LICENSE-MIT b/vendor/gix-ref/LICENSE-MIT deleted file mode 100644 index 969d061..0000000 --- a/vendor/gix-ref/LICENSE-MIT +++ /dev/null @@ -1,17 +0,0 @@ -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/vendor/gix-ref/README.pygix.md b/vendor/gix-ref/README.pygix.md deleted file mode 100644 index 1809f04..0000000 --- a/vendor/gix-ref/README.pygix.md +++ /dev/null @@ -1,17 +0,0 @@ -# Temporary upstream prerequisite - -Source matches `gix-ref/src` at Gitoxide `pygix` HEAD -`e4d2b313b840ea0e00453791882dbc6eb7c636f6`. Its prerequisite commit is -`066b042d68e5697b8272b636272c0c4253884882`, based on public revision -`1d7bac742f70b72ddda0c7294f4a97566b8db596`. - -The patch preserves arbitrary reflog message bytes with `write_all` rather -than BStr text formatting. -The prerequisite remains on the prepared local branch for Sebastian Thiel's -review. This reintegration did not modify or push that branch. - -The source includes the updated base's security-audit fixes. Cargo sibling -dependencies use the pinned public Git source; workspace lints and test-only -dependencies are omitted. This source copy makes packaged builds independent -of the local checkout and unpublished revisions. Remove it after the fix lands -upstream and update the public pin. Original source licenses apply. diff --git a/vendor/gix-ref/src/compare.rs b/vendor/gix-ref/src/compare.rs deleted file mode 100644 index 9224fec..0000000 --- a/vendor/gix-ref/src/compare.rs +++ /dev/null @@ -1,126 +0,0 @@ -use crate::{ - FullName, FullNameRef, Namespace, PartialName, PartialNameRef, Reference, - bstr::{BStr, BString, ByteSlice}, - file, packed, -}; - -macro_rules! impl_partial_eq { - ($left_type:ty, $right_type:ty, $left:ident => $left_bytes:expr, $right:ident => $right_bytes:expr) => { - impl PartialEq<$right_type> for $left_type { - fn eq(&self, other: &$right_type) -> bool { - let $left = self; - let $right = other; - $left_bytes == $right_bytes - } - } - }; -} - -macro_rules! impl_partial_eq_pair { - ($left_type:ty, $right_type:ty, $left:ident => $left_bytes:expr, $right:ident => $right_bytes:expr) => { - impl_partial_eq!($left_type, $right_type, $left => $left_bytes, $right => $right_bytes); - impl_partial_eq!($right_type, $left_type, $right => $right_bytes, $left => $left_bytes); - }; -} - -macro_rules! impl_partial_eq_bytes { - ($type:ty, $value:ident => $bytes:expr) => { - impl_partial_eq_pair!($type, str, $value => $bytes.as_bytes(), other => other.as_bytes()); - impl_partial_eq_pair!($type, &str, $value => $bytes.as_bytes(), other => other.as_bytes()); - impl_partial_eq_pair!($type, String, $value => $bytes.as_bytes(), other => other.as_bytes()); - impl_partial_eq_pair!($type, BStr, $value => $bytes.as_bytes(), other => other.as_bytes()); - impl_partial_eq_pair!($type, &BStr, $value => $bytes.as_bytes(), other => other.as_bytes()); - impl_partial_eq_pair!($type, BString, $value => $bytes.as_bytes(), other => other.as_bytes()); - }; -} - -macro_rules! impl_partial_eq_reference { - ($type:ty, $value:ident => $name:expr) => { - impl_partial_eq!($type, str, $value => $name.as_bytes(), other => other.as_bytes()); - impl_partial_eq!($type, &str, $value => $name.as_bytes(), other => other.as_bytes()); - impl_partial_eq!($type, String, $value => $name.as_bytes(), other => other.as_bytes()); - impl_partial_eq!($type, BStr, $value => $name.as_bytes(), other => other.as_bytes()); - impl_partial_eq!($type, &BStr, $value => $name.as_bytes(), other => other.as_bytes()); - impl_partial_eq!($type, BString, $value => $name.as_bytes(), other => other.as_bytes()); - impl_partial_eq!( - $type, - FullName, - $value => $name.as_bytes(), - other => other.as_bstr().as_bytes() - ); - impl_partial_eq!( - $type, - FullNameRef, - $value => $name.as_bytes(), - other => other.as_bstr().as_bytes() - ); - impl_partial_eq!( - $type, - &FullNameRef, - $value => $name.as_bytes(), - other => other.as_bstr().as_bytes() - ); - }; -} - -impl_partial_eq_bytes!(FullName, value => value.as_bstr()); -impl_partial_eq_bytes!(FullNameRef, value => value.as_bstr()); -impl_partial_eq_bytes!(PartialName, value => value.as_ref().as_bstr()); -impl_partial_eq_bytes!(PartialNameRef, value => value.as_bstr()); -impl_partial_eq_bytes!(Namespace, value => value.as_bstr()); - -impl_partial_eq_pair!( - &FullNameRef, - String, - name => name.as_bstr().as_bytes(), - text => text.as_bytes() -); -impl_partial_eq_pair!( - &FullNameRef, - BString, - name => name.as_bstr().as_bytes(), - text => text.as_bytes() -); -impl_partial_eq_pair!( - &PartialNameRef, - String, - name => name.as_bstr().as_bytes(), - text => text.as_bytes() -); -impl_partial_eq_pair!( - &PartialNameRef, - BString, - name => name.as_bstr().as_bytes(), - text => text.as_bytes() -); - -impl_partial_eq_pair!( - FullName, - FullNameRef, - owned => owned.as_bstr().as_bytes(), - borrowed => borrowed.as_bstr().as_bytes() -); -impl_partial_eq_pair!( - FullName, - &FullNameRef, - owned => owned.as_bstr().as_bytes(), - borrowed => borrowed.as_bstr().as_bytes() -); -impl_partial_eq_pair!( - PartialName, - PartialNameRef, - owned => owned.as_ref().as_bstr().as_bytes(), - borrowed => borrowed.as_bstr().as_bytes() -); -impl_partial_eq_pair!( - PartialName, - &PartialNameRef, - owned => owned.as_ref().as_bstr().as_bytes(), - borrowed => borrowed.as_bstr().as_bytes() -); - -// Keep these comparisons one-way: same-type reference equality is structural, so reverse -// implementations would let references with different targets form a non-transitive chain through their name. -impl_partial_eq_reference!(Reference, value => value.name.as_bstr()); -impl_partial_eq_reference!(file::loose::Reference, value => value.name.as_bstr()); -impl_partial_eq_reference!(packed::Reference<'_>, value => value.name.as_bstr()); diff --git a/vendor/gix-ref/src/fullname.rs b/vendor/gix-ref/src/fullname.rs deleted file mode 100644 index 9ac7616..0000000 --- a/vendor/gix-ref/src/fullname.rs +++ /dev/null @@ -1,279 +0,0 @@ -use crate::{Category, FullName, FullNameRef, Namespace, PartialNameRef, bstr::ByteVec, name::is_pseudo_ref}; -use gix_object::bstr::{BStr, BString, ByteSlice}; -use std::{borrow::Borrow, path::Path}; - -impl TryFrom<&str> for FullName { - type Error = gix_validate::reference::name::Error; - - fn try_from(value: &str) -> Result { - Ok(FullName( - gix_validate::reference::name(value.as_bytes().as_bstr())?.into(), - )) - } -} - -impl TryFrom for FullName { - type Error = gix_validate::reference::name::Error; - - fn try_from(value: String) -> Result { - gix_validate::reference::name(value.as_bytes().as_bstr())?; - Ok(FullName(value.into())) - } -} - -impl TryFrom<&BStr> for FullName { - type Error = gix_validate::reference::name::Error; - - fn try_from(value: &BStr) -> Result { - Ok(FullName(gix_validate::reference::name(value)?.into())) - } -} - -impl TryFrom for FullName { - type Error = gix_validate::reference::name::Error; - - fn try_from(value: BString) -> Result { - gix_validate::reference::name(value.as_ref())?; - Ok(FullName(value)) - } -} - -impl TryFrom<&BString> for FullName { - type Error = gix_validate::reference::name::Error; - - fn try_from(value: &BString) -> Result { - gix_validate::reference::name(value.as_ref())?; - Ok(FullName(value.clone())) - } -} - -impl From for BString { - fn from(name: FullName) -> Self { - name.0 - } -} - -impl<'a> From<&'a FullNameRef> for &'a BStr { - fn from(name: &'a FullNameRef) -> Self { - &name.0 - } -} - -impl<'a> From<&'a FullNameRef> for FullName { - fn from(value: &'a FullNameRef) -> Self { - FullName(value.as_bstr().into()) - } -} - -impl std::fmt::Display for FullName { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - std::fmt::Display::fmt(&self.0, f) - } -} - -impl std::fmt::Display for FullNameRef { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - std::fmt::Display::fmt(&self.0, f) - } -} - -impl FullNameRef { - /// Interpret this fully qualified reference name as partial name. - pub fn as_partial_name(&self) -> &PartialNameRef { - PartialNameRef::new_unchecked(self.0.as_bstr()) - } - - /// Convert this name into the relative path identifying the reference location. - /// Return an error if the name cannot be represented as a native path. - pub fn to_path(&self) -> gix_error::Result<&Path> { - gix_path::from_byte_slice(&self.0) - } - - /// Return ourselves as byte string which is a valid refname - pub fn as_bstr(&self) -> &BStr { - &self.0 - } - - /// Strip well-known prefixes from the name and return it. - /// - /// If there is no such prefix, the original name is returned. - pub fn shorten(&self) -> &BStr { - self.category_and_short_name() - .map_or_else(|| self.0.as_bstr(), |(_, short)| short) - } - - /// Classify this name, or return `None` if it's unclassified. - pub fn category(&self) -> Option> { - self.category_and_short_name().map(|(cat, _)| cat) - } - - /// Classify this name, or return `None` if it's unclassified. If `Some`, - /// the shortened name is returned as well. - pub fn category_and_short_name(&self) -> Option<(Category<'_>, &BStr)> { - let name = self.0.as_bstr(); - for category in &[Category::Tag, Category::LocalBranch, Category::RemoteBranch] { - if let Some(shortened) = name.strip_prefix(category.prefix().as_bytes()) { - return Some((*category, shortened.as_bstr())); - } - } - - for category in &[ - Category::Note, - Category::Bisect, - Category::WorktreePrivate, - Category::Rewritten, - ] { - if name.starts_with(category.prefix().as_ref()) { - return Some(( - *category, - name.strip_prefix(b"refs/") - .expect("we checked for refs/* above") - .as_bstr(), - )); - } - } - - if is_pseudo_ref(name) { - Some((Category::PseudoRef, name)) - } else if let Some(shortened) = name.strip_prefix(Category::MainPseudoRef.prefix().as_bytes()) { - if shortened.starts_with_str("refs/") { - (Category::MainRef, shortened.as_bstr()).into() - } else { - is_pseudo_ref(shortened.into()).then(|| (Category::MainPseudoRef, shortened.as_bstr())) - } - } else if let Some(shortened_with_worktree_name) = - name.strip_prefix(Category::LinkedPseudoRef { name: "".into() }.prefix().as_bytes()) - { - let (name, shortened) = shortened_with_worktree_name.find_byte(b'/').map(|pos| { - ( - shortened_with_worktree_name[..pos].as_bstr(), - shortened_with_worktree_name[pos + 1..].as_bstr(), - ) - })?; - if shortened.starts_with_str("refs/") { - (Category::LinkedRef { name }, shortened.as_bstr()).into() - } else { - is_pseudo_ref(shortened).then(|| (Category::LinkedPseudoRef { name }, shortened.as_bstr())) - } - } else { - None - } - } -} - -/// Conversion -impl Category<'_> { - /// As the inverse of [`FullNameRef::category_and_short_name()`], use the prefix of this category alongside - /// the `short_name` to create a valid fully qualified [reference name](FullName). - /// - /// If `short_name` already contains the prefix that it would receive (and is thus a full name), no duplication will occur. - pub fn to_full_name<'a>(&self, short_name: impl Into<&'a BStr>) -> Result { - let mut out: BString = self.prefix().into(); - let short_name = short_name.into(); - let partial_name = match self { - Category::Note => short_name.strip_prefix("notes/".as_bytes()).unwrap_or(short_name), - Category::MainRef => short_name.strip_prefix("refs/".as_bytes()).unwrap_or(short_name), - Category::LinkedPseudoRef { name } | Category::LinkedRef { name } => { - out.extend_from_slice(name); - out.push(b'/'); - short_name - } - Category::Bisect => short_name.strip_prefix("bisect/".as_bytes()).unwrap_or(short_name), - Category::Rewritten => short_name.strip_prefix("rewritten/".as_bytes()).unwrap_or(short_name), - Category::WorktreePrivate => short_name.strip_prefix("worktree/".as_bytes()).unwrap_or(short_name), - Category::Tag - | Category::LocalBranch - | Category::RemoteBranch - | Category::PseudoRef - | Category::MainPseudoRef => short_name, - }; - if out.is_empty() || !partial_name.starts_with(&out) { - out.extend_from_slice(partial_name); - FullName::try_from(out) - } else { - FullName::try_from(partial_name.as_bstr()) - } - } -} - -impl FullName { - /// Convert this name into the relative path identifying its location, failing if its encoding is unrepresentable. - pub fn to_path(&self) -> gix_error::Result<&Path> { - gix_path::from_byte_slice(&self.0) - } - - /// Dissolve this instance and return the buffer. - pub fn into_inner(self) -> BString { - self.0 - } - - /// Return ourselves as byte string which is a valid refname - pub fn as_bstr(&self) -> &BStr { - self.0.as_bstr() - } - - /// Modify ourself so that we use `namespace` as prefix, if it is not yet in the `namespace` - pub fn prefix_namespace(&mut self, namespace: &Namespace) -> &mut Self { - if !self.0.starts_with_str(&namespace.0) { - self.0.insert_str(0, &namespace.0); - } - self - } - - /// Strip the given `namespace` off the beginning of this name, if it is in this namespace. - pub fn strip_namespace(&mut self, namespace: &Namespace) -> &mut Self { - if self.0.starts_with_str(&namespace.0) { - let prev_len = self.0.len(); - self.0.copy_within(namespace.0.len().., 0); - self.0.resize(prev_len - namespace.0.len(), 0); - } - self - } - - /// Strip well-known prefixes from the name and return it. - /// - /// If there is no such prefix, the original name is returned. - pub fn shorten(&self) -> &BStr { - self.as_ref().shorten() - } - - /// Classify this name, or return `None` if it's unclassified. - pub fn category(&self) -> Option> { - self.as_ref().category() - } - - /// Classify this name, or return `None` if it's unclassified. If `Some`, - /// the shortened name is returned as well. - pub fn category_and_short_name(&self) -> Option<(crate::Category<'_>, &BStr)> { - self.as_ref().category_and_short_name() - } -} - -impl FullNameRef { - /// Return the file name portion of a full name, for instance `main` if the - /// full name was `refs/heads/main`. - pub fn file_name(&self) -> &BStr { - self.0.rsplitn(2, |b| *b == b'/').next().expect("valid ref").as_bstr() - } -} - -impl Borrow for FullName { - #[inline] - fn borrow(&self) -> &FullNameRef { - FullNameRef::new_unchecked(self.0.as_bstr()) - } -} - -impl AsRef for FullName { - fn as_ref(&self) -> &FullNameRef { - self.borrow() - } -} - -impl ToOwned for FullNameRef { - type Owned = FullName; - - fn to_owned(&self) -> Self::Owned { - FullName(self.0.to_owned()) - } -} diff --git a/vendor/gix-ref/src/lib.rs b/vendor/gix-ref/src/lib.rs deleted file mode 100644 index a285ba7..0000000 --- a/vendor/gix-ref/src/lib.rs +++ /dev/null @@ -1,232 +0,0 @@ -//! A crate for handling the references stored in various formats in a git repository. -//! -//! References are also called _refs_ which are used interchangeably. -//! -//! Refs are the way to keep track of objects and come in two flavors. -//! -//! * symbolic refs are pointing to another reference -//! * peeled refs point to the an object by its [`ObjectId`] -//! -//! They can be identified by a relative path and stored in various flavors. -//! -//! * **files** -//! * **[loose][file::Store]** -//! * one reference maps to a file on disk -//! * **packed** -//! * references are stored in a single human-readable file, along with their targets if they are symbolic. -//! -//! Concrete recovery signals such as missing references and reflog identities expose classification-only -//! [`gix_error::ClassificationMarker`] sources. Use `is_not_found()`, `is_corrupted()`, `is_validation()`, or -//! `classify()` on [`gix_error::Exn`] and [`gix_error::Error`] rather than downcasting these sources to classifier -//! error types. Callee errors retain their concrete causes. Locally detected failures may use a single classified -//! [`gix_error::Message`] containing both the diagnostic and its values. -//! Operations return [`gix_error::Error`], with diagnostic [`gix_error::Message`] keys documented where they are added. -//! Inspect these dictionaries with [`metadata()`](gix_error::Error::metadata), and add context available at the call site. -//! For recovery, downcast to [`file::find::NotFound`] to distinguish an absent reference from an absent object, -//! [`file::find::ReferenceDecode`] for loose reference contents that could not be decoded, -//! [`file::transaction::prepare::ReferenceOutOfDate`] or [`file::transaction::prepare::MustNotExist`] for a failed -//! update constraint, and [`file::log::create_or_update::MissingCommitter`] for a missing reflog identity. -//! -//! ## Feature Flags -#![cfg_attr( - all(doc, feature = "document-features"), - doc = ::document_features::document_features!() -)] -#![cfg_attr(all(doc, feature = "document-features"), feature(doc_cfg))] -#![deny(missing_docs, unsafe_code)] - -use gix_hash::{ObjectId, oid}; -pub use gix_object::bstr; -use gix_object::bstr::{BStr, BString}; - -#[path = "store/mod.rs"] -mod store_impl; -pub use store_impl::{file, packed}; - -mod compare; -mod fullname; -/// -pub mod name; -/// -pub mod namespace; -/// -pub mod transaction; - -mod parse; -mod raw; - -pub use raw::Reference; - -mod target; - -/// -pub mod log; - -/// -pub mod store { - /// - pub mod init { - - /// Options for use during [initialization](crate::file::Store::at). - #[derive(Debug, Copy, Clone, Default)] - pub struct Options { - /// How to write the ref-log. - pub write_reflog: super::WriteReflog, - /// The equivalent of `core.precomposeUnicode`. - pub precompose_unicode: bool, - /// If `true`, we will avoid reading from or writing to references that contains Windows device names - /// to avoid side effects. This only needs to be `true` on Windows, but can be `true` on other platforms - /// if they need to remain compatible with Windows. - pub prohibit_windows_device_names: bool, - } - } - /// The way a file store handles the reflog - #[derive(Default, Debug, PartialOrd, PartialEq, Ord, Eq, Hash, Clone, Copy)] - pub enum WriteReflog { - /// Always write the reflog for all references for ref edits, unconditionally. - Always, - /// Write a ref log for ref edits according to the standard rules. - #[default] - Normal, - /// Never write a ref log. - Disable, - } - - /// A thread-local handle for interacting with a [`Store`][crate::Store] to find and iterate references. - #[derive(Clone)] - #[expect( - dead_code, - reason = "the general reference-store handle is scaffolding for planned ref-table support" - )] - pub(crate) struct Handle { - /// A way to access shared state with the requirement that interior mutability doesn't leak or is incorporated into error types - /// if it could. The latter can't happen if references to said internal aren't ever returned. - state: handle::State, - } - - #[expect( - dead_code, - reason = "the general reference-store state is scaffolding for planned ref-table support" - )] - pub(crate) enum State { - Loose { store: file::Store }, - } - - pub(crate) mod general; - - /// - #[path = "general/handle/mod.rs"] - mod handle; - pub use handle::find; - - use crate::file; -} - -/// The git reference store. -/// TODO: Figure out if handles are needed at all, which depends on the ref-table implementation. -#[expect( - dead_code, - reason = "callers still use file::Store directly while this general store awaits ref-table support" -)] -pub(crate) struct Store { - inner: store::State, -} - -/// A validated complete and fully qualified reference name, safe to use for all operations. -#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct FullName(pub(crate) BString); - -/// A validated complete and fully qualified reference name, safe to use for all operations. -#[derive(Hash, Debug, PartialEq, Eq, Ord, PartialOrd)] -#[repr(transparent)] -pub struct FullNameRef(BStr); - -/// A validated and potentially partial reference name, safe to use for common operations. -#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd)] -#[repr(transparent)] -pub struct PartialNameRef(BStr); - -/// A validated and potentially partial reference name, safe to use for common operations. -#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct PartialName(BString); - -/// A _validated_ prefix for references to act as a namespace. -#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct Namespace(BString); - -/// Denotes the kind of reference. -#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub enum Kind { - /// A ref that points to an object id directly. - Object, - /// A ref that points to another reference, adding a level of indirection. - /// - /// It can be resolved to an id using the [`peel_to_id()`][`crate::file::ReferenceExt::peel_to_id()`] method. - Symbolic, -} - -/// The various known categories of references. -/// -/// This translates into a prefix containing all references of a given category. -#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub enum Category<'a> { - /// A tag in `refs/tags` - Tag, - /// A branch in `refs/heads` - LocalBranch, - /// A branch in `refs/remotes` - RemoteBranch, - /// A tag in `refs/notes` - Note, - /// Something outside `ref/` in the current worktree, typically `HEAD`. - PseudoRef, - /// A `PseudoRef`, but referenced so that it will always refer to the main worktree by - /// prefixing it with `main-worktree/`. - MainPseudoRef, - /// Any reference that is prefixed with `main-worktree/refs/` - MainRef, - /// A `PseudoRef` in another _linked_ worktree, never in the main one, like `worktrees//HEAD`. - LinkedPseudoRef { - /// The name of the worktree. - #[cfg_attr(feature = "serde", serde(borrow))] - name: &'a BStr, - }, - /// Any reference that is prefixed with `worktrees//refs/`. - LinkedRef { - /// The name of the worktree. - name: &'a BStr, - }, - /// A ref that is private to each worktree (_linked_ or _main_), with `refs/bisect/` prefix - Bisect, - /// A ref that is private to each worktree (_linked_ or _main_), with `refs/rewritten/` prefix - Rewritten, - /// A ref that is private to each worktree (_linked_ or _main_), with `refs/worktree/` prefix - WorktreePrivate, - // REF_TYPE_NORMAL, /* normal/shared refs inside refs/ */ -} - -/// Denotes a ref target, equivalent to [`Kind`], but with mutable data. -#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub enum Target { - /// A ref that points directly to an object id. - Object(ObjectId), - /// A ref that points to another reference by its validated name, adding a level of indirection. - /// - /// Note that this is an extension of gitoxide which will be helpful in logging all reference changes. - Symbolic(FullName), -} - -/// Denotes a ref target, equivalent to [`Kind`], but with immutable data. -#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] -pub enum TargetRef<'a> { - /// A ref that points directly to an object id. - Object(&'a oid), - /// A ref that points to another reference by its validated name, adding a level of indirection. - Symbolic(&'a FullNameRef), -} diff --git a/vendor/gix-ref/src/log.rs b/vendor/gix-ref/src/log.rs deleted file mode 100644 index 45273ee..0000000 --- a/vendor/gix-ref/src/log.rs +++ /dev/null @@ -1,16 +0,0 @@ -use gix_hash::ObjectId; -use gix_object::bstr::BString; - -/// A parsed ref log line that can be changed -#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct Line { - /// The previous object id. Can be a null-sha to indicate this is a line for a new ref. - pub previous_oid: ObjectId, - /// The new object id. Can be a null-sha to indicate this ref is being deleted. - pub new_oid: ObjectId, - /// The signature of the currently configured committer. - pub signature: gix_actor::Signature, - /// The message providing details about the operation performed in this log line. - pub message: BString, -} diff --git a/vendor/gix-ref/src/name.rs b/vendor/gix-ref/src/name.rs deleted file mode 100644 index e0fedd3..0000000 --- a/vendor/gix-ref/src/name.rs +++ /dev/null @@ -1,295 +0,0 @@ -use std::{convert, convert::Infallible, ffi::OsStr, path::Path}; - -use gix_object::bstr::{BStr, BString, ByteSlice, ByteVec}; - -use crate::{Category, FullName, FullNameRef, PartialName, PartialNameRef}; - -/// The error used in the [`PartialNameRef`]`::try_from`(…) implementations. -pub type Error = gix_validate::reference::name::Error; - -impl Category<'_> { - /// Return the prefix that would contain all references of our kind, or an empty string if the reference would - /// be directly inside of the [`git_dir()`][crate::file::Store::git_dir()]. - pub fn prefix(&self) -> &BStr { - match self { - Category::Tag => b"refs/tags/".as_bstr(), - Category::LocalBranch => b"refs/heads/".as_bstr(), - Category::RemoteBranch => b"refs/remotes/".as_bstr(), - Category::Note => b"refs/notes/".as_bstr(), - Category::MainPseudoRef => b"main-worktree/".as_bstr(), - Category::MainRef => b"main-worktree/refs/".as_bstr(), - Category::PseudoRef => b"".as_bstr(), - Category::LinkedPseudoRef { .. } => b"worktrees/".as_bstr(), - Category::LinkedRef { .. } => b"worktrees/".as_bstr(), - Category::Bisect => b"refs/bisect/".as_bstr(), - Category::Rewritten => b"refs/rewritten/".as_bstr(), - Category::WorktreePrivate => b"refs/worktree/".as_bstr(), - } - } - - /// Returns `true` if the category is private to their worktrees, and never shared with other worktrees. - pub fn is_worktree_private(&self) -> bool { - matches!( - self, - Category::MainPseudoRef - | Category::PseudoRef - | Category::LinkedPseudoRef { .. } - | Category::WorktreePrivate - | Category::Rewritten - | Category::Bisect - ) - } - - /// Returns `true` if this category represents remote-tracking branches, like `refs/remotes//`. - pub fn is_remote_tracking_branch(&self) -> bool { - matches!(self, Category::RemoteBranch) - } -} - -impl FullNameRef { - pub(crate) fn new_unchecked(v: &BStr) -> &Self { - // SAFETY: FullNameRef is transparent and equivalent to a &BStr if provided as reference - #[expect(unsafe_code)] - unsafe { - std::mem::transmute(v) - } - } -} - -impl PartialNameRef { - pub(crate) fn new_unchecked(v: &BStr) -> &Self { - // SAFETY: PartialNameRef is transparent and equivalent to a &BStr if provided as reference - #[expect(unsafe_code)] - unsafe { - std::mem::transmute(v) - } - } -} - -impl PartialNameRef { - pub(crate) fn looks_like_full_name(&self, consider_pseudo_ref: bool) -> bool { - let name = self.0.as_bstr(); - name.starts_with_str("refs/") - || name.starts_with(Category::MainPseudoRef.prefix()) - || name.starts_with(Category::LinkedPseudoRef { name: "".into() }.prefix()) - || (consider_pseudo_ref && is_pseudo_ref(name)) - } - pub(crate) fn construct_full_name_ref<'buf>( - &self, - inbetween: &str, - buf: &'buf mut BString, - consider_pseudo_ref: bool, - ) -> &'buf FullNameRef { - buf.clear(); - if !self.looks_like_full_name(consider_pseudo_ref) { - buf.push_str("refs/"); - } - if !inbetween.is_empty() { - buf.push_str(inbetween); - buf.push_byte(b'/'); - } - buf.extend_from_slice(&self.0); - FullNameRef::new_unchecked(buf.as_bstr()) - } -} - -impl PartialNameRef { - /// Convert this name into the relative path possibly identifying the reference location. - /// Note that it may be only a partial path though. - /// Return an error if the name cannot be represented as a native path. - pub fn to_partial_path(&self) -> gix_error::Result<&Path> { - gix_path::from_byte_slice(self.0.as_bstr()) - } - - /// Provide the name as binary string which is known to be a valid partial ref name. - pub fn as_bstr(&self) -> &BStr { - &self.0 - } -} - -impl PartialName { - /// Append the `component` to ourselves and validate the newly created partial path. - pub fn join(self, component: &BStr) -> Result { - let mut b = self.0; - b.push_byte(b'/'); - b.extend(component.as_bytes()); - gix_validate::reference::name_partial(b.as_ref())?; - Ok(PartialName(b)) - } -} - -impl<'a> convert::TryFrom<&'a BStr> for &'a FullNameRef { - type Error = Error; - - fn try_from(v: &'a BStr) -> Result { - Ok(FullNameRef::new_unchecked(gix_validate::reference::name(v)?)) - } -} - -impl<'a> From<&'a FullNameRef> for &'a PartialNameRef { - fn from(v: &'a FullNameRef) -> Self { - PartialNameRef::new_unchecked(v.0.as_bstr()) - } -} - -impl<'a> convert::TryFrom<&'a OsStr> for &'a PartialNameRef { - type Error = gix_error::Exn; - - fn try_from(v: &'a OsStr) -> Result { - use gix_error::ResultExt; - - let v = gix_path::os_str_into_bstr(v).or_raise_typed(|| Error::InvalidByte { - byte: "".into(), - })?; - Ok(PartialNameRef::new_unchecked(gix_validate::reference::name_partial( - v.as_bstr(), - )?)) - } -} - -mod impls { - use std::borrow::Borrow; - - use crate::{PartialName, PartialNameRef, bstr::ByteSlice}; - - impl Borrow for PartialName { - #[inline] - fn borrow(&self) -> &PartialNameRef { - PartialNameRef::new_unchecked(self.0.as_bstr()) - } - } - - impl AsRef for PartialName { - fn as_ref(&self) -> &PartialNameRef { - self.borrow() - } - } - - impl ToOwned for PartialNameRef { - type Owned = PartialName; - - fn to_owned(&self) -> Self::Owned { - PartialName(self.0.to_owned()) - } - } -} - -impl<'a> convert::TryFrom<&'a BString> for &'a PartialNameRef { - type Error = Error; - - fn try_from(v: &'a BString) -> Result { - Ok(PartialNameRef::new_unchecked(gix_validate::reference::name_partial( - v.as_ref(), - )?)) - } -} - -impl<'a> convert::TryFrom<&'a BStr> for &'a PartialNameRef { - type Error = Error; - - fn try_from(v: &'a BStr) -> Result { - Ok(PartialNameRef::new_unchecked(gix_validate::reference::name_partial(v)?)) - } -} - -impl<'a> convert::TryFrom<&'a PartialName> for &'a PartialNameRef { - type Error = Error; - - fn try_from(v: &'a PartialName) -> Result { - Ok(PartialNameRef::new_unchecked(v.0.as_bstr())) - } -} - -impl<'a> convert::TryFrom<&'a str> for &'a FullNameRef { - type Error = Error; - - fn try_from(v: &'a str) -> Result { - let v = v.as_bytes().as_bstr(); - Ok(FullNameRef::new_unchecked(gix_validate::reference::name(v)?)) - } -} - -impl<'a> convert::TryFrom<&'a str> for &'a PartialNameRef { - type Error = Error; - - fn try_from(v: &'a str) -> Result { - let v = v.as_bytes().as_bstr(); - Ok(PartialNameRef::new_unchecked(gix_validate::reference::name_partial(v)?)) - } -} - -impl<'a> convert::TryFrom<&'a str> for PartialName { - type Error = Error; - - fn try_from(v: &'a str) -> Result { - let v = v.as_bytes().as_bstr(); - Ok(PartialName(gix_validate::reference::name_partial(v)?.to_owned())) - } -} - -#[expect( - clippy::infallible_try_from, - reason = "it's here so that it can be done, even if infallible. Needed for parameters that use `TryFrom` generically." -)] -impl<'a> convert::TryFrom<&'a FullName> for &'a PartialNameRef { - type Error = Infallible; - - fn try_from(v: &'a FullName) -> Result { - Ok(v.as_ref().as_partial_name()) - } -} - -impl<'a> convert::TryFrom<&'a String> for &'a FullNameRef { - type Error = Error; - - fn try_from(v: &'a String) -> Result { - let v = v.as_bytes().as_bstr(); - Ok(FullNameRef::new_unchecked(gix_validate::reference::name(v)?)) - } -} - -impl<'a> convert::TryFrom<&'a String> for &'a PartialNameRef { - type Error = Error; - - fn try_from(v: &'a String) -> Result { - let v = v.as_bytes().as_bstr(); - Ok(PartialNameRef::new_unchecked(gix_validate::reference::name_partial(v)?)) - } -} - -impl convert::TryFrom for PartialName { - type Error = Error; - - fn try_from(v: String) -> Result { - gix_validate::reference::name_partial(v.as_bytes().as_bstr())?; - Ok(PartialName(v.into())) - } -} - -impl convert::TryFrom for PartialName { - type Error = Error; - - fn try_from(v: BString) -> Result { - gix_validate::reference::name_partial(v.as_ref())?; - Ok(PartialName(v)) - } -} - -impl std::fmt::Display for PartialName { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - std::fmt::Display::fmt(&self.0, f) - } -} - -impl std::fmt::Display for PartialNameRef { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - std::fmt::Display::fmt(&self.0, f) - } -} - -/// Note that this method is disagreeing with `gix_validate` as it allows dashes '-' for some reason. -/// Since partial names cannot be created with dashes inside we adjusted this as it's probably unintended or git creates pseudo-refs -/// which wouldn't pass its safety checks. -pub(crate) fn is_pseudo_ref(name: &BStr) -> bool { - name.bytes().all(|b| b.is_ascii_uppercase() || b == b'_') -} diff --git a/vendor/gix-ref/src/namespace.rs b/vendor/gix-ref/src/namespace.rs deleted file mode 100644 index 4b98720..0000000 --- a/vendor/gix-ref/src/namespace.rs +++ /dev/null @@ -1,51 +0,0 @@ -use std::path::Path; - -use gix_object::bstr::{BStr, BString, ByteSlice, ByteVec}; -use gix_path::RelativePath; - -use crate::{FullName, FullNameRef, Namespace, PartialNameRef}; - -impl Namespace { - /// Dissolve ourselves into the interior representation - pub fn into_bstring(self) -> BString { - self.0 - } - /// Return ourselves as - pub fn as_bstr(&self) -> &BStr { - self.0.as_ref() - } - /// Return ourselves as a path for use within the filesystem. - /// Return an error if the namespace cannot be represented as a native path. - pub fn to_path(&self) -> gix_error::Result<&Path> { - gix_path::from_byte_slice(&self.0) - } - /// Append the given `prefix` to this namespace so it becomes usable for prefixed iteration. - /// - /// The prefix is a relative path with slash-separated path components. - pub fn into_namespaced_prefix(mut self, prefix: &RelativePath) -> BString { - self.0.push_str(prefix); - gix_path::to_unix_separators_on_windows(self.0).into_owned() - } - pub(crate) fn into_namespaced_name(mut self, name: &FullNameRef) -> FullName { - self.0.push_str(name.as_bstr()); - FullName(self.0) - } -} - -/// Given a `namespace` 'foo we output 'refs/namespaces/foo', and given 'foo/bar' we output 'refs/namespaces/foo/refs/namespaces/bar'. -/// -/// For more information, consult the [git namespace documentation](https://git-scm.com/docs/gitnamespaces). -pub fn expand<'a, Name, E>(namespace: Name) -> Result -where - Name: TryInto<&'a PartialNameRef, Error = E>, - gix_validate::reference::name::Error: From, -{ - let namespace = &namespace.try_into()?.0; - let mut out = BString::default(); - for component in namespace.split_str(b"/") { - out.push_str("refs/namespaces/"); - out.push_str(component); - out.push_str(b"/"); - } - Ok(Namespace(out)) -} diff --git a/vendor/gix-ref/src/parse.rs b/vendor/gix-ref/src/parse.rs deleted file mode 100644 index 3b6149d..0000000 --- a/vendor/gix-ref/src/parse.rs +++ /dev/null @@ -1,47 +0,0 @@ -use gix_object::bstr::{BStr, ByteSlice}; - -type ParseResult = Result; - -fn is_hex_digit(b: u8) -> bool { - b.is_ascii_hexdigit() -} - -/// Copy from `gix-object`, validating the hash against `object_hash`. -pub fn hex_hash<'a>(i: &mut &'a [u8], object_hash: gix_hash::Kind) -> ParseResult<&'a BStr> { - let len = object_hash.len_in_hex(); - let Some(hex) = i.get(..len) else { - return Err(()); - }; - if !hex.iter().all(|b| is_hex_digit(*b)) { - return Err(()); - } - *i = &i[len..]; - Ok(hex.as_bstr()) -} - -/// All supported hash lengths, if they match perfectly. -pub fn hex_hash_any<'a>(i: &mut &'a [u8]) -> ParseResult<&'a BStr> { - let max = gix_hash::Kind::longest().len_in_hex(); - let len = i.iter().take(max).take_while(|b| is_hex_digit(**b)).count(); - if !gix_hash::Kind::all().iter().any(|kind| kind.len_in_hex() == len) { - return Err(()); - } - let (hex, rest) = i.split_at(len); - *i = rest; - Ok(hex.as_bstr()) -} - -/// Parse CRLF or LF, independently of the platform. -pub fn newline<'a>(i: &mut &'a [u8]) -> ParseResult<&'a [u8]> { - if let Some(rest) = i.strip_prefix(b"\r\n") { - let out = &i[..2]; - *i = rest; - Ok(out) - } else if let Some(rest) = i.strip_prefix(b"\n") { - let out = &i[..1]; - *i = rest; - Ok(out) - } else { - Err(()) - } -} diff --git a/vendor/gix-ref/src/raw.rs b/vendor/gix-ref/src/raw.rs deleted file mode 100644 index c347963..0000000 --- a/vendor/gix-ref/src/raw.rs +++ /dev/null @@ -1,111 +0,0 @@ -use gix_hash::ObjectId; - -use crate::{FullName, Target}; - -/// A fully owned backend agnostic reference -#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct Reference { - /// The path to uniquely identify this ref within its store. - pub name: FullName, - /// The target of the reference, either a symbolic reference by full name or a possibly intermediate object by its id. - pub target: Target, - /// The fully peeled object to which this reference ultimately points to after following all symbolic refs and all annotated - /// tags. Only guaranteed to be set after - /// [`Reference::peel_to_id()`](crate::file::ReferenceExt::peel_to_id) was called or if this reference originated - /// from a packed ref. - pub peeled: Option, -} - -mod convert { - use gix_hash::ObjectId; - - use crate::{ - Target, - raw::Reference, - store_impl::{file::loose, packed}, - }; - - impl From for loose::Reference { - fn from(value: Reference) -> Self { - loose::Reference { - name: value.name, - target: value.target, - } - } - } - - impl From for Reference { - fn from(value: loose::Reference) -> Self { - Reference { - name: value.name, - target: value.target, - peeled: None, - } - } - } - - impl<'p> From> for Reference { - fn from(value: packed::Reference<'p>) -> Self { - Reference { - name: value.name.into(), - target: Target::Object(value.target()), - peeled: value - .object - .map(|hex| ObjectId::from_hex(hex).expect("parser validation")), - } - } - } -} - -mod access { - use gix_object::bstr::ByteSlice; - - use crate::{FullNameRef, Namespace, Target, raw::Reference}; - - impl Reference { - /// Returns the kind of reference based on its target - pub fn kind(&self) -> crate::Kind { - self.target.kind() - } - - /// Return the full validated name of the reference, with the given namespace stripped if possible. - /// - /// If the reference name wasn't prefixed with `namespace`, `None` is returned instead. - pub fn name_without_namespace(&self, namespace: &Namespace) -> Option<&FullNameRef> { - self.name - .0 - .as_bstr() - .strip_prefix(namespace.0.as_bytes()) - .map(|stripped| FullNameRef::new_unchecked(stripped.as_bstr())) - } - - /// Strip the given namespace from our name as well as the name, but not the reference we point to. - pub fn strip_namespace(&mut self, namespace: &Namespace) -> &mut Self { - self.name.strip_namespace(namespace); - if let Target::Symbolic(name) = &mut self.target { - name.strip_namespace(namespace); - } - self - } - } -} - -#[cfg(test)] -mod tests { - use gix_testtools::size_ok; - - use super::*; - - #[test] - fn size_of_reference() { - let actual = std::mem::size_of::(); - let sha1 = 80; - let sha256_extra = 24; - let expected = sha1 + sha256_extra; - assert!( - size_ok(actual, expected), - "let's not let it change size undetected: {actual} <~ {expected}" - ); - } -} diff --git a/vendor/gix-ref/src/store/file/find.rs b/vendor/gix-ref/src/store/file/find.rs deleted file mode 100644 index 7e57781..0000000 --- a/vendor/gix-ref/src/store/file/find.rs +++ /dev/null @@ -1,479 +0,0 @@ -use gix_error::{ErrorExt, Message, Result, ResultExt, message}; - -use std::{ - borrow::Cow, - io::{self, Read}, - path::{Path, PathBuf}, -}; - -use crate::{ - BStr, BString, FullNameRef, PartialName, PartialNameRef, Reference, file, - name::is_pseudo_ref, - store_impl::{file::loose, packed}, -}; - -/// ### Finding References - notes about precomposed unicode. -/// -/// Generally, ref names and the target of symbolic refs are stored as-is if [`Self::precompose_unicode`] is `false`. -/// If `true`, refs are stored as precomposed unicode in `packed-refs`, but stored as is on disk as it is then assumed -/// to be indifferent, i.e. `"a\u{308}"` is the same as `"ä"`. -/// -/// This also means that when refs are packed for transmission to another machine, both their names and the target of -/// symbolic references need to be precomposed. -/// -/// Namespaces are left as is as they never get past the particular repository that uses them. -impl file::Store { - /// Find a single reference by the given `path` which is required to be a valid reference name. - /// - /// Returns `Ok(None)` if no such ref exists. - /// - /// ### Note - /// - /// * The lookup algorithm follows the one in [the git documentation][git-lookup-docs]. - /// * The packed buffer is checked for modifications each time the method is called. See [`file::Store::try_find_packed()`] - /// for a version with more control. - /// - /// [git-lookup-docs]: https://github.com/git/git/blob/5d5b1473453400224ebb126bf3947e0a3276bdf5/Documentation/revisions.txt#L34-L46 - pub fn try_find<'a, Name, E>(&self, partial: Name) -> Result> - where - Name: TryInto<&'a PartialNameRef, Error = E>, - std::result::Result<&'a PartialNameRef, E>: ResultExt, - { - let packed = self.assure_packed_refs_uptodate()?; - self.find_one_with_verified_input( - partial - .try_into() - .or_raise(|| message("The ref name or path is not a valid ref name"))?, - packed.as_ref().map(|b| &***b), - ) - } - - /// Like [`file::Store::try_find()`], returning `None` for a non-existing reference. - /// - /// Find only loose references, that is references that aren't in the packed-refs buffer. - /// All symbolic references are loose references. - /// `HEAD` is always a loose reference. - pub fn try_find_loose<'a, Name, E>(&self, partial: Name) -> Result> - where - Name: TryInto<&'a PartialNameRef, Error = E>, - std::result::Result<&'a PartialNameRef, E>: ResultExt, - { - self.find_one_with_verified_input( - partial - .try_into() - .or_raise(|| message("The ref name or path is not a valid ref name"))?, - None, - ) - .map(|r| r.map(Into::into)) - } - - /// Similar to [`file::Store::find()`], but allows to pass a snapshotted packed buffer instead. - pub fn try_find_packed<'a, Name, E>( - &self, - partial: Name, - packed: Option<&packed::Buffer>, - ) -> Result> - where - Name: TryInto<&'a PartialNameRef, Error = E>, - std::result::Result<&'a PartialNameRef, E>: ResultExt, - { - self.find_one_with_verified_input( - partial - .try_into() - .or_raise(|| message("The ref name or path is not a valid ref name"))?, - packed, - ) - } - - pub(crate) fn find_one_with_verified_input( - &self, - partial_name: &PartialNameRef, - packed: Option<&packed::Buffer>, - ) -> Result> { - let mut buf = BString::default(); - let mut precomposed_partial_name_storage = packed - .filter(|_| self.precompose_unicode) - .and_then(|_| { - use gix_object::bstr::ByteSlice; - let precomposed = partial_name.0.to_str().ok()?; - let precomposed = gix_utils::str::precompose_path(Path::new(precomposed).into()); - match precomposed { - Cow::Owned(precomposed) => { - Some(gix_path::into_bstr(precomposed).map(|name| PartialName(name.into_owned()))) - } - Cow::Borrowed(_) => None, - } - }) - .transpose()?; - let precomposed_partial_name = precomposed_partial_name_storage - .as_ref() - .map(std::convert::AsRef::as_ref); - for consider_pseudo_ref in [true, false] { - if !consider_pseudo_ref && !is_pseudo_ref(partial_name.as_bstr()) { - break; - } - 'try_directories: for inbetween in &["", "tags", "heads", "remotes"] { - match self.find_inner( - inbetween, - partial_name, - precomposed_partial_name, - packed, - &mut buf, - consider_pseudo_ref, - ) { - Ok(Some(r)) => return Ok(Some(r)), - Ok(None) => { - if consider_pseudo_ref && is_pseudo_ref(partial_name.as_bstr()) { - break 'try_directories; - } - continue; - } - Err(err) => return Err(err), - } - } - } - if partial_name.as_bstr() != "HEAD" { - if let Some(mut precomposed) = precomposed_partial_name_storage { - precomposed = precomposed.join("HEAD".into()).expect("HEAD is valid name"); - precomposed_partial_name_storage = Some(precomposed); - } - self.find_inner( - "remotes", - partial_name - .to_owned() - .join("HEAD".into()) - .expect("HEAD is valid name") - .as_ref(), - precomposed_partial_name_storage - .as_ref() - .map(std::convert::AsRef::as_ref), - None, - &mut buf, - true, /* consider-pseudo-ref */ - ) - } else { - Ok(None) - } - } - - /// Resolve and read a candidate. Read failures include [metadata](gix_error::Error::metadata()) `path` (native path), - /// the file that failed. - fn find_inner( - &self, - inbetween: &str, - partial_name: &PartialNameRef, - precomposed_partial_name: Option<&PartialNameRef>, - packed: Option<&packed::Buffer>, - path_buf: &mut BString, - consider_pseudo_ref: bool, - ) -> Result> { - let full_name = precomposed_partial_name - .unwrap_or(partial_name) - .construct_full_name_ref(inbetween, path_buf, consider_pseudo_ref); - // Canonicalization can turn a Kelvin sign into the ASCII name of a pseudo-ref. - let restore_spelling = precomposed_partial_name.is_some() - && !matches!( - full_name.category(), - Some( - crate::Category::PseudoRef - | crate::Category::MainPseudoRef - | crate::Category::LinkedPseudoRef { .. } - ) - ); - let content_buf = match self.ref_contents(full_name) { - Ok(content_buf) => content_buf, - Err(err) if err.kind() == io::ErrorKind::NotADirectory => return Ok(None), - Err(err) => { - return Err(err.and_raise(read_reference_error(self.reference_path(full_name)?))); - } - }; - - match content_buf { - None => { - if let Some(packed) = packed - && let Some(full_name) = packed::find::transform_full_name_for_lookup(full_name) - { - let full_name_backing; - let full_name = match &self.namespace { - Some(namespace) => { - full_name_backing = namespace.to_owned().into_namespaced_name(full_name); - full_name_backing.as_ref() - } - None => full_name, - }; - if let Some(packed_ref) = packed.try_find_full_name(full_name)? { - let mut res: Reference = packed_ref.into(); - if let Some(namespace) = &self.namespace { - res.strip_namespace(namespace); - } - if restore_spelling { - let original = - partial_name.construct_full_name_ref(inbetween, path_buf, consider_pseudo_ref); - res.name = packed::find::transform_full_name_for_lookup(original) - .expect("precomposition does not change the reference category") - .to_owned(); - } - return Ok(Some(res)); - } - } - Ok(None) - } - Some(content) => { - let full_name = if restore_spelling { - partial_name.construct_full_name_ref(inbetween, path_buf, consider_pseudo_ref) - } else { - full_name - }; - let relative_path = full_name.to_path()?; - Ok(Some( - loose::Reference::try_from_path(full_name.to_owned(), &content, self.object_hash) - .map(Into::into) - .map(|mut r: Reference| { - if let Some(namespace) = &self.namespace { - r.strip_namespace(namespace); - } - r - }) - .or_raise(|| ReferenceDecode { - relative_path: relative_path.to_owned(), - })?, - )) - } - } - } -} - -impl file::Store { - pub(crate) fn to_base_dir_and_relative_name<'a>( - &self, - name: &'a FullNameRef, - is_reflog: bool, - ) -> Result<(Cow<'_, Path>, &'a FullNameRef)> { - let commondir = self.common_dir_resolved(); - let linked_git_dir = |worktree_name: &BStr| -> Result { - Ok(commondir.join("worktrees").join(gix_path::from_bstr(worktree_name)?)) - }; - name.category_and_short_name().map_or_else( - || Ok((commondir.into(), name)), - |(c, sn)| { - use crate::Category::*; - let sn = FullNameRef::new_unchecked(sn); - Ok(match c { - LinkedPseudoRef { name: worktree_name } => { - if is_reflog { - (linked_git_dir(worktree_name)?.into(), sn) - } else { - (commondir.into(), name) - } - } - Tag | LocalBranch | RemoteBranch | Note => (commondir.into(), name), - MainRef | MainPseudoRef => (commondir.into(), sn), - LinkedRef { name: worktree_name } => { - if sn.category().is_some_and(|cat| cat.is_worktree_private()) { - if is_reflog { - (linked_git_dir(worktree_name)?.into(), sn) - } else { - (commondir.into(), name) - } - } else { - (commondir.into(), sn) - } - } - PseudoRef | Bisect | Rewritten | WorktreePrivate => (self.git_dir.as_path().into(), name), - }) - }, - ) - } - - /// Implements the logic required to transform a fully qualified refname into a filesystem path - pub(crate) fn reference_path_with_base<'b>(&self, name: &'b FullNameRef) -> Result<(Cow<'_, Path>, Cow<'b, Path>)> { - let (base, name) = self.to_base_dir_and_relative_name(name, false)?; - Ok(( - base, - match &self.namespace { - None => gix_path::to_native_path_on_windows(name.as_bstr()), - Some(namespace) => { - gix_path::to_native_path_on_windows(namespace.to_owned().into_namespaced_name(name).into_inner()) - } - }?, - )) - } - - /// Implements the logic required to transform a fully qualified refname into a filesystem path - pub(crate) fn reference_path(&self, name: &FullNameRef) -> Result { - let (base, relative_path) = self.reference_path_with_base(name)?; - Ok(base.join(relative_path)) - } - - /// If `prohibit_windows_device_names` is set, check that `name` does not - /// contain a path component that matches a reserved Windows device name. - pub(crate) fn check_windows_device_name(&self, name: &FullNameRef) -> io::Result<()> { - if !self.prohibit_windows_device_names { - return Ok(()); - } - let (_, relative_path) = self - .reference_path_with_base(name) - .map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?; - if relative_path - .components() - .filter_map(|c| gix_path::try_os_str_into_bstr(c.as_os_str().into()).ok()) - .any(|c| gix_validate::path::component_is_windows_device(c.as_ref())) - { - Err(std::io::Error::other(format!( - "Illegal use of reserved Windows device name in \"{}\"", - name.as_bstr() - ))) - } else { - Ok(()) - } - } - - /// Read the file contents with a verified full reference path and return it in the given vector if possible. - pub(crate) fn ref_contents(&self, name: &FullNameRef) -> io::Result>> { - self.check_windows_device_name(name)?; - let (base, relative_path) = self - .reference_path_with_base(name) - .map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?; - let ref_path = base.join(&relative_path); - match std::fs::File::open(&ref_path) { - Ok(mut file) => { - let mut buf = Vec::with_capacity(128); - if let Err(err) = file.read_to_end(&mut buf) { - return if ref_path.is_dir() { Ok(None) } else { Err(err) }; - } - Ok(buf.into()) - } - Err(err) if err.kind() == io::ErrorKind::NotFound => { - #[cfg(windows)] - if path_has_file_prefix(base.as_ref(), relative_path.as_ref()) { - return Err(io::Error::new(io::ErrorKind::NotADirectory, err)); - } - Ok(None) - } - #[cfg(windows)] - Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => { - if path_has_file_prefix(base.as_ref(), relative_path.as_ref()) { - Err(io::Error::new(io::ErrorKind::NotADirectory, err)) - } else { - Ok(None) - } - } - Err(err) => Err(err), - } - } -} - -#[cfg(windows)] -fn path_has_file_prefix(base: &Path, relative_path: &Path) -> bool { - let mut path = base.to_owned(); - let mut components = relative_path.components().peekable(); - while let Some(component) = components.next() { - if components.peek().is_none() { - break; - } - path.push(component.as_os_str()); - match std::fs::metadata(&path) { - Ok(metadata) if metadata.is_file() => return true, - Ok(_) => {} - Err(err) if err.kind() == io::ErrorKind::NotFound => return false, - Err(_) => {} - } - } - false -} - -impl file::Store { - /// Similar to [`file::Store::try_find()`] but a non-existing ref is treated as error. - pub fn find<'a, Name, E>(&self, partial: Name) -> Result - where - Name: TryInto<&'a PartialNameRef, Error = E>, - std::result::Result<&'a PartialNameRef, E>: ResultExt, - { - let packed = self.assure_packed_refs_uptodate()?; - self.find_existing_inner(partial, packed.as_ref().map(|b| &***b)) - } - - /// Similar to [`file::Store::find()`], but supports a stable packed buffer. - pub fn find_packed<'a, Name, E>(&self, partial: Name, packed: Option<&packed::Buffer>) -> Result - where - Name: TryInto<&'a PartialNameRef, Error = E>, - std::result::Result<&'a PartialNameRef, E>: ResultExt, - { - self.find_existing_inner(partial, packed) - } - - /// Similar to [`file::Store::find()`] won't handle packed-refs. - pub fn find_loose<'a, Name, E>(&self, partial: Name) -> Result - where - Name: TryInto<&'a PartialNameRef, Error = E>, - std::result::Result<&'a PartialNameRef, E>: ResultExt, - { - self.find_existing_inner(partial, None).map(Into::into) - } - - /// Similar to [`file::Store::find()`] but a non-existing ref is treated as error. - pub(crate) fn find_existing_inner<'a, Name, E>( - &self, - partial: Name, - packed: Option<&packed::Buffer>, - ) -> Result - where - Name: TryInto<&'a PartialNameRef, Error = E>, - std::result::Result<&'a PartialNameRef, E>: ResultExt, - { - let path = partial - .try_into() - .or_raise(|| message("The ref name or path is not a valid ref name"))?; - match self.find_one_with_verified_input(path, packed) { - Ok(Some(r)) => Ok(r), - Ok(None) => Err(NotFound { - name: path.as_bstr().to_owned(), - } - .raise()), - Err(err) => Err(err), - } - } -} - -/// The raised error's [metadata](gix_error::Error::metadata()) `path` (native path) identifies the reference file that -/// could not be read. -pub(super) fn read_reference_error(path: impl Into) -> Message { - Message::new("Could not read reference").with("path", path.into()) -} - -/// A reference lookup found no matching name, including a missing symbolic referent. -#[derive(Debug)] -pub struct NotFound { - /// The name whose lookup failed. It may have been discovered while following symbolic references. - pub name: BString, -} - -impl std::fmt::Display for NotFound { - #[allow(clippy::unnecessary_debug_formatting)] - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "The ref partially named {:?} could not be found", self.name) - } -} - -impl std::error::Error for NotFound { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - Some(const { &gix_error::ClassificationMarker::NOT_FOUND }) - } -} - -/// A loose reference was found at this path, but its contents could not be decoded. -/// The decoding error is retained as a cause in the exception. -#[derive(Debug)] -pub struct ReferenceDecode { - /// The resolved reference path, relative to the Git directory. - pub relative_path: PathBuf, -} - -impl std::fmt::Display for ReferenceDecode { - #[allow(clippy::unnecessary_debug_formatting)] - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "The reference at {:?} could not be decoded", self.relative_path) - } -} - -impl std::error::Error for ReferenceDecode {} diff --git a/vendor/gix-ref/src/store/file/log/iter.rs b/vendor/gix-ref/src/store/file/log/iter.rs deleted file mode 100644 index dd2b211..0000000 --- a/vendor/gix-ref/src/store/file/log/iter.rs +++ /dev/null @@ -1,196 +0,0 @@ -use gix_error::Result; -use gix_error::{ErrorExt, Message, ResultExt}; - -use gix_object::bstr::ByteSlice; - -use crate::{FullNameRef, file, store_impl::file::log}; - -/// Returns a forward iterator over the given `lines`, starting from the first line in the file and ending at the last. -/// -/// Note that `lines` are an entire reflog file. -/// -/// This iterator is useful when the ref log file is going to be rewritten which forces processing of the entire file. -/// It will continue parsing even if individual log entries failed to parse, leaving it to the driver to decide whether to -/// abort or continue. -pub fn forward(lines: &[u8]) -> Forward<'_> { - Forward { - inner: lines.as_bstr().lines().enumerate(), - } -} - -/// An iterator yielding parsed lines in a file from start to end, oldest to newest. -pub struct Forward<'a> { - inner: std::iter::Enumerate>, -} - -impl<'a> Iterator for Forward<'a> { - type Item = Result>; - - /// Decode failures include [metadata](gix_error::Error::metadata()) `line` (one-based position) and `from_end` - /// (whether counting from the end). - fn next(&mut self) -> Option { - self.inner - .next() - .map(|(ln, line)| log::LineRef::from_bytes(line).or_raise(|| invalid_reflog_entry(ln + 1, false))) - } -} - -/// A platform to store a buffer to hold ref log lines for iteration. -#[must_use = "Iterators should be obtained from this platform"] -pub struct Platform<'a, 's> { - /// The store containing the reflogs - pub store: &'s file::Store, - /// The full name of the reference whose reflog to retrieve. - pub name: &'a FullNameRef, - /// A reusable buffer for storing log lines read from disk. - pub buf: Vec, -} - -impl Platform<'_, '_> { - /// Return a forward iterator over all log-lines, most recent to oldest. - pub fn rev(&mut self) -> std::io::Result>> { - self.buf.clear(); - self.buf.resize(1024 * 4, 0); - let path = self - .store - .reflog_path(self.name) - .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidInput, err))?; - self.store.reflog_iter_rev_inner(&path, &mut self.buf) - } - - /// Return a forward iterator over all log-lines, oldest to most recent. - pub fn all(&mut self) -> std::io::Result>> { - self.buf.clear(); - let path = self - .store - .reflog_path(self.name) - .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidInput, err))?; - self.store.reflog_iter_inner(&path, &mut self.buf) - } -} - -/// An iterator yielding parsed lines in a file in reverse, most recent to oldest. -pub struct Reverse<'a, F> { - buf: &'a mut [u8], - count: usize, - read_and_pos: Option<(F, u64)>, - last_nl_pos: Option, -} - -/// An iterator over entries of the `log` file in reverse, using `buf` as sliding window. -/// -/// Note that `buf` must be big enough to capture typical line length or else partial lines will be parsed and probably fail -/// in the process. -/// -/// This iterator is very expensive in terms of I/O operations and shouldn't be used to read more than the last few entries of the log. -/// Use a forward iterator instead for these cases. -/// -/// It will continue parsing even if individual log entries failed to parse, leaving it to the driver to decide whether to -/// abort or continue. -pub fn reverse(mut log: F, buf: &mut [u8]) -> std::io::Result> -where - F: std::io::Read + std::io::Seek, -{ - let pos = log.seek(std::io::SeekFrom::End(0))?; - if buf.is_empty() { - return Err(std::io::Error::other( - "Zero sized buffers are not allowed, use 256 bytes or more for typical logs", - )); - } - Ok(Reverse { - buf, - count: 0, - read_and_pos: Some((log, pos)), - last_nl_pos: None, - }) -} - -impl Iterator for Reverse<'_, F> -where - F: std::io::Read + std::io::Seek, -{ - type Item = Result; - - /// Decode failures include [metadata](gix_error::Error::metadata()) `line` (one-based position) and `from_end` - /// (whether counting from the end). - fn next(&mut self) -> Option { - match (self.last_nl_pos.take(), self.read_and_pos.take()) { - // Initial state - load first data block - (None, Some((mut read, pos))) => { - let npos = pos.saturating_sub(self.buf.len() as u64); - if let Err(err) = read.seek(std::io::SeekFrom::Start(npos)) { - return Some(Err(err.raise())); - } - - let n = (pos - npos) as usize; - if n == 0 { - return None; - } - let buf = &mut self.buf[..n]; - if let Err(err) = read.read_exact(buf) { - return Some(Err(err.raise())); - } - - let last_byte = *buf.last().expect("we have read non-zero bytes before"); - self.last_nl_pos = Some(if last_byte != b'\n' { buf.len() } else { buf.len() - 1 }); - self.read_and_pos = Some((read, npos)); - self.next() - } - // Has data block and can extract lines from it, load new blocks as needed - (Some(end), Some(read_and_pos)) => match self.buf[..end].rfind_byte(b'\n') { - Some(start) => { - self.read_and_pos = Some(read_and_pos); - self.last_nl_pos = Some(start); - let buf = &self.buf[start + 1..end]; - let res = Some( - log::LineRef::from_bytes(buf) - .or_raise(|| invalid_reflog_entry(self.count + 1, true)) - .map(Into::into), - ); - self.count += 1; - res - } - None => { - let (mut read, last_read_pos) = read_and_pos; - if last_read_pos == 0 { - let buf = &self.buf[..end]; - Some( - log::LineRef::from_bytes(buf) - .or_raise(|| invalid_reflog_entry(self.count + 1, true)) - .map(Into::into), - ) - } else { - let npos = last_read_pos.saturating_sub((self.buf.len() - end) as u64); - if npos == last_read_pos { - return Some(Err(std::io::Error::other(format!( - "buffer too small for line size, got until {:?}", - self.buf.as_bstr() - )) - .raise())); - } - let n = (last_read_pos - npos) as usize; - self.buf.copy_within(0..end, n); - if let Err(err) = read.seek(std::io::SeekFrom::Start(npos)) { - return Some(Err(err.raise())); - } - if let Err(err) = read.read_exact(&mut self.buf[..n]) { - return Some(Err(err.raise())); - } - self.read_and_pos = Some((read, npos)); - self.last_nl_pos = Some(n + end); - self.next() - } - } - }, - // depleted - (None, None) => None, - (Some(_), None) => unreachable!("BUG: Invalid state: we never discard only our file, always both."), - } - } -} - -fn invalid_reflog_entry(line: usize, from_end: bool) -> Message { - Message::new("Invalid reflog entry") - .with("line", line) - .with("from_end", from_end) -} diff --git a/vendor/gix-ref/src/store/file/log/line.rs b/vendor/gix-ref/src/store/file/log/line.rs deleted file mode 100644 index 859253d..0000000 --- a/vendor/gix-ref/src/store/file/log/line.rs +++ /dev/null @@ -1,201 +0,0 @@ -use gix_hash::ObjectId; - -use crate::{log::Line, store_impl::file::log::LineRef}; - -impl LineRef<'_> { - /// Convert this instance into its mutable counterpart - pub fn to_owned(&self) -> Line { - (*self).into() - } -} - -mod write { - use gix_error::{Result, bail}; - use std::io; - - use gix_object::bstr::{BStr, ByteSlice}; - - use crate::log::Line; - - /// Output - impl Line { - /// Serialize this instance to `out` in the git serialization format for ref log lines. - pub fn write_to(&self, out: &mut dyn io::Write) -> io::Result<()> { - write!(out, "{} {} ", self.previous_oid, self.new_oid)?; - self.signature.write_to(out)?; - writeln!( - out, - "\t{}", - check_newlines(self.message.as_ref()).map_err(io::Error::other)? - ) - } - } - - fn check_newlines(input: &BStr) -> Result<&BStr> { - if input.find_byte(b'\n').is_some() { - bail!(gix_error::validation(r"Messages must not contain newlines (\n)")); - } - Ok(input) - } -} - -impl LineRef<'_> { - /// The previous object id of the ref. It will be a null hash if there was no previous id as - /// this ref is being created. - pub fn previous_oid(&self) -> ObjectId { - ObjectId::from_hex(self.previous_oid).expect("parse validation") - } - /// The new object id of the ref, or a null hash if it is removed. - pub fn new_oid(&self) -> ObjectId { - ObjectId::from_hex(self.new_oid).expect("parse validation") - } -} - -impl<'a> From> for Line { - fn from(v: LineRef<'a>) -> Self { - Line { - previous_oid: v.previous_oid(), - new_oid: v.new_oid(), - signature: v.signature.into(), - message: v.message.into(), - } - } -} - -mod decode { - use gix_error::{ErrorExt, Message, OptionExt, Result, ResultExt, corruption, ensure}; - use gix_object::bstr::{BStr, ByteSlice}; - - use crate::{file::log::LineRef, parse::hex_hash_any}; - - impl<'a> LineRef<'a> { - /// Decode a reflog line from the given bytes. - /// - /// Valid input starts with the previous object id, the new object id, a - /// signature, and an optional tab-separated message, for example: - /// - /// `0123456789012345678901234567890123456789 89abcdef89abcdef89abcdef89abcdef89abcdef Name 1700000000 +0000\tmessage` - /// - /// Errors include [metadata](gix_error::Error::metadata()) `input` (bytes), the first input line without its - /// trailing newline. - pub fn from_bytes(input: &'a [u8]) -> Result> { - decode(input).or_raise(|| Message::new("Could not decode reflog line").with_input(first_line(input))) - } - } - - /// Return the first line from `input`, without its trailing newline. - /// - /// If `input` contains no newline, all of `input` is returned. - fn first_line(input: &[u8]) -> &[u8] { - let line_end = input.iter().position(|b| *b == b'\n').unwrap_or(input.len()); - &input[..line_end] - } - - /// Parse one reflog line from `bytes`. - /// - /// Only one line is parsed; any bytes after the first newline are - /// ignored. If the line has no tab separator, the message is empty. - /// - /// Return an error if the first line does not match the reflog line - /// format. - fn decode(bytes: &[u8]) -> Result> { - let invalid = || corruption("Malformed reflog line"); - let line = first_line(bytes); - let (mut head, message) = match line.find_byte(b'\t') { - Some(tab) => (&line[..tab], line[tab + 1..].as_bstr()), - None => (line, BStr::new(b"")), - }; - - let old = hex_hash_any(&mut head).map_err(|()| invalid().raise())?; - head = head.strip_prefix(b" ").ok_or_raise(invalid)?; - let new = hex_hash_any(&mut head).map_err(|()| invalid().raise())?; - head = head.strip_prefix(b" ").ok_or_raise(invalid)?; - let signature = gix_actor::signature::decode(&mut head).or_raise(|| corruption("Invalid reflog signature"))?; - ensure!(head.is_empty(), invalid()); - Ok(LineRef { - previous_oid: old, - new_oid: new, - signature, - message, - }) - } - - #[cfg(test)] - mod test_decode { - use super::*; - - fn with_newline(mut v: Vec) -> Vec { - v.push(b'\n'); - v - } - - mod invalid { - use super::decode; - - #[test] - fn completely_bogus_shows_error_with_context() { - let input = b"definitely not a log entry".as_slice(); - decode(input).expect_err("this should fail"); - } - - #[test] - fn missing_whitespace_between_signature_and_message() { - let line = "0000000000000000000000000000000000000000 0000000000000000000000000000000000000000 one 1234567890 -0000message"; - decode(line.as_bytes()).expect_err("this should fail"); - } - } - - const NULL_SHA1: &[u8] = b"0000000000000000000000000000000000000000"; - - #[test] - fn entry_with_empty_message() { - let line_without_nl: Vec<_> = b"0000000000000000000000000000000000000000 0000000000000000000000000000000000000000 name 1234567890 -0000".to_vec(); - let line_with_nl = with_newline(line_without_nl.clone()); - for input in &[line_without_nl, line_with_nl] { - assert_eq!( - decode(input.as_slice()).expect("successful parsing"), - LineRef { - previous_oid: NULL_SHA1.as_bstr(), - new_oid: NULL_SHA1.as_bstr(), - signature: gix_actor::SignatureRef { - name: b"name".as_bstr(), - email: b"foo@example.com".as_bstr(), - time: "1234567890 -0000" - }, - message: b"".as_bstr(), - } - ); - } - } - - #[test] - fn entry_with_message_without_newline_and_with_newline() { - let line_without_nl: Vec<_> = b"a5828ae6b52137b913b978e16cd2334482eb4c1f 89b43f80a514aee58b662ad606e6352e03eaeee4 Sebastian Thiel 1618030561 +0800\tpull --ff-only: Fast-forward".to_vec(); - let line_with_nl = with_newline(line_without_nl.clone()); - - for input in &[line_without_nl, line_with_nl] { - let res = decode(input.as_slice()).expect("successful parsing"); - let actual = LineRef { - previous_oid: b"a5828ae6b52137b913b978e16cd2334482eb4c1f".as_bstr(), - new_oid: b"89b43f80a514aee58b662ad606e6352e03eaeee4".as_bstr(), - signature: gix_actor::SignatureRef { - name: b"Sebastian Thiel".as_bstr(), - email: b"foo@example.com".as_bstr(), - time: "1618030561 +0800", - }, - message: b"pull --ff-only: Fast-forward".as_bstr(), - }; - assert_eq!(res, actual); - assert_eq!(actual.previous_oid(), "a5828ae6b52137b913b978e16cd2334482eb4c1f"); - assert_eq!(actual.new_oid(), "89b43f80a514aee58b662ad606e6352e03eaeee4"); - } - } - - #[test] - fn two_lines_in_a_row_with_and_without_newline() { - let lines = b"0000000000000000000000000000000000000000 0000000000000000000000000000000000000000 one 1234567890 -0000\t\n0000000000000000000000000000000000000000 0000000000000000000000000000000000000000 two 1234567890 -0000\thello"; - let parsed = decode(lines.as_slice()).expect("parse single line"); - assert_eq!(parsed.message, b"".as_bstr(), "first message is empty"); - } - } -} diff --git a/vendor/gix-ref/src/store/file/log/mod.rs b/vendor/gix-ref/src/store/file/log/mod.rs deleted file mode 100644 index da583da..0000000 --- a/vendor/gix-ref/src/store/file/log/mod.rs +++ /dev/null @@ -1,22 +0,0 @@ -use gix_object::bstr::BStr; - -pub use super::loose::reflog::create_or_update; - -/// -pub mod iter; -mod line; - -/// A parsed ref log line. -#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct LineRef<'a> { - /// The previous object id in hexadecimal. Use [`LineRef::previous_oid()`] to get a more usable form. - pub previous_oid: &'a BStr, - /// The new object id in hexadecimal. Use [`LineRef::new_oid()`] to get a more usable form. - pub new_oid: &'a BStr, - /// The signature of the currently configured committer. - #[cfg_attr(feature = "serde", serde(borrow))] - pub signature: gix_actor::SignatureRef<'a>, - /// The message providing details about the operation performed in this log line. - pub message: &'a BStr, -} diff --git a/vendor/gix-ref/src/store/file/loose/iter.rs b/vendor/gix-ref/src/store/file/loose/iter.rs deleted file mode 100644 index 7259dd7..0000000 --- a/vendor/gix-ref/src/store/file/loose/iter.rs +++ /dev/null @@ -1,105 +0,0 @@ -use std::path::{Path, PathBuf}; - -use gix_fs::walkdir::DirEntryIter; -use gix_object::bstr::ByteSlice; -use gix_path::RelativePath; - -use crate::{BString, FullName, file::iter::LooseThenPacked, store_impl::file}; - -/// An iterator over all valid loose reference paths as seen from a particular base directory. -pub(in crate::store_impl::file) struct SortedLoosePaths { - pub(crate) base: PathBuf, - /// An prefix like `refs/heads/foo/` or `refs/heads/prefix` that a returned reference must match against.. - prefix: Option, - /// A suffix like `HEAD` that a returned reference must match against.. - suffix: Option, - file_walk: Option, -} - -impl SortedLoosePaths { - pub fn at( - path: &Path, - base: PathBuf, - prefix: Option, - suffix: Option, - precompose_unicode: bool, - ) -> Self { - let depth = if suffix.is_some() { 1 } else { usize::MAX }; - SortedLoosePaths { - base, - prefix, - suffix, - file_walk: path.is_dir().then(|| { - // serial iteration as we expect most refs in packed-refs anyway. - gix_fs::walkdir_sorted_new(path, depth, precompose_unicode).into_iter() - }), - } - } -} - -impl Iterator for SortedLoosePaths { - type Item = std::io::Result<(PathBuf, FullName)>; - - fn next(&mut self) -> Option { - for entry in self.file_walk.as_mut()?.by_ref() { - match entry { - Ok(entry) => { - if !entry.file_type().is_ok_and(|ft| ft.is_file()) { - continue; - } - let full_path = entry.path().into_owned(); - let full_name = full_path - .strip_prefix(&self.base) - .expect("prefix-stripping cannot fail as base is within our root"); - let Ok(full_name) = gix_path::into_bstr(full_name) - .map(|name| gix_path::to_unix_separators_on_windows(name).into_owned()) - else { - continue; - }; - if let Some(prefix) = &self.prefix - && !full_name.starts_with(prefix) - { - continue; - } - if let Some(suffix) = &self.suffix - && !full_name.ends_with(suffix) - { - continue; - } - if gix_validate::reference::name_partial(full_name.as_bstr()).is_ok() { - let name = FullName(full_name); - return Some(Ok((full_path, name))); - } else { - continue; - } - } - Err(err) => return Some(Err(err.into_io_error().expect("no symlink related errors"))), - } - } - None - } -} - -impl file::Store { - /// Return an iterator over all loose references, notably not including any packed ones, in lexical order. - /// Each of the references may fail to parse and the iterator will not stop if parsing fails, allowing the caller - /// to see all files that look like references whether valid or not. - /// - /// Reference files that do not constitute valid names will be silently ignored. - pub fn loose_iter(&self) -> std::io::Result> { - self.iter_packed(None) - } - - /// Return an iterator over all loose references that start with the given `prefix`. - /// - /// Otherwise, it's similar to [`loose_iter()`](file::Store::loose_iter()). - /// - /// Note that if a prefix isn't using a trailing `/`, like in `refs/heads/foo`, it will effectively - /// start the traversal in the parent directory, e.g. `refs/heads/` and list everything inside that - /// starts with `foo`, like `refs/heads/foo` and `refs/heads/foobar`. - /// - /// Prefixes are relative paths with slash-separated components. - pub fn loose_iter_prefixed(&self, prefix: &RelativePath) -> std::io::Result> { - self.iter_prefixed_packed(prefix, None) - } -} diff --git a/vendor/gix-ref/src/store/file/loose/mod.rs b/vendor/gix-ref/src/store/file/loose/mod.rs deleted file mode 100644 index a2d68da..0000000 --- a/vendor/gix-ref/src/store/file/loose/mod.rs +++ /dev/null @@ -1,113 +0,0 @@ -use crate::{FullName, Kind, Target}; - -/// A git _ref_ which is stored in a file. -#[derive(Debug, PartialOrd, PartialEq, Ord, Eq, Hash, Clone)] -pub struct Reference { - /// The path to uniquely identify this ref within its store. - pub name: FullName, - /// The target of the reference, either a symbolic reference by full name or an object by its id. - pub target: Target, -} - -impl Reference { - /// Return the kind of ref. - pub fn kind(&self) -> Kind { - self.target.kind() - } -} - -/// -pub(crate) mod reflog; - -/// -pub(crate) mod iter; -/// -pub mod reference; - -mod init { - use std::path::PathBuf; - - use crate::store_impl::file; - - impl file::Store { - /// Create a new instance at the given `git_dir`, which commonly is a standard git repository with a - /// `refs/` subdirectory. - /// Use [`at_opts()`](Self::at_opts) to adjust options. - /// - /// Note that if [`precompose_unicode`](crate::store::init::Options::precompose_unicode) is set in the options, - /// the `git_dir` is also expected to use precomposed unicode, or else some operations that strip prefixes will fail. - pub fn at(git_dir: PathBuf, object_hash: gix_hash::Kind) -> Self { - Self::at_opts(git_dir, object_hash, Default::default()) - } - - /// Create a new instance at the given `git_dir`, which commonly is a standard git repository with a - /// `refs/` subdirectory. - /// Use [`Options`](crate::store::init::Options) to adjust settings. - /// - /// Note that if [`precompose_unicode`](crate::store::init::Options::precompose_unicode) is set in the options, - /// the `git_dir` is also expected to use precomposed unicode, or else some operations that strip prefixes will fail. - pub fn at_opts( - git_dir: PathBuf, - object_hash: gix_hash::Kind, - crate::store::init::Options { - write_reflog, - precompose_unicode, - prohibit_windows_device_names, - }: crate::store::init::Options, - ) -> Self { - file::Store { - git_dir, - packed_buffer_mmap_threshold: packed_refs_mmap_threshold(), - common_dir: None, - write_reflog, - namespace: None, - prohibit_windows_device_names, - packed: gix_fs::SharedFileSnapshotMut::new().into(), - object_hash, - precompose_unicode, - } - } - - /// Like [`at()`][file::Store::at()], but for _linked_ work-trees which use `git_dir` as private ref store and `common_dir` for - /// shared references. - /// Use [`for_linked_worktree_opts()`](Self::for_linked_worktree_opts) to adjust options. - /// - /// Note that if [`precompose_unicode`](crate::store::init::Options::precompose_unicode) is set, the `git_dir` and - /// `common_dir` are also expected to use precomposed unicode, or else some operations that strip prefixes will fail. - pub fn for_linked_worktree(git_dir: PathBuf, common_dir: PathBuf, object_hash: gix_hash::Kind) -> Self { - Self::for_linked_worktree_opts(git_dir, common_dir, object_hash, Default::default()) - } - - /// Like [`at_opts()`][Self::at_opts()], but for _linked_ work-trees which use `git_dir` as private ref store and `common_dir` for - /// shared references. - /// - /// Note that if [`precompose_unicode`](crate::store::init::Options::precompose_unicode) is set, the `git_dir` and - /// `common_dir` are also expected to use precomposed unicode, or else some operations that strip prefixes will fail. - pub fn for_linked_worktree_opts( - git_dir: PathBuf, - common_dir: PathBuf, - object_hash: gix_hash::Kind, - crate::store::init::Options { - write_reflog, - precompose_unicode, - prohibit_windows_device_names, - }: crate::store::init::Options, - ) -> Self { - file::Store { - git_dir, - packed_buffer_mmap_threshold: packed_refs_mmap_threshold(), - common_dir: Some(common_dir), - write_reflog, - namespace: None, - prohibit_windows_device_names, - packed: gix_fs::SharedFileSnapshotMut::new().into(), - object_hash, - precompose_unicode, - } - } - } - - fn packed_refs_mmap_threshold() -> u64 { - if cfg!(windows) { u64::MAX } else { 32 * 1024 } - } -} diff --git a/vendor/gix-ref/src/store/file/loose/reference/decode.rs b/vendor/gix-ref/src/store/file/loose/reference/decode.rs deleted file mode 100644 index dab154a..0000000 --- a/vendor/gix-ref/src/store/file/loose/reference/decode.rs +++ /dev/null @@ -1,84 +0,0 @@ -use gix_error::{ErrorExt, Message, Result, ResultExt, bail}; - -use gix_hash::ObjectId; -use gix_object::bstr::BString; - -use crate::{FullName, Target, parse::hex_hash, store_impl::file::loose::Reference}; - -enum MaybeUnsafeState { - Id(ObjectId), - UnvalidatedPath(BString), -} - -impl TryFrom for Target { - type Error = gix_error::Error; - - /// Invalid symbolic targets include [metadata](gix_error::Error::metadata()) `target` (bytes), as read from the - /// reference contents. - fn try_from(v: MaybeUnsafeState) -> Result { - Ok(match v { - MaybeUnsafeState::Id(id) => Target::Object(id), - MaybeUnsafeState::UnvalidatedPath(name) => { - Target::Symbolic(match gix_validate::reference::name(name.as_ref()) { - Ok(_) => FullName(name), - Err(_) if name == "refs/heads/.invalid" => { - bail!(gix_error::unsupported( - "This reference uses an unsupported storage backend, such as reftable" - )); - } - Err(err) => { - bail!(err.and_raise(Message::new("Invalid symbolic reference target").with("target", name))); - } - }) - } - }) - } -} - -impl Reference { - /// Create a new reference named `name` from the loose reference file contents in `path_contents`, - /// parsing object ids as `object_hash`. - /// - /// Errors include [metadata](gix_error::Error::metadata()) `input` (bytes), the supplied reference contents. - pub fn try_from_path(name: FullName, path_contents: &[u8], object_hash: gix_hash::Kind) -> Result { - Ok(Reference { - name, - target: Target::try_from(parse(path_contents, object_hash).map_err(|()| { - gix_error::corruption("Reference content could not be parsed").with_input(path_contents) - })?) - .or_raise(|| Message::new("Could not decode reference").with_input(path_contents))?, - }) - } -} - -/// Parse the contents of a loose reference file. -/// -/// A *symbolic* reference starts with `ref: `, may have additional spaces before -/// the path, and returns [`MaybeUnsafeState::UnvalidatedPath`] with the path -/// bytes up to the next NUL byte (just like Git), line ending, or the end of input. The path -/// is validated later when it is converted into a [`Target`]. -/// -/// A *direct* reference starts with a hexadecimal object id and returns -/// [`MaybeUnsafeState::Id`]. -/// -/// If neither reference form can be parsed, an error is returned. -fn parse(mut i: &[u8], object_hash: gix_hash::Kind) -> std::result::Result { - if let Some(rest) = i.strip_prefix(b"ref: ") { - i = rest; - while i.first() == Some(&b' ') { - i = &i[1..]; - } - let path_end = i - .iter() - .position(|b| *b == b'\0' || *b == b'\r' || *b == b'\n') - .unwrap_or(i.len()); - let path = i[..path_end].into(); - Ok(MaybeUnsafeState::UnvalidatedPath(path)) - } else { - let hex = hex_hash(&mut i, object_hash)?; - if i.first().is_some_and(u8::is_ascii_hexdigit) { - return Err(()); - } - Ok(MaybeUnsafeState::Id(ObjectId::from_hex(hex).expect("prior validation"))) - } -} diff --git a/vendor/gix-ref/src/store/file/loose/reference/logiter.rs b/vendor/gix-ref/src/store/file/loose/reference/logiter.rs deleted file mode 100644 index 0b762ec..0000000 --- a/vendor/gix-ref/src/store/file/loose/reference/logiter.rs +++ /dev/null @@ -1,45 +0,0 @@ -use crate::store_impl::{ - file, - file::{log, loose::Reference}, -}; -use gix_error::Result; - -impl Reference { - /// Returns true if a reflog exists in the given `store`. - /// - /// Please note that this method shouldn't be used to check if a log exists before trying to read it, but instead - /// is meant to be the fastest possible way to determine if a log exists or not. - /// If the caller needs to know if it's readable, try to read the log instead with a reverse or forward iterator. - pub fn log_exists(&self, store: &file::Store) -> Result { - store.reflog_exists(self.name.as_ref()) - } - /// Return a reflog reverse iterator for this ref, reading chunks from the back into the fixed buffer `buf`, in the given `store`. - /// - /// The iterator will traverse log entries from most recent to oldest, reading the underlying file in chunks from the back. - /// Return `Ok(None)` if no reflog exists. - pub fn log_iter_rev<'b>( - &self, - store: &file::Store, - buf: &'b mut [u8], - ) -> std::io::Result>> { - let path = store - .reflog_path(self.name.as_ref()) - .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidInput, err))?; - store.reflog_iter_rev_inner(&path, buf) - } - - /// Return a reflog forward iterator for this ref and write its file contents into `buf`, in the given `store`. - /// - /// The iterator will traverse log entries from oldest to newest. - /// Return `Ok(None)` if no reflog exists. - pub fn log_iter<'a, 'b: 'a>( - &'a self, - store: &file::Store, - buf: &'b mut Vec, - ) -> std::io::Result>> + 'a>> { - let path = store - .reflog_path(self.name.as_ref()) - .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidInput, err))?; - store.reflog_iter_inner(&path, buf) - } -} diff --git a/vendor/gix-ref/src/store/file/loose/reference/mod.rs b/vendor/gix-ref/src/store/file/loose/reference/mod.rs deleted file mode 100644 index 3e5ce06..0000000 --- a/vendor/gix-ref/src/store/file/loose/reference/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub(crate) mod logiter; - -/// -pub mod decode; diff --git a/vendor/gix-ref/src/store/file/loose/reflog.rs b/vendor/gix-ref/src/store/file/loose/reflog.rs deleted file mode 100644 index 01d78ab..0000000 --- a/vendor/gix-ref/src/store/file/loose/reflog.rs +++ /dev/null @@ -1,262 +0,0 @@ -use gix_error::Result; -use gix_error::{Message, ResultExt, message}; - -use std::{ - io::Read, - path::{Path, PathBuf}, -}; - -use crate::{ - FullNameRef, - store_impl::{file, file::log}, -}; - -impl file::Store { - /// Returns true if a reflog exists for the given reference `name`. - /// - /// Please note that this method shouldn't be used to check if a log exists before trying to read it, but instead - /// is meant to be the fastest possible way to determine if a log exists or not. - /// If the caller needs to know if it's readable, try to read the log instead with a reverse or forward iterator. - pub fn reflog_exists<'a, Name, E>(&self, name: Name) -> Result - where - Name: TryInto<&'a FullNameRef, Error = E>, - std::result::Result<&'a FullNameRef, E>: ResultExt, - { - let name = name - .try_into() - .or_raise(|| message("The reflog name or path is not a valid ref name"))?; - Ok(self.reflog_path(name)?.is_file()) - } - - /// Return a reflog reverse iterator for the given fully qualified `name`, reading chunks from the back into the fixed buffer `buf`. - /// - /// The iterator will traverse log entries from most recent to oldest, reading the underlying file in chunks from the back. - /// Return `Ok(None)` if no reflog exists. - /// - /// Read failures include [metadata](gix_error::Error::metadata()) `path` (native path), the resolved reflog path. - pub fn reflog_iter_rev<'a, 'b, Name, E>( - &self, - name: Name, - buf: &'b mut [u8], - ) -> Result>> - where - Name: TryInto<&'a FullNameRef, Error = E>, - std::result::Result<&'a FullNameRef, E>: ResultExt, - { - let name = name - .try_into() - .or_raise(|| message("The reflog name or path is not a valid ref name"))?; - let path = self.reflog_path(name)?; - self.reflog_iter_rev_inner(&path, buf) - .or_raise(|| read_reflog_error(path)) - } - - pub(crate) fn reflog_iter_rev_inner<'b>( - &self, - path: &Path, - buf: &'b mut [u8], - ) -> std::io::Result>> { - if path.is_dir() { - return Ok(None); - } - match std::fs::File::open(path) { - Ok(file) => Ok(Some(log::iter::reverse(file, buf)?)), - Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None), - Err(err) => Err(err), - } - } - - /// Return a reflog forward iterator for the given fully qualified `name` and write its file contents into `buf`. - /// - /// The iterator will traverse log entries from oldest to newest. - /// Return `Ok(None)` if no reflog exists. - /// - /// Read failures include [metadata](gix_error::Error::metadata()) `path` (native path), the resolved reflog path. - pub fn reflog_iter<'a, 'b, Name, E>( - &self, - name: Name, - buf: &'b mut Vec, - ) -> Result>> - where - Name: TryInto<&'a FullNameRef, Error = E>, - std::result::Result<&'a FullNameRef, E>: ResultExt, - { - let name = name - .try_into() - .or_raise(|| message("The reflog name or path is not a valid ref name"))?; - let path = self.reflog_path(name)?; - self.reflog_iter_inner(&path, buf).or_raise(|| read_reflog_error(path)) - } - - pub(crate) fn reflog_iter_inner<'b>( - &self, - path: &Path, - buf: &'b mut Vec, - ) -> std::io::Result>> { - match std::fs::File::open(path) { - Ok(mut file) => { - buf.clear(); - if let Err(err) = file.read_to_end(buf) { - return if path.is_dir() { Ok(None) } else { Err(err) }; - } - Ok(Some(log::iter::forward(buf))) - } - Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None), - #[cfg(windows)] - Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => Ok(None), - Err(err) => Err(err), - } - } -} - -impl file::Store { - /// Implements the logic required to transform a fully qualified refname into its log name - pub(crate) fn reflog_path(&self, name: &FullNameRef) -> Result { - let (base, rela_path) = self.reflog_base_and_relative_path(name)?; - Ok(base.join(rela_path)) - } -} - -/// The raised error's [metadata](gix_error::Error::metadata()) `path` (native path) identifies the resolved reflog path -/// that could not be read. -fn read_reflog_error(path: PathBuf) -> Message { - Message::new("Could not read reflog").with("path", path) -} - -/// -pub mod create_or_update { - use std::{ - borrow::Cow, - io::Write, - path::{Path, PathBuf}, - }; - - use gix_error::{ErrorExt, Message, OptionExt, Result, ResultExt}; - use gix_hash::{ObjectId, oid}; - use gix_object::bstr::BStr; - - use crate::store_impl::{file, file::WriteReflog}; - - impl file::Store { - /// Append a reflog entry. Filesystem failures include [metadata](gix_error::Error::metadata()) `path` (native - /// path), the affected file or directory. - /// A missing identity is reported as [`MissingCommitter`] only when a log entry must actually be written. - pub(crate) fn reflog_create_or_append( - &self, - name: &FullNameRef, - previous_oid: Option, - new: &oid, - committer: Option>, - message: &BStr, - mut force_create_reflog: bool, - ) -> Result { - let (reflog_base, full_name) = self.reflog_base_and_relative_path(name)?; - match self.write_reflog { - WriteReflog::Normal | WriteReflog::Always => { - if self.write_reflog == WriteReflog::Always { - force_create_reflog = true; - } - let mut options = std::fs::OpenOptions::new(); - options.append(true).read(false); - let log_path = reflog_base.join(&full_name); - - if force_create_reflog || self.should_autocreate_reflog(&full_name) { - let parent_dir = log_path.parent().expect("always with parent directory"); - gix_tempfile::create_dir::all(parent_dir, Default::default(), 0) - .or_raise(|| Message::new("Could not create reflog directory").with("path", parent_dir))?; - options.create(true); - } - - let file_for_appending = match options.open(&log_path) { - Ok(f) => Some(f), - Err(err) if err.kind() == std::io::ErrorKind::NotFound => None, - Err(err) => { - // TODO: when Kind::IsADirectory becomes stable, use that. - if log_path.is_dir() { - gix_tempfile::remove_dir::empty_depth_first(log_path.clone()) - .and_then(|_| options.open(&log_path)) - .map(Some) - .or_raise(|| open_reflog_for_appending_error(log_path.as_path()))? - } else { - return Err(err.and_raise(open_reflog_for_appending_error(log_path))); - } - } - }; - - if let Some(mut file) = file_for_appending { - let committer = committer.ok_or_raise(|| MissingCommitter)?; - write!(file, "{} {} ", previous_oid.unwrap_or_else(|| new.kind().null()), new) - .and_then(|_| committer.trim().write_to(&mut file)) - .and_then(|_| { - if !message.is_empty() { - file.write_all(b"\t")?; - file.write_all(message)?; - file.write_all(b"\n") - } else { - writeln!(file) - } - }) - .or_raise(|| { - Message::new("Could not append reflog entry").with("path", log_path.as_path()) - })?; - } - Ok(()) - } - WriteReflog::Disable => Ok(()), - } - } - - fn should_autocreate_reflog(&self, full_name: &Path) -> bool { - full_name.starts_with("refs/heads/") - || full_name.starts_with("refs/remotes/") - || full_name.starts_with("refs/notes/") - || full_name.starts_with("refs/worktree/") // NOTE: git does not write reflogs for worktree private refs - || full_name == Path::new("HEAD") - } - - /// Returns the base paths for all reflogs - pub(in crate::store_impl::file) fn reflog_base_and_relative_path<'a>( - &self, - name: &'a FullNameRef, - ) -> Result<(PathBuf, Cow<'a, Path>)> { - let is_reflog = true; - let (base, name) = self.to_base_dir_and_relative_name(name, is_reflog)?; - Ok(( - base.join("logs"), - match &self.namespace { - None => gix_path::to_native_path_on_windows(name.as_bstr()), - Some(namespace) => gix_path::to_native_path_on_windows( - namespace.to_owned().into_namespaced_name(name).into_inner(), - ), - }?, - )) - } - } - - /// The raised error's [metadata](gix_error::Error::metadata()) `path` (native path) identifies the reflog that could - /// not be opened for appending. - fn open_reflog_for_appending_error(path: impl Into) -> Message { - Message::new("Could not open reflog for appending").with("path", path.into()) - } - - /// A reflog entry requires a committer identity which wasn't provided. - #[derive(Debug)] - pub struct MissingCommitter; - - impl std::fmt::Display for MissingCommitter { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str("reflog messages need a committer which isn't set") - } - } - - impl std::error::Error for MissingCommitter { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - Some(const { &gix_error::ClassificationMarker::VALIDATION }) - } - } - - #[cfg(test)] - mod tests; - - use crate::FullNameRef; -} diff --git a/vendor/gix-ref/src/store/file/loose/reflog/create_or_update/tests.rs b/vendor/gix-ref/src/store/file/loose/reflog/create_or_update/tests.rs deleted file mode 100644 index 85ee6f7..0000000 --- a/vendor/gix-ref/src/store/file/loose/reflog/create_or_update/tests.rs +++ /dev/null @@ -1,271 +0,0 @@ -use gix_actor::Signature; -use gix_date::parse::TimeBuf; -use gix_object::bstr::ByteSlice; -use gix_testtools::tempfile::TempDir; - -use super::*; - -use gix_testtools::{Result, TestResult}; - -static SHA1_TO_SHA256_HASHES: std::sync::LazyLock> = - std::sync::LazyLock::new(|| { - [ - ( - "28ce6a8b26aa170e1de65536fe8abe1832bd3242", - "28ce6a8b26aa170e1de65536fe8abe1832bd3242000000000000000000000000", - ), - ( - "0000000000000000000000111111111111111111", - "0000000000000000000000111111111111111111000000000000000000000000", - ), - ] - .into() - }); - -/// Convert a hexadecimal SHA-1 hash or the corresponding SHA-256 hash into an `ObjectId` or -/// _panic_. -fn hex_to_id(hex: &str) -> gix_hash::ObjectId { - match gix_testtools::object_hash() { - gix_hash::Kind::Sha1 => gix_hash::ObjectId::from_hex(hex.as_bytes()).expect("40 bytes hex"), - gix_hash::Kind::Sha256 => gix_hash::ObjectId::from_hex( - SHA1_TO_SHA256_HASHES - .get(hex) - .unwrap_or_else(|| panic!("SHA-1 {hex} wasn't mapped to SHA-256 yet")) - .as_bytes(), - ) - .expect("64 bytes hex"), - _ => unimplemented!(), - } -} - -fn empty_store(writemode: WriteReflog) -> Result<(TempDir, file::Store)> { - let dir = TempDir::new()?; - let store = file::Store::at_opts( - dir.path().into(), - gix_testtools::object_hash(), - crate::store::init::Options { - write_reflog: writemode, - ..Default::default() - }, - ); - Ok((dir, store)) -} - -fn reflog_lines(store: &file::Store, name: &str, buf: &mut Vec) -> Result> { - store - .reflog_iter(name, buf)? - .expect("existing reflog") - .map(|l| l.map(crate::log::Line::from)) - .collect::, _>>() - .map_err(Into::into) -} - -const WRITE_MODES: &[WriteReflog] = &[WriteReflog::Normal, WriteReflog::Disable, WriteReflog::Always]; - -#[test] -fn should_autocreate_is_unaffected_by_writemode() -> TestResult { - let (_keep, store) = empty_store(WriteReflog::Disable)?; - for should_create_name in &["HEAD", "refs/heads/main", "refs/remotes/any", "refs/notes/any"] { - assert!(store.should_autocreate_reflog(Path::new(should_create_name))); - } - for should_not_create_name in &["FETCH_HEAD", "SOMETHING", "refs/special/this", "refs/tags/0.1.0"] { - assert!(!store.should_autocreate_reflog(Path::new(should_not_create_name))); - } - Ok(()) -} - -#[test] -fn missing_reflog_creates_it_even_if_similarly_named_empty_dir_exists_and_append_log_lines() -> TestResult { - for mode in WRITE_MODES { - let (_keep, store) = empty_store(*mode)?; - let full_name_str = "refs/heads/main"; - let full_name: &FullNameRef = full_name_str.try_into()?; - let new = hex_to_id("28ce6a8b26aa170e1de65536fe8abe1832bd3242"); - let committer = Signature { - name: "committer".into(), - email: "committer@example.com".into(), - time: gix_date::parse_header("1234 +0800").unwrap(), - }; - store.reflog_create_or_append( - full_name, - None, - &new, - committer.to_ref(&mut TimeBuf::default()).into(), - b"the message".as_bstr(), - false, - )?; - - let mut buf = Vec::new(); - match mode { - WriteReflog::Normal | WriteReflog::Always => { - assert_eq!( - reflog_lines(&store, full_name_str, &mut buf)?, - vec![crate::log::Line { - previous_oid: gix_testtools::object_hash().null(), - new_oid: new, - signature: committer.clone(), - message: "the message".into() - }] - ); - let previous = hex_to_id("0000000000000000000000111111111111111111"); - buf.clear(); - store.reflog_create_or_append( - full_name, - Some(previous), - &new, - committer.to_ref(&mut TimeBuf::default()).into(), - b"next message".as_bstr(), - false, - )?; - - let lines = reflog_lines(&store, full_name_str, &mut buf)?; - assert_eq!(lines.len(), 2, "now there is another line"); - assert_eq!( - lines.last().expect("non-empty"), - &crate::log::Line { - previous_oid: previous, - new_oid: new, - signature: committer.clone(), - message: "next message".into() - } - ); - } - WriteReflog::Disable => { - assert!( - store.reflog_iter(full_name, &mut buf)?.is_none(), - "there is no logs in disabled mode" - ); - } - } - - // create onto existing directory - let full_name_str = "refs/heads/other"; - let full_name: &FullNameRef = full_name_str.try_into()?; - let reflog_path = store.reflog_path(full_name_str.try_into()?)?; - let directory_in_place_of_reflog = reflog_path.join("empty-a").join("empty-b"); - std::fs::create_dir_all(directory_in_place_of_reflog)?; - - buf.clear(); - store.reflog_create_or_append( - full_name, - None, - &new, - committer.to_ref(&mut TimeBuf::default()).into(), - b"more complicated reflog creation".as_bstr(), - false, - )?; - - match mode { - WriteReflog::Normal | WriteReflog::Always => { - assert_eq!( - reflog_lines(&store, full_name_str, &mut buf)?.len(), - 1, - "reflog was written despite directory" - ); - assert!( - reflog_path.is_file(), - "the empty directory was replaced with the reflog file" - ); - } - WriteReflog::Disable => { - assert!( - store.reflog_iter(full_name_str, &mut buf)?.is_none(), - "reflog still doesn't exist" - ); - assert!( - store.reflog_iter_rev(full_name_str, &mut buf)?.is_none(), - "reflog still doesn't exist" - ); - assert!(reflog_path.is_dir(), "reflog directory wasn't touched"); - } - } - } - Ok(()) -} - -#[test] -fn non_empty_reflog_directory_preserves_open_error_context() -> TestResult { - let (_keep, store) = empty_store(WriteReflog::Normal)?; - let name: &FullNameRef = "refs/heads/main".try_into()?; - let path = store.reflog_path(name)?; - std::fs::create_dir_all(&path)?; - std::fs::write(path.join("keep"), b"not an empty directory")?; - - let err = store - .reflog_create_or_append( - name, - None, - &gix_testtools::object_hash().null(), - None, - b"update".as_bstr(), - false, - ) - .expect_err("a non-empty directory cannot be replaced with a reflog"); - insta::assert_debug_snapshot!(gix_testtools::redact_debug_snapshot(&(err), &[(&(store.git_dir()).to_string_lossy(), "")]), "the conflicting directory exists", @r#" - Could not open reflog for appending, path="/logs/refs/heads/main" - - Caused by: - 0: I/O error (Other) - 1: Directory not empty - "#); - let details = err.metadata().next().expect("reflog open context"); - assert!( - err.downcast_any_ref::().is_some(), - "reflog open context retains its message type" - ); - assert_eq!(details.len(), 1, "open context contains only the path"); - assert_eq!( - details["path"], - gix_error::MetadataValue::Path(path), - "the reflog path remains a native path" - ); - assert!( - err.downcast_any_ref::().is_some(), - "the directory removal error remains accessible" - ); - assert!(!err.is_not_found(), "the conflicting directory exists"); - assert!( - !err.is_corrupted(), - "a directory collision does not imply corrupt reflog contents" - ); - Ok(()) -} - -#[test] -fn reflog_write_normalizes_committer_name_and_email_like_git() -> TestResult { - let (_keep, store) = empty_store(WriteReflog::Always)?; - let full_name_str = "refs/heads/main"; - let full_name: &FullNameRef = full_name_str.try_into()?; - let new = hex_to_id("28ce6a8b26aa170e1de65536fe8abe1832bd3242"); - let committer = Signature { - name: " committer\n ".into(), - email: " committer@example.com\n ".into(), - time: gix_date::parse_header("1234 +0800").unwrap(), - }; - - store.reflog_create_or_append( - full_name, - None, - &new, - committer.to_ref(&mut TimeBuf::default()).into(), - b"the message".as_bstr(), - false, - )?; - - let mut buf = Vec::new(); - assert_eq!( - reflog_lines(&store, full_name_str, &mut buf)?, - vec![crate::log::Line { - previous_oid: gix_testtools::object_hash().null(), - new_oid: new, - signature: Signature { - name: "committer".into(), - email: "committer@example.com".into(), - time: gix_date::parse_header("1234 +0800").unwrap(), - }, - message: "the message".into() - }], - "it trimmed whitespace as basic fix for slightly malformed signatures" - ); - Ok(()) -} diff --git a/vendor/gix-ref/src/store/file/mod.rs b/vendor/gix-ref/src/store/file/mod.rs deleted file mode 100644 index 97e49c1..0000000 --- a/vendor/gix-ref/src/store/file/mod.rs +++ /dev/null @@ -1,143 +0,0 @@ -use std::path::PathBuf; - -use crate::{Namespace, store::WriteReflog}; - -/// A store for reference which uses plain files. -/// -/// Each ref is represented as a single file on disk in a folder structure that follows the relative path -/// used to identify [references][crate::Reference]. -#[derive(Debug, Clone)] -pub struct Store { - /// The location at which loose references can be found as per conventions of a typical git repository. - /// - /// Typical base paths are `.git` repository folders. - git_dir: PathBuf, - /// Possibly the common directory at which to find shared references. Only set if this `Store` is for a work tree. - common_dir: Option, - /// The kind of hash to assume in a couple of situations. Note that currently we are able to read any valid hash from files - /// which might want to change one day. - object_hash: gix_hash::Kind, - /// The amount of bytes needed for `mmap` to be used to open packed refs. - packed_buffer_mmap_threshold: u64, - - /// The way to handle reflog edits - pub write_reflog: WriteReflog, - /// The namespace to use for edits and reads - pub namespace: Option, - /// This is only needed on Windows, where some device names are reserved at any level of a path, so that - /// reading or writing `refs/heads/CON` for example would read from the console, or write to it. - pub prohibit_windows_device_names: bool, - /// If set, we will convert decomposed unicode like `a\u308` into precomposed unicode like `ä` when reading - /// ref names from disk. - /// Note that this is an internal operation that isn't observable on the outside, but it's needed for lookups - /// to packed-refs or symlinks to work correctly. - /// Iterated references will be returned verbatim, thus when sending them over the wire they have to be precomposed - /// as needed. - pub precompose_unicode: bool, - /// A packed buffer which can be mapped in one version and shared as such. - /// It's updated only in one spot, which is prior to reading it based on file stamps. - /// Doing it like this has the benefit of being able to hand snapshots out to people without blocking others from updating it. - packed: packed::modifiable::MutableSharedBuffer, -} - -mod access { - use std::path::Path; - - /// Mutation - impl file::Store { - /// Set the amount of `bytes` needed for the `.git/packed-refs` file to be memory mapped. - /// Returns the previous value, which is always 32KB. - pub fn set_packed_buffer_mmap_threshold(&mut self, mut bytes: u64) -> u64 { - std::mem::swap(&mut self.packed_buffer_mmap_threshold, &mut bytes); - bytes - } - } - - use crate::{Target, file}; - - /// Access - impl file::Store { - /// Return the `.git` directory at which all references are loaded. - /// - /// For worktrees, this is the linked work-tree private ref location, - /// then [`common_dir()`][file::Store::common_dir()] is `Some(parent_git_dir)`. - pub fn git_dir(&self) -> &Path { - &self.git_dir - } - - /// If this is a linked work tree, there will be `Some(git_dir)` pointing to the parent repository, - /// while [`git_dir()`][file::Store::git_dir()] points to the location holding linked work-tree private references. - pub fn common_dir(&self) -> Option<&Path> { - self.common_dir.as_deref() - } - - /// Similar to [`common_dir()`][file::Store::common_dir()], but it will produce either the common-dir, or the git-dir if the former - /// isn't present. - /// - /// This is also the directory in which the packed references file would be placed. - pub fn common_dir_resolved(&self) -> &Path { - self.common_dir.as_deref().unwrap_or(&self.git_dir) - } - - /// Return `Some(true)` if this is a freshly initialized ref store without any observable changes. - /// Return `None` if `HEAD` couldn't be read. - /// - /// This is the case if: - /// - /// * the ref-store is valid - /// * `HEAD` exists - /// * `HEAD` still points to `default_ref` - /// * there are no packed refs - /// * There are no observable references in `refs/` - pub fn is_pristine(&self, default_ref: &crate::FullNameRef) -> Option { - let head = self.find_loose("HEAD").ok()?; - match head.target { - Target::Object(_) => return Some(false), - Target::Symbolic(name) => { - if name.as_ref() != default_ref { - return Some(false); - } - } - } - if self.loose_iter().ok()?.filter_map(Result::ok).next().is_some() { - return Some(false); - } - if self.packed_refs_path().is_file() { - return Some(false); - } - Some(true) - } - } -} - -/// A transaction on a file store -pub struct Transaction<'s, 'p> { - store: &'s Store, - packed_transaction: Option, - updates: Option>, - packed_refs: transaction::PackedRefs<'p>, -} - -/// -pub mod loose; -mod overlay_iter; - -/// -pub mod iter { - pub use super::overlay_iter::{LooseThenPacked, Platform}; -} - -/// -pub mod log; - -/// -pub mod find; - -/// -pub mod transaction; - -/// -pub mod packed; - -mod raw_ext; -pub use raw_ext::ReferenceExt; diff --git a/vendor/gix-ref/src/store/file/overlay_iter.rs b/vendor/gix-ref/src/store/file/overlay_iter.rs deleted file mode 100644 index 6aa8656..0000000 --- a/vendor/gix-ref/src/store/file/overlay_iter.rs +++ /dev/null @@ -1,448 +0,0 @@ -use gix_error::{Result, ResultExt, message}; - -use gix_object::bstr::ByteSlice; -use gix_path::RelativePath; -use std::{ - borrow::Cow, - cmp::Ordering, - io::Read, - iter::Peekable, - path::{Path, PathBuf}, -}; - -use crate::{ - BStr, FullName, Namespace, Reference, - file::loose::{self, iter::SortedLoosePaths}, - store_impl::{file, packed}, -}; - -/// An iterator stepping through sorted input of loose references and packed references, preferring loose refs over otherwise -/// equivalent packed references. -/// -/// All errors will be returned verbatim, while packed errors are depleted first if loose refs also error. -pub struct LooseThenPacked<'p, 's> { - git_dir: &'s Path, - common_dir: Option<&'s Path>, - object_hash: gix_hash::Kind, - namespace: Option<&'s Namespace>, - iter_packed: Option>>, - iter_git_dir: Peekable, - iter_common_dir: Option>, - buf: Vec, -} - -enum IterKind { - Git, - GitAndConsumeCommon, - Common, -} - -/// An intermediate structure to hold shared state alive long enough for iteration to happen. -#[must_use = "Iterators should be obtained from this platform"] -pub struct Platform<'s> { - store: &'s file::Store, - packed: Option, -} - -impl<'p> LooseThenPacked<'p, '_> { - fn strip_namespace(&self, mut r: Reference) -> Reference { - if let Some(namespace) = &self.namespace { - r.strip_namespace(namespace); - } - r - } - - fn loose_iter(&mut self, kind: IterKind) -> &mut Peekable { - match kind { - IterKind::GitAndConsumeCommon => { - drop(self.iter_common_dir.as_mut().map(Iterator::next)); - &mut self.iter_git_dir - } - IterKind::Git => &mut self.iter_git_dir, - IterKind::Common => self - .iter_common_dir - .as_mut() - .expect("caller knows there is a common iter"), - } - } - - fn convert_packed(&mut self, packed: Result>) -> Result { - packed.map(Into::into).map(|r| self.strip_namespace(r)) - } - - /// Read failures include [metadata](gix_error::Error::metadata()) `path` (native path), the loose reference being - /// visited. - fn convert_loose(&mut self, res: std::io::Result<(PathBuf, FullName)>) -> Result { - let buf = &mut self.buf; - let git_dir = self.git_dir; - let common_dir = self.common_dir; - let (refpath, name) = res.or_raise(|| message("Could not traverse reference directory"))?; - std::fs::File::open(&refpath) - .and_then(|mut f| { - buf.clear(); - f.read_to_end(buf) - }) - .or_raise(|| file::find::read_reference_error(refpath.as_path()))?; - loose::Reference::try_from_path(name, buf, self.object_hash) - .or_raise(|| { - let relative_path = refpath - .strip_prefix(git_dir) - .ok() - .or_else(|| common_dir.and_then(|common_dir| refpath.strip_prefix(common_dir).ok())) - .expect("one of our bases contains the path"); - file::find::ReferenceDecode { - relative_path: relative_path.into(), - } - }) - .map(Into::into) - .map(|r| self.strip_namespace(r)) - } -} - -impl Iterator for LooseThenPacked<'_, '_> { - type Item = Result; - - fn next(&mut self) -> Option { - fn advance_to_non_private(iter: &mut Peekable) { - while let Some(Ok((_path, name))) = iter.peek() { - if name.category().is_some_and(|cat| cat.is_worktree_private()) { - iter.next(); - } else { - break; - } - } - } - - fn peek_loose<'a>( - git_dir: &'a mut Peekable, - common_dir: Option<&'a mut Peekable>, - ) -> Option<(&'a std::io::Result<(PathBuf, FullName)>, IterKind)> { - match common_dir { - Some(common_dir) => match (git_dir.peek(), { - advance_to_non_private(common_dir); - common_dir.peek() - }) { - (None, None) => None, - (None, Some(res)) | (Some(_), Some(res @ Err(_))) => Some((res, IterKind::Common)), - (Some(res), None) | (Some(res @ Err(_)), Some(_)) => Some((res, IterKind::Git)), - (Some(r_gitdir @ Ok((_, git_dir_name))), Some(r_cd @ Ok((_, common_dir_name)))) => { - match git_dir_name.cmp(common_dir_name) { - Ordering::Less => Some((r_gitdir, IterKind::Git)), - Ordering::Equal => Some((r_gitdir, IterKind::GitAndConsumeCommon)), - Ordering::Greater => Some((r_cd, IterKind::Common)), - } - } - }, - None => git_dir.peek().map(|r| (r, IterKind::Git)), - } - } - match self.iter_packed.as_mut() { - Some(packed_iter) => match ( - peek_loose(&mut self.iter_git_dir, self.iter_common_dir.as_mut()), - packed_iter.peek(), - ) { - (None, None) => None, - (None, Some(_)) | (Some(_), Some(Err(_))) => { - let res = packed_iter.next().expect("peeked value exists"); - Some(self.convert_packed(res)) - } - (Some((_, kind)), None) | (Some((Err(_), kind)), Some(_)) => { - let res = self.loose_iter(kind).next().expect("prior peek"); - Some(self.convert_loose(res)) - } - (Some((Ok((_, loose_name)), kind)), Some(Ok(packed))) => match loose_name.as_ref().cmp(packed.name) { - Ordering::Less => { - let res = self.loose_iter(kind).next().expect("prior peek"); - Some(self.convert_loose(res)) - } - Ordering::Equal => { - drop(packed_iter.next()); - let res = self.loose_iter(kind).next().expect("prior peek"); - Some(self.convert_loose(res)) - } - Ordering::Greater => { - let res = packed_iter.next().expect("name retrieval configured"); - Some(self.convert_packed(res)) - } - }, - }, - None => match peek_loose(&mut self.iter_git_dir, self.iter_common_dir.as_mut()) { - None => None, - Some((_, kind)) => self.loose_iter(kind).next().map(|res| self.convert_loose(res)), - }, - } - } -} - -impl<'repo> Platform<'repo> { - /// Return an iterator over all references, loose or packed, sorted by their name. - /// - /// Errors are returned similarly to what would happen when loose and packed refs were iterated by themselves. - pub fn all<'p>(&'p self) -> std::io::Result> { - self.store.iter_packed(self.packed.as_ref().map(|b| &***b)) - } - - /// As [`iter(…)`](file::Store::iter()), but filters by `prefix`, i.e. "refs/heads/" or - /// "refs/heads/feature-". - /// - /// Note that if a prefix isn't using a trailing `/`, like in `refs/heads/foo`, it will effectively - /// start the traversal in the parent directory, e.g. `refs/heads/` and list everything inside that - /// starts with `foo`, like `refs/heads/foo` and `refs/heads/foobar`. - /// - /// Prefixes are relative paths with slash-separated components. - pub fn prefixed<'p>(&'p self, prefix: &RelativePath) -> std::io::Result> { - self.store - .iter_prefixed_packed(prefix, self.packed.as_ref().map(|b| &***b)) - } - - /// Return an iterator over the pseudo references, like `HEAD` or `FETCH_HEAD`, or anything else suffixed with `HEAD` - /// in the root of the `.git` directory, sorted by name. - pub fn pseudo<'p>(&'p self) -> std::io::Result> { - self.store.iter_pseudo() - } -} - -impl file::Store { - /// Return a platform to obtain iterator over all references, or prefixed ones, loose or packed, sorted by their name. - /// - /// Errors are returned similarly to what would happen when loose and packed refs were iterated by themselves. - /// - /// Note that since packed-refs are storing refs as precomposed unicode if [`Self::precompose_unicode`] is true, for consistency - /// we also return loose references as precomposed unicode. - pub fn iter(&self) -> Result> { - Ok(Platform { - store: self, - packed: self.assure_packed_refs_uptodate()?, - }) - } -} - -#[derive(Debug)] -pub(crate) enum IterInfo<'a> { - Base { - base: &'a Path, - precompose_unicode: bool, - }, - BaseAndIterRoot { - base: &'a Path, - iter_root: PathBuf, - prefix: PathBuf, - precompose_unicode: bool, - }, - PrefixAndBase { - base: &'a Path, - prefix: &'a Path, - precompose_unicode: bool, - }, - ComputedIterationRoot { - /// The root to iterate over - iter_root: PathBuf, - /// The top-level directory as boundary of all references, used to create their short-names after iteration. - base: &'a Path, - /// The original prefix. - prefix: Cow<'a, BStr>, - /// If `true`, we will convert decomposed into precomposed unicode. - precompose_unicode: bool, - }, - Pseudo { - base: &'a Path, - precompose_unicode: bool, - }, -} - -impl<'a> IterInfo<'a> { - fn prefix(&self) -> Result>> { - match self { - IterInfo::Base { .. } => None, - IterInfo::PrefixAndBase { prefix, .. } => Some(gix_path::into_bstr(*prefix)), - IterInfo::BaseAndIterRoot { prefix, .. } => Some(gix_path::into_bstr(prefix.clone())), - IterInfo::ComputedIterationRoot { prefix, .. } => Some(Ok(prefix.clone())), - IterInfo::Pseudo { .. } => None, - } - .transpose() - } - - fn into_iter(self) -> Peekable { - match self { - IterInfo::Base { - base, - precompose_unicode, - } => SortedLoosePaths::at(&base.join("refs"), base.into(), None, None, precompose_unicode), - IterInfo::BaseAndIterRoot { - base, - iter_root, - prefix: _, - precompose_unicode, - } => SortedLoosePaths::at(&iter_root, base.into(), None, None, precompose_unicode), - IterInfo::PrefixAndBase { - base, - prefix, - precompose_unicode, - } => SortedLoosePaths::at(&base.join(prefix), base.into(), None, None, precompose_unicode), - IterInfo::ComputedIterationRoot { - iter_root, - base, - prefix, - precompose_unicode, - } => SortedLoosePaths::at( - &iter_root, - base.into(), - Some(prefix.into_owned()), - None, - precompose_unicode, - ), - IterInfo::Pseudo { - base, - precompose_unicode, - } => SortedLoosePaths::at(base, base.into(), None, Some("HEAD".into()), precompose_unicode), - } - .peekable() - } - - fn from_prefix(base: &'a Path, prefix: &'a RelativePath, precompose_unicode: bool) -> std::io::Result { - let prefix_path = gix_path::from_bstr(prefix.as_ref().as_bstr()) - .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidInput, err))?; - let iter_root = base.join(&prefix_path); - if prefix.as_ref().ends_with(b"/") { - Ok(IterInfo::BaseAndIterRoot { - base, - iter_root, - prefix: prefix_path.into_owned(), - precompose_unicode, - }) - } else { - let iter_root = iter_root - .parent() - .expect("a parent is always there unless empty") - .to_owned(); - Ok(IterInfo::ComputedIterationRoot { - base, - prefix: prefix.as_ref().as_bstr().into(), - iter_root, - precompose_unicode, - }) - } - } -} - -impl file::Store { - /// Return an iterator over all references, loose or `packed`, sorted by their name. - /// - /// Errors are returned similarly to what would happen when loose and packed refs were iterated by themselves. - pub fn iter_packed<'s, 'p>( - &'s self, - packed: Option<&'p packed::Buffer>, - ) -> std::io::Result> { - match self.namespace.as_ref() { - Some(namespace) => { - let prefix = namespace - .to_path() - .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidInput, err))?; - self.iter_from_info( - IterInfo::PrefixAndBase { - base: self.git_dir(), - prefix, - precompose_unicode: self.precompose_unicode, - }, - self.common_dir().map(|base| IterInfo::PrefixAndBase { - base, - prefix, - precompose_unicode: self.precompose_unicode, - }), - packed, - ) - } - None => self.iter_from_info( - IterInfo::Base { - base: self.git_dir(), - precompose_unicode: self.precompose_unicode, - }, - self.common_dir().map(|base| IterInfo::Base { - base, - precompose_unicode: self.precompose_unicode, - }), - packed, - ), - } - } - - /// Return an iterator over the pseudo references, like `HEAD` or `FETCH_HEAD`, or anything else suffixed with `HEAD` - /// in the root of the `.git` directory, sorted by name. - /// - /// Errors are returned similarly to what would happen when loose refs were iterated by themselves. - pub fn iter_pseudo<'p>(&'_ self) -> std::io::Result> { - self.iter_from_info( - IterInfo::Pseudo { - base: self.git_dir(), - precompose_unicode: self.precompose_unicode, - }, - None, - None, - ) - } - - /// As [`iter(…)`](file::Store::iter()), but filters by `prefix`, i.e. `refs/heads/` or - /// `refs/heads/feature-`. - /// Note that if a prefix isn't using a trailing `/`, like in `refs/heads/foo`, it will effectively - /// start the traversal in the parent directory, e.g. `refs/heads/` and list everything inside that - /// starts with `foo`, like `refs/heads/foo` and `refs/heads/foobar`. - /// - /// Prefixes are relative paths with slash-separated components. - pub fn iter_prefixed_packed<'s, 'p>( - &'s self, - prefix: &RelativePath, - packed: Option<&'p packed::Buffer>, - ) -> std::io::Result> { - match self.namespace.as_ref() { - None => { - let git_dir_info = IterInfo::from_prefix(self.git_dir(), prefix, self.precompose_unicode)?; - let common_dir_info = self - .common_dir() - .map(|base| IterInfo::from_prefix(base, prefix, self.precompose_unicode)) - .transpose()?; - self.iter_from_info(git_dir_info, common_dir_info, packed) - } - Some(namespace) => { - let prefix = namespace.to_owned().into_namespaced_prefix(prefix); - let prefix = prefix.as_bstr().try_into().map_err(std::io::Error::other)?; - let git_dir_info = IterInfo::from_prefix(self.git_dir(), prefix, self.precompose_unicode)?; - let common_dir_info = self - .common_dir() - .map(|base| IterInfo::from_prefix(base, prefix, self.precompose_unicode)) - .transpose()?; - self.iter_from_info(git_dir_info, common_dir_info, packed) - } - } - } - - fn iter_from_info<'s, 'p>( - &'s self, - git_dir_info: IterInfo<'_>, - common_dir_info: Option>, - packed: Option<&'p packed::Buffer>, - ) -> std::io::Result> { - Ok(LooseThenPacked { - git_dir: self.git_dir(), - common_dir: self.common_dir(), - object_hash: self.object_hash, - iter_packed: match packed { - Some(packed) => Some( - match git_dir_info - .prefix() - .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidInput, err))? - { - Some(prefix) => packed.iter_prefixed(prefix.into_owned()), - None => packed.iter(), - } - .map_err(std::io::Error::other)? - .peekable(), - ), - None => None, - }, - iter_git_dir: git_dir_info.into_iter(), - iter_common_dir: common_dir_info.map(IterInfo::into_iter), - buf: Vec::new(), - namespace: self.namespace.as_ref(), - }) - } -} diff --git a/vendor/gix-ref/src/store/file/packed.rs b/vendor/gix-ref/src/store/file/packed.rs deleted file mode 100644 index e57754c..0000000 --- a/vendor/gix-ref/src/store/file/packed.rs +++ /dev/null @@ -1,100 +0,0 @@ -use gix_error::{Result, ResultExt, message}; - -use std::path::PathBuf; - -use crate::store_impl::{file, packed}; - -impl file::Store { - /// Return a packed transaction ready to receive updates. Use this to create or update `packed-refs`. - /// Note that if you already have a [`packed::Buffer`] then use its [`packed::Buffer::into_transaction()`] method instead. - pub(crate) fn packed_transaction(&self, lock_mode: gix_lock::acquire::Fail) -> Result { - let lock = gix_lock::File::acquire_to_update_resource(self.packed_refs_path(), lock_mode, None, 0) - .or_raise(|| message("Could not lock packed refs"))?; - // We 'steal' the possibly existing packed buffer which may safe time if it's already there and fresh. - // If nothing else is happening, nobody will get to see the soon stale buffer either, but if so, they will pay - // for reloading it. That seems preferred over always loading up a new one. - Ok(packed::Transaction::new_from_pack_and_lock( - self.assure_packed_refs_uptodate()?, - lock, - self.precompose_unicode, - self.namespace.clone(), - )) - } - - /// Try to open a new packed buffer. It's not an error if it doesn't exist, but yields `Ok(None)`. - /// - /// Note that it will automatically be memory mapped if it exceeds the default threshold of 32KB. - /// Change the threshold with [file::Store::set_packed_buffer_mmap_threshold()]. - pub fn open_packed_buffer(&self) -> Result> { - match packed::Buffer::open( - self.packed_refs_path(), - self.packed_buffer_mmap_threshold, - self.object_hash, - ) { - Ok(buf) => Ok(Some(buf)), - Err(err) if err.is_not_found() => Ok(None), - Err(err) => Err(err), - } - } - - /// Return a possibly cached packed buffer with shared ownership. At retrieval it will assure it's up to date, but - /// after that it can be considered a snapshot as it cannot change anymore. - /// - /// Use this to make successive calls to [`file::Store::try_find_packed()`] - /// or obtain iterators using [`file::Store::iter_packed()`] in a way that assures the packed-refs content won't change. - pub fn cached_packed_buffer(&self) -> Result> { - self.assure_packed_refs_uptodate() - } - - /// Return the path at which packed-refs would usually be stored - pub fn packed_refs_path(&self) -> PathBuf { - self.common_dir_resolved().join("packed-refs") - } - - pub(crate) fn packed_refs_lock_path(&self) -> PathBuf { - let mut p = self.packed_refs_path(); - p.set_extension("lock"); - p - } -} - -/// An up-to-date snapshot of the packed refs buffer. -pub type SharedBufferSnapshot = gix_fs::SharedFileSnapshot; - -pub(crate) mod modifiable { - use gix_error::{Message, Result, ResultExt}; - use gix_parallel::OwnShared; - - use crate::{file, packed}; - - pub(crate) type MutableSharedBuffer = OwnShared>; - - impl file::Store { - /// Forcefully reload the packed refs buffer. - /// - /// This method should be used if it's clear that the buffer on disk has changed, to - /// make the latest changes visible before other operations are done on this instance. - /// - /// As some filesystems don't have nanosecond granularity, changes are likely to be missed - /// if they happen within one second otherwise. - /// - /// [Metadata](gix_error::Error::metadata()) `path` (native path) identifies a packed-refs file whose modification - /// time could not be read. - pub fn force_refresh_packed_buffer(&self) -> Result { - self.packed.force_refresh(|| { - let path = self.packed_refs_path(); - let modified = path - .metadata() - .and_then(|metadata| metadata.modified()) - .or_raise(|| Message::new("Could not read packed refs modification time").with("path", path))?; - self.open_packed_buffer().map(|packed| Some(modified).zip(packed)) - }) - } - pub(crate) fn assure_packed_refs_uptodate(&self) -> Result> { - self.packed.recent_snapshot( - || self.packed_refs_path().metadata().and_then(|m| m.modified()).ok(), - || self.open_packed_buffer(), - ) - } - } -} diff --git a/vendor/gix-ref/src/store/file/raw_ext.rs b/vendor/gix-ref/src/store/file/raw_ext.rs deleted file mode 100644 index f499d4a..0000000 --- a/vendor/gix-ref/src/store/file/raw_ext.rs +++ /dev/null @@ -1,234 +0,0 @@ -use gix_error::Result; -use std::collections::BTreeSet; - -use gix_error::{ErrorExt, Message, OptionExt, ResultExt, bail, corruption, message, not_found}; -use gix_hash::ObjectId; - -use crate::{ - Target, packed, - raw::Reference, - store_impl::{file, file::log}, -}; - -pub trait Sealed {} -impl Sealed for crate::Reference {} - -/// A trait to extend [Reference][crate::Reference] with functionality requiring a [file::Store]. -pub trait ReferenceExt: Sealed { - /// A step towards obtaining forward or reverse iterators on reference logs. - fn log_iter<'a, 's>(&'a self, store: &'s file::Store) -> log::iter::Platform<'a, 's>; - - /// For details, see [`Reference::log_exists()`]. - fn log_exists(&self, store: &file::Store) -> Result; - - /// Follow all symbolic targets this reference might point to and peel the underlying object - /// to the end of the tag-chain, returning the first non-tag object the annotated tag points to, - /// using `objects` to access them and `store` to lookup symbolic references. - /// - /// This is useful to learn where this reference is ultimately pointing to after following all symbolic - /// refs and all annotated tags to the first non-tag object. - #[deprecated = "Use `peel_to_id()` instead"] - fn peel_to_id_in_place(&mut self, store: &file::Store, objects: &dyn gix_object::Find) -> Result; - - /// Follow all symbolic targets this reference might point to and peel the underlying object - /// to the end of the tag-chain, returning the first non-tag object the annotated tag points to, - /// using `objects` to access them and `store` to lookup symbolic references. - /// - /// This is useful to learn where this reference is ultimately pointing to after following all symbolic - /// refs and all annotated tags to the first non-tag object. - /// - /// Note that this method mutates `self` in place if it does not already point to a - /// non-symbolic object. - fn peel_to_id(&mut self, store: &file::Store, objects: &dyn gix_object::Find) -> Result; - - /// Like [`ReferenceExt::peel_to_id_in_place()`], but with support for a known stable `packed` buffer - /// to use for resolving symbolic links. - #[deprecated = "Use `peel_to_id_packed()` instead"] - fn peel_to_id_in_place_packed( - &mut self, - store: &file::Store, - objects: &dyn gix_object::Find, - packed: Option<&packed::Buffer>, - ) -> Result; - - /// Like [`ReferenceExt::peel_to_id()`], but with support for a known stable `packed` buffer to - /// use for resolving symbolic links. - /// Object lookup failures include [metadata](gix_error::Error::metadata()) `object_id` (hex text) and `reference` - /// (name bytes). - /// Missing objects are classified as not found; lookup errors retain their own classifications. - fn peel_to_id_packed( - &mut self, - store: &file::Store, - objects: &dyn gix_object::Find, - packed: Option<&packed::Buffer>, - ) -> Result; - - /// Like [`ReferenceExt::follow()`], but follows all symbolic references while gracefully handling loops, - /// altering this instance in place. - #[deprecated = "Use `follow_to_object_packed()` instead"] - fn follow_to_object_in_place_packed( - &mut self, - store: &file::Store, - packed: Option<&packed::Buffer>, - ) -> Result; - - /// Like [`ReferenceExt::follow()`], but follows all symbolic references while gracefully handling loops, - /// altering this instance in place. - /// Cycle failures include [metadata](gix_error::Error::metadata()) `path` (native path); depth-limit failures include - /// `max_depth` (unsigned integer). - fn follow_to_object_packed(&mut self, store: &file::Store, packed: Option<&packed::Buffer>) -> Result; - - /// Follow this symbolic reference one level and return the ref it refers to. - /// - /// Returns `None` if this is not a symbolic reference, hence the leaf of the chain. - fn follow(&self, store: &file::Store) -> Option>; - - /// Follow this symbolic reference one level and return the ref it refers to, - /// possibly providing access to `packed` references for lookup if it contains the referent. - /// - /// Returns `None` if this is not a symbolic reference, hence the leaf of the chain. - fn follow_packed(&self, store: &file::Store, packed: Option<&packed::Buffer>) -> Option>; -} - -impl ReferenceExt for Reference { - fn log_iter<'a, 's>(&'a self, store: &'s file::Store) -> log::iter::Platform<'a, 's> { - log::iter::Platform { - store, - name: self.name.as_ref(), - buf: Vec::new(), - } - } - - fn log_exists(&self, store: &file::Store) -> Result { - store.reflog_exists(self.name.as_ref()) - } - - fn peel_to_id_in_place(&mut self, store: &file::Store, objects: &dyn gix_object::Find) -> Result { - self.peel_to_id(store, objects) - } - - fn peel_to_id(&mut self, store: &file::Store, objects: &dyn gix_object::Find) -> Result { - let packed = store.assure_packed_refs_uptodate()?; - self.peel_to_id_packed(store, objects, packed.as_ref().map(|b| &***b)) - } - - fn peel_to_id_in_place_packed( - &mut self, - store: &file::Store, - objects: &dyn gix_object::Find, - packed: Option<&packed::Buffer>, - ) -> Result { - self.peel_to_id_packed(store, objects, packed) - } - - /// Object lookup failures include [metadata](gix_error::Error::metadata()) `object_id` (hex text) and `reference` - /// (name bytes). - fn peel_to_id_packed( - &mut self, - store: &file::Store, - objects: &dyn gix_object::Find, - packed: Option<&packed::Buffer>, - ) -> Result { - match self.peeled { - Some(peeled) => { - self.target = Target::Object(peeled.to_owned()); - Ok(peeled) - } - None => { - let mut object_id = self.follow_to_object_packed(store, packed)?; - let mut buf = Vec::new(); - let peeled_id = loop { - let gix_object::Data { - kind, - data, - object_hash: hash_kind, - } = objects - .try_find(&object_id, &mut buf) - .or_raise(|| { - Message::new("Could not peel reference to an object") - .with("object_id", object_id.to_string()) - .with("reference", self.name.as_bstr()) - })? - .ok_or_raise(|| { - not_found("Could not peel reference to an object: object could not be found") - .with("object_id", object_id.to_string()) - .with("reference", self.name.as_bstr()) - })?; - match kind { - gix_object::Kind::Tag => { - object_id = gix_object::TagRefIter::from_bytes(data, hash_kind) - .target_id() - .or_raise(|| { - message!("Could not decode tag {object_id} as referred to by {:?}", self.name.0) - .corrupted() - })?; - } - _ => break object_id, - } - }; - self.peeled = Some(peeled_id); - self.target = Target::Object(peeled_id); - Ok(peeled_id) - } - } - } - - fn follow_to_object_in_place_packed( - &mut self, - store: &file::Store, - packed: Option<&packed::Buffer>, - ) -> Result { - self.follow_to_object_packed(store, packed) - } - - /// Cycle failures include [metadata](gix_error::Error::metadata()) `path` (native path); depth-limit failures include - /// `max_depth` (unsigned integer). - fn follow_to_object_packed(&mut self, store: &file::Store, packed: Option<&packed::Buffer>) -> Result { - match self.target { - Target::Object(id) => Ok(id), - Target::Symbolic(_) => { - let mut seen = BTreeSet::new(); - let cursor = &mut *self; - while let Some(next) = cursor.follow_packed(store, packed) { - let next = next?; - if seen.contains(&next.name) { - bail!( - corruption("Aborting symbolic reference cycle") - .with("path", store.reference_path(cursor.name.as_ref())?) - ); - } - *cursor = next; - seen.insert(cursor.name.clone()); - const MAX_REF_DEPTH: usize = 5; - if seen.len() == MAX_REF_DEPTH { - bail!(Message::new("Symbolic reference depth limit exceeded").with("max_depth", MAX_REF_DEPTH)); - } - } - let oid = self.target.try_id().expect("peeled ref").to_owned(); - Ok(oid) - } - } - } - - fn follow(&self, store: &file::Store) -> Option> { - let packed = match store.assure_packed_refs_uptodate() { - Ok(packed) => packed, - Err(err) => return Some(Err(err)), - }; - self.follow_packed(store, packed.as_ref().map(|b| &***b)) - } - - fn follow_packed(&self, store: &file::Store, packed: Option<&packed::Buffer>) -> Option> { - match &self.target { - Target::Object(_) => None, - Target::Symbolic(full_name) => match store.try_find_packed(full_name.as_ref(), packed) { - Ok(Some(next)) => Some(Ok(next)), - Ok(None) => Some(Err(file::find::NotFound { - name: full_name.as_bstr().to_owned(), - } - .raise())), - Err(err) => Some(Err(err)), - }, - } - } -} diff --git a/vendor/gix-ref/src/store/file/transaction/commit.rs b/vendor/gix-ref/src/store/file/transaction/commit.rs deleted file mode 100644 index 8707da4..0000000 --- a/vendor/gix-ref/src/store/file/transaction/commit.rs +++ /dev/null @@ -1,186 +0,0 @@ -use gix_error::{ErrorExt, Message, Result, ResultExt, message}; - -use crate::{ - Target, - store_impl::file::{Transaction, transaction::PackedRefs}, - transaction::{Change, LogChange, RefEdit, RefLog}, -}; - -impl Transaction<'_, '_> { - /// Make all [prepared][Transaction::prepare()] permanent and return the performed edits which represent the current - /// state of the affected refs in the ref store in that instant. Please note that the obtained edits may have been - /// adjusted to contain more dependent edits or additional information. - /// `committer` is used in the reflog and only if the reflog is actually written, which is why it is optional. Please note - /// that if `None` is passed and the reflog needs to be written, the operation will be aborted late and a few refs may have been - /// successfully committed already, making clear the non-atomic nature of multi-file edits. - /// - /// On error the transaction may have been performed partially, depending on the nature of the error, and no attempt to roll back - /// partial changes is made. - /// - /// In this stage, we perform the following operations: - /// - /// * update the ref log - /// * move updated refs into place - /// * delete reflogs and empty parent directories - /// * delete packed refs - /// * delete their corresponding reference (if applicable) - /// along with empty parent directories - /// - /// Note that transactions will be prepared automatically as needed. - /// Per-reference failures include [metadata](gix_error::Error::metadata()) `reference` (bytes), the affected name. - /// A missing reflog identity is identifiable as [`file::log::create_or_update::MissingCommitter`](crate::file::log::create_or_update::MissingCommitter). - pub fn commit<'a>(self, committer: impl Into>>) -> Result> { - self.commit_inner(committer.into()) - } - - /// Per-reference failures include [metadata](gix_error::Error::metadata()) `reference` (bytes), the affected name. - fn commit_inner(self, committer: Option>) -> Result> { - let mut updates = self.updates.expect("BUG: must call prepare before commit"); - let delete_loose_refs = matches!( - self.packed_refs, - PackedRefs::DeletionsAndNonSymbolicUpdatesRemoveLooseSourceReference(_) - ); - - // Perform updates first so live commits remain referenced - for change in &mut updates { - assert!(!change.update.deref, "Deref mode is turned into splits and turned off"); - match &change.update.change { - // reflog first, then reference - Change::Update { log, new, expected } => { - let lock = change.lock.take(); - let (update_ref, update_reflog) = match log.mode { - RefLog::Only => (false, true), - RefLog::AndReference => (true, true), - }; - if update_reflog { - let log_update = match new { - Target::Symbolic(_) => { - // Special HACK: no reflog for symref changes as there is no OID involved which the reflog needs. - // Unless, the ref is new and we can obtain a peeled id - // identified by the expectation of what could be there, as is the case when cloning. - match expected { - PreviousValue::ExistingMustMatch(Target::Object(oid)) => { - Some((Some(gix_hash::ObjectId::null(oid.kind())), oid)) - } - _ => None, - } - } - Target::Object(new_oid) => { - let previous = match expected { - // Here, this means that the ref already existed, and that it will receive (even transitively) - // the given value - PreviousValue::MustExistAndMatch(Target::Object(oid)) => Some(oid.to_owned()), - _ => None, - } - .or(change.leaf_referent_previous_oid); - Some((previous, new_oid)) - } - }; - if let Some((previous, new_oid)) = log_update { - let do_update = previous.as_ref() != Some(new_oid); - if do_update { - self.store - .reflog_create_or_append( - change.update.name.as_ref(), - previous, - new_oid, - committer, - log.message.as_ref(), - log.force_create_reflog, - ) - .or_raise(|| { - Message::new("Could not update reflog") - .with("reference", change.update.name.as_bstr()) - })?; - } - } - } - // Don't do anything else while keeping the lock after potentially updating the reflog. - // We delay deletion of the reference and dropping the lock to after the packed-refs were - // safely written. - if delete_loose_refs && matches!(new, Target::Object(_)) { - change.lock = lock; - continue; - } - if update_ref && let Some(Err(err)) = lock.map(gix_lock::Marker::commit) { - // TODO: when Kind::IsADirectory becomes stable, use that. - let err = if err.instance.resource_path().is_dir() { - gix_tempfile::remove_dir::empty_depth_first(err.instance.resource_path()) - .map_err(std::io::Error::other) - .and_then(|_| err.instance.commit().map_err(|err| err.error)) - .err() - } else { - Some(err.error) - }; - - if let Some(err) = err { - return Err(err.and_raise( - Message::new("Could not commit reference").with("reference", change.name()), - )); - } - } - } - Change::Delete { .. } => {} - } - } - - for change in &mut updates { - let (reflog_root, relative_name) = self.store.reflog_base_and_relative_path(change.update.name.as_ref())?; - match &change.update.change { - Change::Update { .. } => {} - Change::Delete { .. } => { - // Reflog deletion happens first in case it fails a ref without log is less terrible than - // a log without a reference. - let reflog_path = reflog_root.join(relative_name); - if let Err(err) = std::fs::remove_file(&reflog_path) { - if err.kind() != std::io::ErrorKind::NotFound { - return Err( - err.and_raise(Message::new("Could not delete reflog").with("reference", change.name())) - ); - } - } else { - gix_tempfile::remove_dir::empty_upward_until_boundary( - reflog_path.parent().expect("never without parent"), - &reflog_root, - ) - .ok(); - } - } - } - } - - if let Some(t) = self.packed_transaction { - t.commit() - .or_raise(|| message("Could not commit packed-ref transaction"))?; - // Always refresh ourselves right away to avoid races. We ignore errors as there may be many reasons this fails, and it's not - // critical to be done here. In other words, the pack may be refreshed at a later time and then it might work. - self.store.force_refresh_packed_buffer().ok(); - } - - for change in &mut updates { - let take_lock_and_delete = match &change.update.change { - Change::Update { - log: LogChange { mode, .. }, - new, - .. - } => delete_loose_refs && *mode == RefLog::AndReference && matches!(new, Target::Object(_)), - Change::Delete { log: mode, .. } => *mode == RefLog::AndReference, - }; - if take_lock_and_delete { - let lock = change.lock.take(); - let reference_path = self.store.reference_path(change.update.name.as_ref())?; - if let Err(err) = std::fs::remove_file(reference_path) - && err.kind() != std::io::ErrorKind::NotFound - { - return Err( - err.and_raise(Message::new("Could not delete reference").with("reference", change.name())) - ); - } - drop(lock); - } - } - Ok(updates.into_iter().map(|edit| edit.update).collect()) - } -} - -use crate::transaction::PreviousValue; diff --git a/vendor/gix-ref/src/store/file/transaction/mod.rs b/vendor/gix-ref/src/store/file/transaction/mod.rs deleted file mode 100644 index 6d1550c..0000000 --- a/vendor/gix-ref/src/store/file/transaction/mod.rs +++ /dev/null @@ -1,101 +0,0 @@ -use std::fmt::Formatter; - -use gix_hash::ObjectId; -use gix_object::bstr::BString; - -use crate::{ - store_impl::{file, file::Transaction}, - transaction::RefEdit, -}; - -/// How to handle packed refs during a transaction -#[derive(Default)] -pub enum PackedRefs<'a> { - /// Only propagate deletions of references. This is the default. - /// This means deleted references are removed from disk if they are loose and from the packed-refs file if they are present. - #[default] - DeletionsOnly, - /// Propagate deletions as well as updates to references which are peeled and contain an object id. - /// - /// This means deleted references are removed from disk if they are loose and from the packed-refs file if they are present, - /// while updates are also written into the loose file as well as into packed-refs, potentially creating an entry. - DeletionsAndNonSymbolicUpdates(Box), - /// Propagate deletions as well as updates to references which are peeled and contain an object id. Furthermore delete the - /// reference which is originally updated if it exists. If it doesn't, the new value will be written into the packed ref right away. - /// Note that this doesn't affect symbolic references at all, which can't be placed into packed refs. - /// - /// Thus, this is similar to `DeletionsAndNonSymbolicUpdates`, but removes the loose reference after the update, leaving only their copy - /// in `packed-refs`. - DeletionsAndNonSymbolicUpdatesRemoveLooseSourceReference(Box), -} - -#[derive(Debug)] -pub(in crate::store_impl::file) struct Edit { - update: RefEdit, - lock: Option, - /// Set if this update is coming from a symbolic reference and used to make it appear like it is the one that is handled, - /// instead of the referent reference. - parent_index: Option, - /// For symbolic refs, this is the previous OID to put into the reflog instead of our own previous value. It's the - /// peeled value of the leaf referent. - leaf_referent_previous_oid: Option, -} - -impl Edit { - fn name(&self) -> BString { - self.update.name.0.clone() - } -} - -impl std::borrow::Borrow for Edit { - fn borrow(&self) -> &RefEdit { - &self.update - } -} - -impl std::borrow::BorrowMut for Edit { - fn borrow_mut(&mut self) -> &mut RefEdit { - &mut self.update - } -} - -/// Edits -impl file::Store { - /// Open a transaction with the given `edits`, and determine how to fail if a `lock` cannot be obtained. - /// A snapshot of packed references will be obtained automatically if needed to fulfill this transaction - /// and will be provided as result of a successful transaction. Note that upon transaction failure, packed-refs - /// will never have been altered. - /// - /// The transaction inherits the parent namespace. - pub fn transaction(&self) -> Transaction<'_, '_> { - Transaction { - store: self, - packed_transaction: None, - updates: None, - packed_refs: PackedRefs::default(), - } - } -} - -impl<'p> Transaction<'_, 'p> { - /// Configure the way packed refs are handled during the transaction - pub fn packed_refs(mut self, packed_refs: PackedRefs<'p>) -> Self { - self.packed_refs = packed_refs; - self - } -} - -impl std::fmt::Debug for Transaction<'_, '_> { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Transaction") - .field("store", self.store) - .field("edits", &self.updates.as_ref().map(Vec::len)) - .finish_non_exhaustive() - } -} - -/// -pub mod prepare; - -/// -pub mod commit; diff --git a/vendor/gix-ref/src/store/file/transaction/prepare.rs b/vendor/gix-ref/src/store/file/transaction/prepare.rs deleted file mode 100644 index 87d697c..0000000 --- a/vendor/gix-ref/src/store/file/transaction/prepare.rs +++ /dev/null @@ -1,465 +0,0 @@ -use gix_error::{ClassificationMarker, ErrorExt, Message, Result, ResultExt, bail, message, not_found}; - -use crate::{ - FullName, FullNameRef, Reference, Target, packed, - packed::transaction::buffer_into_transaction, - store_impl::{ - file, - file::{ - Transaction, loose, - transaction::{Edit, PackedRefs}, - }, - }, - transaction::{Change, LogChange, PreviousValue, RefEdit, RefEditsExt, RefLog}, -}; - -impl Transaction<'_, '_> { - /// Read the current value of a reference from loose storage, falling back to packed refs. - /// - /// Must be called while holding the lock for `name` so the subsequent CAS check - /// compares against a value that cannot be changed concurrently. - fn read_existing_ref( - store: &file::Store, - name: &FullNameRef, - packed: Option<&packed::Buffer>, - ) -> Result> { - let loose = store - .ref_contents(name) - .or_raise(|| message("Could not read existing reference"))? - // Git permits replacing malformed loose references, but I/O errors must propagate. - .and_then(|buf| loose::Reference::try_from_path(name.to_owned(), &buf, store.object_hash).ok()) - .map(Reference::from); - match (loose, packed) { - (None, Some(packed)) => packed.try_find(name).map(|reference| reference.map(Into::into)), - (reference, _) => Ok(reference), - } - } - - fn lock_ref_and_apply_change( - store: &file::Store, - lock_fail_mode: gix_lock::acquire::Fail, - packed: Option<&packed::Buffer>, - change: &mut Edit, - direct_to_packed_refs: bool, - ) -> Result { - use std::io::Write; - assert!( - change.lock.is_none(), - "locks can only be acquired once and it's all or nothing" - ); - - // Reject Windows reserved device names before acquiring the lock. - // The lock file itself (e.g. `CON.lock`) is also a device name, - // so acquiring it would fail or open the device instead of - // returning the configured validation error. - store - .check_windows_device_name(change.update.name.as_ref()) - .or_raise(|| message("Invalid reference filename"))?; - - let lock = match &mut change.update.change { - Change::Delete { expected, .. } => { - let (base, relative_path) = store.reference_path_with_base(change.update.name.as_ref())?; - let lock = gix_lock::Marker::acquire_to_hold_resource( - base.join(relative_path.as_ref()), - lock_fail_mode, - Some(base.clone().into_owned()), - 0, - )?; - - let existing_ref = Self::read_existing_ref(store, change.update.name.as_ref(), packed)?; - - match (&expected, &existing_ref) { - (PreviousValue::MustNotExist, _) => { - panic!("BUG: MustNotExist constraint makes no sense if references are to be deleted") - } - (PreviousValue::ExistingMustMatch(_) | PreviousValue::Any, None) - | (PreviousValue::MustExist | PreviousValue::Any, Some(_)) => {} - (PreviousValue::MustExist | PreviousValue::MustExistAndMatch(_), None) => { - bail!(not_found("The reference to delete must exist")); - } - ( - PreviousValue::MustExistAndMatch(previous) | PreviousValue::ExistingMustMatch(previous), - Some(existing), - ) => { - let actual = existing.target.clone(); - if *previous != actual { - let context = message!("Expected reference content {previous}"); - return Err(ReferenceOutOfDate { - full_name: change.name(), - actual, - } - .and_raise(context)); - } - } - } - - // Keep the previous value for the caller and ourselves. Maybe they want to keep a log of sorts. - if let Some(existing) = existing_ref { - *expected = PreviousValue::MustExistAndMatch(existing.target); - } - - Some(lock) - } - Change::Update { expected, new, .. } => { - let (base, relative_path) = store.reference_path_with_base(change.update.name.as_ref())?; - let obtain_lock = || { - gix_lock::File::acquire_to_update_resource( - base.join(relative_path.as_ref()), - lock_fail_mode, - Some(base.clone().into_owned()), - 0, - ) - }; - let mut lock = obtain_lock()?; - - let existing_ref = Self::read_existing_ref(store, change.update.name.as_ref(), packed)?; - - match (&expected, &existing_ref) { - (PreviousValue::Any, _) - | (PreviousValue::MustExist, Some(_)) - | (PreviousValue::MustNotExist | PreviousValue::ExistingMustMatch(_), None) => {} - (PreviousValue::MustExist, None) => { - bail!(not_found("The reference to update must exist")); - } - (PreviousValue::MustNotExist, Some(existing)) => { - if existing.target != *new { - let context = message!("Expected the reference not to exist when writing {new}"); - return Err(MustNotExist { - full_name: change.name(), - actual: existing.target.clone(), - } - .and_raise(context)); - } - } - ( - PreviousValue::MustExistAndMatch(previous) | PreviousValue::ExistingMustMatch(previous), - Some(existing), - ) => { - if *previous != existing.target { - let actual = existing.target.clone(); - let context = message!("Expected reference content {previous}"); - return Err(ReferenceOutOfDate { - full_name: change.name(), - actual, - } - .and_raise(context)); - } - } - - (PreviousValue::MustExistAndMatch(previous), None) => { - bail!("The reference must exist with content {previous}".not_found()); - } - } - - fn new_would_change_existing(new: &Target, existing: &Target) -> (bool, bool) { - match (new, existing) { - (Target::Object(new), Target::Object(old)) => (old != new, false), - (Target::Symbolic(new), Target::Symbolic(old)) => (old != new, true), - (Target::Object(_), _) => (true, false), - (Target::Symbolic(_), _) => (true, true), - } - } - - let (is_effective, is_symbolic) = if let Some(existing) = existing_ref { - let (effective, is_symbolic) = new_would_change_existing(new, &existing.target); - *expected = PreviousValue::MustExistAndMatch(existing.target); - (effective, is_symbolic) - } else { - (true, matches!(new, Target::Symbolic(_))) - }; - - let keep_lock_for_loose_source_delete = direct_to_packed_refs && matches!(new, Target::Object(_)); - if (is_effective && !direct_to_packed_refs) || is_symbolic { - lock.with_mut(|file| match new { - Target::Object(oid) => writeln!(file, "{oid}"), - Target::Symbolic(name) => writeln!(file, "ref: {}", name.0), - }) - .or_raise(|| message("Could not write loose reference"))?; - Some(lock.close().or_raise(|| message("Could not close reference lock"))?) - } else if keep_lock_for_loose_source_delete { - Some(lock.close().or_raise(|| message("Could not close reference lock"))?) - } else { - None - } - } - }; - change.lock = lock; - Ok(()) - } -} - -impl Transaction<'_, '_> { - /// Prepare for calling [`commit(…)`][Transaction::commit()] in a way that can be rolled back perfectly. - /// - /// If the operation succeeds, the transaction can be committed or dropped to cause a rollback automatically. - /// Rollbacks happen automatically on failure and they tend to be perfect. - /// This method is idempotent. - /// - /// Failed edits identify the requested name in [metadata](gix_error::Error::metadata()) `reference` (bytes). - /// `referent` records the resolved name only when it differs from the requested name. - /// [`ReferenceOutOfDate`] and [`MustNotExist`] retain the actual target observed while holding the lock. - pub fn prepare( - self, - edits: impl IntoIterator, - ref_files_lock_fail_mode: gix_lock::acquire::Fail, - packed_refs_lock_fail_mode: gix_lock::acquire::Fail, - ) -> Result { - self.prepare_inner( - &mut edits.into_iter(), - ref_files_lock_fail_mode, - packed_refs_lock_fail_mode, - ) - } - - /// Failed edits include [metadata](gix_error::Error::metadata()) `reference` (requested name bytes) and `referent` - /// (resolved name bytes). - fn prepare_inner( - mut self, - edits: &mut dyn Iterator, - ref_files_lock_fail_mode: gix_lock::acquire::Fail, - packed_refs_lock_fail_mode: gix_lock::acquire::Fail, - ) -> Result { - assert!(self.updates.is_none(), "BUG: Must not call prepare(…) multiple times"); - let store = self.store; - let mut updates: Vec<_> = edits - .map(|update| Edit { - update, - lock: None, - parent_index: None, - leaf_referent_previous_oid: None, - }) - .collect(); - updates - .pre_process( - &mut |name| { - let symbolic_refs_are_never_packed = None; - store - .find_existing_inner(name, symbolic_refs_are_never_packed) - .map(|r| r.target) - .ok() - }, - &mut |idx, update| Edit { - update, - lock: None, - parent_index: Some(idx), - leaf_referent_previous_oid: None, - }, - ) - .or_raise(|| message("Could not preprocess reference edits"))?; - - let mut maybe_updates_for_packed_refs = match self.packed_refs { - PackedRefs::DeletionsAndNonSymbolicUpdates(_) - | PackedRefs::DeletionsAndNonSymbolicUpdatesRemoveLooseSourceReference(_) => Some(0_usize), - PackedRefs::DeletionsOnly => None, - }; - if maybe_updates_for_packed_refs.is_some() - || self.store.packed_refs_path().is_file() - || self.store.packed_refs_lock_path().is_file() - { - let mut edits_for_packed_transaction = Vec::::new(); - let mut needs_packed_refs_lookups = false; - for edit in &updates { - let log_mode = match edit.update.change { - Change::Update { - log: LogChange { mode, .. }, - .. - } => mode, - Change::Delete { log, .. } => log, - }; - if log_mode == RefLog::Only { - continue; - } - let name = match possibly_adjust_name_for_prefixes(edit.update.name.as_ref()) { - Some(n) => n, - None => continue, - }; - if let Some(ref mut num_updates) = maybe_updates_for_packed_refs - && let Change::Update { - new: Target::Object(_), .. - } = edit.update.change - { - edits_for_packed_transaction.push(RefEdit { - name, - ..edit.update.clone() - }); - *num_updates += 1; - continue; - } - match edit.update.change { - Change::Update { - expected: PreviousValue::ExistingMustMatch(_) | PreviousValue::MustExistAndMatch(_), - .. - } => needs_packed_refs_lookups = true, - Change::Delete { .. } => { - edits_for_packed_transaction.push(RefEdit { - name, - ..edit.update.clone() - }); - } - _ => { - needs_packed_refs_lookups = true; - } - } - } - - if !edits_for_packed_transaction.is_empty() || needs_packed_refs_lookups { - // What follows means that we will only create a transaction if we have to access packed refs for looking - // up current ref values, or that we definitely have a transaction if we need to make updates. Otherwise - // we may have no transaction at all which isn't required if we had none and would only try making deletions. - let packed_transaction: Option<_> = - if maybe_updates_for_packed_refs.unwrap_or(0) > 0 || self.store.packed_refs_lock_path().is_file() { - // We have to create a packed-ref even if it doesn't exist - self.store.packed_transaction(packed_refs_lock_fail_mode)?.into() - } else { - // A packed transaction is optional - we only have deletions that can't be made if - // no packed-ref file exists anyway - self.store - .assure_packed_refs_uptodate()? - .map(|p| { - buffer_into_transaction( - p, - packed_refs_lock_fail_mode, - self.store.precompose_unicode, - self.store.namespace.clone(), - ) - }) - .transpose()? - }; - if let Some(transaction) = packed_transaction { - self.packed_transaction = Some(match &mut self.packed_refs { - PackedRefs::DeletionsAndNonSymbolicUpdatesRemoveLooseSourceReference(f) - | PackedRefs::DeletionsAndNonSymbolicUpdates(f) => { - transaction.prepare(&mut edits_for_packed_transaction.into_iter(), &**f)? - } - PackedRefs::DeletionsOnly => transaction - .prepare(&mut edits_for_packed_transaction.into_iter(), &gix_object::find::Never)?, - }); - } - } - } - - for cid in 0..updates.len() { - let change = &mut updates[cid]; - if let Err(err) = Self::lock_ref_and_apply_change( - self.store, - ref_files_lock_fail_mode, - self.packed_transaction.as_ref().and_then(packed::Transaction::buffer), - change, - matches!( - self.packed_refs, - PackedRefs::DeletionsAndNonSymbolicUpdatesRemoveLooseSourceReference(_) - ), - ) { - let referent = change.name(); - let mut ref_name = referent.clone(); - let mut cursor = change.parent_index; - while let Some(parent_idx) = cursor { - let parent = &updates[parent_idx]; - ref_name = parent.name(); - cursor = parent.parent_index; - } - let differs = ref_name != referent; - let mut context = Message::new("Could not prepare reference edit").with("reference", ref_name); - if differs { - context = context.with("referent", referent); - } - return Err(err.and_raise(context)); - } - - // traverse parent chain from leaf/peeled ref and set the leaf previous oid accordingly - // to help with their reflog entries - if let (Some(crate::TargetRef::Object(oid)), Some(parent_idx)) = - (change.update.change.previous_value(), change.parent_index) - { - let oid = oid.to_owned(); - let mut parent_idx_cursor = Some(parent_idx); - while let Some(parent) = parent_idx_cursor.take().map(|idx| &mut updates[idx]) { - parent_idx_cursor = parent.parent_index; - parent.leaf_referent_previous_oid = Some(oid); - } - } - } - self.updates = Some(updates); - Ok(self) - } - - /// Rollback all intermediate state and return the `RefEdits` as we know them thus far. - /// - /// Note that they have been altered compared to what was initially provided as they have - /// been split and know about their current state on disk. - /// - /// # Note - /// - /// A rollback happens automatically as this instance is dropped as well. - pub fn rollback(self) -> Vec { - self.updates - .map(|updates| updates.into_iter().map(|u| u.update).collect()) - .unwrap_or_default() - } -} - -fn possibly_adjust_name_for_prefixes(name: &FullNameRef) -> Option { - match name.category_and_short_name() { - Some((c, sn)) => { - use crate::Category::*; - let sn = FullNameRef::new_unchecked(sn); - match c { - Bisect | Rewritten | WorktreePrivate | LinkedPseudoRef { .. } | PseudoRef | MainPseudoRef => None, - Tag | LocalBranch | RemoteBranch | Note => name.into(), - MainRef | LinkedRef { .. } => sn - .category() - .is_some_and(|cat| !cat.is_worktree_private()) - .then_some(sn), - } - .map(ToOwned::to_owned) - } - None => Some(name.to_owned()), // allow (uncategorized/very special) refs to be packed - } -} - -/// A reference changed since the caller obtained its expected value. -/// Retrying requires reading and reconciling the new value first. -#[derive(Debug)] -pub struct ReferenceOutOfDate { - /// The reference whose target did not match the expected value. - pub full_name: crate::bstr::BString, - /// The target observed while holding the reference lock. - pub actual: Target, -} - -impl std::fmt::Display for ReferenceOutOfDate { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "The reference {:?} changed to {}", self.full_name, self.actual) - } -} - -impl std::error::Error for ReferenceOutOfDate { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - Some(const { &ClassificationMarker::CONFLICT }) - } -} - -/// A reference exists with a different target although the edit required its absence. -#[derive(Debug)] -pub struct MustNotExist { - /// The reference which unexpectedly exists. - pub full_name: crate::bstr::BString, - /// The target observed while holding the reference lock. - pub actual: Target, -} - -impl std::fmt::Display for MustNotExist { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "The reference {:?} already exists with content {}", - self.full_name, self.actual - ) - } -} - -impl std::error::Error for MustNotExist { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - Some(const { &ClassificationMarker::CONFLICT }) - } -} diff --git a/vendor/gix-ref/src/store/general/handle/find.rs b/vendor/gix-ref/src/store/general/handle/find.rs deleted file mode 100644 index 3835ce8..0000000 --- a/vendor/gix-ref/src/store/general/handle/find.rs +++ /dev/null @@ -1,32 +0,0 @@ -use gix_error::{Result, ResultExt, message}; - -use crate::{PartialNameRef, Reference, store}; - -use crate::store::handle; - -impl store::Handle { - /// TODO: actually implement this with handling of the packed buffer. - pub fn try_find<'a, Name, E>(&self, partial: Name) -> Result> - where - Name: TryInto<&'a PartialNameRef, Error = E>, - std::result::Result<&'a PartialNameRef, E>: ResultExt, - { - let _name = partial.try_into().or_raise(|| message("Invalid reference name"))?; - match &self.state { - handle::State::Loose { .. } => { - todo!() - } - } - } -} - -impl store::Handle { - /// Similar to [`crate::file::Store::find()`] but a non-existing ref is treated as error. - pub fn find<'a, Name, E>(&self, _partial: Name) -> Result - where - Name: TryInto<&'a PartialNameRef, Error = E>, - std::result::Result<&'a PartialNameRef, E>: ResultExt, - { - todo!() - } -} diff --git a/vendor/gix-ref/src/store/general/handle/mod.rs b/vendor/gix-ref/src/store/general/handle/mod.rs deleted file mode 100644 index 6b33ebf..0000000 --- a/vendor/gix-ref/src/store/general/handle/mod.rs +++ /dev/null @@ -1,43 +0,0 @@ -#![allow(dead_code)] -use crate::{Namespace, store}; - -#[derive(Clone)] -pub(crate) enum State { - Loose { store: crate::file::Store }, -} - -impl crate::Store { - /// Return a new handle which sees all references if `namespace` is `None` or all read and write operations are limited - /// to the given `namespace` if `Some`. - pub fn to_handle(&self) -> store::Handle { - Self::new_handle_inner(&self.inner, None) - } - - /// As above, but supports a namespace to be set - pub fn to_handle_namespaced(&self, namespace: Option) -> store::Handle { - Self::new_handle_inner(&self.inner, namespace) - } - - fn new_handle_inner(state: &store::State, namespace: Option) -> store::Handle { - store::Handle { - state: match state { - store::State::Loose { store } => store::handle::State::Loose { - store: { - let mut store = store.clone(); - store.namespace = namespace; - store - }, - }, - }, - } - } -} - -/// -pub mod find; - -mod iter { - // impl store::Handle { - // pub fn iter<'p, 's>(&'s self, packed: Option<&'p packed::Buffer>) -> std::io::Result> { - // } -} diff --git a/vendor/gix-ref/src/store/general/init.rs b/vendor/gix-ref/src/store/general/init.rs deleted file mode 100644 index bb5dd14..0000000 --- a/vendor/gix-ref/src/store/general/init.rs +++ /dev/null @@ -1,38 +0,0 @@ -use gix_error::{Message, Result, ResultExt}; - -use std::path::PathBuf; - -use crate::file; - -#[expect( - dead_code, - reason = "callers still initialize file::Store directly while the general store awaits ref-table support" -)] -impl crate::Store { - /// Create a new store at the given location, typically the `.git/` directory. - /// Use [`at_opts()`](Self::at_opts) to adjust options. - /// - /// Note that if [`precompose_unicode`](crate::store::init::Options::precompose_unicode) is set in the options, - /// the `git_dir` is also expected to use precomposed unicode, or else some operations that strip prefixes will fail. - pub fn at(git_dir: PathBuf, object_hash: gix_hash::Kind) -> Result { - Self::at_opts(git_dir, object_hash, Default::default()) - } - - /// Create a new store at the given location, typically the `.git/` directory. - /// Use [`opts`](crate::store::init::Options) to adjust settings. - /// - /// Note that if [`precompose_unicode`](crate::store::init::Options::precompose_unicode) is set in the options, - /// the `git_dir` is also expected to use precomposed unicode, or else some operations that strip prefixes will fail. - /// - /// Errors include [metadata](gix_error::Error::metadata()) `path` (native path), the reference store directory. - pub fn at_opts(git_dir: PathBuf, object_hash: gix_hash::Kind, opts: crate::store::init::Options) -> Result { - // for now, just try to read the directory - later we will do that naturally as we have to figure out if it's a ref-table or not. - std::fs::read_dir(&git_dir) - .or_raise(|| Message::new("Could not access reference store").with("path", git_dir.as_path()))?; - Ok(crate::Store { - inner: crate::store::State::Loose { - store: file::Store::at_opts(git_dir, object_hash, opts), - }, - }) - } -} diff --git a/vendor/gix-ref/src/store/general/mod.rs b/vendor/gix-ref/src/store/general/mod.rs deleted file mode 100644 index 6adc4f6..0000000 --- a/vendor/gix-ref/src/store/general/mod.rs +++ /dev/null @@ -1 +0,0 @@ -mod init; diff --git a/vendor/gix-ref/src/store/mod.rs b/vendor/gix-ref/src/store/mod.rs deleted file mode 100644 index 6691098..0000000 --- a/vendor/gix-ref/src/store/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -/// -pub mod file; - -/// -pub mod packed; diff --git a/vendor/gix-ref/src/store/packed/buffer.rs b/vendor/gix-ref/src/store/packed/buffer.rs deleted file mode 100644 index 9d9d293..0000000 --- a/vendor/gix-ref/src/store/packed/buffer.rs +++ /dev/null @@ -1,106 +0,0 @@ -use std::path::PathBuf; - -use gix_error::{ErrorExt, Message, Result, ResultExt, corruption, message}; - -use crate::store_impl::packed; - -impl AsRef<[u8]> for packed::Buffer { - fn as_ref(&self) -> &[u8] { - &self.data.as_ref()[self.offset..] - } -} - -impl AsRef<[u8]> for packed::Backing { - fn as_ref(&self) -> &[u8] { - match self { - packed::Backing::InMemory(data) => data, - packed::Backing::Mapped(map) => map, - } - } -} - -/// Initialization -impl packed::Buffer { - fn open_with_backing(backing: packed::Backing, path: PathBuf, object_hash: gix_hash::Kind) -> Result { - let (backing, offset) = { - let (offset, sorted) = { - let mut input = backing.as_ref(); - if *input.first().unwrap_or(&b' ') == b'#' { - let header = packed::decode::header(&mut input).map_err(|()| { - corruption("The header could not be parsed, even though first line started with '#'").raise() - })?; - let offset = backing.as_ref().len() - input.len(); - (offset, header.sorted) - } else { - (0, false) - } - }; - - if !sorted { - // this implementation is likely slower than what git does, but it's less code, too. - let mut entries = packed::Iter::new(&backing.as_ref()[offset..], object_hash) - .or_raise(|| message("Could not iterate unsorted packed refs"))? - .collect::>>()?; - entries.sort_by_key(|e| e.name.as_bstr()); - let mut serialized = Vec::::new(); - for entry in entries { - serialized.extend_from_slice(entry.target); - serialized.push(b' '); - serialized.extend_from_slice(entry.name.as_bstr()); - serialized.push(b'\n'); - if let Some(object) = entry.object { - serialized.push(b'^'); - serialized.extend_from_slice(object); - serialized.push(b'\n'); - } - } - (packed::Backing::InMemory(serialized), 0) - } else { - (backing, offset) - } - }; - Ok(packed::Buffer { - offset, - data: backing, - path, - object_hash, - }) - } - - /// Open the file at `path`, parsing object ids as `object_hash`, and map it into memory if the file size is larger - /// than `use_memory_map_if_larger_than_bytes`. - /// - /// In order to allow fast lookups and optimizations, the contents of the packed refs must be sorted. - /// If that's not the case, they will be sorted on the fly with the data being written into a memory buffer. - /// - /// I/O failures include [metadata](gix_error::Error::metadata()) `path` (native path), the packed-refs file. - pub fn open(path: PathBuf, use_memory_map_if_larger_than_bytes: u64, object_hash: gix_hash::Kind) -> Result { - let backing = (|| -> std::io::Result { - Ok( - if std::fs::metadata(&path)?.len() <= use_memory_map_if_larger_than_bytes { - packed::Backing::InMemory(std::fs::read(&path)?) - } else { - packed::Backing::Mapped( - // SAFETY: Git replaces packed-refs rather than changing a mapped file in place. - #[expect(unsafe_code)] - unsafe { - memmap2::MmapOptions::new().map_copy_read_only(&std::fs::File::open(&path)?)? - }, - ) - }, - ) - })() - .or_raise(|| Message::new("Could not open packed refs").with("path", path.as_path()))?; - Self::open_with_backing(backing, path, object_hash) - } - - /// Open a buffer from `bytes`, which is the content of a typical `packed-refs` file, parsing object ids as - /// `object_hash`. - /// - /// In order to allow fast lookups and optimizations, the contents of the packed refs must be sorted. - /// If that's not the case, they will be sorted on the fly. - pub fn from_bytes(bytes: &[u8], object_hash: gix_hash::Kind) -> Result { - let backing = packed::Backing::InMemory(bytes.into()); - Self::open_with_backing(backing, PathBuf::from(""), object_hash) - } -} diff --git a/vendor/gix-ref/src/store/packed/decode/mod.rs b/vendor/gix-ref/src/store/packed/decode/mod.rs deleted file mode 100644 index 6cb6ca1..0000000 --- a/vendor/gix-ref/src/store/packed/decode/mod.rs +++ /dev/null @@ -1,155 +0,0 @@ -use gix_error::{ErrorExt, OptionExt, Result, ResultExt}; -use gix_object::bstr::{BStr, ByteSlice}; - -use crate::{parse, store_impl::packed}; - -#[derive(Debug, PartialEq, Eq)] -enum Peeled { - Unspecified, - Partial, - Fully, -} - -/// Information parsed from the header of a packed ref file -#[derive(Debug, PartialEq, Eq)] -pub struct Header { - peeled: Peeled, - pub sorted: bool, -} - -impl Default for Header { - fn default() -> Self { - Header { - peeled: Peeled::Unspecified, - sorted: false, - } - } -} - -/// Return the bytes before the next line ending as a [`BStr`]. -/// -/// On success, `input` is advanced past the line ending. The returned slice -/// does not include the line ending. -fn until_line_end_without_separator<'a>(input: &mut &'a [u8]) -> std::result::Result<&'a BStr, ()> { - let line_end = input.iter().position(|b| *b == b'\r' || *b == b'\n').ok_or(())?; - let out = input[..line_end].as_bstr(); - let mut maybe_start_of_newline = &input[line_end..]; - parse::newline(&mut maybe_start_of_newline)?; - *input = maybe_start_of_newline; - Ok(out) -} - -/// Parse a `packed-refs` header line. -/// -/// A valid header starts with `# pack-refs with: ` and ends with a line ending. -/// Known space-separated traits after the prefix populate the returned -/// [`Header`]: `peeled`, `fully-peeled`, and `sorted`. Unknown traits are -/// ignored. -/// -/// On success, `input` is advanced past the entire header line, including its -/// line ending. -pub fn header(input: &mut &[u8]) -> std::result::Result { - let Some(rest) = input.strip_prefix(b"# pack-refs with: ") else { - return Err(()); - }; - *input = rest; - let traits = until_line_end_without_separator(input)?; - let mut peeled = Peeled::Unspecified; - let mut sorted = false; - for token in traits.split_str(b" ") { - if token == b"fully-peeled" { - peeled = Peeled::Fully; - } else if token == b"peeled" { - peeled = Peeled::Partial; - } else if token == b"sorted" { - sorted = true; - } - } - Ok(Header { peeled, sorted }) -} - -/// Parse one packed reference entry and its optional peeled object line. -/// -/// The reference line has the form ` ` followed by a -/// line ending. If the following line starts with `^`, it is parsed as the -/// peeled object id for the returned [`packed::Reference`]. -/// Object ids are parsed according to `object_hash`. -/// -/// On success, `input` is advanced past the reference line and, if present, the -/// peeled object line. -pub fn reference<'a>(input: &mut &'a [u8], object_hash: gix_hash::Kind) -> Result> { - let invalid = || gix_error::corruption("Malformed packed reference"); - let target = parse::hex_hash(input, object_hash).map_err(|()| invalid().raise())?; - *input = input.strip_prefix(b" ").ok_or_raise(invalid)?; - let name = - <&crate::FullNameRef>::try_from(until_line_end_without_separator(input).map_err(|()| invalid().raise())?) - .or_raise(invalid)?; - - let object = if let Some(rest) = input.strip_prefix(b"^") { - *input = rest; - let object = parse::hex_hash(input, object_hash).map_err(|()| invalid().raise())?; - parse::newline(input).map_err(|()| invalid().raise())?; - Some(object) - } else { - None - }; - - Ok(packed::Reference { name, target, object }) -} - -/// Extract the name bytes from a packed-refs record without validating the -/// name as a [`FullNameRef`][crate::FullNameRef] or the hash as hex. -/// -/// Used by the binary search in [`super::Buffer::binary_search_by`], where -/// only the name bytes are needed for ordered comparison — the final match -/// is re-parsed through [`reference`] which validates fully. -/// -/// Returns `None` when the record does not have the expected -/// `` shape (including an empty name or input -/// shorter than the hash plus separator). -pub(crate) fn name_at_record_start(input: &[u8], object_hash: gix_hash::Kind) -> Option<&[u8]> { - let hex_len = object_hash.len_in_hex(); - if input.get(hex_len) != Some(&b' ') { - return None; - } - let after = &input[hex_len + 1..]; - let end = after.iter().position(|&b| b == b'\r' || b == b'\n')?; - if end == 0 { - return None; - } - if after[end] == b'\r' && after.get(end + 1) != Some(&b'\n') { - return None; - } - Some(&after[..end]) -} - -/// Return the byte offset of the packed-refs record containing `offset`. -/// -/// If `offset` points into a peeled object line, the returned offset points to -/// the owning reference record instead. -pub(crate) fn record_start_at_offset(input: &[u8], offset: usize) -> usize { - input[..offset] - .rfind(b"\n") - .and_then(|pos| { - let candidate = pos + 1; - let b = input.get(candidate)?; - if *b == b'^' { - input[..pos].rfind(b"\n").map(|pos| pos + 1) - } else { - Some(candidate) - } - }) - .unwrap_or(0) -} - -/// Return the packed-refs record containing `offset`. -/// -/// The returned slice begins at the owning reference line. It includes the -/// remainder of `input`, which is enough for callers that only need to parse -/// the first record. -pub(crate) fn record_at_offset(input: &[u8], offset: usize) -> &[u8] { - &input[record_start_at_offset(input, offset)..] -} - -#[cfg(test)] -mod tests; diff --git a/vendor/gix-ref/src/store/packed/decode/tests.rs b/vendor/gix-ref/src/store/packed/decode/tests.rs deleted file mode 100644 index 19aa445..0000000 --- a/vendor/gix-ref/src/store/packed/decode/tests.rs +++ /dev/null @@ -1,319 +0,0 @@ -mod reference { - use crate::{ - FullNameRef, - store_impl::{packed, packed::decode}, - }; - - const HASH_KIND: gix_hash::Kind = gix_hash::Kind::Sha1; - - #[test] - fn invalid() { - let mut input = b"# what looks like a comment".as_slice(); - assert!(decode::reference(&mut input, HASH_KIND).is_err()); - let mut input = b"^e9cdc958e7ce2290e2d7958cdb5aa9323ef35d37\n".as_slice(); - assert!(decode::reference(&mut input, HASH_KIND).is_err(), "lonely peel"); - let mut input = - b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa refs/heads/main\n".as_slice(); - assert!( - decode::reference(&mut input, gix_hash::Kind::Sha1).is_err(), - "sha1 refs reject sha256-sized ids" - ); - let mut input = b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa refs/heads/main\n".as_slice(); - assert!( - decode::reference(&mut input, gix_hash::Kind::Sha256).is_err(), - "sha256 refs reject sha1-sized ids" - ); - } - - #[test] - fn uppercase_hex() -> gix_testtools::TestResult { - let mut input: &[u8] = b"D53C4B0F91F1B29769C9430F2D1C0BCAB1170C75 refs/heads/uppercase -^E9CDC958E7CE2290E2D7958CDB5AA9323EF35D37\n"; - let parsed = decode::reference(&mut input, HASH_KIND).unwrap(); - - assert!(input.is_empty(), "exhausted"); - assert_eq!(parsed.name, FullNameRef::new_unchecked("refs/heads/uppercase".into())); - assert_eq!(parsed.target(), "d53c4b0f91f1b29769c9430f2d1c0bcab1170c75"); - assert_eq!(parsed.object(), "e9cdc958e7ce2290e2d7958cdb5aa9323ef35d37"); - Ok(()) - } - - #[test] - fn sha256_hex() -> gix_testtools::TestResult { - let mut input: &[u8] = b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa refs/heads/main\n"; - let parsed = decode::reference(&mut input, gix_hash::Kind::Sha256).unwrap(); - - assert!(input.is_empty(), "exhausted"); - assert_eq!(parsed.name, FullNameRef::new_unchecked("refs/heads/main".into())); - assert_eq!(parsed.target().kind(), gix_hash::Kind::Sha256); - Ok(()) - } - - #[test] - fn two_refs_in_a_row() -> gix_testtools::TestResult { - let mut input: &[u8] = b"d53c4b0f91f1b29769c9430f2d1c0bcab1170c75 refs/heads/alternates-after-packs-and-loose -^e9cdc958e7ce2290e2d7958cdb5aa9323ef35d37\neaae9c1bc723209d793eb93f5587fa2604d5cd92 refs/heads/avoid-double-lookup\n"; - let parsed = decode::reference(&mut input, HASH_KIND).unwrap(); - - assert_eq!( - parsed, - packed::Reference { - name: FullNameRef::new_unchecked("refs/heads/alternates-after-packs-and-loose".into()), - target: "d53c4b0f91f1b29769c9430f2d1c0bcab1170c75".into(), - object: Some("e9cdc958e7ce2290e2d7958cdb5aa9323ef35d37".into()) - } - ); - assert_eq!(parsed.target(), "d53c4b0f91f1b29769c9430f2d1c0bcab1170c75"); - assert_eq!(parsed.object(), "e9cdc958e7ce2290e2d7958cdb5aa9323ef35d37"); - - let parsed = decode::reference(&mut input, HASH_KIND).unwrap(); - assert!(input.is_empty(), "exhausted"); - assert_eq!( - parsed.name, - FullNameRef::new_unchecked("refs/heads/avoid-double-lookup".into()) - ); - assert_eq!(parsed.target, "eaae9c1bc723209d793eb93f5587fa2604d5cd92"); - assert!(parsed.object.is_none()); - Ok(()) - } -} - -mod record_at_offset { - use crate::{bstr::ByteSlice, store_impl::packed::decode}; - - const INPUT: &[u8] = b"1111111111111111111111111111111111111111 refs/heads/main -2222222222222222222222222222222222222222 refs/tags/v1 -^3333333333333333333333333333333333333333 -4444444444444444444444444444444444444444 refs/tags/v2\n"; - - fn offset_of(needle: &[u8]) -> usize { - INPUT.find(needle).expect("needle is present in input") - } - - #[test] - fn first_record_starts_at_zero() { - let offset = offset_of(b"refs/heads/main"); - assert_eq!( - decode::record_start_at_offset(INPUT, offset), - 0, - "offsets before the first line ending belong to the first record", - ); - assert_eq!(decode::record_at_offset(INPUT, offset), INPUT); - } - - #[test] - fn offset_on_later_record_finds_that_record_start() { - let record_start = offset_of(b"2222222222222222222222222222222222222222"); - let offset = offset_of(b"refs/tags/v1"); - assert_eq!( - decode::record_start_at_offset(INPUT, offset), - record_start, - "offsets within a later ref line resolve to that ref line", - ); - assert_eq!(decode::record_at_offset(INPUT, offset), &INPUT[record_start..]); - } - - #[test] - fn offset_on_peeled_line_finds_owning_record_start() { - let record_start = offset_of(b"2222222222222222222222222222222222222222"); - let offset = offset_of(b"^3333333333333333333333333333333333333333"); - assert_eq!( - decode::record_start_at_offset(INPUT, offset), - record_start, - "peeled lines are part of the preceding packed-ref record", - ); - assert_eq!(decode::record_at_offset(INPUT, offset), &INPUT[record_start..]); - } - - #[test] - fn offset_after_peeled_record_finds_next_record_start() { - let record_start = offset_of(b"4444444444444444444444444444444444444444"); - let offset = offset_of(b"refs/tags/v2"); - assert_eq!( - decode::record_start_at_offset(INPUT, offset), - record_start, - "the record after a peeled line starts at its own hash", - ); - assert_eq!(decode::record_at_offset(INPUT, offset), &INPUT[record_start..]); - } -} - -mod name_at_record_start { - use crate::store_impl::packed::decode; - - const SHA1: gix_hash::Kind = gix_hash::Kind::Sha1; - const SHA256: gix_hash::Kind = gix_hash::Kind::Sha256; - - #[test] - fn extracts_name_terminated_by_lf() { - let input = b"d53c4b0f91f1b29769c9430f2d1c0bcab1170c75 refs/heads/main\n"; - assert_eq!( - decode::name_at_record_start(input, SHA1), - Some(b"refs/heads/main".as_slice()), - ); - } - - #[test] - fn extracts_name_terminated_by_crlf() { - let input = b"d53c4b0f91f1b29769c9430f2d1c0bcab1170c75 refs/heads/main\r\n"; - assert_eq!( - decode::name_at_record_start(input, SHA1), - Some(b"refs/heads/main".as_slice()), - "CRLF is accepted, matching `parse::newline`", - ); - } - - #[test] - fn rejects_name_terminated_by_bare_cr() { - let input = b"d53c4b0f91f1b29769c9430f2d1c0bcab1170c75 refs/heads/main\rrefs/heads/next\n"; - assert_eq!( - decode::name_at_record_start(input, SHA1), - None, - "bare CR is rejected so the binary-search fast path agrees with the full record parser", - ); - } - - #[test] - fn does_not_validate_name_or_hash_contents() { - let input = b"ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ not!a!valid!refname\n"; - assert_eq!( - decode::name_at_record_start(input, SHA1), - Some(b"not!a!valid!refname".as_slice()), - "the binary-search comparator only needs name bytes; validation happens at the match site", - ); - } - - #[test] - fn rejects_missing_space_after_hash() { - let input = b"d53c4b0f91f1b29769c9430f2d1c0bcab1170c75-refs/heads/main\n"; - assert_eq!( - decode::name_at_record_start(input, SHA1), - None, - "lines that do not have the expected shape are reported as a parse failure", - ); - } - - #[test] - fn rejects_truncated_input_without_newline() { - let input = b"d53c4b0f91f1b29769c9430f2d1c0bcab1170c75 refs/heads/main"; - assert_eq!( - decode::name_at_record_start(input, SHA1), - None, - "missing line terminator means we can't bound the name", - ); - } - - #[test] - fn rejects_empty_name() { - let input = b"d53c4b0f91f1b29769c9430f2d1c0bcab1170c75 \n"; - assert_eq!( - decode::name_at_record_start(input, SHA1), - None, - "an empty name is treated as a shape mismatch so the slow path can surface it as a parse error", - ); - } - - #[test] - fn rejects_input_shorter_than_hash() { - assert_eq!(decode::name_at_record_start(b"", SHA1), None); - assert_eq!(decode::name_at_record_start(b"short", SHA1), None); - assert_eq!( - decode::name_at_record_start(b"d53c4b0f91f1b29769c9430f2d1c0bcab1170c75", SHA1), - None, - "input must be at least to be considered", - ); - } - - #[test] - fn sha256_record() { - let input = b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa refs/heads/main\n"; - assert_eq!( - decode::name_at_record_start(input, SHA256), - Some(b"refs/heads/main".as_slice()), - ); - assert_eq!( - decode::name_at_record_start(input, SHA1), - None, - "with a SHA1 hash kind the SHA256-sized prefix is not followed by a space at the expected offset", - ); - } - - #[test] - fn ignores_trailing_peeled_line() { - let input = - b"d53c4b0f91f1b29769c9430f2d1c0bcab1170c75 refs/tags/v1\n^e9cdc958e7ce2290e2d7958cdb5aa9323ef35d37\n"; - assert_eq!( - decode::name_at_record_start(input, SHA1), - Some(b"refs/tags/v1".as_slice()), - "only the first line is considered; the peeled line is parsed by the match-site decoder", - ); - } -} - -mod header { - use gix_object::bstr::ByteSlice; - - use crate::store_impl::packed::{ - decode, - decode::{Header, Peeled}, - }; - - #[test] - fn invalid() { - let mut input = b"# some user comment".as_slice(); - assert!(decode::header(&mut input).is_err(), "something the user put there"); - assert_eq!(input[0], b'#', "it consumed nothing"); - let mut input = b"# pack-refs: ".as_slice(); - assert!(decode::header(&mut input).is_err(), "looks right but isn't"); - let mut input = b" # pack-refs with: ".as_slice(); - assert!(decode::header(&mut input).is_err(), "does not start with #"); - } - - #[test] - fn valid_fully_peeled_stored() -> gix_testtools::TestResult { - let mut input: &[u8] = b"# pack-refs with: peeled fully-peeled sorted \nsomething else"; - let header = decode::header(&mut input).expect("valid input"); - - assert_eq!(input.as_bstr(), "something else", "remainder starts after newline"); - assert_eq!( - header, - Header { - peeled: Peeled::Fully, - sorted: true - } - ); - Ok(()) - } - - #[test] - fn valid_peeled_unsorted() -> gix_testtools::TestResult { - let mut input: &[u8] = b"# pack-refs with: peeled\n"; - let header = decode::header(&mut input).unwrap(); - - assert!(input.is_empty()); - assert_eq!( - header, - Header { - peeled: Peeled::Partial, - sorted: false - } - ); - Ok(()) - } - - #[test] - fn valid_empty() -> gix_testtools::TestResult { - let mut input: &[u8] = b"# pack-refs with: \n"; - let header = decode::header(&mut input).unwrap(); - - assert!(input.is_empty()); - assert_eq!( - header, - Header { - peeled: Peeled::Unspecified, - sorted: false - } - ); - Ok(()) - } -} diff --git a/vendor/gix-ref/src/store/packed/find.rs b/vendor/gix-ref/src/store/packed/find.rs deleted file mode 100644 index c7ffbb7..0000000 --- a/vendor/gix-ref/src/store/packed/find.rs +++ /dev/null @@ -1,122 +0,0 @@ -use gix_error::{Message, OptionExt, Result, ResultExt, message}; - -use gix_object::bstr::{BStr, BString}; - -use crate::{FullNameRef, PartialNameRef, store_impl::packed}; - -/// packed-refs specific functionality -impl packed::Buffer { - /// Find a reference with the given `name` and return it. - /// - /// Note that it will look it up verbatim and does not deal with namespaces or special prefixes like - /// `main-worktree/` or `worktrees//`, as this is left to the caller. - pub fn try_find<'a, Name, E>(&self, name: Name) -> Result>> - where - Name: TryInto<&'a PartialNameRef, Error = E>, - std::result::Result<&'a PartialNameRef, E>: ResultExt, - { - let name = name - .try_into() - .or_raise(|| message("The ref name or path is not a valid ref name"))?; - let mut buf = BString::default(); - for inbetween in &["", "tags", "heads", "remotes"] { - let (name, was_absolute) = if name.looks_like_full_name(false) { - let name = FullNameRef::new_unchecked(name.as_bstr()); - let name = match transform_full_name_for_lookup(name) { - None => return Ok(None), - Some(name) => name, - }; - (name, true) - } else { - let full_name = name.construct_full_name_ref(inbetween, &mut buf, false); - (full_name, false) - }; - match self.try_find_full_name(name)? { - Some(r) => return Ok(Some(r)), - None if was_absolute => return Ok(None), - None => continue, - } - } - Ok(None) - } - - /// Look up a resolved name. Decode failures include [metadata](gix_error::Error::metadata()) `name` (bytes), the - /// requested full name. - pub(crate) fn try_find_full_name(&self, name: &FullNameRef) -> Result>> { - match self.binary_search_by(name.as_bstr()) { - Ok(line_start) => { - let mut input = &self.as_ref()[line_start..]; - packed::decode::reference(&mut input, self.object_hash).map(Some) - } - Err((parse_failure, _)) => { - if parse_failure { - Err(gix_error::corruption("Malformed packed reference record").into()) - } else { - Ok(None) - } - } - } - .or_raise(|| Message::new("Could not decode packed reference").with("name", name.as_bstr())) - } - - /// Find a reference with the given `name` and return it. - pub fn find<'a, Name, E>(&self, name: Name) -> Result> - where - Name: TryInto<&'a PartialNameRef, Error = E>, - std::result::Result<&'a PartialNameRef, E>: ResultExt, - { - let name = name - .try_into() - .or_raise(|| message("The ref name or path is not a valid ref name"))?; - self.try_find::<_, std::convert::Infallible>(name)? - .ok_or_raise(|| crate::file::find::NotFound { - name: name.as_bstr().to_owned(), - }) - } - - /// Perform a binary search where `Ok(pos)` is the beginning of the line that matches `name` perfectly and `Err(pos)` - /// is the beginning of the line at which `name` could be inserted to still be in sort order. - pub(in crate::store_impl::packed) fn binary_search_by( - &self, - full_name: &BStr, - ) -> std::result::Result { - let a = self.as_ref(); - let mut encountered_parse_failure = false; - a.binary_search_by_key(&full_name.as_ref(), |b: &u8| { - let ofs = std::ptr::from_ref::(b) as usize - a.as_ptr() as usize; - let line = packed::decode::record_at_offset(a, ofs); - // The binary search only needs the name bytes for ordered - // comparison; skip ref-name and hex-hash validation here and let - // the final match site re-parse the record via `decode::reference` - // (which validates fully). This saves the `log₂(n)` per-query. - match packed::decode::name_at_record_start(line, self.object_hash) { - Some(name) => name, - None => { - encountered_parse_failure = true; - &[] - } - } - }) - .map(|pos| packed::decode::record_start_at_offset(a, pos)) - .map_err(|pos| { - ( - encountered_parse_failure, - packed::decode::record_start_at_offset(a, pos), - ) - }) - } -} - -pub(crate) fn transform_full_name_for_lookup(name: &FullNameRef) -> Option<&FullNameRef> { - match name.category_and_short_name() { - Some((c, sn)) => { - use crate::Category::*; - Some(match c { - MainRef | LinkedRef { .. } => FullNameRef::new_unchecked(sn), - Tag | RemoteBranch | LocalBranch | Bisect | Rewritten | Note => name, - MainPseudoRef | PseudoRef | LinkedPseudoRef { .. } | WorktreePrivate => return None, - }) - } - None => Some(name), - } -} diff --git a/vendor/gix-ref/src/store/packed/iter.rs b/vendor/gix-ref/src/store/packed/iter.rs deleted file mode 100644 index eb5281f..0000000 --- a/vendor/gix-ref/src/store/packed/iter.rs +++ /dev/null @@ -1,124 +0,0 @@ -use gix_error::{ErrorExt, Message, Result, corruption}; - -use gix_object::bstr::{BString, ByteSlice}; - -use crate::store_impl::{packed, packed::decode}; - -/// packed-refs specific functionality -impl packed::Buffer { - /// Return an iterator of references stored in this packed refs buffer, ordered by reference name. - /// Header failures include [metadata](gix_error::Error::metadata()) `input` (bytes), the first line without its - /// newline. - /// - /// # Note - /// - /// There is no namespace support in packed iterators. It can be emulated using `iter_prefixed(…)`. - pub fn iter(&self) -> Result> { - packed::Iter::new(self.as_ref(), self.object_hash) - } - - /// Return an iterator yielding only references matching the given prefix, ordered by reference name. - /// Header failures include [metadata](gix_error::Error::metadata()) `input` (bytes), the first selected line without - /// its newline. - pub fn iter_prefixed(&self, prefix: BString) -> Result> { - let first_record_with_prefix = self.binary_search_by(prefix.as_bstr()).unwrap_or_else(|(_, pos)| pos); - packed::Iter::new_with_prefix( - &self.as_ref()[first_record_with_prefix..], - self.object_hash, - Some(prefix), - ) - } -} - -impl<'a> Iterator for packed::Iter<'a> { - type Item = Result>; - - /// Decode failures include [metadata](gix_error::Error::metadata()) `line` (one-based line number within this - /// iterator's input) and `input` (line bytes). - fn next(&mut self) -> Option { - if self.cursor.is_empty() { - return None; - } - - let start = self.cursor; - match decode::reference(&mut self.cursor, self.object_hash) { - Ok(reference) => { - self.current_line += 1 + usize::from(reference.object.is_some()); - if let Some(ref prefix) = self.prefix - && !reference.name.as_bstr().starts_with_str(prefix) - { - self.cursor = &[]; - return None; - } - Some(Ok(reference)) - } - Err(err) => { - self.cursor = start; - let (failed_line, next_cursor) = self - .cursor - .find_byte(b'\n') - .map_or((self.cursor, &[][..]), |pos| self.cursor.split_at(pos + 1)); - self.cursor = next_cursor; - let line_number = self.current_line; - self.current_line += 1; - - Some(Err(err.and_raise( - Message::new("Invalid packed reference") - .with_input(failed_line.strip_suffix(b"\n").unwrap_or(failed_line)) - .with("line", line_number), - ))) - } - } - } -} - -impl<'a> packed::Iter<'a> { - /// Return a new iterator after successfully parsing the possibly existing first line of the given `packed` refs buffer, - /// parsing object ids as `object_hash`. - /// Header failures include [metadata](gix_error::Error::metadata()) `input` (bytes), the first line without its - /// newline. - pub fn new(packed: &'a [u8], object_hash: gix_hash::Kind) -> Result { - Self::new_with_prefix(packed, object_hash, None) - } - - /// Returns an iterator whose references will only match `prefix`. - /// - /// It assumes that the underlying `packed` buffer is indeed sorted and parses object ids as `object_hash`. - /// Header failures include [metadata](gix_error::Error::metadata()) `input` (bytes), the first line without its - /// newline. - pub(in crate::store_impl::packed) fn new_with_prefix( - packed: &'a [u8], - object_hash: gix_hash::Kind, - prefix: Option, - ) -> Result { - if packed.is_empty() { - Ok(packed::Iter { - cursor: packed, - object_hash, - prefix, - current_line: 1, - }) - } else if packed[0] == b'#' { - let mut input = packed; - decode::header(&mut input).map_err(|()| { - corruption("Invalid packed reference header") - .with_input(packed.lines().next().unwrap_or(packed)) - .raise() - })?; - let refs = input; - Ok(packed::Iter { - cursor: refs, - object_hash, - prefix, - current_line: 2, - }) - } else { - Ok(packed::Iter { - cursor: packed, - object_hash, - prefix, - current_line: 1, - }) - } - } -} diff --git a/vendor/gix-ref/src/store/packed/mod.rs b/vendor/gix-ref/src/store/packed/mod.rs deleted file mode 100644 index 24cfe40..0000000 --- a/vendor/gix-ref/src/store/packed/mod.rs +++ /dev/null @@ -1,100 +0,0 @@ -use std::path::PathBuf; - -use gix_hash::ObjectId; -use gix_object::bstr::{BStr, BString}; -use memmap2::Mmap; - -use crate::{FullNameRef, Namespace, file, transaction::RefEdit}; - -#[derive(Debug)] -enum Backing { - /// The buffer is loaded entirely in memory, along with the `offset` to the first record past the header. - InMemory(Vec), - /// The buffer is mapping the file on disk, along with the offset to the first record past the header - Mapped(Mmap), -} - -/// A buffer containing a packed-ref file that is either memory mapped or fully in-memory depending on a cutoff. -/// -/// The buffer is guaranteed to be sorted as per the packed-ref rules which allows some operations to be more efficient. -#[derive(Debug)] -pub struct Buffer { - data: Backing, - /// The hash kind to expect when parsing packed references. - object_hash: gix_hash::Kind, - /// The offset to the first record, how many bytes to skip past the header - offset: usize, - /// The path from which we were loaded - path: PathBuf, -} - -struct Edit { - inner: RefEdit, - peeled: Option, -} - -/// A transaction for editing packed references -pub(crate) struct Transaction { - buffer: Option, - edits: Option>, - lock: Option, - // It just has to be kept alive, hence no reads - closed_lock: Option, - precompose_unicode: bool, - /// The namespace to use when preparing or writing refs - namespace: Option, -} - -/// A reference as parsed from the `packed-refs` file -#[derive(Debug, PartialEq, Eq)] -pub struct Reference<'a> { - /// The validated full name of the reference. - pub name: &'a FullNameRef, - /// The target object id of the reference, hex encoded. - pub target: &'a BStr, - /// The fully peeled object id, hex encoded, that the ref is ultimately pointing to - /// i.e. when all indirections are removed. - pub object: Option<&'a BStr>, -} - -impl Reference<'_> { - /// Decode the target as object - pub fn target(&self) -> ObjectId { - gix_hash::ObjectId::from_hex(self.target).expect("parser validation") - } - - /// Decode the object this reference is ultimately pointing to. Note that this is - /// the [`target()`][Reference::target()] if this is not a fully peeled reference like a tag. - pub fn object(&self) -> ObjectId { - self.object.map_or_else( - || self.target(), - |id| ObjectId::from_hex(id).expect("parser validation"), - ) - } -} - -/// An iterator over references in a packed refs file -pub struct Iter<'a> { - /// The position at which to parse the next reference - cursor: &'a [u8], - /// The hash kind to expect when parsing packed references. - object_hash: gix_hash::Kind, - /// The next line, starting at 1 - current_line: usize, - /// If set, references returned will match the prefix, the first failed match will stop all iteration. - prefix: Option, -} - -mod decode; - -/// -pub mod iter; - -/// -pub mod buffer; - -/// -pub mod find; - -/// -pub mod transaction; diff --git a/vendor/gix-ref/src/store/packed/transaction.rs b/vendor/gix-ref/src/store/packed/transaction.rs deleted file mode 100644 index 7957f40..0000000 --- a/vendor/gix-ref/src/store/packed/transaction.rs +++ /dev/null @@ -1,280 +0,0 @@ -use std::{borrow::Cow, fmt::Formatter, io::Write, path::Path}; - -use gix_error::{Message, Result, ResultExt, bail, message, not_found}; - -use crate::{ - FullNameRef, Namespace, Target, file, - store_impl::{packed, packed::Edit}, - transaction::{Change, RefEdit}, -}; - -pub(crate) const HEADER_LINE: &[u8] = b"# pack-refs with: peeled fully-peeled sorted \n"; - -/// Access and instantiation -impl packed::Transaction { - pub(crate) fn new_from_pack_and_lock( - buffer: Option, - lock: gix_lock::File, - precompose_unicode: bool, - namespace: Option, - ) -> Self { - packed::Transaction { - buffer, - edits: None, - lock: Some(lock), - closed_lock: None, - precompose_unicode, - namespace, - } - } -} - -impl std::fmt::Debug for packed::Transaction { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.debug_struct("packed::Transaction") - .field("edits", &self.edits.as_ref().map(Vec::len)) - .field("lock", &self.lock) - .finish_non_exhaustive() - } -} - -/// Access -impl packed::Transaction { - /// Returns our packed buffer - pub fn buffer(&self) -> Option<&packed::Buffer> { - self.buffer.as_ref().map(|b| &***b) - } -} - -/// Lifecycle -impl packed::Transaction { - /// Prepare the transaction by checking all edits for applicability. - /// Use `objects` to access objects for the purpose of peeling them - this is only used if packed-refs are involved. - /// Object lookup failures include [metadata](gix_error::Error::metadata()) `object_id` (hex text) and `reference` - /// (name bytes). - /// Missing objects are classified as not found; lookup errors retain their own classifications. - pub fn prepare(mut self, edits: &mut dyn Iterator, objects: &dyn gix_object::Find) -> Result { - assert!(self.edits.is_none(), "BUG: cannot call prepare(…) more than once"); - let buffer = &self.buffer; - // Remove all edits which are deletions that aren't here in the first place - let mut prepared_edits = Vec::new(); - for mut edit in edits { - use gix_object::bstr::ByteSlice; - if self.precompose_unicode { - let precomposed = edit - .name - .0 - .to_str() - .ok() - .map(|name| gix_utils::str::precompose_path(Path::new(name).into())); - if let Some(Cow::Owned(precomposed)) = precomposed { - edit.name.0 = gix_path::into_bstr(precomposed)?.into_owned(); - } - } - if let Some(namespace) = &self.namespace { - edit.name = namespace.clone().into_namespaced_name(edit.name.as_ref()); - } - if matches!(edit.change, Change::Delete { .. }) - && buffer.as_ref().is_some_and(|b| b.find(edit.name.as_ref()).is_err()) - { - continue; - } - prepared_edits.push(Edit { - inner: edit, - peeled: None, - }); - } - let mut edits = prepared_edits; - - let mut buf = Vec::new(); - for edit in &mut edits { - if let Change::Update { - new: Target::Object(new), - .. - } = edit.inner.change - { - let mut next_id = new; - edit.peeled = loop { - let data = objects - .try_find(&next_id, &mut buf) - .or_raise(|| peel_reference_error(&next_id, edit.inner.name.as_ref()))?; - match data { - Some(gix_object::Data { - kind: gix_object::Kind::Tag, - data, - object_hash: hash_kind, - }) => { - next_id = gix_object::TagRefIter::from_bytes(data, hash_kind) - .target_id() - .or_raise(|| gix_error::message!("Couldn't get target object id from tag {next_id}")) - .or_raise(|| peel_reference_error(&next_id, edit.inner.name.as_ref()))?; - } - Some(_) => { - break if next_id == new { None } else { Some(next_id) }; - } - None => { - bail!( - not_found("Could not peel packed reference: object could not be found") - .with("object_id", next_id.to_string()) - .with("reference", edit.inner.name.as_bstr()) - ); - } - } - }; - } - } - - if edits.is_empty() { - self.closed_lock = self - .lock - .take() - .map(gix_lock::File::close) - .transpose() - .or_raise(|| message("Could not close unused packed reference lock"))?; - } else { - // NOTE that we don't do any additional checks here but apply all edits unconditionally. - // This is because this transaction system is internal and will be used correctly from the - // loose ref store transactions, which do the necessary checking. - } - self.edits = Some(edits); - Ok(self) - } - - /// Commit the prepared transaction. - /// - /// Please note that actual edits invalidated existing packed buffers. - /// Note: There is the potential to write changes into memory and return such a packed-refs buffer for reuse. - pub fn commit(self) -> Result { - let mut edits = self.edits.expect("BUG: cannot call commit() before prepare(…)"); - if edits.is_empty() { - return Ok(()); - } - - let mut file = self.lock.expect("a write lock for applying changes"); - let refs_sorted: Box>>> = match self.buffer.as_ref() { - Some(buffer) => Box::new(buffer.iter()?), - None => Box::new(std::iter::empty()), - }; - - let mut refs_sorted = refs_sorted.peekable(); - - edits.sort_by(|l, r| l.inner.name.as_bstr().cmp(r.inner.name.as_bstr())); - let mut peekable_sorted_edits = edits.iter().peekable(); - - file.with_mut(|f| f.write_all(HEADER_LINE)) - .or_raise(|| message("Could not write packed refs header"))?; - - let mut num_written_lines = 0; - loop { - match (refs_sorted.peek(), peekable_sorted_edits.peek()) { - (Some(Err(_)), _) => { - let err = refs_sorted.next().expect("next").expect_err("err"); - return Err(err); - } - (None, None) => { - break; - } - (Some(Ok(_)), None) => { - let pref = refs_sorted.next().expect("next").expect("no err"); - num_written_lines += 1; - file.with_mut(|out| write_packed_ref(out, pref)) - .or_raise(|| message("Could not write packed reference"))?; - } - (Some(Ok(pref)), Some(edit)) => { - use std::cmp::Ordering::*; - match pref.name.as_bstr().cmp(edit.inner.name.as_bstr()) { - Less => { - let pref = refs_sorted.next().expect("next").expect("valid"); - num_written_lines += 1; - file.with_mut(|out| write_packed_ref(out, pref)) - .or_raise(|| message("Could not write packed reference"))?; - } - Greater => { - let edit = peekable_sorted_edits.next().expect("next"); - file.with_mut(|out| write_edit(out, edit, &mut num_written_lines)) - .or_raise(|| message("Could not write packed reference edit"))?; - } - Equal => { - let _pref = refs_sorted.next().expect("next").expect("valid"); - let edit = peekable_sorted_edits.next().expect("next"); - file.with_mut(|out| write_edit(out, edit, &mut num_written_lines)) - .or_raise(|| message("Could not write packed reference edit"))?; - } - } - } - (None, Some(_)) => { - let edit = peekable_sorted_edits.next().expect("next"); - file.with_mut(|out| write_edit(out, edit, &mut num_written_lines)) - .or_raise(|| message("Could not write packed reference edit"))?; - } - } - } - - if num_written_lines == 0 { - std::fs::remove_file(file.resource_path()).or_raise(|| message("Could not delete empty packed refs"))?; - } else { - file.commit().or_raise(|| message("Could not commit packed refs"))?; - } - drop(refs_sorted); - Ok(()) - } -} - -/// The raised error's [metadata](gix_error::Error::metadata()) `object_id` (hex text) and `reference` (name bytes) -/// identify the object and packed reference being peeled. -fn peel_reference_error(object_id: &gix_hash::oid, reference: &FullNameRef) -> Message { - Message::new("Could not peel packed reference") - .with("object_id", object_id.to_string()) - .with("reference", reference.as_bstr()) -} - -fn write_packed_ref(out: &mut dyn std::io::Write, pref: packed::Reference<'_>) -> std::io::Result<()> { - write!(out, "{} ", pref.target)?; - out.write_all(pref.name.as_bstr())?; - out.write_all(b"\n")?; - if let Some(object) = pref.object { - writeln!(out, "^{object}")?; - } - Ok(()) -} - -fn write_edit(out: &mut dyn std::io::Write, edit: &Edit, lines_written: &mut i32) -> std::io::Result<()> { - match edit.inner.change { - Change::Delete { .. } => {} - Change::Update { - new: Target::Object(target_oid), - .. - } => { - write!(out, "{target_oid} ")?; - out.write_all(edit.inner.name.as_bstr())?; - out.write_all(b"\n")?; - if let Some(object) = edit.peeled { - writeln!(out, "^{object}")?; - } - *lines_written += 1; - } - Change::Update { - new: Target::Symbolic(_), - .. - } => unreachable!("BUG: packed refs cannot contain symbolic refs, catch that in prepare(…)"), - } - Ok(()) -} - -/// Convert this buffer to be used as the basis for a transaction. -pub(crate) fn buffer_into_transaction( - buffer: file::packed::SharedBufferSnapshot, - lock_mode: gix_lock::acquire::Fail, - precompose_unicode: bool, - namespace: Option, -) -> Result { - let lock = gix_lock::File::acquire_to_update_resource(&buffer.path, lock_mode, None, 0)?; - Ok(packed::Transaction { - buffer: Some(buffer), - lock: Some(lock), - closed_lock: None, - edits: None, - precompose_unicode, - namespace, - }) -} diff --git a/vendor/gix-ref/src/target.rs b/vendor/gix-ref/src/target.rs deleted file mode 100644 index 0fb4106..0000000 --- a/vendor/gix-ref/src/target.rs +++ /dev/null @@ -1,154 +0,0 @@ -use std::fmt; - -use gix_hash::{ObjectId, oid}; - -use crate::{FullName, FullNameRef, Kind, Target, TargetRef}; - -impl TargetRef<'_> { - /// Returns the kind of the target the ref is pointing to. - pub fn kind(&self) -> Kind { - match self { - TargetRef::Symbolic(_) => Kind::Symbolic, - TargetRef::Object(_) => Kind::Object, - } - } - /// Interpret this target as object id which maybe `None` if it is symbolic. - pub fn try_id(&self) -> Option<&oid> { - match self { - TargetRef::Symbolic(_) => None, - TargetRef::Object(oid) => Some(oid), - } - } - /// Interpret this target as object id or **panic** if it is symbolic. - pub fn id(&self) -> &oid { - match self { - TargetRef::Symbolic(_) => panic!("BUG: tries to obtain object id from symbolic target"), - TargetRef::Object(oid) => oid, - } - } - /// Interpret this target as name of the reference it points to which maybe `None` if it an object id. - pub fn try_name(&self) -> Option<&FullNameRef> { - match self { - TargetRef::Symbolic(name) => Some(name), - TargetRef::Object(_) => None, - } - } - /// Convert this instance into an owned version, without consuming it. - pub fn into_owned(self) -> Target { - self.into() - } -} - -impl Target { - /// Returns the kind of the target the ref is pointing to. - pub fn kind(&self) -> Kind { - match self { - Target::Symbolic(_) => Kind::Symbolic, - Target::Object(_) => Kind::Object, - } - } - - /// Return true if this is a peeled target with a null hash - pub fn is_null(&self) -> bool { - match self { - Target::Object(oid) => oid.is_null(), - Target::Symbolic(_) => false, - } - } - - /// Interpret this owned Target as shared Target - pub fn to_ref(&self) -> TargetRef<'_> { - match self { - Target::Object(oid) => TargetRef::Object(oid), - Target::Symbolic(name) => TargetRef::Symbolic(name.as_ref()), - } - } - - /// Interpret this target as object id which maybe `None` if it is symbolic. - pub fn try_id(&self) -> Option<&oid> { - match self { - Target::Symbolic(_) => None, - Target::Object(oid) => Some(oid), - } - } - /// Interpret this target as object id or panic if it is symbolic. - pub fn id(&self) -> &oid { - match self { - Target::Symbolic(_) => panic!("BUG: tries to obtain object id from symbolic target"), - Target::Object(oid) => oid, - } - } - /// Return the contained object id or panic - pub fn into_id(self) -> ObjectId { - match self { - Target::Symbolic(_) => panic!("BUG: expected peeled reference target but found symbolic one"), - Target::Object(oid) => oid, - } - } - - /// Return the contained object id if the target is peeled or itself if it is not. - pub fn try_into_id(self) -> Result { - match self { - Target::Symbolic(_) => Err(self), - Target::Object(oid) => Ok(oid), - } - } - /// Interpret this target as name of the reference it points to which maybe `None` if it an object id. - pub fn try_name(&self) -> Option<&FullNameRef> { - match self { - Target::Symbolic(name) => Some(name.as_ref()), - Target::Object(_) => None, - } - } -} - -impl<'a> From> for Target { - fn from(src: TargetRef<'a>) -> Self { - match src { - TargetRef::Object(oid) => Target::Object(oid.to_owned()), - TargetRef::Symbolic(name) => Target::Symbolic(name.to_owned()), - } - } -} - -impl<'a> PartialEq> for Target { - fn eq(&self, other: &TargetRef<'a>) -> bool { - match (self, other) { - (Target::Object(lhs), TargetRef::Object(rhs)) => lhs == rhs, - (Target::Symbolic(lhs), TargetRef::Symbolic(rhs)) => lhs.as_bstr() == rhs.as_bstr(), - _ => false, - } - } -} - -impl From for Target { - fn from(id: ObjectId) -> Self { - Target::Object(id) - } -} - -impl TryFrom for ObjectId { - type Error = Target; - - fn try_from(value: Target) -> Result { - match value { - Target::Object(id) => Ok(id), - Target::Symbolic(_) => Err(value), - } - } -} - -impl From for Target { - fn from(name: FullName) -> Self { - Target::Symbolic(name) - } -} - -impl fmt::Display for Target { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Target::Object(oid) => oid.fmt(f), - Target::Symbolic(name) => write!(f, "ref: {}", name.as_bstr()), - } - } -} diff --git a/vendor/gix-ref/src/transaction/ext.rs b/vendor/gix-ref/src/transaction/ext.rs deleted file mode 100644 index 8129a4e..0000000 --- a/vendor/gix-ref/src/transaction/ext.rs +++ /dev/null @@ -1,118 +0,0 @@ -use gix_object::bstr::BString; - -use crate::{ - PartialNameRef, - transaction::{Change, LogChange, PreviousValue, RefEdit, RefLog, Target}, -}; - -/// An extension trait to perform commonly used operations on edits across different ref stores. -pub trait RefEditsExt -where - T: std::borrow::Borrow + std::borrow::BorrowMut, -{ - /// Return true if each ref `name` has exactly one `edit` across multiple ref edits - fn assure_one_name_has_one_edit(&self) -> Result<(), BString>; - - /// Split all symbolic refs into updates for the symbolic ref as well as all their referents if the `deref` flag is enabled. - /// - /// Note no action is performed if deref isn't specified. - fn extend_with_splits_of_symbolic_refs( - &mut self, - find: &mut dyn FnMut(&PartialNameRef) -> Option, - make_entry: &mut dyn FnMut(usize, RefEdit) -> T, - ) -> Result<(), std::io::Error>; - - /// All processing steps in one and in the correct order. - /// - /// Users call this to assure derefs are honored and duplicate checks are done. - fn pre_process( - &mut self, - find: &mut dyn FnMut(&PartialNameRef) -> Option, - make_entry: &mut dyn FnMut(usize, RefEdit) -> T, - ) -> Result<(), std::io::Error> { - self.extend_with_splits_of_symbolic_refs(find, make_entry)?; - self.assure_one_name_has_one_edit().map_err(|name| { - std::io::Error::new( - std::io::ErrorKind::AlreadyExists, - format!("A reference named '{name}' has multiple edits"), - ) - }) - } -} - -impl RefEditsExt for Vec -where - E: std::borrow::Borrow + std::borrow::BorrowMut, -{ - fn assure_one_name_has_one_edit(&self) -> Result<(), BString> { - let mut names: Vec<_> = self.iter().map(|e| &e.borrow().name).collect(); - names.sort(); - match names.windows(2).find(|v| v[0] == v[1]) { - Some(name) => Err(name[0].as_bstr().to_owned()), - None => Ok(()), - } - } - - fn extend_with_splits_of_symbolic_refs( - &mut self, - find: &mut dyn FnMut(&PartialNameRef) -> Option, - make_entry: &mut dyn FnMut(usize, RefEdit) -> E, - ) -> Result<(), std::io::Error> { - let mut new_edits = Vec::new(); - let mut first = 0; - let mut round = 1; - loop { - for (eid, edit) in self[first..].iter_mut().enumerate().map(|(eid, v)| (eid + first, v)) { - let edit = edit.borrow_mut(); - if !edit.deref { - continue; - } - - // we can't tell what happened and we are here because it's a non-existing ref or an invalid one. - // In any case, we don't want the following algorithms to try dereffing it and assume they deal with - // broken refs gracefully. - edit.deref = false; - if let Some(Target::Symbolic(referent)) = find(edit.name.as_ref().as_partial_name()) { - new_edits.push(make_entry( - eid, - match &mut edit.change { - Change::Delete { - expected: previous, - log: mode, - } => { - let current_mode = *mode; - *mode = RefLog::Only; - RefEdit::delete_with_log(referent, previous.clone(), current_mode).with_deref(true) - } - Change::Update { log, expected, new } => { - let current = std::mem::replace( - log, - LogChange { - message: log.message.clone(), - mode: RefLog::Only, - force_create_reflog: log.force_create_reflog, - }, - ); - let next = std::mem::replace(expected, PreviousValue::Any); - RefEdit::update_with_log(referent, new.clone(), next, current).with_deref(true) - } - }, - )); - } - } - if new_edits.is_empty() { - break Ok(()); - } - if round == 5 { - break Err(std::io::Error::new( - std::io::ErrorKind::WouldBlock, - format!("Could not follow all splits after {round} rounds, assuming reference cycle"), - )); - } - round += 1; - first = self.len(); - - self.append(&mut new_edits); - } - } -} diff --git a/vendor/gix-ref/src/transaction/mod.rs b/vendor/gix-ref/src/transaction/mod.rs deleted file mode 100644 index cdb597a..0000000 --- a/vendor/gix-ref/src/transaction/mod.rs +++ /dev/null @@ -1,199 +0,0 @@ -//! **Transactions** are the only way make changes to the ref store in order to increase the chance of consistency in a multi-threaded -//! environment. -//! -//! Transactions currently allow to… -//! -//! * create or update reference -//! * delete references -//! -//! The following guarantees are made: -//! -//! * transactions are prepared which is when other writers are prevented from changing them -//! - errors during preparations will cause a perfect rollback -//! * prepared transactions are committed to finalize the change -//! - errors when committing while leave the ref store in an inconsistent, but operational state. -use gix_object::bstr::BString; - -use crate::{FullName, Target}; - -/// A change to the reflog. -#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] -pub struct LogChange { - /// How to treat the reference log. - pub mode: RefLog, - /// If set, create a reflog even though it would otherwise not be the case as prohibited by general rules. - /// Note that ref-log writing might be prohibited in the entire repository which is when this flag has no effect either. - pub force_create_reflog: bool, - /// The message to put into the reference log. It must be a single line, hence newlines are forbidden. - /// The string can be empty to indicate there should be no message at all. - pub message: BString, -} - -impl Default for LogChange { - fn default() -> Self { - LogChange { - mode: RefLog::AndReference, - force_create_reflog: false, - message: Default::default(), - } - } -} - -/// The desired value of an updated value -#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] -pub enum PreviousValue { - /// No requirements are made towards the current value, and the new value is set unconditionally. - Any, - /// The reference must exist and may have any value. - MustExist, - /// Create the ref only, hence the reference must not exist. - MustNotExist, - /// The ref _must_ exist and have the given value. - MustExistAndMatch(Target), - /// The ref _may_ exist and have the given value, or may not exist at all. - ExistingMustMatch(Target), -} - -/// A description of an edit to perform. -#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] -pub enum Change { - /// If previous is not `None`, the ref must exist and its `oid` must agree with the `previous`, and - /// we function like `update`. - /// Otherwise it functions as `create-or-update`. - Update { - /// The desired change to the reference log. - log: LogChange, - /// The expected value already present in the reference. - /// If a ref was existing previously this field will be overwritten with `MustExistAndMatch(actual_value)` for use after - /// the transaction was committed successfully. - expected: PreviousValue, - /// The new state of the reference, either for updating an existing one or creating a new one. - new: Target, - }, - /// Delete a reference and optionally check if `previous` is its content. - Delete { - /// The expected value of the reference, with the `MustNotExist` variant being invalid. - /// - /// If a previous ref existed, this value will be filled in automatically as `MustExistAndMatch(actual_value)` and - /// can be accessed if the transaction was committed successfully. - expected: PreviousValue, - /// How to treat the reference log during deletion. - log: RefLog, - }, -} - -impl Change { - /// Return references to values that are the new value after the change is applied, if this is an update. - pub fn new_value(&self) -> Option> { - match self { - Change::Update { new, .. } => new.to_ref().into(), - Change::Delete { .. } => None, - } - } - - /// Return references to values that are in common between all variants and denote the previous observed value. - pub fn previous_value(&self) -> Option> { - match self { - Change::Update { - expected: PreviousValue::MustExistAndMatch(previous) | PreviousValue::ExistingMustMatch(previous), - .. - } - | Change::Delete { - expected: PreviousValue::MustExistAndMatch(previous) | PreviousValue::ExistingMustMatch(previous), - .. - } => previous, - _ => return None, - } - .to_ref() - .into() - } -} - -/// A reference that is to be changed -#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] -pub struct RefEdit { - /// The change itself - pub change: Change, - /// The name of the reference to apply the change to - pub name: FullName, - /// If set, symbolic references identified by `name` will be dereferenced to have the `change` applied to their target. - /// This flag has no effect if the reference isn't symbolic. - pub deref: bool, -} - -/// Lifecycle -impl RefEdit { - /// Create an edit that applies `change` to `name` without dereferencing symbolic references. - pub fn new(name: FullName, change: Change) -> Self { - RefEdit { - change, - name, - deref: false, - } - } - - /// Create an update that sets `name` to `new` if its current state satisfies `expected`, recording - /// `reflog_message` with standard reference-log handling and without dereferencing symbolic references. - pub fn update( - name: FullName, - new: impl Into, - expected: PreviousValue, - reflog_message: impl Into, - ) -> Self { - RefEdit::update_with_log( - name, - new, - expected, - LogChange { - message: reflog_message.into(), - ..Default::default() - }, - ) - } - - /// Create an update that sets `name` to `new` if its current state satisfies `expected`, using `log` to configure - /// reference-log handling and without dereferencing symbolic references. - pub fn update_with_log(name: FullName, new: impl Into, expected: PreviousValue, log: LogChange) -> Self { - RefEdit::new( - name, - Change::Update { - log, - expected, - new: new.into(), - }, - ) - } - - /// Create a deletion of `name` and its reference log if its current state satisfies `expected`, without - /// dereferencing symbolic references. - pub fn delete(name: FullName, expected: PreviousValue) -> Self { - RefEdit::delete_with_log(name, expected, RefLog::AndReference) - } - - /// Create a deletion of `name` if its current state satisfies `expected`, using `log` to configure reference-log - /// handling and without dereferencing symbolic references. - pub fn delete_with_log(name: FullName, expected: PreviousValue, log: RefLog) -> Self { - RefEdit::new(name, Change::Delete { expected, log }) - } -} - -/// Builders -impl RefEdit { - /// Set whether symbolic references are dereferenced before applying the edit. - pub fn with_deref(mut self, deref: bool) -> Self { - self.deref = deref; - self - } -} - -/// The way to deal with the Reflog in deletions. -#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] -pub enum RefLog { - /// Delete or update the reference and the log - AndReference, - /// Delete or update only the reflog - Only, -} - -mod ext; -pub use ext::RefEditsExt; diff --git a/vendor/gix-submodule/Cargo.toml b/vendor/gix-submodule/Cargo.toml deleted file mode 100644 index c64ce60..0000000 --- a/vendor/gix-submodule/Cargo.toml +++ /dev/null @@ -1,33 +0,0 @@ - -[package] -name = "gix-submodule" -version = "0.35.0" -repository = "https://github.com/GitoxideLabs/gitoxide" -license = "MIT OR Apache-2.0" -description = "A crate of the gitoxide project dealing git submodules" -authors = ["Sebastian Thiel "] -edition = "2024" -rust-version = "1.88" -include = ["/src/**/*", "/LICENSE-*"] - -[lib] -doctest = false - -[features] -## Enable support for the SHA-1 hash by forwarding the feature to dependencies. -sha1 = ["gix-refspec/sha1"] -## Enable support for the SHA-256 hash by forwarding the feature to dependencies. -sha256 = ["gix-refspec/sha256"] - -[dependencies] -gix-pathspec = { version = "^0.21.0", git = "https://github.com/GitoxideLabs/gitoxide", rev = "1d7bac742f70b72ddda0c7294f4a97566b8db596" } -gix-error = { version = "^0.4.0", git = "https://github.com/GitoxideLabs/gitoxide", rev = "1d7bac742f70b72ddda0c7294f4a97566b8db596", features = ["bstr"] } -gix-refspec = { version = "^0.46.0", git = "https://github.com/GitoxideLabs/gitoxide", rev = "1d7bac742f70b72ddda0c7294f4a97566b8db596" } -gix-config = { version = "^0.61.0", git = "https://github.com/GitoxideLabs/gitoxide", rev = "1d7bac742f70b72ddda0c7294f4a97566b8db596" } -gix-path = { version = "^0.13.0", git = "https://github.com/GitoxideLabs/gitoxide", rev = "1d7bac742f70b72ddda0c7294f4a97566b8db596" } -gix-url = { version = "^0.39.0", git = "https://github.com/GitoxideLabs/gitoxide", rev = "1d7bac742f70b72ddda0c7294f4a97566b8db596" } - -bstr = { version = "1.12.0", default-features = false } - -[package.metadata.docs.rs] -features = ["sha1"] diff --git a/vendor/gix-submodule/LICENSE-APACHE b/vendor/gix-submodule/LICENSE-APACHE deleted file mode 100644 index f433b1a..0000000 --- a/vendor/gix-submodule/LICENSE-APACHE +++ /dev/null @@ -1,177 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS diff --git a/vendor/gix-submodule/LICENSE-MIT b/vendor/gix-submodule/LICENSE-MIT deleted file mode 100644 index 969d061..0000000 --- a/vendor/gix-submodule/LICENSE-MIT +++ /dev/null @@ -1,17 +0,0 @@ -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/vendor/gix-submodule/README.pygix.md b/vendor/gix-submodule/README.pygix.md deleted file mode 100644 index 07e5a28..0000000 --- a/vendor/gix-submodule/README.pygix.md +++ /dev/null @@ -1,17 +0,0 @@ -# Temporary upstream prerequisite - -Source matches `gix-submodule/src` at Gitoxide `pygix` HEAD -`e4d2b313b840ea0e00453791882dbc6eb7c636f6`. Its prerequisite commit is -`e4d2b313b840ea0e00453791882dbc6eb7c636f6`, based on public revision -`1d7bac742f70b72ddda0c7294f4a97566b8db596`. - -The patch queries submodule fields and activity using literal byte names -without formatting them through UTF-8. -The prerequisite remains on the prepared local branch for Sebastian Thiel's -review. This reintegration did not modify or push that branch. - -The source includes the updated base's security-audit fixes. Cargo sibling -dependencies use the pinned public Git source; workspace lints and test-only -dependencies are omitted. This source copy makes packaged builds independent -of the local checkout and unpublished revisions. Remove it after the fix lands -upstream and update the public pin. Original source licenses apply. diff --git a/vendor/gix-submodule/src/access.rs b/vendor/gix-submodule/src/access.rs deleted file mode 100644 index 9d93e90..0000000 --- a/vendor/gix-submodule/src/access.rs +++ /dev/null @@ -1,234 +0,0 @@ -use gix_error::Result; -use std::{collections::HashSet, path::Path}; - -use bstr::{BStr, BString, ByteSlice}; -use gix_error::{OptionExt, ResultExt, bail, message, validation}; - -use crate::{ - File, IsActivePlatform, - config::{Branch, FetchRecurse, Ignore, Update}, -}; - -/// High-Level Access -/// -/// Note that all methods perform validation of the requested value and report issues right away. -/// If a bypass is needed, use [`config()`](File::config()) for direct access. -impl File { - /// Return the underlying configuration file. - /// - /// Note that it might have been merged with values from another configuration file and may - /// thus not be accurately reflecting that state of a `.gitmodules` file anymore. - pub fn config(&self) -> &gix_config::File { - &self.config - } - - /// Return the path at which the `.gitmodules` file lives, if it is known. - pub fn config_path(&self) -> Option<&Path> { - self.config.sections().filter_map(|s| s.meta().path.as_deref()).next() - } - - /// Return the unvalidated names of the submodules for which configuration is present. - /// - /// Note that these exact names have to be used for querying submodule values. - pub fn names(&self) -> impl Iterator { - let mut seen = HashSet::<&BStr>::default(); - self.config - .sections_by_name("submodule") - .into_iter() - .flatten() - .filter_map(move |s| { - s.header() - .subsection_name() - .filter(|_| std::ptr::eq(s.meta(), self.config.meta())) - .filter(|name| seen.insert(*name)) - }) - } - - /// Similar to [Self::is_active_platform()], but automatically applies it to each name to learn if a submodule is active or not. - pub fn names_and_active_state<'a>( - &'a self, - config: &'a gix_config::File, - defaults: gix_pathspec::Defaults, - attributes: &'a mut ( - dyn FnMut( - &BStr, - gix_pathspec::attributes::glob::pattern::Case, - bool, - &mut gix_pathspec::attributes::search::Outcome, - ) -> bool - + 'a - ), - ) -> Result)> + 'a> { - let mut platform = self.is_active_platform(config, defaults)?; - let iter = self - .names() - .map(move |name| (name, platform.is_active(config, name, attributes))); - Ok(iter) - } - - /// Return a platform which allows to check if a submodule name is active or inactive. - /// Use `defaults` for parsing the pathspecs used to later match on names via `submodule.active` configuration retrieved from `config`. - /// - /// All `submodule.active` pathspecs are considered to be top-level specs and match the name of submodules, which are active - /// on inclusive match. - /// The full algorithm is described as [hierarchy of rules](https://git-scm.com/docs/gitsubmodules#_active_submodules). - pub fn is_active_platform( - &self, - config: &gix_config::File, - defaults: gix_pathspec::Defaults, - ) -> Result { - let search = config - .strings("submodule.active") - .map(|patterns| -> Result<_> { - let patterns = patterns - .into_iter() - .map(|pattern| gix_pathspec::parse(&pattern, defaults)) - .collect::, _>>()?; - gix_pathspec::Search::from_specs(patterns, None, std::path::Path::new("")) - }) - .transpose()?; - Ok(IsActivePlatform { search }) - } - - /// Given the `relative_path` (as seen from the root of the worktree) of a submodule with possibly platform-specific - /// component separators, find the submodule's name associated with this path, or `None` if none was found. - /// - /// Note that this does a linear search and compares `relative_path` in a normalized form to the same form of the path - /// associated with the submodule. - pub fn name_by_path(&self, relative_path: &BStr) -> Option<&BStr> { - self.names() - .filter_map(|n| self.path(n).ok().map(|p| (n, p))) - .find_map(|(n, p)| (p == relative_path).then_some(n)) - } -} - -/// Per-Submodule Access -impl File { - /// Return the path relative to the root directory of the working tree at which the submodule is expected to be checked out. - /// It's an error if the path doesn't exist as it's the only way to associate a path in the index with additional submodule - /// information, like the URL to fetch from. - /// Invalid path bytes are stored as `input` in [`gix_error::Message::values`]. - /// After [wrapping](gix_error::Error::from_error()), inspect them with [metadata](gix_error::Error::metadata()). - /// - /// ### Deviation - /// - /// Git currently allows absolute paths to be used when adding submodules, but fails later as it can't find the submodule by - /// relative path anymore. Let's play it safe here. - pub fn path(&self, name: &BStr) -> Result { - let path_bstr = self.config.string_by("submodule", Some(name), "path").ok_or_raise(|| { - message!("The submodule '{name}' was missing its 'path' field or it was empty").validation() - })?; - if path_bstr.is_empty() { - bail!("The submodule '{name}' was missing its 'path' field or it was empty".validation()); - } - let path = gix_path::from_bstr(path_bstr.as_bstr())?; - if path.is_absolute() { - bail!(validation(format!("The path of submodule '{name}' needs to be relative")).with_input(path_bstr)); - } - if gix_path::normalize(path, "".as_ref()).is_none() { - bail!(validation("The path would lead outside of the repository worktree").with_input(path_bstr)); - } - Ok(path_bstr) - } - - /// Retrieve the `url` field of the submodule named `name`. It's an error if it doesn't exist or is empty. - /// Parse failures include the URL bytes as `input` [metadata](gix_error::Error::metadata()). - pub fn url(&self, name: &BStr) -> Result { - let url = self.config.string_by("submodule", Some(name), "url").ok_or_raise(|| { - message!("The submodule '{name}' was missing its 'url' field or it was empty").validation() - })?; - - if url.is_empty() { - bail!("The submodule '{name}' was missing its 'url' field or it was empty".validation()); - } - gix_url::Url::from_bytes(url.as_ref()) - .or_raise(|| validation(format!("The url of submodule '{name}' could not be parsed")).with_input(url)) - } - - /// Retrieve the `update` field of the submodule named `name`, if present. - /// Invalid value or command bytes are stored as `input` in [`gix_error::Message::values`]. - /// After [wrapping](gix_error::Error::from_error()), inspect them with [metadata](gix_error::Error::metadata()). - pub fn update(&self, name: &BStr) -> Result> { - let mut value_is_from_modules_file = None; - let our_meta = self.config.meta(); - let value: Update = match self.config.string_filter_by("submodule", Some(name), "update", |meta| { - value_is_from_modules_file = Some(std::ptr::eq(meta, our_meta)); - true - }) { - Some(v) => v.as_bstr().try_into().map_err(|()| { - message!("The 'update' field of submodule '{name}' was invalid") - .with_input(v) - .validation_error() - })?, - None => return Ok(None), - }; - - if let Update::Command(cmd) = &value - && value_is_from_modules_file.unwrap_or_default() - { - bail!( - "The 'update' field of submodule '{name}' tried to set a command to be shared" - .validation() - .with_input(cmd.to_owned()) - ); - } - Ok(Some(value)) - } - - /// Retrieve the `branch` field of the submodule named `name`, or `None` if unset. - /// - /// Note that `Default` is implemented for [`Branch`]. - /// Parse failures include the branch bytes as `input` [metadata](gix_error::Error::metadata()). - pub fn branch(&self, name: &BStr) -> Result> { - let branch = match self.config.string_by("submodule", Some(name), "branch") { - Some(v) => v, - None => return Ok(None), - }; - - Branch::try_from(branch.as_ref()).map(Some).or_raise(|| { - message!("The 'branch' field of submodule '{name}' couldn't be turned into a valid fetch refspec") - .validation() - .with_input(branch) - }) - } - - /// Retrieve the `fetchRecurseSubmodules` field of the submodule named `name`, or `None` if unset. - /// - /// Note that if it's unset, it should be retrieved from `fetch.recurseSubmodules` in the configuration. - /// Invalid value bytes are stored as `input` in [`gix_error::Message::values`]. - /// After [wrapping](gix_error::Error::from_error()), inspect them with [metadata](gix_error::Error::metadata()). - pub fn fetch_recurse(&self, name: &BStr) -> Result> { - FetchRecurse::new( - self.config - .boolean_by("submodule", Some(name), "fetchRecurseSubmodules"), - ) - .map_err(|value| { - message!("The 'fetchRecurseSubmodules' field of submodule '{name}' was invalid") - .with_input(value) - .validation_error() - }) - } - - /// Retrieve the `ignore` field of the submodule named `name`, or `None` if unset. - /// Invalid value bytes are stored as `input` in [`gix_error::Message::values`]. - /// After [wrapping](gix_error::Error::from_error()), inspect them with [metadata](gix_error::Error::metadata()). - pub fn ignore(&self, name: &BStr) -> Result> { - self.config - .string_by("submodule", Some(name), "ignore") - .map(|value| { - Ignore::try_from(value.as_ref()).map_err(|()| { - message!("The 'ignore' field of submodule '{name}' was invalid") - .with_input(value) - .validation_error() - }) - }) - .transpose() - } - - /// Retrieve the `shallow` field of the submodule named `name`, or `None` if unset. - /// - /// If `true`, the submodule will be checked out with `depth = 1`. If unset, `false` is assumed. - pub fn shallow(&self, name: &BStr) -> Result> { - self.config.boolean_by("submodule", Some(name), "shallow") - } -} diff --git a/vendor/gix-submodule/src/config.rs b/vendor/gix-submodule/src/config.rs deleted file mode 100644 index 456dc7d..0000000 --- a/vendor/gix-submodule/src/config.rs +++ /dev/null @@ -1,142 +0,0 @@ -use bstr::{BStr, BString, ByteSlice}; -use gix_error::{MetadataValue, Result}; - -/// Determine how the submodule participates in `git status` queries. This setting also affects `git diff`. -#[derive(Default, Debug, Clone, Copy, Ord, PartialOrd, Eq, PartialEq, Hash)] -pub enum Ignore { - /// Submodule changes won't be considered at all, which is the fastest option. - All, - /// Ignore any changes to the submodule working tree, only show committed differences between the `HEAD` of the submodule - /// compared to the recorded commit in the superproject. - Dirty, - /// Only ignore untracked files in the submodule, but show modifications to the submodule working tree as well as differences - /// between the recorded commit in the superproject and the checked-out commit in the submodule. - Untracked, - /// No modifications to the submodule are ignored, which shows untracked files, modified files in the submodule worktree as well as - /// differences between the recorded commit in the superproject and the checked-out commit in the submodule. - #[default] - None, -} - -impl TryFrom<&BStr> for Ignore { - type Error = (); - - fn try_from(value: &BStr) -> std::result::Result { - Ok(match value.as_bytes() { - b"all" => Ignore::All, - b"dirty" => Ignore::Dirty, - b"untracked" => Ignore::Untracked, - b"none" => Ignore::None, - _ => return Err(()), - }) - } -} - -/// Determine how to recurse into this module from the superproject when fetching. -/// -/// Generally, a fetch is only performed if the submodule commit referenced by the superproject isn't already -/// present in the submodule repository. -/// -/// Note that when unspecified, the `fetch.recurseSubmodules` configuration variable should be used instead. -#[derive(Default, Debug, Clone, Copy, Ord, PartialOrd, Eq, PartialEq, Hash)] -pub enum FetchRecurse { - /// Fetch only changed submodules. - #[default] - OnDemand, - /// Fetch all populated submodules, changed or not. - /// - /// This skips the work needed to determine whether a submodule has changed in the first place, but may work - /// more as some fetches might not be necessary. - Always, - /// Submodules are never fetched. - Never, -} - -impl FetchRecurse { - /// Check if `boolean` is set and translate it the respective variant, or check the underlying string - /// value for non-boolean options. - /// On error, it returns the obtained string value which would be the invalid value. - pub fn new(boolean: Result>) -> std::result::Result, BString> { - Ok(match boolean { - Ok(Some(value)) => Some(if value { - FetchRecurse::Always - } else { - FetchRecurse::Never - }), - Ok(None) => None, - Err(err) => { - let Some(MetadataValue::Bytes(input)) = err.metadata().find_map(|values| values.get("input")) else { - unreachable!("gix-config-value validation errors retain their input as bytes"); - }; - if input != b"on-demand" { - return Err(input.clone().into()); - } - Some(FetchRecurse::OnDemand) - } - }) - } -} - -/// Describes the branch that should be tracked on the remote. -#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] -pub enum Branch { - /// The name of the remote branch should be the same as the one currently checked out in the superproject. - CurrentInSuperproject, - /// The validated remote-only branch that could be used for fetching. - Name(BString), -} - -impl Default for Branch { - fn default() -> Self { - Branch::Name("HEAD".into()) - } -} - -impl TryFrom<&BStr> for Branch { - type Error = gix_error::Error; - - fn try_from(value: &BStr) -> Result { - if value == "." { - return Ok(Branch::CurrentInSuperproject); - } - - gix_refspec::parse(value, gix_refspec::parse::Operation::Fetch) - .map(|spec| Branch::Name(spec.source().expect("no object").to_owned())) - } -} - -/// Determine how `git submodule update` should deal with this submodule to bring it up-to-date with the -/// super-project's expectations. -#[derive(Default, Debug, Clone, Hash, PartialOrd, PartialEq, Ord, Eq)] -pub enum Update { - /// The commit recorded in the superproject should be checked out on a detached `HEAD`. - #[default] - Checkout, - /// The current branch in the submodule will be rebased onto the commit recorded in the superproject. - Rebase, - /// The commit recorded in the superproject will merged into the current branch of the submodule. - Merge, - /// A custom command to be called like ` hash-of-submodule-commit` that is to be executed to - /// perform the submodule update. - /// - /// Note that this variant is only allowed if the value is coming from an override. Thus it's not allowed to distribute - /// arbitrary commands via `.gitmodules` for security reasons. - Command(BString), - /// The submodule update is not performed at all. - None, -} - -impl TryFrom<&BStr> for Update { - type Error = (); - - fn try_from(value: &BStr) -> std::result::Result { - Ok(match value.as_bstr().as_bytes() { - b"checkout" => Update::Checkout, - b"rebase" => Update::Rebase, - b"merge" => Update::Merge, - b"none" => Update::None, - command if command.first() == Some(&b'!') => Update::Command(command[1..].to_owned().into()), - _ => return Err(()), - }) - } -} diff --git a/vendor/gix-submodule/src/is_active_platform.rs b/vendor/gix-submodule/src/is_active_platform.rs deleted file mode 100644 index aaa5008..0000000 --- a/vendor/gix-submodule/src/is_active_platform.rs +++ /dev/null @@ -1,40 +0,0 @@ -use bstr::BStr; -use gix_error::Result; - -use crate::IsActivePlatform; - -impl IsActivePlatform { - /// Returns `true` if the submodule named `name` is active or `false` otherwise. - /// `config` is the configuration that was passed to the originating [modules file](crate::File). - /// `attributes(relative_path, case, is_dir, outcome)` provides a way to resolve the attributes mentioned - /// in `submodule.active` pathspecs that are evaluated in the platforms git configuration. - /// - /// A submodule's active state is determined in the following order - /// - /// * it's `submodule..active` is set in `config` - /// * it matches a `submodule.active` pathspec either positively or negatively via `:!` - /// * it's active if it has any `url` set in `config` - pub fn is_active( - &mut self, - config: &gix_config::File, - name: &BStr, - attributes: &mut dyn FnMut( - &BStr, - gix_pathspec::attributes::glob::pattern::Case, - bool, - &mut gix_pathspec::attributes::search::Outcome, - ) -> bool, - ) -> Result { - if let Some(val) = config.boolean_by("submodule", Some(name), "active")? { - return Ok(val); - } - if let Some(val) = self.search.as_mut().map(|search| { - search - .pattern_matching_relative_path(name, Some(true), attributes) - .is_some_and(|m| !m.is_excluded()) - }) { - return Ok(val); - } - Ok(config.string_by("submodule", Some(name), "url").is_some()) - } -} diff --git a/vendor/gix-submodule/src/lib.rs b/vendor/gix-submodule/src/lib.rs deleted file mode 100644 index f47edf8..0000000 --- a/vendor/gix-submodule/src/lib.rs +++ /dev/null @@ -1,124 +0,0 @@ -//! Primitives for describing git submodules. -#![deny(missing_docs)] -#![forbid(unsafe_code)] - -use bstr::ByteSlice; -use gix_error::Result; - -/// All relevant information about a git module, typically from `.gitmodules` files. -/// -/// Note that overrides from other configuration might be relevant, which is why this type -/// can be used to take these into consideration when presented with other configuration -/// from the superproject. -#[derive(Clone)] -pub struct File { - config: gix_config::File, -} - -mod access; - -/// -pub mod config; - -/// -pub mod is_active_platform; - -/// A platform to keep the state necessary to perform repeated active checks, created by [File::is_active_platform()]. -pub struct IsActivePlatform { - pub(crate) search: Option, -} - -/// Mutation -impl File { - /// This can be used to let `config` override some values we know about submodules, namely… - /// - /// * `url` - /// * `fetchRecurseSubmodules` - /// * `ignore` - /// * `update` - /// * `branch` - /// - /// These values aren't validated yet, which will happen upon query. The caller must provide - /// trusted configuration; each copied value retains its section's origin metadata. - pub fn append_submodule_overrides(&mut self, config: &gix_config::File) -> Result<&mut Self> { - let mut config_to_append = gix_config::File::new(config.meta_owned()); - for (module_name, section) in config - .sections_by_name("submodule") - .into_iter() - .flatten() - .filter_map(|s| s.header().subsection_name().map(|n| (n, s))) - { - let values = ["url", "fetchRecurseSubmodules", "ignore", "update", "branch"] - .map(|field| (field, section.value(field))); - if values.iter().all(|(_, value)| value.is_none()) { - continue; - } - config_to_append.set_meta(section.meta().clone()); - let mut destination = config_to_append.new_section("submodule", module_name)?; - for (field, value) in values { - if let Some(value) = value { - destination.push(field, Some(value.as_bstr()))?; - } - } - } - - self.config.append(config_to_append)?; - Ok(self) - } -} - -/// -pub mod init { - use gix_error::{Result, validation}; - use std::path::PathBuf; - - use gix_error::ResultExt; - - use crate::File; - - impl std::fmt::Debug for File { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("File") - .field("config_path", &self.config_path()) - .field("config", &format_args!("r#\"{}\"#", self.config)) - .finish() - } - } - - impl File { - /// Parse `bytes` as git configuration, typically from `.gitmodules`, without doing any further validation. - /// `path` can be provided to keep track of where the file was read from in the underlying [`config`](Self::config()) - /// instance. - /// `config` is used to [apply value overrides](File::append_submodule_overrides), which can be empty if overrides - /// should be applied at a later time. - /// - /// Future access to the module information is lazy and configuration errors are exposed there on a per-value basis. - /// - /// ### Security Considerations - /// - /// The information itself should be used with care as it can direct the caller to fetch from remotes. It is, however, - /// on the caller to assure the input data can be trusted. - pub fn from_bytes(bytes: &[u8], path: impl Into>, config: &gix_config::File) -> Result { - let metadata = { - let mut meta = gix_config::file::Metadata::api(); - meta.path = path.into(); - meta - }; - let modules = gix_config::File::from_parse_events_no_includes( - gix_config::parse::Events::from_bytes(bytes, None) - .or_raise(|| validation("Could not parse submodule configuration"))?, - metadata, - ); - - let mut res = Self { config: modules }; - res.append_submodule_overrides(config) - .or_raise(|| validation("Could not apply submodule configuration overrides"))?; - Ok(res) - } - - /// Turn ourselves into the underlying parsed configuration file. - pub fn into_config(self) -> gix_config::File { - self.config - } - } -} diff --git a/vendor/gix-transport/Cargo.toml b/vendor/gix-transport/Cargo.toml deleted file mode 100644 index 65c527b..0000000 --- a/vendor/gix-transport/Cargo.toml +++ /dev/null @@ -1,117 +0,0 @@ - -[package] -name = "gix-transport" -version = "0.60.0" -repository = "https://github.com/GitoxideLabs/gitoxide" -license = "MIT OR Apache-2.0" -description = "A crate of the gitoxide project dedicated to implementing the git transport layer" -authors = ["Sebastian Thiel "] -edition = "2024" -include = ["/src/**/*", "/LICENSE-*"] -rust-version = "1.88" - -[lib] -doctest = false - -[features] -default = [] - -## If set, blocking implementations of the typical git transports become available in `crate::client::blocking_io` -blocking-client = ["gix-packetline/blocking-io"] -## Implies `blocking-client`, and adds support for the http and https transports. -http-client = [ - "base64", - "parking_lot", - "gix-utils/io-pipe", - "blocking-client", - "gix-credentials", -] -## Implies `http-client`, and adds support for the http and https transports using the Rust bindings for `libcurl`. -## -## If used in conjunction with other blocking implementations like `http-client-reqwest`, this one takes precedence. -http-client-curl = ["curl", "http-client"] -## Implies `http-client-curl` and enables `rustls` for creating `https://` connections. -## If used in conjunction with `http-client-curl-openssl`, this feature will take precedence. -http-client-curl-rust-tls = ["http-client-curl", "curl/rustls"] -## Implies `http-client-curl` and enables `ssl` for creating `https://` connections. -## If used in conjunction with `http-client-curl-rust-tls`, the `rust-tls` feature will take precedence. -http-client-curl-openssl = ["http-client-curl", "curl/ssl"] -## Implies `http-client` and adds support for http transports using the blocking version of `reqwest`. -## NOTE: `https://` is NOT supported by default. You must enable one of the `http-client-reqwest-rust-tls` -## or `http-client-reqwest-native-tls` features to enable HTTPS support. -http-client-reqwest = ["reqwest", "http-client"] -## Stacks with `http-client-reqwest` and enables `https://` via the `rustls` crate. -http-client-reqwest-rust-tls = ["http-client-reqwest", "reqwest/rustls"] -## Stacks with `http-client-reqwest` and enables `https://` via the `rustls` crate. -## This also makes use of `trust-dns` to avoid `getaddrinfo`, but note it comes with its own problems. -http-client-reqwest-rust-tls-trust-dns = [ - "http-client-reqwest", - "reqwest/rustls", - "reqwest/hickory-dns", -] -## Stacks with `http-client-reqwest` and enables `https://` via the `native-tls` crate. -http-client-reqwest-native-tls = ["http-client-reqwest", "reqwest/native-tls"] -## Allows sending credentials over cleartext HTTP. For testing purposes only. -http-client-insecure-credentials = [] -## If set, an async implementations of the git transports becomes available in `crate::client::async_io`. -## Suitable for implementing your own transports while using git's way of communication, typically in conjunction with a custom server. -## **Note** that the _blocking_ client has a wide range of available transports, with the _async_ version of it supporting only the TCP based `git` transport leaving you -## with the responsibility to providing such an implementation of `futures-io::AsyncRead/AsyncWrite` yourself. -async-client = [ - "gix-packetline/async-io", - "async-trait", - "futures-lite", - "futures-io", - "pin-project-lite", -] - -#! ### Other -## Data structures implement `serde::Serialize` and `serde::Deserialize`. -serde = ["dep:serde", "bstr/serde"] - -[dependencies] -gix-trace = { version = "^0.2.0", git = "https://github.com/GitoxideLabs/gitoxide", rev = "1d7bac742f70b72ddda0c7294f4a97566b8db596" } -gix-utils = { version = "^0.4.0", git = "https://github.com/GitoxideLabs/gitoxide", rev = "1d7bac742f70b72ddda0c7294f4a97566b8db596", optional = true } -gix-command = { version = "^0.11.0", git = "https://github.com/GitoxideLabs/gitoxide", rev = "1d7bac742f70b72ddda0c7294f4a97566b8db596" } -gix-error = { version = "^0.4.0", git = "https://github.com/GitoxideLabs/gitoxide", rev = "1d7bac742f70b72ddda0c7294f4a97566b8db596" } -gix-path = { version = "^0.13.0", git = "https://github.com/GitoxideLabs/gitoxide", rev = "1d7bac742f70b72ddda0c7294f4a97566b8db596" } -gix-url = { version = "^0.39.0", git = "https://github.com/GitoxideLabs/gitoxide", rev = "1d7bac742f70b72ddda0c7294f4a97566b8db596" } -gix-sec = { version = "^0.15.0", git = "https://github.com/GitoxideLabs/gitoxide", rev = "1d7bac742f70b72ddda0c7294f4a97566b8db596" } -gix-packetline = { version = "^0.23.0", git = "https://github.com/GitoxideLabs/gitoxide", rev = "1d7bac742f70b72ddda0c7294f4a97566b8db596" } -gix-credentials = { version = "^0.41.0", git = "https://github.com/GitoxideLabs/gitoxide", rev = "1d7bac742f70b72ddda0c7294f4a97566b8db596", optional = true } -gix-quote = { version = "^0.9.0", git = "https://github.com/GitoxideLabs/gitoxide", rev = "1d7bac742f70b72ddda0c7294f4a97566b8db596" } - -serde = { version = "1.0.114", optional = true, default-features = false, features = [ - "std", - "derive", -] } -bstr = { version = "1.12.0", default-features = false, features = [ - "std", - "unicode", -] } -parking_lot = { version = "0.12.4", optional = true } - -# for async-client -async-trait = { version = "0.1.51", optional = true } -futures-io = { version = "0.3.32", optional = true } -futures-lite = { version = "2.1.0", optional = true } -pin-project-lite = { version = "0.2.6", optional = true } - -# for http-client -base64 = { version = "0.22.1", optional = true } - -# for http-client-curl. Additional configuration should be performed on higher levels of the dependency tree. -curl = { version = "0.4", optional = true, default-features = false } - -# for http-client-reqwest -# all but the 'default-tls' feature -reqwest = { version = "0.13.4", optional = true, default-features = false, features = ["blocking", "charset", "http2"] } - -## If used in conjunction with `async-client`, the `connect()` method will become available along with supporting the git protocol over TCP, -## where the TCP stream is created using this crate. -async-std = { version = "1.12.0", optional = true } - -document-features = { version = "0.2.0", optional = true } - -[package.metadata.docs.rs] -features = ["http-client-curl", "document-features", "serde"] diff --git a/vendor/gix-transport/LICENSE-APACHE b/vendor/gix-transport/LICENSE-APACHE deleted file mode 100644 index f433b1a..0000000 --- a/vendor/gix-transport/LICENSE-APACHE +++ /dev/null @@ -1,177 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS diff --git a/vendor/gix-transport/LICENSE-MIT b/vendor/gix-transport/LICENSE-MIT deleted file mode 100644 index 969d061..0000000 --- a/vendor/gix-transport/LICENSE-MIT +++ /dev/null @@ -1,17 +0,0 @@ -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/vendor/gix-transport/README.pygix.md b/vendor/gix-transport/README.pygix.md deleted file mode 100644 index e31f1d5..0000000 --- a/vendor/gix-transport/README.pygix.md +++ /dev/null @@ -1,17 +0,0 @@ -# Temporary upstream prerequisite - -Source matches `gix-transport/src` at Gitoxide `pygix` HEAD -`e4d2b313b840ea0e00453791882dbc6eb7c636f6`. Its prerequisite commit is -`ab1cb834df8f48e907f81e4b7471b06b4c9f1aa0`, based on public revision -`1d7bac742f70b72ddda0c7294f4a97566b8db596`. - -The patch provides per-client reqwest configuration for the pure Rust TLS -provider, with lazy initialization and redirect policy. -The prerequisite remains on the prepared local branch for Sebastian Thiel's -review. This reintegration did not modify or push that branch. - -The source includes the updated base's security-audit fixes. Cargo sibling -dependencies use the pinned public Git source; workspace lints and test-only -dependencies are omitted. This source copy makes packaged builds independent -of the local checkout and unpublished revisions. Remove it after the fix lands -upstream and update the public pin. Original source licenses apply. diff --git a/vendor/gix-transport/src/client/async_io/bufread_ext.rs b/vendor/gix-transport/src/client/async_io/bufread_ext.rs deleted file mode 100644 index 7da6bf7..0000000 --- a/vendor/gix-transport/src/client/async_io/bufread_ext.rs +++ /dev/null @@ -1,140 +0,0 @@ -use gix_error::Result; -use std::{ - io, - ops::{Deref, DerefMut}, -}; - -use async_trait::async_trait; -use futures_io::{AsyncBufRead, AsyncRead}; - -use crate::{ - Protocol, - client::{Error, MessageKind}, - packetline::{PacketLineRef, async_io::WithSidebands, read::ProgressAction}, -}; - -/// A function `f(is_error, text)` receiving progress or error information. -/// As it is not a future itself, it must not block. If IO is performed within the function, be sure to spawn -/// it onto an executor. -pub type HandleProgress<'a> = Box ProgressAction + 'a>; - -/// This trait exists to get a version of a `gix_packetline::Provider` without type parameters, -/// but leave support for reading lines directly without forcing them through `String`. -/// -/// For the sake of usability, it also implements [`std::io::BufRead`] making it trivial to -/// read pack files while keeping open the option to read individual lines with low overhead. -#[async_trait(?Send)] -pub trait ReadlineBufRead: AsyncBufRead { - /// Read a packet line into the internal buffer and return it. - /// - /// Returns `None` if the end of iteration is reached because of one of the following: - /// - /// * natural EOF - /// * ERR packet line encountered - /// * A `delimiter` packet line encountered - async fn readline(&mut self) -> Option>>>; - - /// Read a line similar to `BufRead::read_line()`, but assure it doesn't try to find newlines - /// which might concatenate multiple distinct packet lines. - /// - /// Making this a trait method allows to handle differences between async and blocking. - async fn readline_str(&mut self, line: &mut String) -> io::Result; -} - -/// Provide even more access to the underlying packet reader. -#[async_trait(?Send)] -pub trait ExtendedBufRead<'a>: ReadlineBufRead { - /// Set the handler to which progress will be delivered. - /// - /// Note that this is only possible if packet lines are sent in side band mode. - fn set_progress_handler(&mut self, handle_progress: Option>); - /// Peek the next data packet line. Maybe None if the next line is a packet we stop at, queryable using - /// [`stopped_at()`][ExtendedBufRead::stopped_at()]. - async fn peek_data_line(&mut self) -> Option>>; - /// Resets the reader to allow reading past a previous stop, and sets delimiters according to the - /// given protocol. - fn reset(&mut self, version: Protocol); - /// Return the kind of message at which the reader stopped. - fn stopped_at(&self) -> Option; -} - -#[async_trait(?Send)] -impl ReadlineBufRead for Box { - async fn readline(&mut self) -> Option>>> { - self.deref_mut().readline().await - } - async fn readline_str(&mut self, line: &mut String) -> io::Result { - self.deref_mut().readline_str(line).await - } -} - -#[async_trait(?Send)] -impl<'a, T: ExtendedBufRead<'a> + ?Sized + 'a + Unpin> ExtendedBufRead<'a> for Box { - fn set_progress_handler(&mut self, handle_progress: Option>) { - self.deref_mut().set_progress_handler(handle_progress); - } - - async fn peek_data_line(&mut self) -> Option>> { - self.deref_mut().peek_data_line().await - } - - fn reset(&mut self, version: Protocol) { - self.deref_mut().reset(version); - } - - fn stopped_at(&self) -> Option { - self.deref().stopped_at() - } -} - -#[async_trait(?Send)] -impl ReadlineBufRead for WithSidebands<'_, T, for<'b> fn(bool, &'b [u8]) -> ProgressAction> { - async fn readline(&mut self) -> Option>>> { - self.read_data_line().await - } - async fn readline_str(&mut self, line: &mut String) -> io::Result { - self.read_line_to_string(line).await - } -} - -#[async_trait(?Send)] -impl<'a, T: AsyncRead + Unpin> ReadlineBufRead for WithSidebands<'a, T, HandleProgress<'a>> { - async fn readline(&mut self) -> Option>>> { - self.read_data_line().await - } - async fn readline_str(&mut self, line: &mut String) -> io::Result { - self.read_line_to_string(line).await - } -} - -#[async_trait(?Send)] -impl<'a, T: AsyncRead + Unpin> ExtendedBufRead<'a> for WithSidebands<'a, T, HandleProgress<'a>> { - fn set_progress_handler(&mut self, handle_progress: Option>) { - self.set_progress_handler(handle_progress); - } - async fn peek_data_line(&mut self) -> Option>> { - match self.peek_data_line().await { - Some(Ok(Ok(line))) => Some(Ok(Ok(line))), - Some(Ok(Err(err))) => Some(Ok(Err(err.into()))), - Some(Err(err)) => Some(Err(err)), - None => None, - } - } - fn reset(&mut self, version: Protocol) { - match version { - Protocol::V0 | Protocol::V1 => self.reset_with(&[gix_packetline::PacketLineRef::Flush]), - Protocol::V2 => self.reset_with(&[ - gix_packetline::PacketLineRef::Delimiter, - gix_packetline::PacketLineRef::Flush, - ]), - } - } - fn stopped_at(&self) -> Option { - self.stopped_at().map(|l| match l { - gix_packetline::PacketLineRef::Flush => MessageKind::Flush, - gix_packetline::PacketLineRef::Delimiter => MessageKind::Delimiter, - gix_packetline::PacketLineRef::ResponseEnd => MessageKind::ResponseEnd, - gix_packetline::PacketLineRef::Data(_) => unreachable!("data cannot be a delimiter"), - }) - } -} diff --git a/vendor/gix-transport/src/client/async_io/connect.rs b/vendor/gix-transport/src/client/async_io/connect.rs deleted file mode 100644 index 4310382..0000000 --- a/vendor/gix-transport/src/client/async_io/connect.rs +++ /dev/null @@ -1,51 +0,0 @@ -pub use crate::client::non_io_types::connect::Options; - -#[cfg(feature = "async-std")] -pub(crate) mod function { - use crate::client::{async_io::Transport, git::async_io::Connection}; - use gix_error::Result; - use gix_error::{ResultExt, bail, message}; - - /// A general purpose connector connecting to a repository identified by the given `url`. - /// - /// This includes connections to - /// [git daemons][Connection::new_tcp()] only at the moment. - /// - /// Use `options` to further control specifics of the transport resulting from the connection. - pub async fn connect(url: Url, options: super::Options) -> Result> - where - Url: TryInto, - E: std::error::Error + Send + Sync + 'static, - { - let mut url = url.try_into().or_raise(|| message("Could not parse URL"))?; - Ok(match url.scheme { - gix_url::Scheme::Git => { - if url.user().is_some() { - bail!( - "The url {:?} contains information that would not be used by the {} protocol".validation(), - url.to_bstring(), - url.scheme - ); - } - let path = std::mem::take(&mut url.path); - Box::new( - Connection::new_tcp( - url.host().expect("host is present in url"), - url.port, - path, - options.version, - options.trace, - ) - .await - .or_raise(|| message("connection failed"))?, - ) - } - scheme => { - bail!("The '{scheme}' protocol is currently unsupported".unsupported()); - } - }) - } -} - -#[cfg(feature = "async-std")] -pub use function::connect; diff --git a/vendor/gix-transport/src/client/async_io/mod.rs b/vendor/gix-transport/src/client/async_io/mod.rs deleted file mode 100644 index 4728b00..0000000 --- a/vendor/gix-transport/src/client/async_io/mod.rs +++ /dev/null @@ -1,15 +0,0 @@ -#![allow(clippy::double_must_use)] // `async_trait` generates `#[must_use]` on boxed futures. - -mod bufread_ext; -pub use bufread_ext::{ExtendedBufRead, HandleProgress, ReadlineBufRead}; - -mod request; -pub use request::RequestWriter; - -mod traits; -pub use traits::{SetServiceResponse, Transport, TransportV2Ext}; - -/// -pub mod connect; -#[cfg(feature = "async-std")] -pub use connect::function::connect; diff --git a/vendor/gix-transport/src/client/async_io/request.rs b/vendor/gix-transport/src/client/async_io/request.rs deleted file mode 100644 index ebe3404..0000000 --- a/vendor/gix-transport/src/client/async_io/request.rs +++ /dev/null @@ -1,126 +0,0 @@ -use std::{ - io, - pin::Pin, - task::{Context, Poll}, -}; - -use futures_io::AsyncWrite; -use pin_project_lite::pin_project; - -use crate::{ - client::{MessageKind, WriteMode, async_io::ExtendedBufRead}, - packetline::async_io::{Writer, encode}, -}; - -pin_project! { - /// A [`Write`][io::Write] implementation optimized for writing packet lines. - /// A type implementing `Write` for packet lines, which when done can be transformed into a `Read` for - /// obtaining the response. - pub struct RequestWriter<'a> { - on_into_read: MessageKind, - #[pin] - writer: Writer>, - reader: Box + Unpin + 'a>, - trace: bool, - } -} -impl futures_io::AsyncWrite for RequestWriter<'_> { - fn poll_write(self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> Poll> { - self.project().writer.poll_write(cx, buf) - } - - fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - self.project().writer.poll_flush(cx) - } - - fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - self.project().writer.poll_close(cx) - } -} - -/// methods with bonds to IO -impl<'a> RequestWriter<'a> { - /// Create a new instance from a `writer` (commonly a socket), a `reader` into which to transform once the - /// writes are finished, along with configuration for the `write_mode` and information about which message to write - /// when this instance is converted [into a `reader`][RequestWriter::into_read()] to read the request's response. - /// If `trace` is true, `gix_trace` will be used on every written message or data. - pub fn new_from_bufread( - writer: W, - reader: Box + Unpin + 'a>, - write_mode: WriteMode, - on_into_read: MessageKind, - trace: bool, - ) -> Self { - let mut writer = Writer::new(Box::new(writer) as Box); - match write_mode { - WriteMode::Binary => writer.enable_binary_mode(), - WriteMode::OneLfTerminatedLinePerWriteCall => writer.enable_text_mode(), - } - RequestWriter { - on_into_read, - writer, - reader, - trace, - } - } - - /// Write the given message as packet line. - pub async fn write_message(&mut self, message: MessageKind) -> io::Result<()> { - match message { - MessageKind::Flush => { - if self.trace { - gix_trace::trace!(">> FLUSH"); - } - encode::write_packet_line(&gix_packetline::PacketLineRef::Flush, self.writer.inner_mut()).await - } - MessageKind::Delimiter => { - if self.trace { - gix_trace::trace!(">> DELIM"); - } - encode::write_packet_line(&gix_packetline::PacketLineRef::Delimiter, self.writer.inner_mut()).await - } - MessageKind::ResponseEnd => { - if self.trace { - gix_trace::trace!(">> RESPONSE_END"); - } - encode::write_packet_line(&gix_packetline::PacketLineRef::ResponseEnd, self.writer.inner_mut()).await - } - MessageKind::Text(t) => { - if self.trace { - use bstr::ByteSlice; - gix_trace::trace!(">> {}", t.as_bstr()); - } - encode::write_text(&gix_packetline::TextRef::from(t), self.writer.inner_mut()).await - } - } - .map(|_| ()) - } - /// Discard the ability to write and turn this instance into the reader for obtaining the other side's response. - /// - /// Doing so will also write the message type this instance was initialized with. - pub async fn into_read(mut self) -> std::io::Result + Unpin + 'a>> { - use futures_lite::AsyncWriteExt; - self.write_message(self.on_into_read).await?; - self.writer.inner_mut().flush().await?; - Ok(self.reader) - } - - /// Dissolve this instance into its write and read handles without any message-writing side-effect as in [`RequestWriter::into_read()`]. - /// - /// Furthermore, the writer will not encode everything it writes as packetlines, but write everything verbatim into the - /// underlying channel. - /// - /// # Note - /// - /// It's of utmost importance to drop the request writer before reading the response as these might be inter-dependent, depending on - /// the underlying transport mechanism. Failure to do so may result in a deadlock depending on how the write and read mechanism - /// is implemented. - pub fn into_parts( - self, - ) -> ( - Box, - Box + Unpin + 'a>, - ) { - (self.writer.into_inner(), self.reader) - } -} diff --git a/vendor/gix-transport/src/client/async_io/traits.rs b/vendor/gix-transport/src/client/async_io/traits.rs deleted file mode 100644 index fb16c75..0000000 --- a/vendor/gix-transport/src/client/async_io/traits.rs +++ /dev/null @@ -1,150 +0,0 @@ -use std::ops::DerefMut; - -use async_trait::async_trait; -use bstr::BString; -use futures_lite::io::AsyncWriteExt; - -use crate::{ - Protocol, Service, - client::{ - Capabilities, Error, MessageKind, TransportWithoutIO, WriteMode, - async_io::{ExtendedBufRead, ReadlineBufRead, request::RequestWriter}, - }, -}; - -/// The response of the [`handshake()`][Transport::handshake()] method. -pub struct SetServiceResponse<'a> { - /// The protocol the service can provide. May be different from the requested one - pub actual_protocol: Protocol, - /// The capabilities parsed from the server response. - pub capabilities: Capabilities, - /// In protocol version one, this is set to a list of refs and their peeled counterparts. - pub refs: Option>, -} - -/// All methods provided here must be called in the correct order according to the [communication protocol][Protocol] -/// used to connect to them. -/// It does, however, know just enough to be able to provide a higher-level interface than would otherwise be possible. -/// Thus the consumer of this trait will not have to deal with packet lines at all. -/// **Note that** whenever a `Read` trait or `Write` trait is produced, it must be exhausted. -#[async_trait(?Send)] -pub trait Transport: TransportWithoutIO { - /// Initiate connection to the given service and send the given `extra_parameters` along with it. - /// - /// `extra_parameters` are interpreted as `key=value` pairs if the second parameter is `Some` or as `key` - /// if it is None. - /// - /// Returns the service capabilities according to the actual [Protocol] it supports, - /// and possibly a list of refs to be obtained. - /// This means that asking for an unsupported protocol might result in a protocol downgrade to the given one - /// if [TransportWithoutIO::supported_protocol_versions()] includes it or is empty. - /// Exhaust the returned [BufReader][SetServiceResponse::refs] for a list of references in case of protocol V1 - /// before making another request. - async fn handshake<'a>( - &mut self, - service: Service, - extra_parameters: &'a [(&'a str, Option<&'a str>)], - ) -> Result, Error>; - - /// Get a writer for sending data and obtaining the response. It can be configured in various ways - /// to support the task at hand. - /// `write_mode` determines how calls to the `write(…)` method are interpreted, and `on_into_read` determines - /// which message to write when the writer is turned into the response reader using [`into_read()`][RequestWriter::into_read()]. - /// If `trace` is `true`, then all packetlines written and received will be traced using facilities provided by the `gix_trace` crate. - fn request( - &mut self, - write_mode: WriteMode, - on_into_read: MessageKind, - trace: bool, - ) -> Result, Error>; -} - -// Would be nice if the box implementation could auto-forward to all implemented traits. -#[async_trait(?Send)] -impl Transport for Box { - async fn handshake<'a>( - &mut self, - service: Service, - extra_parameters: &'a [(&'a str, Option<&'a str>)], - ) -> Result, Error> { - self.deref_mut().handshake(service, extra_parameters).await - } - - fn request( - &mut self, - write_mode: WriteMode, - on_into_read: MessageKind, - trace: bool, - ) -> Result, Error> { - self.deref_mut().request(write_mode, on_into_read, trace) - } -} - -// Would be nice if the box implementation could auto-forward to all implemented traits. -#[async_trait(?Send)] -impl Transport for &mut T { - async fn handshake<'a>( - &mut self, - service: Service, - extra_parameters: &'a [(&'a str, Option<&'a str>)], - ) -> Result, Error> { - self.deref_mut().handshake(service, extra_parameters).await - } - - fn request( - &mut self, - write_mode: WriteMode, - on_into_read: MessageKind, - trace: bool, - ) -> Result, Error> { - self.deref_mut().request(write_mode, on_into_read, trace) - } -} - -/// An extension trait to add more methods to everything implementing [`Transport`]. -#[async_trait(?Send)] -pub trait TransportV2Ext { - /// Invoke a protocol V2 style `command` with given `capabilities` and optional command specific `arguments`. - /// The `capabilities` were communicated during the handshake. - /// If `trace` is `true`, then all packetlines written and received will be traced using facilities provided by the `gix_trace` crate. - /// - /// _Note:_ panics if [handshake][Transport::handshake()] wasn't performed beforehand. - async fn invoke<'a>( - &mut self, - command: &str, - capabilities: impl Iterator>)> + 'a, - arguments: Option + 'a>, - trace: bool, - ) -> Result + Unpin + '_>, Error>; -} - -#[async_trait(?Send)] -impl TransportV2Ext for T { - async fn invoke<'a>( - &mut self, - command: &str, - capabilities: impl Iterator>)> + 'a, - arguments: Option + 'a>, - trace: bool, - ) -> Result + Unpin + '_>, Error> { - let mut writer = self.request(WriteMode::OneLfTerminatedLinePerWriteCall, MessageKind::Flush, trace)?; - writer.write_all(format!("command={command}").as_bytes()).await?; - for (name, value) in capabilities { - match value { - Some(value) => { - writer - .write_all(format!("{}={}", name, value.as_ref()).as_bytes()) - .await - } - None => writer.write_all(name.as_bytes()).await, - }?; - } - if let Some(arguments) = arguments { - writer.write_message(MessageKind::Delimiter).await?; - for argument in arguments { - writer.write_all(argument.as_ref()).await?; - } - } - Ok(writer.into_read().await?) - } -} diff --git a/vendor/gix-transport/src/client/blocking_io/bufread_ext.rs b/vendor/gix-transport/src/client/blocking_io/bufread_ext.rs deleted file mode 100644 index dc167e5..0000000 --- a/vendor/gix-transport/src/client/blocking_io/bufread_ext.rs +++ /dev/null @@ -1,130 +0,0 @@ -use gix_error::Result; -use std::{ - io, - ops::{Deref, DerefMut}, -}; - -use gix_packetline::{PacketLineRef, blocking_io::WithSidebands, read::ProgressAction}; - -use crate::{ - Protocol, - client::{Error, MessageKind}, -}; -/// A function `f(is_error, text)` receiving progress or error information. -pub type HandleProgress<'a> = Box ProgressAction + 'a>; - -/// This trait exists to get a version of a `gix_packetline::Provider` without type parameters, -/// but leave support for reading lines directly without forcing them through `String`. -/// -/// For the sake of usability, it also implements [`std::io::BufRead`] making it trivial to -/// read pack files while keeping open the option to read individual lines with low overhead. -pub trait ReadlineBufRead: io::BufRead { - /// Read a packet line into the internal buffer and return it. - /// - /// Returns `None` if the end of iteration is reached because of one of the following: - /// - /// * natural EOF - /// * ERR packet line encountered - /// * A `delimiter` packet line encountered - fn readline(&mut self) -> Option>>>; - - /// Read a line similar to `BufRead::read_line()`, but assure it doesn't try to find newlines - /// which might concatenate multiple distinct packet lines. - /// - /// Making this a trait method allows to handle differences between async and blocking. - fn readline_str(&mut self, line: &mut String) -> io::Result; -} - -/// Provide even more access to the underlying packet reader. -pub trait ExtendedBufRead<'a>: ReadlineBufRead { - /// Set the handler to which progress will be delivered. - /// - /// Note that this is only possible if packet lines are sent in side band mode. - fn set_progress_handler(&mut self, handle_progress: Option>); - /// Peek the next data packet line. Maybe None if the next line is a packet we stop at, queryable using - /// [`stopped_at()`][ExtendedBufRead::stopped_at()]. - fn peek_data_line(&mut self) -> Option>>; - /// Resets the reader to allow reading past a previous stop, and sets delimiters according to the - /// given protocol. - fn reset(&mut self, version: Protocol); - /// Return the kind of message at which the reader stopped. - fn stopped_at(&self) -> Option; -} - -impl ReadlineBufRead for Box { - fn readline(&mut self) -> Option>>> { - ReadlineBufRead::readline(self.deref_mut()) - } - fn readline_str(&mut self, line: &mut String) -> io::Result { - ReadlineBufRead::readline_str(self.deref_mut(), line) - } -} - -impl<'a, T: ExtendedBufRead<'a> + ?Sized + 'a> ExtendedBufRead<'a> for Box { - fn set_progress_handler(&mut self, handle_progress: Option>) { - self.deref_mut().set_progress_handler(handle_progress); - } - - fn peek_data_line(&mut self) -> Option>> { - self.deref_mut().peek_data_line() - } - - fn reset(&mut self, version: Protocol) { - self.deref_mut().reset(version); - } - - fn stopped_at(&self) -> Option { - self.deref().stopped_at() - } -} - -impl ReadlineBufRead for WithSidebands<'_, T, fn(bool, &[u8]) -> ProgressAction> { - fn readline(&mut self) -> Option>>> { - self.read_data_line() - } - - fn readline_str(&mut self, line: &mut String) -> io::Result { - self.read_line_to_string(line) - } -} - -impl<'a, T: io::Read> ReadlineBufRead for WithSidebands<'a, T, HandleProgress<'a>> { - fn readline(&mut self) -> Option>>> { - self.read_data_line() - } - - fn readline_str(&mut self, line: &mut String) -> io::Result { - self.read_line_to_string(line) - } -} - -impl<'a, T: io::Read> ExtendedBufRead<'a> for WithSidebands<'a, T, HandleProgress<'a>> { - fn set_progress_handler(&mut self, handle_progress: Option>) { - self.set_progress_handler(handle_progress); - } - fn peek_data_line(&mut self) -> Option>> { - match self.peek_data_line() { - Some(Ok(Ok(line))) => Some(Ok(Ok(line))), - Some(Ok(Err(err))) => Some(Ok(Err(err.into()))), - Some(Err(err)) => Some(Err(err)), - None => None, - } - } - fn reset(&mut self, version: Protocol) { - match version { - Protocol::V0 | Protocol::V1 => self.reset_with(&[gix_packetline::PacketLineRef::Flush]), - Protocol::V2 => self.reset_with(&[ - gix_packetline::PacketLineRef::Delimiter, - gix_packetline::PacketLineRef::Flush, - ]), - } - } - fn stopped_at(&self) -> Option { - self.stopped_at().map(|l| match l { - gix_packetline::PacketLineRef::Flush => MessageKind::Flush, - gix_packetline::PacketLineRef::Delimiter => MessageKind::Delimiter, - gix_packetline::PacketLineRef::ResponseEnd => MessageKind::ResponseEnd, - gix_packetline::PacketLineRef::Data(_) => unreachable!("data cannot be a delimiter"), - }) - } -} diff --git a/vendor/gix-transport/src/client/blocking_io/connect.rs b/vendor/gix-transport/src/client/blocking_io/connect.rs deleted file mode 100644 index a0d7604..0000000 --- a/vendor/gix-transport/src/client/blocking_io/connect.rs +++ /dev/null @@ -1,90 +0,0 @@ -pub use crate::client::non_io_types::connect::Options; - -pub(crate) mod function { - use crate::client::blocking_io::Transport; - #[cfg(feature = "http-client-curl")] - use crate::client::blocking_io::http::curl::Curl; - #[cfg(all(feature = "http-client-reqwest", not(feature = "http-client-curl")))] - use crate::client::blocking_io::http::reqwest::Remote as Reqwest; - - use gix_error::Result; - use gix_error::{ResultExt, bail, message}; - - /// A general purpose connector connecting to a repository identified by the given `url`. - /// - /// This includes connections to - /// [local repositories](crate::client::blocking_io::file::connect()), - /// [repositories over ssh](crate::client::blocking_io::ssh::connect()), - /// [git daemons](crate::client::blocking_io::connect::connect()), - /// and if compiled in connections to [git repositories over https](crate::client::blocking_io::http::connect()). - /// - /// Use `options` to further control specifics of the transport resulting from the connection. - pub fn connect(url: Url, options: super::Options) -> Result> - where - Url: TryInto, - E: std::error::Error + Send + Sync + 'static, - { - let mut url = url.try_into().or_raise(|| message("Could not parse URL"))?; - Ok(match url.scheme { - gix_url::Scheme::Ext | gix_url::Scheme::Helper(_) | gix_url::Scheme::HelperUrl(_) => { - bail!("The '{}' protocol is currently unsupported".unsupported(), url.scheme); - } - gix_url::Scheme::File => { - if url.user().is_some() || url.password().is_some() || url.host().is_some() || url.port.is_some() { - bail!( - "The url {:?} contains information that would not be used by the {} protocol".validation(), - url.to_bstring(), - url.scheme - ); - } - Box::new( - crate::client::blocking_io::file::connect(url.path, options.version, options.trace) - .or_raise(|| message("connection failed"))?, - ) - } - gix_url::Scheme::Ssh => Box::new({ - crate::client::blocking_io::ssh::connect(url, options.version, options.ssh, options.trace) - .or_raise(|| message("connection failed"))? - }), - gix_url::Scheme::Git => { - if url.user().is_some() { - bail!( - "The url {:?} contains information that would not be used by the {} protocol".validation(), - url.to_bstring(), - url.scheme - ); - } - Box::new({ - let path = std::mem::take(&mut url.path); - crate::client::git::blocking_io::connect( - url.host().expect("host is present in url"), - path, - options.version, - url.port, - options.trace, - ) - .or_raise(|| message("connection failed"))? - }) - } - #[cfg(not(any(feature = "http-client-curl", feature = "http-client-reqwest")))] - gix_url::Scheme::Https | gix_url::Scheme::Http => { - bail!( - "'{}' is not compiled in. Compile with the 'http-client-curl' or 'http-client-reqwest' cargo feature".unsupported(), - url.scheme - ); - } - #[cfg(feature = "http-client-curl")] - gix_url::Scheme::Https | gix_url::Scheme::Http => Box::new( - crate::client::blocking_io::http::connect::(url, options.version, options.trace), - ), - #[cfg(all(feature = "http-client-reqwest", not(feature = "http-client-curl")))] - gix_url::Scheme::Https | gix_url::Scheme::Http => Box::new(crate::client::blocking_io::http::connect::< - Reqwest, - >( - url, options.version, options.trace - )), - }) - } -} - -pub use function::connect; diff --git a/vendor/gix-transport/src/client/blocking_io/file.rs b/vendor/gix-transport/src/client/blocking_io/file.rs deleted file mode 100644 index 99408ca..0000000 --- a/vendor/gix-transport/src/client/blocking_io/file.rs +++ /dev/null @@ -1,504 +0,0 @@ -use gix_error::Result; -use std::{ - any::Any, - borrow::Cow, - ffi::OsString, - io::Write, - process::{self, Stdio}, -}; - -use bstr::{BStr, BString, ByteSlice, io::BufReadExt}; - -use crate::{ - Protocol, Service, - client::{ - self, MessageKind, WriteMode, - blocking_io::{RequestWriter, SetServiceResponse, ssh}, - git::blocking_io::Connection, - }, -}; - -// from https://github.com/git/git/blob/20de7e7e4f4e9ae52e6cc7cfaa6469f186ddb0fa/environment.c#L115:L115 -const ENV_VARS_TO_REMOVE: &[&str] = &[ - "GIT_ALTERNATE_OBJECT_DIRECTORIES", - "GIT_CONFIG", - "GIT_CONFIG_PARAMETERS", - "GIT_OBJECT_DIRECTORY", - "GIT_DIR", - "GIT_WORK_TREE", - "GIT_IMPLICIT_WORK_TREE", - "GIT_GRAFT_FILE", - "GIT_INDEX_FILE", - "GIT_NO_REPLACE_OBJECTS", - "GIT_REPLACE_REF_BASE", - "GIT_PREFIX", - "GIT_INTERNAL_SUPER_PREFIX", - "GIT_SHALLOW_FILE", - "GIT_COMMON_DIR", - "GIT_CONFIG_COUNT", -]; - -/// A utility to spawn a helper process to actually transmit data, possibly over `ssh`. -/// -/// It can only be instantiated using the local [`connect()`] or [ssh connect][super::ssh::connect()]. -pub struct SpawnProcessOnDemand { - desired_version: Protocol, - url: gix_url::Url, - path: BString, - ssh_cmd: Option<(OsString, ssh::ProgramKind)>, - /// The environment variables to set in the invoked command. - envs: Vec<(&'static str, String)>, - ssh_disallow_shell: bool, - connection: Option, process::ChildStdin>>, - child: Option, - trace: bool, -} - -impl SpawnProcessOnDemand { - pub(crate) fn new_ssh( - url: gix_url::Url, - program: impl Into, - path: BString, - ssh_kind: ssh::ProgramKind, - ssh_disallow_shell: bool, - version: Protocol, - trace: bool, - ) -> SpawnProcessOnDemand { - SpawnProcessOnDemand { - url, - path, - ssh_cmd: Some((program.into(), ssh_kind)), - envs: Default::default(), - ssh_disallow_shell, - child: None, - connection: None, - desired_version: version, - trace, - } - } - - fn new_local(path: BString, version: Protocol, trace: bool) -> SpawnProcessOnDemand { - SpawnProcessOnDemand { - url: gix_url::Url::from_parts(gix_url::Scheme::File, None, None, None, None, path.clone(), true) - .expect("valid url"), - path, - ssh_cmd: None, - envs: if version != Protocol::V1 { - vec![("GIT_PROTOCOL", format!("version={}", version as usize))] - } else { - Default::default() - }, - ssh_disallow_shell: false, - child: None, - connection: None, - desired_version: version, - trace, - } - } - - fn prepare_command( - &self, - service: Service, - ) -> std::result::Result<(gix_command::Prepare, Option, OsString), client::Error> { - let (mut cmd, ssh_kind, cmd_name) = match &self.ssh_cmd { - Some((command, kind)) => ( - kind.prepare_invocation(command, &self.url, self.desired_version, self.ssh_disallow_shell) - .map_err(client::Error::SshInvocation)? - .stderr(Stdio::piped()), - Some(*kind), - command.to_owned(), - ), - None => ( - gix_command::prepare(service.as_str()).stderr(Stdio::null()), - None, - OsString::from(service.as_str()), - ), - }; - if self.path.trim().first() == Some(&b'-') { - return Err(client::Error::AmbiguousPath { - path: self.path.clone(), - }); - } - let repo_path = if self.ssh_cmd.is_some() { - cmd.args.push(service.as_str().into()); - gix_quote::single(self.path.as_ref()).to_os_str_lossy().into_owned() - } else { - self.path.to_os_str_lossy().into_owned() - }; - cmd.args.push(repo_path); - Ok((cmd, ssh_kind, cmd_name)) - } - - /// Prepare an invocation for when the program of `service`, e.g. `git-upload-pack`, isn't present in `PATH`, - /// which can happen on Windows in particular, where `git` may be installed without its subcommands - /// being directly available (#2313). - /// - /// Prefer the same program from git's own `--exec-path`, which is where `git` itself finds it, and - /// otherwise let the `git` itself run it as subcommand. - /// This is only used for local repositories - remote shells keep the standard invocation. - fn prepare_fallback_command(&self, service: Service) -> (gix_command::Prepare, OsString) { - let (mut cmd, cmd_name) = match gix_path::env::core_dir_program(service.as_str()) { - Some(program) => { - let cmd_name = program.clone().into_os_string(); - (gix_command::prepare(program).stderr(Stdio::null()), cmd_name) - } - None => { - let subcommand = service.as_git_subcommand(); - let git = gix_path::env::exe_invocation(); - let cmd = gix_command::prepare(git).stderr(Stdio::null()).arg(subcommand); - - let mut cmd_name: OsString = git.into(); - cmd_name.push(" "); - cmd_name.push(subcommand); - (cmd, cmd_name) - } - }; - cmd.args.push(self.path.to_os_str_lossy().into_owned()); - (cmd, cmd_name) - } -} - -impl client::TransportWithoutIO for SpawnProcessOnDemand { - fn set_identity(&mut self, identity: gix_sec::identity::Account) -> std::result::Result<(), client::Error> { - if self.url.scheme == gix_url::Scheme::Ssh { - self.url - .set_user((!identity.username.is_empty()).then_some(identity.username)); - Ok(()) - } else { - Err(client::Error::AuthenticationUnsupported) - } - } - - fn to_url(&self) -> Cow<'_, BStr> { - Cow::Owned(self.url.to_bstring()) - } - - fn connection_persists_across_multiple_requests(&self) -> bool { - true - } - - fn configure(&mut self, _config: &dyn Any) -> Result { - Ok(()) - } -} - -impl Drop for SpawnProcessOnDemand { - fn drop(&mut self) { - if let Some(mut child) = self.child.take() { - // The child process (e.g. `ssh`) may still be running at this point, so kill it before joining/waiting. - // In the happy-path case, it should have already exited gracefully, but in error cases or if the user - // interrupted the operation, it will likely still be running. - child.kill().ok(); - child.wait().ok(); - } - } -} - -struct ReadStdoutFailOnError { - recv: std::sync::mpsc::Receiver, - read: std::process::ChildStdout, -} - -impl std::io::Read for ReadStdoutFailOnError { - fn read(&mut self, buf: &mut [u8]) -> std::io::Result { - let res = self.read.read(buf); - self.swap_err_if_present_in_stderr(buf.len(), res) - } -} - -impl ReadStdoutFailOnError { - fn swap_err_if_present_in_stderr(&self, wanted: usize, res: std::io::Result) -> std::io::Result { - match self.recv.try_recv().ok() { - Some(err) => Err(err), - None => match res { - Ok(n) if n == wanted => Ok(n), - Ok(n) => { - // TODO: fix this - // When parsing refs this seems to happen legitimately - // (even though we read packet lines only and should always know exactly how much to read) - // Maybe this still happens in `read_exact()` as sometimes we just don't get enough bytes - // despite knowing how many. - // To prevent deadlock, we have to set a timeout which slows down legitimate parts of the protocol. - // This code was specifically written to make the `cargo` test-suite pass, and we can reduce - // the timeouts even more once there is a native ssh transport that is used by `cargo`, it will - // be able to handle these properly. - // Alternatively, one could implement something like `read2` to avoid blocking on stderr entirely. - self.recv - .recv_timeout(std::time::Duration::from_millis(5)) - .ok() - .map_or(Ok(n), Err) - } - Err(err) => Err(self.recv.recv().ok().unwrap_or(err)), - }, - } - } -} - -fn supervise_stderr( - ssh_kind: ssh::ProgramKind, - stderr: std::process::ChildStderr, - stdout: std::process::ChildStdout, -) -> ReadStdoutFailOnError { - let (send, recv) = std::sync::mpsc::sync_channel(1); - std::thread::Builder::new() - .name("supervise ssh stderr".into()) - .stack_size(128 * 1024) - .spawn(gix_trace::in_thread(move || -> std::io::Result<()> { - let mut process_stderr = std::io::stderr(); - for line in std::io::BufReader::new(stderr).byte_lines() { - let line = line?; - match ssh_kind.line_to_err(line.into()) { - Ok(err) => { - send.send(err).ok(); - } - Err(line) => { - process_stderr.write_all(&line).ok(); - writeln!(&process_stderr).ok(); - } - } - } - Ok(()) - })) - .expect("named threads with small stack work on all platforms"); - ReadStdoutFailOnError { read: stdout, recv } -} - -impl client::blocking_io::Transport for SpawnProcessOnDemand { - fn handshake<'a>( - &mut self, - service: Service, - extra_parameters: &'a [(&'a str, Option<&'a str>)], - ) -> std::result::Result, client::Error> { - let (cmd, ssh_kind, cmd_name) = self.prepare_command(service)?; - let envs = std::mem::take(&mut self.envs); - let into_std_command = |mut cmd: gix_command::Prepare, command: &std::ffi::OsStr| { - cmd.stdin = Stdio::piped(); - cmd.stdout = Stdio::piped(); - - let mut cmd = std::process::Command::try_from(cmd).map_err(|err| client::Error::InvokeProgram { - source: std::io::Error::new(std::io::ErrorKind::InvalidInput, err), - command: command.to_owned(), - })?; - for env_to_remove in ENV_VARS_TO_REMOVE { - cmd.env_remove(env_to_remove); - } - cmd.envs(envs.iter().map(|(k, v)| (k, v))); - Ok::<_, client::Error>(cmd) - }; - - let mut cmd = into_std_command(cmd, &cmd_name)?; - gix_trace::debug!(command = ?cmd, "gix_transport::SpawnProcessOnDemand"); - let mut child = match cmd.spawn() { - Ok(child) => child, - Err(err) if ssh_kind.is_none() && err.kind() == std::io::ErrorKind::NotFound => { - // The service program wasn't found in `PATH`, but as `git` itself can be found, - // the service can still be run through it (#2313). - let (cmd, cmd_name) = self.prepare_fallback_command(service); - let mut cmd = into_std_command(cmd, &cmd_name)?; - gix_trace::debug!(command = ?cmd, "gix_transport::SpawnProcessOnDemand (fallback)"); - cmd.spawn().map_err(|err| client::Error::InvokeProgram { - source: err, - command: cmd_name, - })? - } - Err(err) => { - return Err(client::Error::InvokeProgram { - source: err, - command: cmd_name, - }); - } - }; - let stdout: Box = match ssh_kind { - Some(ssh_kind) => Box::new(supervise_stderr( - ssh_kind, - child.stderr.take().expect("configured beforehand"), - child.stdout.take().expect("configured"), - )), - None => Box::new(child.stdout.take().expect("stdout configured")), - }; - self.connection = Some(Connection::new_for_spawned_process( - stdout, - child.stdin.take().expect("stdin configured"), - self.desired_version, - self.path.clone(), - self.trace, - )); - self.child = Some(child); - self.connection - .as_mut() - .expect("connection to be there right after setting it") - .handshake(service, extra_parameters) - } - - fn request( - &mut self, - write_mode: WriteMode, - on_into_read: MessageKind, - trace: bool, - ) -> std::result::Result, client::Error> { - self.connection - .as_mut() - .ok_or(client::Error::MissingHandshake)? - .request(write_mode, on_into_read, trace) - } -} - -/// Connect to a locally readable repository at `path` using the given `desired_version`. -/// If `trace` is `true`, all packetlines received or sent will be passed to the facilities of the `gix-trace` crate. -/// -/// This will spawn a `git` process locally. -pub fn connect( - path: impl Into, - desired_version: Protocol, - trace: bool, -) -> std::result::Result { - Ok(SpawnProcessOnDemand::new_local(path.into(), desired_version, trace)) -} - -#[cfg(test)] -mod tests { - mod local { - use crate::{Protocol, Service, client::blocking_io::file::SpawnProcessOnDemand}; - - #[test] - fn fallback_command_prefers_the_core_dir_program_and_can_always_run_git_itself() { - let transport = SpawnProcessOnDemand::new_local("/repo/path".into(), Protocol::V2, false); - let (cmd, name) = transport.prepare_fallback_command(Service::UploadPack); - - assert!(!cmd.use_shell, "a shell is never used to run the local service program"); - assert_eq!( - cmd.args.last().map(std::path::Path::new), - Some(std::path::Path::new("/repo/path")), - "the repository path is the final argument" - ); - match gix_path::env::core_dir_program(Service::UploadPack.as_str()) { - Some(program) => { - assert_eq!( - std::path::Path::new(&cmd.command), - program, - "the program shipped with Git in its `--exec-path` is used directly" - ); - assert!( - std::path::Path::new(&cmd.command).is_absolute(), - "which also means it's an absolute path" - ); - assert_eq!(cmd.args.len(), 1, "there is no sub-command, just the repo path"); - assert_eq!(name, program.into_os_string()); - } - None => { - assert_eq!( - std::path::Path::new(&cmd.command), - gix_path::env::exe_invocation(), - "without it, `git` itself runs the service as subcommand" - ); - assert_eq!( - cmd.args.first().and_then(|arg| arg.to_str()), - Some("upload-pack"), - "the sub-command is passed as separate argument, without the 'git-' prefix" - ); - assert_eq!(cmd.args.len(), 2, "sub-command and repo path"); - } - } - } - } - - mod ssh { - mod connect { - use crate::{Protocol, client::blocking_io::ssh}; - - #[test] - fn path() { - for (url, expected) in [ - ("ssh://host.xy/~/repo", "~/repo"), - ("ssh://host.xy/~username/repo", "~username/repo"), - ("user@host.xy:/username/repo", "/username/repo"), - ("user@host.xy:username/repo", "username/repo"), - ("user@host.xy:../username/repo", "../username/repo"), - ("user@host.xy:~/repo", "~/repo"), - ] { - let url = gix_url::parse(url).expect("valid url"); - let cmd = ssh::connect(url, Protocol::V1, Default::default(), false).expect("parse success"); - assert_eq!(cmd.path, expected, "the path will be substituted by the remote shell"); - } - } - - #[test] - fn upload_pack_invocation_preserves_scp_like_path_distinction() -> gix_testtools::TestResult { - for (url, expected) in [ - ( - "git@forge.com:/path/repo", - &["ssh", "git@forge.com", "git-upload-pack", "'/path/repo'"][..], - ), - ( - "git@forge.com:path/repo", - &["ssh", "git@forge.com", "git-upload-pack", "'path/repo'"][..], - ), - ( - "ssh://git@forge.com/path/repo", - &["ssh", "git@forge.com", "git-upload-pack", "'/path/repo'"][..], - ), - ] { - let url = gix_url::parse(url)?; - let cmd = ssh::connect(url, Protocol::V1, Default::default(), false)?; - assert_eq!( - command_and_args(cmd.prepare_command(crate::Service::UploadPack)?.0), - expected, - "the remote shell command must match Git's parsed repository path" - ); - } - Ok(()) - } - - #[test] - fn ambiguous_host_disallowed() { - let mut matched_error_diagnostics = Vec::new(); - for url in [ - "ssh://-oProxyCommand=open$IFS-aCalculator/foo", - "user@-oProxyCommand=open$IFS-aCalculator:username/repo", - ] { - let url = gix_url::parse(url).expect("valid url"); - let options = ssh::connect::Options { - command: Some("unrecognized".into()), - disallow_shell: false, - kind: None, - }; - let failure = ssh::connect(url, Protocol::V1, options, false) - .err() - .expect("the operation must fail"); - matched_error_diagnostics.push(gix_testtools::redact_debug_snapshot(&failure, &[])); - assert!( - matches!(failure, ssh::Error::AmbiguousHostName { host } if host == "-oProxyCommand=open$IFS-aCalculator"), - "ambiguous host disallowed" - ); - } - insta::assert_debug_snapshot!(matched_error_diagnostics, "ambiguous host disallowed", @r#" - [ - AmbiguousHostName { - host: "-oProxyCommand=open$IFS-aCalculator", - }, - AmbiguousHostName { - host: "-oProxyCommand=open$IFS-aCalculator", - }, - ] - "#); - } - - fn command_and_args(cmd: gix_command::Prepare) -> Vec { - let program = cmd.command.clone(); - let cmd = - std::process::Command::try_from(cmd).expect("command fixture can be represented by the platform"); - let expected_program = std::process::Command::try_from(gix_command::prepare(&program)) - .expect("command fixture can be represented by the platform"); - assert_eq!( - cmd.get_program(), - expected_program.get_program(), - "the selected SSH program follows the platform's command lookup" - ); - std::iter::once(program.as_os_str()) - .chain(cmd.get_args()) - .map(|arg| arg.to_string_lossy().into_owned()) - .collect() - } - } - } -} diff --git a/vendor/gix-transport/src/client/blocking_io/http/curl/mod.rs b/vendor/gix-transport/src/client/blocking_io/http/curl/mod.rs deleted file mode 100644 index cd3202d..0000000 --- a/vendor/gix-transport/src/client/blocking_io/http/curl/mod.rs +++ /dev/null @@ -1,160 +0,0 @@ -use std::{ - sync::{ - Arc, - mpsc::{Receiver, SyncSender}, - }, - thread, -}; - -use gix_error::{ClassificationMarker, Error, Result, ResultExt, message}; -use gix_utils::io; -use parking_lot::Mutex; - -use crate::client::blocking_io::http::{self, traits::PostBodyDataKind}; - -mod remote; - -/// Options to configure the `curl` HTTP handler. -#[derive(Default)] -pub struct Options { - /// If `true` and runtime configuration is possible for `curl` backends, certificates revocation will be checked. - /// - /// This only works on windows apparently. Ignored if `None`. - pub schannel_check_revoke: Option, -} - -pub(crate) fn curl_is_retryable(err: &curl::Error) -> bool { - err.is_couldnt_connect() - || err.is_couldnt_resolve_proxy() - || err.is_couldnt_resolve_host() - || err.is_operation_timedout() - || err.is_recv_error() - || err.is_send_error() - || err.is_http2_error() - || err.is_http2_stream_error() - || err.is_ssl_connect_error() - || err.is_partial_file() -} - -/// A utility to abstract interactions with curl handles. -pub struct Curl { - req: SyncSender, - res: Receiver, - handle: Option>, - config: http::Options, - redirected_base_url: Arc>>, -} - -impl Curl { - fn restore_thread_after_failure(&mut self) -> Error { - let err_that_brought_thread_down = self - .handle - .take() - .expect("thread handle present") - .join() - .expect("handler thread should never panic") - .expect_err("something should have gone wrong with curl (we join on error only)"); - let (handle, req, res, redirected_base_url) = remote::new(); - self.handle = Some(handle); - self.req = req; - self.res = res; - self.redirected_base_url = redirected_base_url; - err_that_brought_thread_down - } - - fn make_request( - &mut self, - url: &str, - base_url: &str, - headers: impl IntoIterator>, - upload_body_kind: Option, - ) -> Result> { - let mut list = curl::easy::List::new(); - for header in headers { - list.append(header.as_ref()) - .map_err(|err| { - if curl_is_retryable(&err) { - Error::from_error(ClassificationMarker::with_source(gix_error::Class::Retryable, err)) - } else { - Error::from_error(err) - } - }) - .or_raise(|| message("Could not add HTTP header"))?; - } - if self - .req - .send(remote::Request { - url: url.to_owned(), - base_url: base_url.to_owned(), - headers: list, - upload_body_kind, - config: self.config.clone(), - }) - .is_err() - { - return Err(self.restore_thread_after_failure()); - } - let remote::Response { - headers, - body, - upload_body, - } = match self.res.recv() { - Ok(res) => res, - Err(_) => return Err(self.restore_thread_after_failure()), - }; - Ok(http::PostResponse { - post_body: upload_body, - headers, - body, - }) - } -} - -impl Default for Curl { - fn default() -> Self { - let (handle, req, res, redirected_base_url) = remote::new(); - Curl { - handle: Some(handle), - req, - res, - config: http::Options::default(), - redirected_base_url, - } - } -} - -impl http::Http for Curl { - type Headers = io::pipe::Reader; - type ResponseBody = io::pipe::Reader; - type PostBody = io::pipe::Writer; - - fn get( - &mut self, - url: &str, - base_url: &str, - headers: impl IntoIterator>, - ) -> Result> { - self.make_request(url, base_url, headers, None).map(Into::into) - } - - fn post( - &mut self, - url: &str, - base_url: &str, - headers: impl IntoIterator>, - body: PostBodyDataKind, - ) -> Result> { - self.make_request(url, base_url, headers, Some(body)) - } - - fn configure(&mut self, config: &dyn std::any::Any) -> Result { - if let Some(config) = config.downcast_ref::() { - self.config = config.clone(); - } - Ok(()) - } - - fn redirected_base_url(&self) -> Option { - self.redirected_base_url.lock().clone() - } -} diff --git a/vendor/gix-transport/src/client/blocking_io/http/curl/remote.rs b/vendor/gix-transport/src/client/blocking_io/http/curl/remote.rs deleted file mode 100644 index 1726efb..0000000 --- a/vendor/gix-transport/src/client/blocking_io/http/curl/remote.rs +++ /dev/null @@ -1,982 +0,0 @@ -use std::{ - io, - io::{Read, Write}, - sync::{ - Arc, - mpsc::{Receiver, SyncSender, TrySendError, sync_channel}, - }, - thread, - time::Duration, -}; - -use bstr::ByteSlice; -use curl::easy::{Auth, Easy2}; -use gix_error::{ClassificationMarker, Error, ErrorExt, OptionExt, Result, ResultExt, message}; -use gix_utils::io::pipe; -use parking_lot::Mutex; - -use crate::client::blocking_io::http::{ - self, - curl::curl_is_retryable, - options::{FollowRedirects, HttpVersion, ProxyAuthMethod, SslVersion}, - redirect::{self, Action as RedirectAction}, - traits::PostBodyDataKind, -}; - -fn classify_curl(err: curl::Error) -> Error { - if curl_is_retryable(&err) { - Error::from_error(ClassificationMarker::with_source(gix_error::Class::Retryable, err)) - } else { - Error::from_error(err) - } -} - -macro_rules! curl { - ($expr:expr) => { - $expr - .map_err(classify_curl) - .or_raise(|| message("Curl operation failed"))? - }; -} - -enum StreamOrBuffer { - Stream(pipe::Reader), - Buffer(std::io::Cursor>), -} - -/// Shared output for the redirected base URL of the active request. -/// -/// Before request setup calls `track_redirects()`, this points to a private `None` value created by `Default`. -/// During a request it points to the worker-shared output, allowing redirect headers observed by curl callbacks to -/// update the transport-visible base URL before `perform()` returns. -type SharedRedirectedBaseUrl = Arc>>; - -#[derive(Default)] -struct Handler { - /// Sends response headers to the consumer until the request finishes or an error is reported. - send_header: Option, - /// Sends response body chunks to the consumer until the request finishes or an error is reported. - send_data: Option, - /// Provides the optional upload body to curl, either streamed from the caller or buffered for known-size uploads. - receive_body: Option, - /// The I/O failure that made a callback abort the active transfer. - io_error: Option, - /// `true` once the status line of the current response header block was parsed. - checked_status: bool, - /// Status code of the current response header block, used to associate following headers with redirects. - current_status: Option, - /// Challenges from the current 401 response, sent together with its error after all headers arrive. - authentication: crate::client::AuthenticationRequired, - /// Whether the previous header was an authentication challenge, so an obsolete folded header line, - /// one beginning with a space or tab to continue the previous header's value, can be appended to it. - continuing_authentication_header: bool, - /// Last non-success status reported to the caller, or `200` for a successful transfer. - last_status: usize, - /// Redirect policy configured for the current request sequence. - follow: FollowRedirects, - /// Per-request redirect behavior. - redirect_action: RedirectAction, - /// URL curl is currently requested to fetch, including any previously published redirect target. - request_url: String, - /// Caller-provided base URL used to derive the transport-visible base URL after redirects. - base_url: String, - redirected_base_url: SharedRedirectedBaseUrl, -} - -impl Handler { - fn reset(&mut self) { - self.io_error = None; - self.checked_status = false; - self.current_status = None; - self.authentication.www_authenticate.clear(); - self.continuing_authentication_header = false; - self.last_status = 0; - self.follow = FollowRedirects::default(); - self.redirect_action = RedirectAction::Stop; - } - - fn transfer_error(&mut self, err: curl::Error) -> io::Error { - match self.io_error.take() { - Some(source) => io::Error::new(source.kind(), source.and_raise(err)), - None => io::Error::new( - if curl_is_retryable(&err) { - io::ErrorKind::ConnectionReset - } else { - io::ErrorKind::Other - }, - err, - ), - } - } - - fn track_redirects( - &mut self, - request_url: String, - base_url: String, - redirected_base_url: SharedRedirectedBaseUrl, - redirect_action: RedirectAction, - ) { - self.request_url = request_url; - self.base_url = base_url; - self.redirected_base_url = redirected_base_url; - self.redirect_action = redirect_action; - } - - fn parse_status_inner(data: &[u8]) -> Result { - let code = data - .split(|b| *b == b' ') - .nth(1) - .ok_or_raise(|| message("Expected HTTP/ STATUS"))?; - let code = std::str::from_utf8(code).or_error()?; - code.parse::().or_error() - } - fn parse_status(data: &[u8], follow: FollowRedirects) -> Option<(usize, Error)> { - let valid_end = match follow { - FollowRedirects::Initial | FollowRedirects::All => 308, - FollowRedirects::None => 299, - }; - match Self::parse_status_inner(data) { - Ok(status) if !(200..=valid_end).contains(&status) => { - let failure = message!("Received HTTP status {status}"); - let failure = match status { - 403 => failure.permission_denied_error(), - 409 => failure.conflict_error(), - 501 => failure.unsupported_error(), - _ => failure.raise(), - }; - Some((status, failure)) - } - Ok(_) => None, - Err(err) => Some((500, err)), - } - } - - fn collect_authentication_header(&mut self, data: &[u8]) { - if matches!(data.first(), Some(b' ' | b'\t')) { - if self.continuing_authentication_header - && let Some(value) = self.authentication.www_authenticate.last_mut() - { - let continuation = data.trim_ascii(); - if !continuation.is_empty() { - if !value.is_empty() { - value.push(b' '); - } - value.extend_from_slice(continuation); - } - } - return; - } - self.continuing_authentication_header = false; - if let Some(colon) = data.find_byte(b':') - && data[..colon].eq_ignore_ascii_case(b"www-authenticate") - { - self.authentication - .www_authenticate - .push(data[colon + 1..].trim_ascii().into()); - self.continuing_authentication_header = true; - } - } - - /// Record a redirected base URL while curl is still reporting response headers. - /// - /// Curl provides the normalized final URL through `effective_url()` only after `perform()` finishes - /// successfully. Redirected authentication failures need the new base before returning the 401 error so the retry - /// targets the redirected URL, which means we have to inspect and resolve the raw `Location` header here. - fn publish_redirect_location(&self, data: &[u8]) { - if self.redirect_action != RedirectAction::Follow || !self.current_status.is_some_and(is_redirect_status) { - return; - } - - let Some((name, value)) = std::str::from_utf8(data).ok().and_then(|line| line.split_once(':')) else { - return; - }; - if !name.eq_ignore_ascii_case("location") { - return; - } - - let Some(location) = absolute_location(&self.request_url, value.trim()) else { - return; - }; - - let Ok(new_base_url) = redirect::base_url(&location, &self.base_url, self.request_url.clone()) else { - return; - }; - *self.redirected_base_url.lock() = Some(new_base_url); - } -} - -/// Convert a raw `Location` header into the absolute URL curl would eventually follow. -/// -/// Unlike reqwest's error path, the curl header callback only gives us the server-provided value. Absolute and -/// scheme-relative locations can be completed directly, while path-only locations must be resolved against the current -/// request URL. -fn absolute_location(request_url: &str, location: &str) -> Option { - if location.starts_with("http://") || location.starts_with("https://") { - return Some(location.to_owned()); - } - - let request_url = gix_url::parse(request_url).ok()?; - let mut out = format!("{}://", request_url.scheme.as_str()); - if location.starts_with("//") { - out.push_str(location.trim_start_matches('/')); - return Some(out); - } - out.push_str(request_url.host()?); - if let Some(port) = request_url.port { - out.push(':'); - out.push_str(&port.to_string()); - } - let request_path = request_url.original_path().to_str().ok()?; - out.push_str(&resolve_location_path(request_path, location)); - Some(out) -} - -/// Resolve a path-only `Location` value in `location` against the current `request_path`. -/// -/// This mirrors URL redirect resolution for the subset curl does not expose during header processing: root-relative -/// paths, relative paths, and query/fragment-only updates, including normalization of `.` and `..` path segments. -fn resolve_location_path(request_path: &str, location: &str) -> String { - let (path, suffix) = split_url_path_suffix(location); - if path.starts_with('/') { - let mut out = normalize_url_path(path); - out.push_str(suffix); - return out; - } - - let (request_path, _) = split_url_path_suffix(request_path); - if path.is_empty() { - let mut out = request_path.to_owned(); - out.push_str(suffix); - return out; - } - - let base_dir = request_path.rsplit_once('/').map_or("", |(dir, _)| dir); - let mut merged = String::new(); - merged.push_str(base_dir); - merged.push('/'); - merged.push_str(path); - let mut out = normalize_url_path(&merged); - out.push_str(suffix); - out -} - -fn split_url_path_suffix(input: &str) -> (&str, &str) { - input - .find(['?', '#']) - .map_or((input, ""), |suffix| input.split_at(suffix)) -} - -/// Normalize URL path segments while preserving absolute and trailing-slash shape. -fn normalize_url_path(path: &str) -> String { - let absolute = path.starts_with('/'); - let trailing_slash = path.ends_with('/'); - let mut segments = Vec::new(); - for segment in path.split('/') { - match segment { - "" | "." => {} - ".." => { - segments.pop(); - } - segment => segments.push(segment), - } - } - - let mut out = String::new(); - if absolute { - out.push('/'); - } - out.push_str(&segments.join("/")); - if trailing_slash && !out.ends_with('/') { - out.push('/'); - } - if out.is_empty() { - out.push('/'); - } - out -} - -impl curl::easy::Handler for Handler { - fn write(&mut self, data: &[u8]) -> std::result::Result { - drop(self.send_header.take()); // signal header readers to stop trying - match self.send_data.as_mut() { - Some(writer) => writer.write_all(data).map(|_| data.len()).or_else(|err| { - self.io_error = Some(err); - Ok(0) - }), - None => Ok(0), // nothing more to receive, reader is done - } - } - fn read(&mut self, data: &mut [u8]) -> std::result::Result { - match self.receive_body.as_mut() { - Some(StreamOrBuffer::Stream(reader)) => reader.read(data), - Some(StreamOrBuffer::Buffer(cursor)) => cursor.read(data), - None => Ok(0), // nothing more to read/writer depleted - } - .map_err(|err| { - self.io_error = Some(err); - curl::easy::ReadError::Abort - }) - } - - fn header(&mut self, data: &[u8]) -> bool { - if self.send_header.is_none() { - return true; - } - let header_block_is_done = matches!(data, b"\r\n" | b"\n"); - if header_block_is_done { - if let Some(writer) = self.send_header.as_mut() { - if self.current_status == Some(401) { - writer - .channel - .send(Err(io::Error::new( - io::ErrorKind::PermissionDenied, - std::mem::take(&mut self.authentication), - ))) - .ok(); - } - writer.write_all(data).ok(); - } - self.checked_status = false; - self.current_status = None; - self.continuing_authentication_header = false; - return true; - } - if self.checked_status { - self.publish_redirect_location(data); - if self.current_status == Some(401) { - self.collect_authentication_header(data); - } else if let Some(writer) = self.send_header.as_mut() { - writer.write_all(data).ok(); - } - } else { - self.checked_status = true; - self.last_status = 200; - self.current_status = Handler::parse_status_inner(data).ok(); - self.authentication.www_authenticate.clear(); - self.continuing_authentication_header = false; - let err = self.current_status.and_then(|status| { - if self.redirect_action == RedirectAction::RejectConfiguredHeaders && is_redirect_status(status) { - Some(( - status, - message("refusing to follow redirect after request headers were configured").raise(), - )) - } else { - Handler::parse_status(data, self.follow) - } - }); - if let Some((status, err)) = err { - self.last_status = status; - // A 401 needs its headers before the caller can ask a credential helper for an account. - if status != 401 - && let Some(writer) = self.send_header.as_mut() - { - writer - .channel - .send(Err(io::Error::new( - if (500..600).contains(&status) && status != 501 { - io::ErrorKind::ConnectionAborted - } else { - io::ErrorKind::Other - }, - err, - ))) - .ok(); - } - } - } - true - } -} - -pub struct Request { - pub url: String, - pub base_url: String, - pub headers: curl::easy::List, - pub upload_body_kind: Option, - pub config: http::Options, -} - -pub struct Response { - pub headers: pipe::Reader, - pub body: pipe::Reader, - pub upload_body: pipe::Writer, -} - -type Worker = ( - thread::JoinHandle, - SyncSender, - Receiver, - SharedRedirectedBaseUrl, -); - -pub fn new() -> Worker { - let redirected_base_url_shared = Arc::new(Mutex::new(None)); - let redirected_base_url_shared_out = redirected_base_url_shared.clone(); - let (req_send, req_recv) = sync_channel(0); - let (res_send, res_recv) = sync_channel(0); - let handle = std::thread::spawn(gix_trace::in_thread(move || -> Result { - let mut handle = Easy2::new(Handler::default()); - // We don't wait for the possibility for pipelining to become clear, and curl tries to reuse connections by default anyway. - curl!(handle.pipewait(false)); - curl!(handle.tcp_keepalive(true)); - - let mut follow = None; - - for Request { - url, - base_url, - mut headers, - upload_body_kind, - config: - http::Options { - extra_headers, - follow_redirects, - low_speed_limit_bytes_per_second, - low_speed_time_seconds, - connect_timeout, - proxy, - no_proxy, - proxy_auth_method, - user_agent, - proxy_authenticate, - verbose, - ssl_ca_info, - ssl_version, - ssl_verify, - http_version, - backend, - }, - } in req_recv - { - let redirected_base_url = redirected_base_url_shared.lock().clone(); - let effective_url = redirect::swap_tails(redirected_base_url.as_deref(), &base_url, url.clone()); - curl!(handle.url(&effective_url)); - - curl!(handle.post(upload_body_kind.is_some())); - let has_extra_headers = !extra_headers.is_empty(); - for header in extra_headers { - curl!(headers.append(&header)); - } - // needed to avoid sending Expect: 100-continue, which adds another response and only CURL wants that - curl!(headers.append("Expect:")); - curl!(handle.verbose(verbose)); - - if let Some(ca_info) = ssl_ca_info { - curl!(handle.cainfo(ca_info)); - } - - if let Some(ref mut curl_options) = backend.as_ref().and_then(|backend| backend.lock().ok()) - && let Some(opts) = curl_options.downcast_mut::() - && let Some(enabled) = opts.schannel_check_revoke - { - curl!(handle.ssl_options(curl::easy::SslOpt::new().no_revoke(!enabled))); - } - - if let Some(ssl_version) = ssl_version { - let (min, max) = ssl_version.min_max(); - if min == max { - curl!(handle.ssl_version(to_curl_ssl_version(min))); - } else { - curl!(handle.ssl_min_max_version(to_curl_ssl_version(min), to_curl_ssl_version(max))); - } - } - - curl!(handle.ssl_verify_peer(ssl_verify)); - curl!(handle.ssl_verify_host(ssl_verify)); - - if let Some(http_version) = http_version { - let version = match http_version { - HttpVersion::V1_1 => curl::easy::HttpVersion::V11, - HttpVersion::V2 => curl::easy::HttpVersion::V2, - }; - // Failing to set the version isn't critical, and may indeed fail depending on the version - // of libcurl we are built against. - // Furthermore, `git` itself doesn't actually check for errors when configuring curl at all, - // treating all or most flags as non-critical. - handle.http_version(version).ok(); - } - - let mut proxy_auth_action = None; - if let Some(proxy) = proxy { - curl!(handle.proxy(&proxy)); - let proxy_type = if proxy.starts_with("socks5h") { - curl::easy::ProxyType::Socks5Hostname - } else if proxy.starts_with("socks5") { - curl::easy::ProxyType::Socks5 - } else if proxy.starts_with("socks4a") { - curl::easy::ProxyType::Socks4a - } else if proxy.starts_with("socks") { - curl::easy::ProxyType::Socks4 - } else { - curl::easy::ProxyType::Http - }; - curl!(handle.proxy_type(proxy_type)); - - if let Some((obtain_creds_action, authenticate)) = proxy_authenticate { - let creds = authenticate.lock().expect("no panics in other threads")(obtain_creds_action) - .or_raise(|| message("Could not obtain proxy credentials"))? - .expect("action to fetch credentials"); - curl!(handle.proxy_username(&creds.identity.username)); - curl!(handle.proxy_password(&creds.identity.password)); - proxy_auth_action = Some((creds.next, authenticate)); - } - } - if let Some(no_proxy) = no_proxy { - curl!(handle.noproxy(&no_proxy)); - } - if let Some(user_agent) = user_agent { - curl!(handle.useragent(&user_agent)); - } - curl!(handle.transfer_encoding(false)); - if let Some(timeout) = connect_timeout { - curl!(handle.connect_timeout(timeout)); - } - { - let mut auth = Auth::new(); - match proxy_auth_method { - ProxyAuthMethod::AnyAuth => auth - .basic(true) - .digest(true) - .digest_ie(true) - .gssnegotiate(true) - .ntlm(true) - .aws_sigv4(true), - ProxyAuthMethod::Basic => auth.basic(true), - ProxyAuthMethod::Digest => auth.digest(true), - ProxyAuthMethod::Negotiate => auth.digest_ie(true), - ProxyAuthMethod::Ntlm => auth.ntlm(true), - }; - curl!(handle.proxy_auth(&auth)); - } - curl!(handle.tcp_keepalive(true)); - - if low_speed_time_seconds > 0 && low_speed_limit_bytes_per_second > 0 { - curl!(handle.low_speed_limit(low_speed_limit_bytes_per_second)); - curl!(handle.low_speed_time(Duration::from_secs(low_speed_time_seconds))); - } - let (receive_data, receive_headers, send_body, mut receive_body) = { - let handler = handle.get_mut(); - let (send, receive_data) = pipe::unidirectional(1); - handler.send_data = Some(send); - let (send, receive_headers) = pipe::unidirectional(1); - handler.send_header = Some(send); - let (send_body, receive_body) = pipe::unidirectional(0); - (receive_data, receive_headers, send_body, receive_body) - }; - - let follow = follow.get_or_insert(follow_redirects); - let may_follow_redirects = matches!(*follow, FollowRedirects::Initial | FollowRedirects::All); - let redirect_action = RedirectAction::from_request(may_follow_redirects, has_extra_headers); - { - let handler = handle.get_mut(); - handler.follow = *follow; - handler.track_redirects( - effective_url.clone(), - base_url.clone(), - redirected_base_url_shared.clone(), - redirect_action, - ); - } - curl!(handle.follow_location(redirect_action == RedirectAction::Follow)); - - if *follow == FollowRedirects::Initial { - *follow = FollowRedirects::None; - } - - if res_send - .send(Response { - headers: receive_headers, - body: receive_data, - upload_body: send_body, - }) - .is_err() - { - break; - } - - handle.get_mut().receive_body = Some(match upload_body_kind { - Some(PostBodyDataKind::Unbounded) | None => StreamOrBuffer::Stream(receive_body), - Some(PostBodyDataKind::BoundedAndFitsIntoMemory) => { - let mut buf = Vec::::with_capacity(512); - receive_body - .read_to_end(&mut buf) - .or_raise(|| message("Could not finish reading all data to post to the remote"))?; - curl!(handle.post_field_size(buf.len() as u64)); - drop(receive_body); - StreamOrBuffer::Buffer(std::io::Cursor::new(buf)) - } - }); - curl!(handle.http_headers(headers)); - - if let Err(err) = handle.perform() { - let handler = handle.get_mut(); - let err = Err(handler.transfer_error(err)); - handler.reset(); - - if let Some((action, authenticate)) = proxy_auth_action { - authenticate.lock().expect("no panics in other threads")(action.erase()).ok(); - } - handler.receive_body.take(); - match (handler.send_header.take(), handler.send_data.take()) { - (Some(header), mut data) => { - if let Err(TrySendError::Disconnected(err) | TrySendError::Full(err)) = - header.channel.try_send(err) - && let Some(body) = data.take() - { - body.channel.try_send(err).ok(); - } - } - (None, Some(body)) => { - body.channel.try_send(err).ok(); - } - (None, None) => {} - } - } else { - let actual_url = curl!(handle.effective_url()).expect("effective url is present and valid UTF-8"); - if actual_url != effective_url { - let new_base_url = redirect::base_url(actual_url, &base_url, url)?; - *redirected_base_url_shared.lock() = Some(new_base_url); - } - - let handler = handle.get_mut(); - if let Some((action, authenticate)) = proxy_auth_action { - authenticate.lock().expect("no panics in other threads")(if handler.last_status == 200 { - action.store() - } else { - action.erase() - }) - .or_raise(|| message("Could not update proxy credentials"))?; - } - handler.reset(); - handler.receive_body.take(); - handler.send_header.take(); - handler.send_data.take(); - } - } - Ok(()) - })); - (handle, req_send, res_recv, redirected_base_url_shared_out) -} - -fn to_curl_ssl_version(vers: SslVersion) -> curl::easy::SslVersion { - use curl::easy::SslVersion::*; - match vers { - SslVersion::Default => Default, - SslVersion::TlsV1 => Tlsv1, - SslVersion::SslV2 => Sslv2, - SslVersion::SslV3 => Sslv3, - SslVersion::TlsV1_0 => Tlsv10, - SslVersion::TlsV1_1 => Tlsv11, - SslVersion::TlsV1_2 => Tlsv12, - SslVersion::TlsV1_3 => Tlsv13, - } -} - -fn is_redirect_status(status: usize) -> bool { - (300..=308).contains(&status) -} - -#[cfg(test)] -mod authentication_tests { - use curl::easy::Handler as _; - use std::io::Read; - - #[test] - fn http_statuses_are_classified_conservatively() { - use gix_error::Class; - for (status, expected) in [ - (403, Some(Class::PermissionDenied)), - (409, Some(Class::Conflict)), - (501, Some(Class::Unsupported)), - (400, None), - ] { - let response = format!("HTTP/1.1 {status} Response\r\n"); - let (_, err) = super::Handler::parse_status(response.as_bytes(), super::FollowRedirects::None) - .expect("the status reports a failure"); - assert_eq!( - err.classify().map(|class| class.class()).collect::>(), - expected.into_iter().collect::>(), - "known HTTP statuses guide recovery; unknown failures remain unclassified" - ); - } - } - - #[test] - fn only_current_challenges_are_collected_and_continuations_are_unfolded() { - let (writer, mut reader) = gix_utils::io::pipe::unidirectional(16); - let mut handler = super::Handler { - send_header: Some(writer), - ..Default::default() - }; - for line in [ - b"HTTP/1.1 200 Connection established\r\n".as_slice(), - b"WWW-Authenticate: ignored\r\n", - b"\r\n", - b"HTTP/1.1 401 Unauthorized\r\n", - b"wWw-AuThEnTiCaTe: Basic\r\n", - b"\t realm=\"example\" \r\n", - b" \t\r\n", - b"X-Unrelated: ignored\r\n", - b" continuation-of-unrelated-header\r\n", - b"WWW-Authenticate: Bearer realm=\"\xff\"\r\n", - b"\r\n", - ] { - assert!( - handler.header(line), - "headers are consumed without aborting the transfer" - ); - } - let error = reader - .read_to_end(&mut Vec::new()) - .expect_err("401 is reported after all challenges have arrived"); - let details = error - .get_ref() - .and_then(|err| err.downcast_ref::()) - .expect("the authentication error retains byte-oriented header values"); - assert_eq!( - details.www_authenticate, - vec![ - bstr::BString::from(r#"Basic realm="example""#), - bstr::BString::from(b"Bearer realm=\"\xff\"".as_slice()), - ], - "folding only extends its own challenge and earlier header blocks do not contribute hints" - ); - } -} - -#[cfg(test)] -mod absolute_location_tests { - use super::absolute_location; - - const REQUEST_URL: &str = "http://example.com:8080/original/repo/info/refs?service=git-upload-pack"; - - #[test] - fn keeps_absolute_locations() { - assert_eq!( - absolute_location( - REQUEST_URL, - "https://redirected.example/repo/info/refs?service=git-upload-pack" - ), - Some("https://redirected.example/repo/info/refs?service=git-upload-pack".to_owned()), - "absolute Location values already contain the authority curl would follow" - ); - } - - #[test] - fn uses_request_scheme_for_scheme_relative_locations() { - assert_eq!( - absolute_location( - REQUEST_URL, - "//redirected.example/repo/info/refs?service=git-upload-pack" - ), - Some("http://redirected.example/repo/info/refs?service=git-upload-pack".to_owned()), - "scheme-relative Location values inherit the scheme of the request URL" - ); - } - - #[test] - fn resolves_root_relative_locations() { - assert_eq!( - absolute_location( - REQUEST_URL, - "/redirected/./repo/../repo/info/refs?service=git-upload-pack" - ), - Some("http://example.com:8080/redirected/repo/info/refs?service=git-upload-pack".to_owned()), - "root-relative Location values keep the request authority and normalize path segments" - ); - } - - #[test] - fn resolves_relative_locations_from_the_request_directory() { - assert_eq!( - absolute_location( - REQUEST_URL, - "../../../redirected/repo/info/refs?service=git-upload-pack" - ), - Some("http://example.com:8080/redirected/repo/info/refs?service=git-upload-pack".to_owned()), - "relative Location values are resolved from the current request directory before normalization" - ); - } - - #[test] - fn resolves_query_only_locations_against_the_request_path() { - assert_eq!( - absolute_location(REQUEST_URL, "?service=git-receive-pack"), - Some("http://example.com:8080/original/repo/info/refs?service=git-receive-pack".to_owned()), - "query-only Location values replace the query while preserving the current request path" - ); - } - - #[test] - fn keeps_percent_encoded_separators_in_relative_locations() { - assert_eq!( - absolute_location( - "http://example.com/a%2Fb/info/refs?service=git-upload-pack", - "../objects/info/packs" - ), - Some("http://example.com/a%2Fb/objects/info/packs".to_owned()), - "relative redirects resolve against the encoded request path" - ); - } - - #[test] - fn keeps_percent_encoded_non_separators_in_relative_locations() { - assert_eq!( - absolute_location( - "http://example.com/a%20b/info/refs?service=git-upload-pack", - "../objects/info/packs" - ), - Some("http://example.com/a%20b/objects/info/packs".to_owned()), - "relative redirects use the original encoded request path" - ); - } -} - -#[cfg(test)] -mod resolve_location_path_tests { - use super::resolve_location_path; - - const REQUEST_PATH: &str = "/original/repo/info/refs?service=git-upload-pack"; - - #[test] - fn keeps_root_relative_locations_rooted_and_normalized() { - assert_eq!( - resolve_location_path( - REQUEST_PATH, - "/redirected/./repo/../repo/info/refs?service=git-upload-pack" - ), - "/redirected/repo/info/refs?service=git-upload-pack", - "root-relative Location paths should ignore the request path and normalize their own segments" - ); - } - - #[test] - fn resolves_relative_locations_from_the_request_directory() { - assert_eq!( - resolve_location_path( - REQUEST_PATH, - "../../../redirected/repo/info/refs?service=git-upload-pack" - ), - "/redirected/repo/info/refs?service=git-upload-pack", - "relative Location paths should resolve from the directory containing the current request path" - ); - } - - #[test] - fn replaces_query_only_locations_on_the_request_path() { - assert_eq!( - resolve_location_path(REQUEST_PATH, "?service=git-receive-pack"), - "/original/repo/info/refs?service=git-receive-pack", - "query-only Location values should preserve the current path and replace only the query" - ); - } - - #[test] - fn replaces_fragment_only_locations_on_the_request_path() { - assert_eq!( - resolve_location_path(REQUEST_PATH, "#advertisement"), - "/original/repo/info/refs#advertisement", - "fragment-only Location values should preserve the current path and replace the query with the fragment" - ); - } - - #[test] - fn strips_request_query_before_resolving_relative_locations() { - assert_eq!( - resolve_location_path(REQUEST_PATH, "../objects/info/packs"), - "/original/repo/objects/info/packs", - "relative Location paths should resolve against the request path without its existing query" - ); - } -} - -#[cfg(test)] -mod tests { - use curl::easy::Handler as _; - - use super::{FollowRedirects, Handler, StreamOrBuffer, io, pipe}; - - #[test] - fn malformed_statuses_preserve_native_errors() { - let (utf8_status, utf8_error) = - Handler::parse_status(b"HTTP/1.1 \xff", FollowRedirects::None).expect("a non-UTF-8 status is rejected"); - let (number_status, number_error) = - Handler::parse_status(b"HTTP/1.1 invalid", FollowRedirects::None).expect("a nonnumeric status is rejected"); - assert!( - utf8_error.downcast_any_ref::().is_some(), - "status decoding retains its native UTF-8 error" - ); - assert!( - number_error.downcast_any_ref::().is_some(), - "status parsing retains its native numeric error" - ); - for (status, err) in [(utf8_status, utf8_error), (number_status, number_error)] { - assert_eq!(status, 500, "malformed statuses use the existing server-error fallback"); - let sources: Vec<_> = err.iter_errors_with_locations().collect(); - assert_eq!(sources.len(), 1, "propagation adds no diagnostic frames"); - assert_eq!( - sources[0].location().expect("the parser error was raised").file(), - file!(), - "native parser errors capture their caller before propagation" - ); - } - } - - #[test] - fn aborted_uploads_preserve_the_callback_error() -> gix_testtools::TestResult { - let (writer, reader) = pipe::unidirectional(1); - writer - .channel - .send(Err(io::Error::other(gix_error::ClassificationMarker::with_source( - gix_error::Class::Retryable, - gix_error::not_found("custom upload source is unavailable"), - ))))?; - let mut handler = Handler { - receive_body: Some(StreamOrBuffer::Stream(reader)), - ..Default::default() - }; - assert!(handler.read(&mut [0]).is_err(), "the callback aborts the transfer"); - // CURLE_ABORTED_BY_CALLBACK is the error curl returns after ReadError::Abort. - let err = gix_error::Error::from_error(handler.transfer_error(curl::Error::new(42))); - insta::assert_debug_snapshot!(err, "the custom upload source retains its retry policy", @" - Custom { - kind: Other, - error: [42] Operation was aborted by an application callback - - Caused by: - 0: I/O error (Other) - 1: custom upload source is unavailable, - } - "); - assert!(err.can_retry(), "the custom upload source retains its retry policy"); - assert!(err.is_not_found(), "other callback classifications survive too"); - assert!(err.downcast_any_ref::().is_some()); - assert!(handler.io_error.is_none(), "a later request cannot reuse this failure"); - Ok(()) - } - - #[test] - fn failed_download_writes_preserve_the_pipe_error() { - let (writer, reader) = pipe::unidirectional(1); - drop(reader); - let mut handler = Handler { - send_data: Some(writer), - ..Default::default() - }; - assert_eq!(handler.write(b"response").expect("short writes abort curl"), 0); - // CURLE_WRITE_ERROR is the error curl returns after a short callback write. - let err = handler.transfer_error(curl::Error::new(23)); - insta::assert_debug_snapshot!(err, "failed download writes preserve the pipe error", @" - Custom { - kind: BrokenPipe, - error: [23] Failed writing received data to disk/application - - Caused by: - 0: I/O error (BrokenPipe) - 1: sending on a closed channel, - } - "); - assert_eq!(err.kind(), io::ErrorKind::BrokenPipe); - assert!(gix_error::classify(&err).can_retry_lenient()); - } -} diff --git a/vendor/gix-transport/src/client/blocking_io/http/mod.rs b/vendor/gix-transport/src/client/blocking_io/http/mod.rs deleted file mode 100644 index 71af2a7..0000000 --- a/vendor/gix-transport/src/client/blocking_io/http/mod.rs +++ /dev/null @@ -1,687 +0,0 @@ -use gix_error::Result; -use std::{ - any::Any, - borrow::Cow, - io::{BufRead, Read}, - path::PathBuf, - sync::{Arc, Mutex}, -}; - -use base64::Engine; -use bstr::BStr; -use gix_error::{ErrorExt, message}; -pub use traits::{GetResponse, Http, PostBodyDataKind, PostResponse}; - -use crate::{ - Protocol, Service, - client::{ - self, MessageKind, - blocking_io::{ - self, ExtendedBufRead, HandleProgress, RequestWriter, SetServiceResponse, - bufread_ext::ReadlineBufRead, - http::options::{HttpVersion, SslVersionRangeInclusive}, - }, - capabilities::blocking_recv::Handshake, - }, - packetline::{PacketLineRef, blocking_io::StreamingPeekableIter}, -}; - -#[cfg(feature = "http-client-curl")] -/// -pub mod curl; - -/// The experimental `reqwest` backend. -/// -/// It doesn't support any of the shared http options yet, but can be seen as example on how to integrate blocking `http` backends. -/// There is also nothing that would prevent it from becoming a fully-featured HTTP backend except for demand and time. -#[cfg(feature = "http-client-reqwest")] -pub mod reqwest; - -mod traits; - -/// -pub mod options { - use gix_error::Result; - /// A function to authenticate a URL. - pub type AuthenticateFn = - dyn FnMut(gix_credentials::helper::Action) -> Result> + Send + Sync; - - /// Possible settings for the `http.followRedirects` configuration option. - #[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] - pub enum FollowRedirects { - /// Follow only the first redirect request, most suitable for typical git requests. - #[default] - Initial, - /// Follow all redirect requests from the server unconditionally - All, - /// Follow no redirect request. - None, - } - - /// The way to configure a proxy for authentication if a username is present in the configured proxy. - #[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] - pub enum ProxyAuthMethod { - /// Automatically pick a suitable authentication method. - #[default] - AnyAuth, - ///HTTP basic authentication. - Basic, - /// Http digest authentication to prevent a password to be passed in clear text. - Digest, - /// GSS negotiate authentication. - Negotiate, - /// NTLM authentication - Ntlm, - } - - /// Available SSL version numbers. - #[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd)] - #[expect(missing_docs)] - pub enum SslVersion { - /// The implementation default, which is unknown to this layer of abstraction. - Default, - TlsV1, - SslV2, - SslV3, - TlsV1_0, - TlsV1_1, - TlsV1_2, - TlsV1_3, - } - - /// Available HTTP version numbers. - #[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd)] - pub enum HttpVersion { - /// Equivalent to HTTP/1.1 - V1_1, - /// Equivalent to HTTP/2 - V2, - } - - /// The desired range of acceptable SSL versions, or the single version to allow if both are set to the same value. - #[derive(Debug, Copy, Clone, PartialEq, Eq)] - pub struct SslVersionRangeInclusive { - /// The smallest allowed ssl version to use. - pub min: SslVersion, - /// The highest allowed ssl version to use. - pub max: SslVersion, - } - - impl SslVersionRangeInclusive { - /// Return `min` and `max` fields in the right order so `min` is smaller or equal to `max`. - pub fn min_max(&self) -> (SslVersion, SslVersion) { - if self.min > self.max { - (self.max, self.min) - } else { - (self.min, self.max) - } - } - } -} - -/// Options to configure http requests. -// TODO: testing most of these fields requires a lot of effort, unless special flags to introspect ongoing requests are added. -#[derive(Clone)] -pub struct Options { - /// Headers to be added to every request. - /// They are applied unconditionally and are expected to be valid as they occur in an HTTP request, like `header: value`, without newlines. - /// - /// Refers to `http.extraHeader` multi-var. - pub extra_headers: Vec, - /// How to handle redirects. - /// - /// Refers to `http.followRedirects`. - pub follow_redirects: options::FollowRedirects, - /// Used in conjunction with `low_speed_time_seconds`, any non-0 value signals the amount of bytes per second at least to avoid - /// aborting the connection. - /// - /// Refers to `http.lowSpeedLimit`. - pub low_speed_limit_bytes_per_second: u32, - /// Used in conjunction with `low_speed_bytes_per_second`, any non-0 value signals the amount seconds the minimal amount - /// of bytes per second isn't reached. - /// - /// Refers to `http.lowSpeedTime`. - pub low_speed_time_seconds: u64, - /// A curl-style proxy declaration of the form `[protocol://][user[:password]@]proxyhost[:port]`. - /// - /// Note that an empty string means the proxy is disabled entirely. - /// Refers to `http.proxy`. - pub proxy: Option, - /// The comma-separated list of hosts to not send through the `proxy`, or `*` to entirely disable all proxying. - pub no_proxy: Option, - /// The way to authenticate against the proxy if the `proxy` field contains a username. - /// - /// Refers to `http.proxyAuthMethod`. - pub proxy_auth_method: options::ProxyAuthMethod, - /// If authentication is needed for the proxy as its URL contains a username, this method must be set to provide a password - /// for it before making the request, and to store it if the connection succeeds. - pub proxy_authenticate: Option<(gix_credentials::helper::Action, Arc>)>, - /// The `HTTP` `USER_AGENT` string presented to an `HTTP` server, notably not the user agent present to the `git` server. - /// - /// If not overridden, it defaults to the user agent provided by `curl`, which is a deviation from how `git` handles this. - /// Thus it's expected from the callers to set it to their application, or use higher-level crates which make it easy to do this - /// more correctly. - /// - /// Using the correct user-agent might affect how the server treats the request. - /// - /// Refers to `http.userAgent`. - pub user_agent: Option, - /// The amount of time we wait until aborting a connection attempt. - /// - /// If `None`, this typically defaults to 2 minutes to 5 minutes. - /// Refers to `gitoxide.http.connectTimeout`. - pub connect_timeout: Option, - /// If enabled, emit additional information about connections and possibly the data received or written. - pub verbose: bool, - /// If set, use this path to point to a file with CA certificates to verify peers. - pub ssl_ca_info: Option, - /// The SSL version or version range to use, or `None` to let the TLS backend determine which versions are acceptable. - pub ssl_version: Option, - /// Controls whether to perform SSL identity verification or not. Turning this off is not recommended and can lead to - /// various security risks. An example where this may be needed is when an internal git server uses a self-signed - /// certificate and the user accepts the associated security risks. - pub ssl_verify: bool, - /// The HTTP version to enforce. If unset, it is implementation defined. - pub http_version: Option, - /// Backend specific options, if available. - pub backend: Option>>, -} - -impl Default for Options { - fn default() -> Self { - Options { - extra_headers: vec![], - follow_redirects: Default::default(), - low_speed_limit_bytes_per_second: 0, - low_speed_time_seconds: 0, - proxy: None, - no_proxy: None, - proxy_auth_method: Default::default(), - proxy_authenticate: None, - user_agent: None, - connect_timeout: None, - verbose: false, - ssl_ca_info: None, - ssl_version: None, - ssl_verify: true, - http_version: None, - backend: None, - } - } -} - -/// A transport for supporting arbitrary http clients by abstracting interactions with them into the [Http] trait. -pub struct Transport { - url: String, - user_agent_header: &'static str, - desired_version: Protocol, - actual_version: Protocol, - http: H, - service: Option, - line_provider: Option>, - identity: Option, - trace: bool, -} - -impl Transport { - /// Create a new instance with `http` as implementation to communicate to `url` using the given `desired_version`. - /// Note that we will always fallback to other versions as supported by the server. - /// If `trace` is `true`, all packetlines received or sent will be passed to the facilities of the `gix-trace` crate. - pub fn new_http(http: H, url: gix_url::Url, desired_version: Protocol, trace: bool) -> Self { - let identity = url - .user() - .zip(url.password()) - .map(|(user, pass)| gix_sec::identity::Account { - username: user.to_string(), - password: pass.to_string(), - oauth_refresh_token: None, - }); - Transport { - url: url.to_bstring().to_string(), - user_agent_header: concat!("User-Agent: git/oxide-", env!("CARGO_PKG_VERSION")), - desired_version, - actual_version: Default::default(), - service: None, - http, - line_provider: None, - identity, - trace, - } - } -} - -impl Transport { - /// Returns the identity that the transport uses when connecting to the remote. - pub fn identity(&self) -> Option<&gix_sec::identity::Account> { - self.identity.as_ref() - } - - fn sync_redirected_base_url(&mut self) { - Self::sync_redirected_base_url_from(&self.http, &mut self.url, &mut self.identity); - } - - /// Update `url` with the backend's accepted redirect target, clearing credentials if the - /// redirected authority must not reuse the original identity. - fn sync_redirected_base_url_from(http: &H, url: &mut String, identity: &mut Option) { - let Some(redirected_url) = http.redirected_base_url() else { - return; - }; - if redirected_url == *url { - return; - } - - if !redirect::can_reuse_identity(&redirected_url, url) { - *identity = None; - } - *url = redirected_url; - } -} - -#[cfg(any(feature = "http-client-curl", feature = "http-client-reqwest"))] -impl Transport { - /// Create a new instance to communicate to `url` using the given `desired_version` of the `git` protocol. - /// If `trace` is `true`, all packetlines received or sent will be passed to the facilities of the `gix-trace` crate. - /// - /// Note that the actual implementation depends on feature toggles. - pub fn new(url: gix_url::Url, desired_version: Protocol, trace: bool) -> Self { - Self::new_http(H::default(), url, desired_version, trace) - } -} - -impl Transport { - fn check_content_type( - service: Service, - kind: &str, - headers: ::Headers, - ) -> std::result::Result<(), client::Error> { - let wanted_content_type = format!("application/x-{}-{}", service.as_str(), kind); - if !headers - .lines() - .collect::, _>>()? - .iter() - .any(|l| { - let mut tokens = l.split(':'); - tokens.next().zip(tokens.next()).is_some_and(|(name, value)| { - name.eq_ignore_ascii_case("content-type") && value.trim() == wanted_content_type - }) - }) - { - return Err(client::Error::Http( - message!( - "Didn't find '{wanted_content_type}' header to indicate 'smart' protocol, and 'dumb' protocol is not supported." - ) - .unsupported_error(), - )); - } - Ok(()) - } - - fn add_basic_auth_if_present(&self, headers: &mut Vec>) -> std::result::Result<(), client::Error> { - if let Some(gix_sec::identity::Account { - username, - password, - oauth_refresh_token: _, - }) = &self.identity - { - #[cfg(not(feature = "http-client-insecure-credentials"))] - if self.url.starts_with("http://") { - return Err(client::Error::AuthenticationRefused( - "Will not send credentials in clear text over http", - )); - } - headers.push(Cow::Owned(format!( - "Authorization: Basic {}", - base64::engine::general_purpose::STANDARD.encode(format!("{username}:{password}")) - ))); - } - Ok(()) - } -} - -fn append_url(base: &str, suffix: &str) -> String { - let mut buf = base.to_owned(); - if base.as_bytes().last() != Some(&b'/') { - buf.push('/'); - } - buf.push_str(suffix); - buf -} - -impl client::TransportWithoutIO for Transport { - fn set_identity(&mut self, identity: gix_sec::identity::Account) -> std::result::Result<(), client::Error> { - self.identity = Some(identity); - Ok(()) - } - - fn to_url(&self) -> Cow<'_, BStr> { - Cow::Borrowed(self.url.as_str().into()) - } - - fn connection_persists_across_multiple_requests(&self) -> bool { - false - } - - fn configure(&mut self, config: &dyn Any) -> Result { - self.http.configure(config) - } -} - -impl blocking_io::Transport for Transport { - fn handshake<'a>( - &mut self, - service: Service, - extra_parameters: &'a [(&'a str, Option<&'a str>)], - ) -> std::result::Result, client::Error> { - let url = append_url(self.url.as_ref(), &format!("info/refs?service={}", service.as_str())); - let static_headers = [Cow::Borrowed(self.user_agent_header)]; - let mut dynamic_headers = Vec::>::new(); - if self.desired_version != Protocol::V1 || !extra_parameters.is_empty() { - let mut parameters = if self.desired_version != Protocol::V1 { - let mut p = format!("version={}", self.desired_version as usize); - if !extra_parameters.is_empty() { - p.push(':'); - } - p - } else { - String::new() - }; - parameters.push_str( - &extra_parameters - .iter() - .map(|(key, value)| match value { - Some(value) => format!("{key}={value}"), - None => key.to_string(), - }) - .collect::>() - .join(":"), - ); - dynamic_headers.push(format!("Git-Protocol: {parameters}").into()); - } - self.add_basic_auth_if_present(&mut dynamic_headers)?; - let GetResponse { headers, mut body } = self - .http - .get(url.as_ref(), &self.url, static_headers.iter().chain(&dynamic_headers)) - .map_err(|err| { - self.sync_redirected_base_url(); - client::Error::Http(err) - })?; - if let Err(err) = >::check_content_type(service, "advertisement", headers) { - const MAX_ERROR_BODY_DRAIN_BYTES: u64 = 1024 * 1024; - std::io::copy( - &mut body.by_ref().take(MAX_ERROR_BODY_DRAIN_BYTES), - &mut std::io::sink(), - ) - .ok(); - self.sync_redirected_base_url(); - return Err(err); - } - self.sync_redirected_base_url(); - - let line_reader = self - .line_provider - .get_or_insert_with(|| StreamingPeekableIter::new(body, &[PacketLineRef::Flush], self.trace)); - - // the service announcement is only sent sometimes depending on the exact server/protocol version/used protocol (http?) - // eat the announcement when its there to avoid errors later (and check that the correct service was announced). - // Ignore the announcement otherwise. - let line_ = line_reader - .peek_line() - .ok_or(client::Error::ExpectedLine("capabilities, version or service"))???; - let line = line_.as_text().ok_or(client::Error::ExpectedLine("text"))?; - - if let Some(announced_service) = line.as_bstr().strip_prefix(b"# service=") { - if announced_service != service.as_str().as_bytes() { - return Err(client::Error::Http( - message!( - "Expected to see service {:?}, but got {:?}", - service.as_str(), - announced_service - ) - .raise(), - )); - } - - line_reader.as_read().read_to_end(&mut Vec::new())?; - } - - let Handshake { - capabilities, - refs, - protocol: actual_protocol, - } = Handshake::from_lines_with_version_detection(line_reader)?; - self.actual_version = actual_protocol; - self.service = Some(service); - Ok(SetServiceResponse { - actual_protocol, - capabilities, - refs, - }) - } - - fn request( - &mut self, - write_mode: client::WriteMode, - on_into_read: MessageKind, - trace: bool, - ) -> std::result::Result, client::Error> { - let service = self.service.ok_or(client::Error::MissingHandshake)?; - let url = append_url(&self.url, service.as_str()); - let static_headers = &[ - Cow::Borrowed(self.user_agent_header), - Cow::Owned(format!("Content-Type: application/x-{}-request", service.as_str())), - format!("Accept: application/x-{}-result", service.as_str()).into(), - ]; - let mut dynamic_headers = Vec::new(); - self.add_basic_auth_if_present(&mut dynamic_headers)?; - if self.actual_version != Protocol::V1 { - dynamic_headers.push(Cow::Owned(format!( - "Git-Protocol: version={}", - self.actual_version as usize - ))); - } - - let all_headers = static_headers.iter().chain(&dynamic_headers); - let PostResponse { - headers, - body, - post_body, - } = self - .http - .post(&url, &self.url, all_headers, write_mode.into()) - .map_err(|err| { - self.sync_redirected_base_url(); - client::Error::Http(err) - })?; - self.sync_redirected_base_url(); - let line_provider = self - .line_provider - .as_mut() - .expect("handshake to have been called first"); - line_provider.replace(body); - Ok(RequestWriter::new_from_bufread( - post_body, - Box::new(HeadersThenBody:: { - service, - headers: Some(headers), - body: line_provider.as_read_without_sidebands(), - }), - write_mode, - on_into_read, - trace, - )) - } -} - -struct HeadersThenBody { - service: Service, - headers: Option, - body: B, -} - -impl HeadersThenBody { - fn handle_headers(&mut self) -> std::io::Result<()> { - if let Some(headers) = self.headers.take() { - >::check_content_type(self.service, "result", headers).map_err(std::io::Error::other)?; - } - Ok(()) - } -} - -impl Read for HeadersThenBody { - fn read(&mut self, buf: &mut [u8]) -> std::io::Result { - self.handle_headers()?; - self.body.read(buf) - } -} - -impl BufRead for HeadersThenBody { - fn fill_buf(&mut self) -> std::io::Result<&[u8]> { - self.handle_headers()?; - self.body.fill_buf() - } - - fn consume(&mut self, amt: usize) { - self.body.consume(amt); - } -} - -impl ReadlineBufRead for HeadersThenBody { - fn readline(&mut self) -> Option>>> { - if let Err(err) = self.handle_headers() { - return Some(Err(err)); - } - self.body.readline() - } - - fn readline_str(&mut self, line: &mut String) -> std::io::Result { - self.handle_headers()?; - self.body.readline_str(line) - } -} - -impl<'a, H: Http, B: ExtendedBufRead<'a> + Unpin> ExtendedBufRead<'a> for HeadersThenBody { - fn set_progress_handler(&mut self, handle_progress: Option>) { - self.body.set_progress_handler(handle_progress); - } - - fn peek_data_line(&mut self) -> Option>> { - if let Err(err) = self.handle_headers() { - return Some(Err(err)); - } - self.body.peek_data_line() - } - - fn reset(&mut self, version: Protocol) { - self.body.reset(version); - } - - fn stopped_at(&self) -> Option { - self.body.stopped_at() - } -} - -/// Connect to the given `url` via HTTP/S using the `desired_version` of the `git` protocol, with `http` as implementation. -/// If `trace` is `true`, all packetlines received or sent will be passed to the facilities of the `gix-trace` crate. -#[cfg(all(feature = "http-client", not(feature = "http-client-curl")))] -pub fn connect_http(http: H, url: gix_url::Url, desired_version: Protocol, trace: bool) -> Transport { - Transport::new_http(http, url, desired_version, trace) -} - -/// Connect to the given `url` via HTTP/S using the `desired_version` of the `git` protocol. -/// If `trace` is `true`, all packetlines received or sent will be passed to the facilities of the `gix-trace` crate. -#[cfg(any(feature = "http-client-curl", feature = "http-client-reqwest"))] -pub fn connect(url: gix_url::Url, desired_version: Protocol, trace: bool) -> Transport { - Transport::new(url, desired_version, trace) -} - -/// -pub mod redirect; - -#[cfg(test)] -mod tests { - use super::*; - use crate::client::blocking_io::Transport as _; - use gix_error::Result; - - struct FailingHttp; - - impl Http for FailingHttp { - type Headers = std::io::Empty; - type ResponseBody = std::io::Empty; - type PostBody = std::io::Sink; - - fn get( - &mut self, - _url: &str, - _base_url: &str, - _headers: impl IntoIterator>, - ) -> Result> { - Err(gix_error::ClassificationMarker::with_source( - gix_error::Class::Retryable, - message("temporary backend failure"), - ) - .and_raise(message("GET failed"))) - } - - fn post( - &mut self, - _url: &str, - _base_url: &str, - _headers: impl IntoIterator>, - _body: PostBodyDataKind, - ) -> Result> { - Err(std::io::Error::from(std::io::ErrorKind::ConnectionRefused).and_raise(message("POST failed"))) - } - - fn configure(&mut self, _config: &dyn Any) -> Result { - Ok(()) - } - } - - #[test] - fn backend_failures_remain_http_errors() { - let mut transport = Transport::new_http( - FailingHttp, - "http://localhost/repo".try_into().expect("valid URL"), - Protocol::V2, - false, - ); - let get_error = transport - .handshake(Service::UploadPack, &[]) - .err() - .expect("GET fails in the backend"); - transport.service = Some(Service::UploadPack); - let post_error = transport - .request(client::WriteMode::Binary, MessageKind::Flush, false) - .err() - .expect("POST fails in the backend"); - - let errors = [get_error, post_error]; - insta::assert_debug_snapshot!(errors, "GET and POST backend failures retain HTTP context and their I/O causes", @" - [ - Http( - GET failed - - Caused by: - 0: temporary backend failure, - ), - Http( - POST failed - - Caused by: - 0: connection refused, - ), - ] - "); - for err in errors { - assert!( - matches!(err, client::Error::Http(_)), - "HTTP backend failures retain their transport error variant: {err:?}" - ); - assert!( - gix_error::classify(&err).can_retry_lenient(), - "retryable backend causes survive conversion: {err:?}" - ); - } - } -} diff --git a/vendor/gix-transport/src/client/blocking_io/http/redirect.rs b/vendor/gix-transport/src/client/blocking_io/http/redirect.rs deleted file mode 100644 index 707f1c5..0000000 --- a/vendor/gix-transport/src/client/blocking_io/http/redirect.rs +++ /dev/null @@ -1,232 +0,0 @@ -use gix_error::{OptionExt, Result, bail, message}; - -#[derive(Default, Clone, Copy, Debug, Eq, PartialEq)] -pub(crate) enum Action { - Follow, - RejectConfiguredHeaders, - #[default] - Stop, -} - -impl Action { - pub(crate) fn from_request(may_follow_redirects: bool, has_configured_request_headers: bool) -> Self { - match (may_follow_redirects, has_configured_request_headers) { - (true, false) => Action::Follow, - (true, true) => Action::RejectConfiguredHeaders, - (false, _) => Action::Stop, - } - } -} - -fn parse_two_urls(a: &str, b: &str) -> Option<(gix_url::Url, gix_url::Url)> { - let a = gix_url::parse(a).ok()?; - let b = gix_url::parse(b).ok()?; - Some((a, b)) -} - -pub(crate) fn scheme_is_safe(redirect_url: &str, original_url: &str) -> bool { - let Some((redirect_url, original_url)) = parse_two_urls(redirect_url, original_url) else { - return false; - }; - - if !matches!(redirect_url.scheme, gix_url::Scheme::Http | gix_url::Scheme::Https) { - return false; - } - - if redirect_url.scheme == original_url.scheme { - return true; - } - - original_url.scheme == gix_url::Scheme::Http && redirect_url.scheme == gix_url::Scheme::Https -} - -pub(crate) fn can_reuse_identity(redirect_url: &str, original_url: &str) -> bool { - let Some((redirect_url, original_url)) = parse_two_urls(redirect_url, original_url) else { - return false; - }; - - if redirect_url.host != original_url.host { - return false; - } - - if redirect_url.scheme == original_url.scheme { - return redirect_url.port_or_default() == original_url.port_or_default(); - } - - if original_url.scheme == gix_url::Scheme::Http && redirect_url.scheme == gix_url::Scheme::Https { - let original_port = original_url.port_or_default(); - let redirect_port = redirect_url.port_or_default(); - return original_port == redirect_port || matches!((original_port, redirect_port), (Some(80), Some(443))); - } - - false -} - -pub(crate) fn base_url(redirect_url: &str, base_url: &str, url: String) -> Result { - let tail = url - .strip_prefix(base_url) - .expect("BUG: caller assures `base_url` is subset of `url`"); - if !scheme_is_safe(redirect_url, base_url) { - bail!( - "Redirect url {redirect_url:?} could not be reconciled with original url {url} as the scheme is insecure or they don't share the same suffix" - ); - } - redirect_url - .strip_suffix(tail) - .ok_or_raise(|| { - message!( - "Redirect url {redirect_url:?} could not be reconciled with original url {url} as the scheme is insecure or they don't share the same suffix" - ) - }) - .map(ToOwned::to_owned) -} - -pub(crate) fn swap_tails(effective_base_url: Option<&str>, base_url: &str, mut url: String) -> String { - match effective_base_url { - Some(effective_base) => { - url.replace_range(..base_url.len(), effective_base); - url - } - None => url, - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn base_url_complete() { - assert_eq!( - base_url( - "https://original/b/info/refs?hi", - "https://original/a", - "https://original/a/info/refs?hi".into() - ) - .unwrap(), - "https://original/b" - ); - } - - #[test] - fn base_url_allows_same_host_scheme_upgrade() { - assert_eq!( - base_url( - "https://original/b/info/refs?hi", - "http://original/a", - "http://original/a/info/refs?hi".into() - ) - .unwrap(), - "https://original/b" - ); - } - - #[test] - fn base_url_allows_cross_authority_redirects_if_the_tail_matches() { - assert_eq!( - base_url( - "https://redirected.org/b/info/refs?hi", - "https://original/a", - "https://original/a/info/refs?hi".into() - ) - .unwrap(), - "https://redirected.org/b" - ); - } - - #[test] - fn base_url_rejects_authority_changes() { - assert!( - base_url( - "http://original/b/info/refs?hi", - "https://original/a", - "https://original/a/info/refs?hi".into() - ) - .is_err(), - "downgrading from https to http must be rejected" - ); - } - - #[test] - fn rejected_redirects_remain_unclassified() { - for redirect in [ - "http://original/b/info/refs?hi", - "ftp://original/b/info/refs?hi", - "https://original/b/other", - ] { - let original = "https://original/a/info/refs?hi"; - let err = base_url(redirect, "https://original/a", original.into()) - .expect_err("the redirect downgrades the scheme, uses another protocol, or changes the suffix"); - assert!( - err.classify().next().is_none(), - "rejecting a redirect by policy does not make it corrupt or invalid caller input" - ); - assert_eq!( - err.to_string(), - format!( - "Redirect url {redirect:?} could not be reconciled with original url {original} as the scheme is insecure or they don't share the same suffix" - ), - "the redirect diagnostic remains unchanged" - ); - } - } - - #[test] - fn can_reuse_identity_complete() { - assert!( - can_reuse_identity("https://original/b", "https://original/a"), - "keeping the same https authority must be allowed" - ); - assert!( - can_reuse_identity("https://original/b", "http://original/a"), - "upgrading from http to https on the same host must be allowed" - ); - assert!( - can_reuse_identity("https://original:8080/b", "http://original:8080/a"), - "upgrading from http to https on the same host and port must be allowed" - ); - assert!( - !can_reuse_identity("http://original/b", "https://original/a"), - "downgrading from https to http must be rejected" - ); - assert!( - !can_reuse_identity("https://redirected.org/b", "https://original/a"), - "changing the host must be rejected" - ); - assert!( - !can_reuse_identity("https://original:444/b", "https://original/a"), - "changing the port on https must be rejected" - ); - assert!( - !can_reuse_identity("https://original:444/b", "http://original/a"), - "upgrading the scheme does not allow changing the port" - ); - for delimiter in ['?', '#'] { - assert!( - !can_reuse_identity( - &format!("https://redirected.org{delimiter}@original/b"), - "https://original/a" - ), - "an authority terminated by {delimiter} must not reuse another host's identity" - ); - } - } - - #[test] - fn swap_tails_complete() { - assert_eq!( - swap_tails(None, "not interesting", "used".into()), - "used", - "without effective base url, it passes url, no redirect happened yet" - ); - assert_eq!( - swap_tails( - Some("https://redirected.org/b"), - "https://original/a", - "https://original/a/info/refs?something".into() - ), - "https://redirected.org/b/info/refs?something", - "the tail stays the same if redirection happened" - ); - } -} diff --git a/vendor/gix-transport/src/client/blocking_io/http/reqwest/mod.rs b/vendor/gix-transport/src/client/blocking_io/http/reqwest/mod.rs deleted file mode 100644 index d598b55..0000000 --- a/vendor/gix-transport/src/client/blocking_io/http/reqwest/mod.rs +++ /dev/null @@ -1,38 +0,0 @@ -use gix_error::Result; -/// An implementation for HTTP requests via `reqwest`. -pub struct Remote { - /// A worker thread which performs the actual request. - handle: Option>, - /// A channel to send requests (work) to the worker thread. - request: std::sync::mpsc::SyncSender, - /// A channel to receive the result of the prior request. - response: std::sync::mpsc::Receiver, - /// A mechanism for configuring the remote. - config: crate::client::blocking_io::http::Options, - /// The effective base URL after an accepted redirect. - redirected_base_url: std::sync::Arc>>, -} - -/// A function to configure a single request prior to sending it, support most complex configuration beyond what's possible with -/// basic `git` http configuration. -pub type ConfigureRequestFn = dyn FnMut(&mut reqwest::blocking::Request) -> Result + Send + Sync + 'static; - -/// A function to configure the HTTP client before it is built, for example to provide a TLS configuration. -pub type ConfigureClientFn = - dyn FnMut(reqwest::blocking::ClientBuilder) -> Result + Send + Sync + 'static; - -/// Options to configure the reqwest HTTP handler. -#[derive(Default)] -pub struct Options { - /// Configure the client on the first request, before building and reusing it for subsequent requests. - /// - /// The callback runs again if the worker is restarted after a failure. Changes to this callback after - /// initialization do not affect an existing client. The transport installs its redirect policy after - /// this callback; use [`Self::configure_request`] for request-specific headers. - pub configure_client: Option>, - /// A function to configure the request that is about to be made. - pub configure_request: Option>, -} - -/// -pub mod remote; diff --git a/vendor/gix-transport/src/client/blocking_io/http/reqwest/remote.rs b/vendor/gix-transport/src/client/blocking_io/http/reqwest/remote.rs deleted file mode 100644 index bb26bed..0000000 --- a/vendor/gix-transport/src/client/blocking_io/http/reqwest/remote.rs +++ /dev/null @@ -1,419 +0,0 @@ -use std::{ - any::Any, - io::{Read, Write}, - str::FromStr, - sync::Arc, -}; - -use gix_error::{Class, Error, ErrorExt, Result, ResultExt, message}; -use gix_utils::io::pipe; -use parking_lot::Mutex; - -use crate::client::blocking_io::http::{ - self, - options::FollowRedirects, - redirect::{self, Action as RedirectAction}, - reqwest::Remote, - traits::PostBodyDataKind, -}; - -fn classify_reqwest(err: reqwest::Error) -> Error { - let class = match err.status() { - Some(reqwest::StatusCode::FORBIDDEN) => Some(Class::PermissionDenied), - Some(reqwest::StatusCode::CONFLICT) => Some(Class::Conflict), - Some(reqwest::StatusCode::NOT_IMPLEMENTED) => Some(Class::Unsupported), - _ if err.is_timeout() || err.is_connect() || err.status().is_some_and(|status| status.is_server_error()) => { - Some(Class::Retryable) - } - _ => None, - }; - match class { - Some(class) => Error::from_error(gix_error::ClassificationMarker::with_source(class, err)), - None => Error::from_error(err), - } -} - -fn authority_changed(curr_url: &reqwest::Url, prev_url: &reqwest::Url) -> bool { - curr_url.scheme() != prev_url.scheme() - || curr_url.host_str() != prev_url.host_str() - || curr_url.port_or_known_default() != prev_url.port_or_known_default() -} - -impl Default for Remote { - fn default() -> Self { - let (req_send, req_recv) = std::sync::mpsc::sync_channel(0); - let (res_send, res_recv) = std::sync::mpsc::sync_channel(0); - let redirected_base_url_shared = Arc::new(Mutex::new(None)); - let redirected_base_url_shared_for_field = redirected_base_url_shared.clone(); - let handle = std::thread::spawn(gix_trace::in_thread(move || -> Result { - let mut follow = None; - let redirect_action = Arc::new(Mutex::new(RedirectAction::Stop)); - let redirect_tail = Arc::new(Mutex::new(String::new())); - - let mut client = None; - - for Request { - url, - base_url, - headers, - upload_body_kind, - config, - } in req_recv - { - if client.is_none() { - let mut builder = reqwest::blocking::ClientBuilder::new() - .connect_timeout(std::time::Duration::from_secs(20)) - .http1_title_case_headers(); - if let Some(backend) = config.backend.as_ref() { - let mut backend = backend - .lock() - .map_err(|_| message("HTTP backend configuration lock is poisoned").raise())?; - if let Some(options) = backend.downcast_mut::() - && let Some(configure_client) = options.configure_client.as_mut() - { - builder = - configure_client(builder).or_raise(|| message("HTTP client configuration failed"))?; - } - } - // Keep Git's redirect policy authoritative after client customization. - client = Some( - builder - .redirect(reqwest::redirect::Policy::custom({ - let redirect_action = redirect_action.clone(); - let redirect_tail = redirect_tail.clone(); - move |attempt| { - match *redirect_action.lock() { - RedirectAction::Follow => { - let curr_url = attempt.url(); - let prev_urls = attempt.previous(); - // emulate default git behaviour which relies on curl default behaviour apparently. - const CURL_DEFAULT_REDIRS: usize = 50; - if prev_urls.len() >= CURL_DEFAULT_REDIRS { - return attempt.error("too many redirects"); - } - - match prev_urls.last() { - Some(prev_url) if !redirect::scheme_is_safe(curr_url.as_str(), prev_url.as_str()) => { - // Don't follow insecure protocol redirects, particularly https-to-http downgrades. - attempt.stop() - } - Some(prev_url) if authority_changed(curr_url, prev_url) => { - // Allowed only if the tail doesn't change. - let redirect_tail = redirect_tail.lock(); - if curr_url.as_str().ends_with(redirect_tail.as_str()) { - attempt.follow() - } else { - let curr_url = curr_url.as_str().to_owned(); - let redirect_tail = redirect_tail.to_string(); - attempt.error(format!( - "redirect url {curr_url:?} does not end with expected request suffix {redirect_tail:?}", - )) - } - } - _ => attempt.follow(), - } - } - RedirectAction::RejectConfiguredHeaders => { - attempt.error("refusing to follow redirect after request headers were configured") - } - RedirectAction::Stop => attempt.stop(), - } - } - })) - .build() - .map_err(classify_reqwest) - .or_raise(|| message("Could not initialize HTTP client"))?, - ); - } - let client = client.as_ref().expect("initialized before making requests"); - let redirected_base_url = redirected_base_url_shared.lock().clone(); - let effective_url = redirect::swap_tails(redirected_base_url.as_deref(), &base_url, url.clone()); - let has_configured_extra_headers = !config.extra_headers.is_empty(); - let mut req_builder = if upload_body_kind.is_some() { - client.post(&effective_url) - } else { - client.get(&effective_url) - } - .headers(headers); - let (post_body_tx, mut post_body_rx) = pipe::unidirectional(0); - let (mut response_body_tx, response_body_rx) = pipe::unidirectional(0); - let (mut headers_tx, headers_rx) = pipe::unidirectional(0); - if res_send - .send(Response { - headers: headers_rx, - body: response_body_rx, - upload_body: post_body_tx, - }) - .is_err() - { - // This means our internal protocol is violated as the one who sent the request isn't listening anymore. - // Shut down as something is off. - break; - } - req_builder = match upload_body_kind { - Some(PostBodyDataKind::BoundedAndFitsIntoMemory) => { - let mut buf = Vec::::with_capacity(512); - post_body_rx - .read_to_end(&mut buf) - .or_raise(|| message("Could not finish reading all data to post to the remote"))?; - req_builder.body(buf) - } - Some(PostBodyDataKind::Unbounded) => req_builder.body(reqwest::blocking::Body::new(post_body_rx)), - None => req_builder, - }; - let mut req = req_builder - .build() - .map_err(classify_reqwest) - .or_raise(|| message("Could not build HTTP request"))?; - let mut has_configure_request = false; - if let Some(ref mut request_options) = config.backend.as_ref().and_then(|backend| backend.lock().ok()) - && let Some(options) = request_options.downcast_mut::() - && let Some(configure_request) = &mut options.configure_request - { - has_configure_request = true; - configure_request(&mut req).or_raise(|| message("Request configuration failed"))?; - } - - let follow = follow.get_or_insert(config.follow_redirects); - let may_follow_redirects = matches!(*follow, FollowRedirects::Initial | FollowRedirects::All); - let has_configured_request_headers = has_configure_request || has_configured_extra_headers; - *redirect_action.lock() = - RedirectAction::from_request(may_follow_redirects, has_configured_request_headers); - url.strip_prefix(&base_url) - .expect("BUG: caller assures `base_url` is subset of `url`") - .clone_into(&mut redirect_tail.lock()); - - if *follow == FollowRedirects::Initial { - *follow = FollowRedirects::None; - } - - let mut www_authenticate = Vec::new(); - let mut res = match client.execute(req).and_then(|res| { - if res.status() == reqwest::StatusCode::UNAUTHORIZED { - www_authenticate = res - .headers() - .get_all(reqwest::header::WWW_AUTHENTICATE) - .iter() - .map(|value| value.as_bytes().into()) - .collect(); - } - res.error_for_status() - }) { - Ok(res) => res, - Err(err) => { - // `error_for_status()` preserves the final URL for HTTP error responses. Capture it here so - // authentication retries after redirected 401 responses use the redirected base URL. - if let Some(actual_url) = err.url().map(reqwest::Url::as_str) - && actual_url != effective_url - { - let new_base_url = redirect::base_url(actual_url, &base_url, url.clone())?; - *redirected_base_url_shared.lock() = Some(new_base_url); - } - let err = match err.status() { - Some(reqwest::StatusCode::UNAUTHORIZED) => std::io::Error::new( - std::io::ErrorKind::PermissionDenied, - crate::client::AuthenticationRequired { www_authenticate }, - ), - Some(status) => { - let kind = if status.is_server_error() && status != reqwest::StatusCode::NOT_IMPLEMENTED - { - std::io::ErrorKind::ConnectionAborted - } else { - std::io::ErrorKind::Other - }; - std::io::Error::new( - kind, - classify_reqwest(err) - .and_raise(message!("Received HTTP status {}", status.as_str())), - ) - } - // Preserve the `reqwest::Error` as the source so the underlying cause -- e.g. a - // connection or TLS failure -- isn't lost. It was previously stringified, which - // dead-ended `source()` and hid the real reason a request failed. See #2140. - None => std::io::Error::other(classify_reqwest(err)), - }; - headers_tx.channel.send(Err(err)).ok(); - continue; - } - }; - - let actual_url = res.url().as_str(); - if actual_url != effective_url.as_str() { - let new_base_url = redirect::base_url(actual_url, &base_url, url)?; - *redirected_base_url_shared.lock() = Some(new_base_url); - } - - let send_headers = { - let headers = res.headers(); - move || -> std::io::Result<()> { - for (name, value) in headers { - headers_tx.write_all(name.as_str().as_bytes())?; - headers_tx.write_all(b":")?; - headers_tx.write_all(value.as_bytes())?; - headers_tx.write_all(b"\n")?; - } - // Make sure this is an FnOnce closure to signal the remote reader we are done. - drop(headers_tx); - Ok(()) - } - }; - - // We don't have to care if anybody is receiving the header, as a matter of fact we cannot fail sending them. - // Thus an error means the receiver failed somehow, but might also have decided not to read headers at all. Fine with us. - send_headers().ok(); - - // reading the response body is streaming and may fail for many reasons. If so, we send the error over the response - // body channel and that's all we can do. - if let Err(err) = std::io::copy(&mut res, &mut response_body_tx) { - response_body_tx.channel.send(Err(err)).ok(); - } - } - Ok(()) - })); - - Remote { - handle: Some(handle), - request: req_send, - response: res_recv, - config: http::Options::default(), - redirected_base_url: redirected_base_url_shared_for_field, - } - } -} - -/// utilities -impl Remote { - fn restore_thread_after_failure(&mut self) -> Error { - let err_that_brought_thread_down = self - .handle - .take() - .expect("thread handle present") - .join() - .expect("handler thread should never panic") - .expect_err("something should have gone wrong with curl (we join on error only)"); - let config = std::mem::take(&mut self.config); - *self = Remote::default(); - self.config = config; - err_that_brought_thread_down.and_raise(message("Could not initialize the http client")) - } - - fn make_request( - &mut self, - url: &str, - base_url: &str, - headers: impl IntoIterator>, - upload_body_kind: Option, - ) -> Result> { - let mut header_map = reqwest::header::HeaderMap::new(); - for header_line in headers { - insert_header(&mut header_map, header_line.as_ref()); - } - for header_line in &self.config.extra_headers { - insert_header(&mut header_map, header_line); - } - if self - .request - .send(Request { - url: url.to_owned(), - base_url: base_url.to_owned(), - headers: header_map, - upload_body_kind, - config: self.config.clone(), - }) - .is_err() - { - return Err(self.restore_thread_after_failure()); - } - - let Response { - headers, - body, - upload_body, - } = match self.response.recv() { - Ok(res) => res, - Err(_) => { - return Err(self.restore_thread_after_failure()); - } - }; - - Ok(http::PostResponse { - post_body: upload_body, - headers, - body, - }) - } -} - -/// Add one `name: value` header line to `header_map`, ignoring malformed or unsupported input in `header_line`. -/// -/// Git configuration may provide arbitrary extra header lines, so invalid names or values are skipped instead of -/// failing request construction. Multiple entries with the same header name are preserved to match curl behavior. -fn insert_header(header_map: &mut reqwest::header::HeaderMap, header_line: &str) { - let Some(colon_pos) = header_line.find(':') else { - return; - }; - let header_name = &header_line[..colon_pos]; - let value = &header_line[colon_pos + 1..]; - - if let Some((key, val)) = reqwest::header::HeaderName::from_str(header_name) - .ok() - .zip(reqwest::header::HeaderValue::try_from(value.trim()).ok()) - { - header_map.append(key, val); - } -} - -impl http::Http for Remote { - type Headers = pipe::Reader; - type ResponseBody = pipe::Reader; - type PostBody = pipe::Writer; - - fn get( - &mut self, - url: &str, - base_url: &str, - headers: impl IntoIterator>, - ) -> Result> { - self.make_request(url, base_url, headers, None).map(Into::into) - } - - fn post( - &mut self, - url: &str, - base_url: &str, - headers: impl IntoIterator>, - post_body_kind: PostBodyDataKind, - ) -> Result> { - self.make_request(url, base_url, headers, Some(post_body_kind)) - } - - fn configure(&mut self, config: &dyn Any) -> Result { - if let Some(config) = config.downcast_ref::() { - self.config = config.clone(); - } - Ok(()) - } - - fn redirected_base_url(&self) -> Option { - self.redirected_base_url.lock().clone() - } -} - -pub(crate) struct Request { - pub url: String, - pub base_url: String, - pub headers: reqwest::header::HeaderMap, - pub upload_body_kind: Option, - pub config: http::Options, -} - -/// A link to a thread who provides data for the contained readers. -/// The expected order is: -/// - write `upload_body` -/// - read `headers` to end -/// - read `body` to hend -pub(crate) struct Response { - pub headers: pipe::Reader, - pub body: pipe::Reader, - pub upload_body: pipe::Writer, -} diff --git a/vendor/gix-transport/src/client/blocking_io/http/traits.rs b/vendor/gix-transport/src/client/blocking_io/http/traits.rs deleted file mode 100644 index b1e99a1..0000000 --- a/vendor/gix-transport/src/client/blocking_io/http/traits.rs +++ /dev/null @@ -1,102 +0,0 @@ -use crate::client::WriteMode; -use gix_error::Result; - -/// The return value of [`Http::get()`]. -pub struct GetResponse { - /// The response headers. - pub headers: H, - /// The response body. - pub body: B, -} - -/// The return value of [`Http::post()`]. -pub struct PostResponse { - /// The body to post to the server as part of the request. - /// - /// **Note**: Implementations should drop the handle to avoid deadlocks. - pub post_body: PB, - /// The headers of the post response. - pub headers: H, - /// The body of the post response. - pub body: B, -} - -/// Whether or not the post body is expected to fit into memory or not. -#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] -pub enum PostBodyDataKind { - /// We know how much data we are sending and think it will fit into memory. This allows to collect it into a buffer - /// and send it with `Content-Length: `. - BoundedAndFitsIntoMemory, - /// We don't know how much data we will send and assume it won't fit into memory. This enables streaming mode. - Unbounded, -} - -impl From for PostBodyDataKind { - fn from(m: WriteMode) -> Self { - match m { - WriteMode::Binary => PostBodyDataKind::Unbounded, - WriteMode::OneLfTerminatedLinePerWriteCall => PostBodyDataKind::BoundedAndFitsIntoMemory, - } - } -} - -impl From> for GetResponse { - fn from(v: PostResponse) -> Self { - GetResponse { - headers: v.headers, - body: v.body, - } - } -} - -/// A trait to abstract the HTTP operations needed to power all git interactions: read via GET and write via POST. -/// Note that 401 must be turned into `std::io::Error(PermissionDenied)`, and other non-success http statuses must be transformed -/// into `std::io::Error(Other)`. -/// The 401 error should wrap [`crate::client::AuthenticationRequired`] with the response's `WWW-Authenticate` values -/// so credential helpers can use the server's authentication hints. -pub trait Http { - /// A type providing headers line by line. - type Headers: std::io::BufRead + Unpin; - /// A type providing the response. - type ResponseBody: std::io::BufRead; - /// A type allowing to write the content to post. - type PostBody: std::io::Write; - - /// Initiate a `GET` request to `url` provided the given `headers`, where `base_url` is so that `base_url + tail == url`. - /// - /// The `base_url` helps to validate redirects and to swap it with the effective base after a redirect. - /// - /// The `headers` are provided verbatim and include both the key as well as the value. - fn get( - &mut self, - url: &str, - base_url: &str, - headers: impl IntoIterator>, - ) -> Result>; - - /// Initiate a `POST` request to `url` providing with the given `headers`, where `base_url` is so that `base_url + tail == url`. - /// - /// The `base_url` helps to validate redirects and to swap it with the effective base after a redirect. - /// - /// The `headers` are provided verbatim and include both the key as well as the value. - /// Note that the [`PostResponse`] contains the [`post_body`][PostResponse::post_body] field which implements [`std::io::Write`] - /// and is expected to receive the body to post to the server. **It must be dropped** before reading the response - /// to prevent deadlocks. - fn post( - &mut self, - url: &str, - base_url: &str, - headers: impl IntoIterator>, - body: PostBodyDataKind, - ) -> Result>; - - /// Pass `config` which can deserialize in the implementation's configuration, as documented separately. - /// - /// The caller must know how that `config` data looks like for the intended implementation. - fn configure(&mut self, config: &dyn std::any::Any) -> Result; - - /// Return the effective base URL after a backend accepted a redirect, if available. - fn redirected_base_url(&self) -> Option { - None - } -} diff --git a/vendor/gix-transport/src/client/blocking_io/mod.rs b/vendor/gix-transport/src/client/blocking_io/mod.rs deleted file mode 100644 index dfb3752..0000000 --- a/vendor/gix-transport/src/client/blocking_io/mod.rs +++ /dev/null @@ -1,20 +0,0 @@ -/// -pub mod connect; - -/// -pub mod file; -/// -#[cfg(feature = "http-client")] -pub mod http; - -mod bufread_ext; -pub use bufread_ext::{ExtendedBufRead, HandleProgress, ReadlineBufRead}; - -mod request; -pub use request::RequestWriter; - -/// -pub mod ssh; - -mod traits; -pub use traits::{SetServiceResponse, Transport, TransportV2Ext}; diff --git a/vendor/gix-transport/src/client/blocking_io/request.rs b/vendor/gix-transport/src/client/blocking_io/request.rs deleted file mode 100644 index afa8845..0000000 --- a/vendor/gix-transport/src/client/blocking_io/request.rs +++ /dev/null @@ -1,110 +0,0 @@ -use std::{io, io::Write}; - -use crate::{ - client::{MessageKind, WriteMode, blocking_io::ExtendedBufRead}, - packetline::blocking_io::{Writer, encode}, -}; - -/// A [`Write`][io::Write] implementation optimized for writing packet lines. -/// A type implementing `Write` for packet lines, which when done can be transformed into a `Read` for -/// obtaining the response. -pub struct RequestWriter<'a> { - on_into_read: MessageKind, - writer: Writer>, - reader: Box + Unpin + 'a>, - trace: bool, -} - -impl io::Write for RequestWriter<'_> { - fn write(&mut self, buf: &[u8]) -> io::Result { - if self.trace { - gix_trace::trace!(">> {}", bstr::BStr::new(buf)); - } - self.writer.write(buf) - } - - fn flush(&mut self) -> io::Result<()> { - self.writer.flush() - } -} - -/// methods with bonds to IO -impl<'a> RequestWriter<'a> { - /// Create a new instance from a `writer` (commonly a socket), a `reader` into which to transform once the - /// writes are finished, along with configuration for the `write_mode` and information about which message to write - /// when this instance is converted into a `reader` to read the request's response. - /// If `trace` is true, `gix_trace` will be used on every written message or data. - pub fn new_from_bufread( - writer: W, - reader: Box + Unpin + 'a>, - write_mode: WriteMode, - on_into_read: MessageKind, - trace: bool, - ) -> Self { - let mut writer = Writer::new(Box::new(writer) as Box); - match write_mode { - WriteMode::Binary => writer.enable_binary_mode(), - WriteMode::OneLfTerminatedLinePerWriteCall => writer.enable_text_mode(), - } - RequestWriter { - on_into_read, - writer, - reader, - trace, - } - } - - /// Write the given message as packet line. - pub fn write_message(&mut self, message: MessageKind) -> io::Result<()> { - match message { - MessageKind::Flush => { - if self.trace { - gix_trace::trace!(">> FLUSH"); - } - encode::write_packet_line(&gix_packetline::PacketLineRef::Flush, self.writer.inner_mut()) - } - MessageKind::Delimiter => { - if self.trace { - gix_trace::trace!(">> DELIM"); - } - encode::write_packet_line(&gix_packetline::PacketLineRef::Delimiter, self.writer.inner_mut()) - } - MessageKind::ResponseEnd => { - if self.trace { - gix_trace::trace!(">> RESPONSE_END"); - } - encode::write_packet_line(&gix_packetline::PacketLineRef::ResponseEnd, self.writer.inner_mut()) - } - MessageKind::Text(t) => { - if self.trace { - gix_trace::trace!(">> {}", bstr::BStr::new(t)); - } - encode::write_text(&gix_packetline::TextRef::from(t), self.writer.inner_mut()) - } - } - .map(|_| ()) - } - - /// Discard the ability to write and turn this instance into the reader for obtaining the other side's response. - /// - /// Doing so will also write the message type this instance was initialized with. - pub fn into_read(mut self) -> std::io::Result + Unpin + 'a>> { - self.write_message(self.on_into_read)?; - self.writer.inner_mut().flush()?; - Ok(self.reader) - } - - /// Dissolve this instance into its write and read handles without any message-writing side-effect as in [`RequestWriter::into_read()`]. - /// - /// Furthermore, the writer will not encode everything it writes as packetlines, but write everything verbatim into the - /// underlying channel. - /// - /// # Note - /// - /// It's of utmost importance to drop the request writer before reading the response as these might be inter-dependent, depending on - /// the underlying transport mechanism. Failure to do so may result in a deadlock depending on how the write and read mechanism - /// is implemented. - pub fn into_parts(self) -> (Box, Box + Unpin + 'a>) { - (self.writer.into_inner(), self.reader) - } -} diff --git a/vendor/gix-transport/src/client/blocking_io/ssh/mod.rs b/vendor/gix-transport/src/client/blocking_io/ssh/mod.rs deleted file mode 100644 index 37f79f9..0000000 --- a/vendor/gix-transport/src/client/blocking_io/ssh/mod.rs +++ /dev/null @@ -1,228 +0,0 @@ -use std::{ - ffi::OsStr, - process::{Command, Stdio}, -}; - -use gix_url::{ArgumentSafety::*, Url}; - -use crate::{Protocol, client::blocking_io::file::SpawnProcessOnDemand}; - -/// The error used in [`connect()`]. -#[derive(Debug)] -#[expect(missing_docs)] -pub enum Error { - UnsupportedScheme(gix_url::Url), - AmbiguousHostName { host: String }, - PrepareCommand(gix_error::Error), -} - -impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Error::UnsupportedScheme(url) => write!( - f, - "The scheme in \"{}\" is not usable for an ssh connection", - url.to_bstring() - ), - Error::AmbiguousHostName { host } => { - write!(f, "Host name '{host}' could be mistaken for a command-line argument") - } - Error::PrepareCommand(_) => f.write_str("Could not prepare the ssh command"), - } - } -} - -impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Error::AmbiguousHostName { .. } => Some(const { &gix_error::ClassificationMarker::VALIDATION }), - Error::UnsupportedScheme(_) => Some(const { &gix_error::ClassificationMarker::UNSUPPORTED }), - Error::PrepareCommand(err) => Some(err), - } - } -} - -/// The kind of SSH programs we have built-in support for. -/// -/// Various different programs exists with different capabilities, and we have a few built in. -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub enum ProgramKind { - /// The standard linux ssh program - Ssh, - /// The `(plink|putty).exe` binaries, typically only on windows. - Plink, - /// The `putty.exe` binary, typically only on windows. - Putty, - /// The `tortoiseplink.exe` binary, only on windows. - TortoisePlink, - /// A minimal ssh client that supports on options. - Simple, -} - -mod program_kind; - -/// -pub mod invocation { - use std::ffi::OsString; - - /// The error returned when producing ssh invocation arguments based on a selected invocation kind. - #[derive(Debug)] - #[expect(missing_docs)] - pub enum Error { - AmbiguousUserName { - user: String, - }, - AmbiguousHostName { - host: String, - }, - Unsupported { - /// The simple command that should have been invoked. - command: OsString, - /// The function that was unsupported - function: &'static str, - }, - } - - impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Error::AmbiguousUserName { user } => { - write!(f, "Username '{user}' could be mistaken for a command-line argument") - } - Error::AmbiguousHostName { host } => { - write!(f, "Host name '{host}' could be mistaken for a command-line argument") - } - Error::Unsupported { function, .. } => { - write!(f, "The 'Simple' ssh variant doesn't support {function}") - } - } - } - } - - impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Error::AmbiguousUserName { .. } | Error::AmbiguousHostName { .. } => { - Some(const { &gix_error::ClassificationMarker::VALIDATION }) - } - Error::Unsupported { .. } => Some(const { &gix_error::ClassificationMarker::UNSUPPORTED }), - } - } - } -} - -/// -pub mod connect { - use std::ffi::{OsStr, OsString}; - - use crate::client::blocking_io::ssh::ProgramKind; - - /// The options for use when [connecting][super::connect()] via the `ssh` protocol. - #[derive(Debug, Clone, Default)] - pub struct Options { - /// The program or script to use. - /// If unset, it defaults to `ssh` or `ssh.exe`, or the program implied by `kind` if that one is set. - pub command: Option, - /// If `true`, a shell must not be used to execute `command`. - /// This defaults to `false`, and a shell can then be used if `command` seems to require it, but won't be - /// used unnecessarily. - pub disallow_shell: bool, - /// The ssh variant further identifying `program`. This determines which arguments will be used - /// when invoking the program. - /// If unset, the `program` basename determines the variant, or an invocation of the `command` itself. - pub kind: Option, - } - - impl Options { - /// Return the configured ssh command, defaulting to `ssh` if neither the `command` nor the `kind` fields are set. - pub fn ssh_command(&self) -> &OsStr { - self.command - .as_deref() - .or_else(|| self.kind.and_then(|kind| kind.exe())) - .unwrap_or_else(|| OsStr::new("ssh")) - } - } -} - -/// Connect to `host` using the ssh program to obtain data from the repository at `path` on the remote. -/// -/// The optional `user` identifies the user's account to which to connect, while `port` allows to specify non-standard -/// ssh ports. -/// -/// The `desired_version` is the preferred protocol version when establishing the connection, but note that it can be -/// downgraded by servers not supporting it. -/// If `trace` is `true`, all packetlines received or sent will be passed to the facilities of the `gix-trace` crate. -#[expect( - clippy::result_large_err, - reason = "will be removed once `gix-error` is used consistently" -)] -pub fn connect( - url: Url, - desired_version: Protocol, - options: connect::Options, - trace: bool, -) -> Result { - if url.scheme != gix_url::Scheme::Ssh || url.host().is_none() { - return Err(Error::UnsupportedScheme(url)); - } - let ssh_cmd = options.ssh_command(); - let kind = determine_client_kind(options.kind, ssh_cmd, &url, options.disallow_shell)?; - let path = gix_url::expand_path::for_shell(url.path.clone()); - Ok(SpawnProcessOnDemand::new_ssh( - url, - ssh_cmd, - path, - kind, - options.disallow_shell, - desired_version, - trace, - )) -} - -#[expect( - clippy::result_large_err, - reason = "will be removed once `gix-error` is used consistently" -)] -fn determine_client_kind( - known_kind: Option, - ssh_cmd: &OsStr, - url: &Url, - disallow_shell: bool, -) -> Result { - let mut kind = known_kind.unwrap_or_else(|| ProgramKind::from(ssh_cmd)); - if known_kind.is_none() && kind == ProgramKind::Simple { - let mut cmd = build_client_feature_check_command(ssh_cmd, url, disallow_shell)?; - gix_trace::debug!(cmd = ?cmd, "invoking `ssh` for feature check"); - kind = if cmd.status().ok().is_some_and(|status| status.success()) { - ProgramKind::Ssh - } else { - ProgramKind::Simple - }; - } - Ok(kind) -} - -#[expect( - clippy::result_large_err, - reason = "will be removed once `gix-error` is used consistently" -)] -fn build_client_feature_check_command(ssh_cmd: &OsStr, url: &Url, disallow_shell: bool) -> Result { - let mut prepare = gix_command::prepare(ssh_cmd) - .stderr(Stdio::null()) - .stdout(Stdio::null()) - .stdin(Stdio::null()) - .command_may_be_shell_script() - .arg("-G") - .arg(match url.host_as_argument() { - Usable(host) => host, - Dangerous(host) => Err(Error::AmbiguousHostName { host: host.into() })?, - Absent => panic!("BUG: host should always be present in SSH URLs"), - }); - if disallow_shell { - prepare.use_shell = false; - } - prepare.try_into().map_err(Error::PrepareCommand) -} - -#[cfg(test)] -mod tests; diff --git a/vendor/gix-transport/src/client/blocking_io/ssh/program_kind.rs b/vendor/gix-transport/src/client/blocking_io/ssh/program_kind.rs deleted file mode 100644 index 238e588..0000000 --- a/vendor/gix-transport/src/client/blocking_io/ssh/program_kind.rs +++ /dev/null @@ -1,135 +0,0 @@ -use std::{ffi::OsStr, io::ErrorKind}; - -use bstr::{BString, ByteSlice, ByteVec}; -use gix_url::ArgumentSafety::*; - -use crate::{ - Protocol, - client::blocking_io::ssh::{self, ProgramKind}, -}; - -impl ProgramKind { - /// Provide the name of the executable that belongs to this kind, or `None` if the kind is `Simple`. - pub fn exe(&self) -> Option<&'static OsStr> { - Some(OsStr::new(match self { - ProgramKind::Ssh => "ssh", - ProgramKind::Plink => "plink", - ProgramKind::Putty => "putty", - ProgramKind::TortoisePlink => "tortoiseplink.exe", - ProgramKind::Simple => return None, - })) - } - - /// Prepare all information needed to invoke the ssh command - pub(crate) fn prepare_invocation( - &self, - ssh_cmd: &OsStr, - url: &gix_url::Url, - desired_version: Protocol, - disallow_shell: bool, - ) -> Result { - let mut prepare = gix_command::prepare(ssh_cmd).command_may_be_shell_script(); - if disallow_shell { - prepare.use_shell = false; - } - match self { - ProgramKind::Ssh => { - if desired_version != Protocol::V1 { - prepare = prepare - .args(["-o", "SendEnv=GIT_PROTOCOL"]) - .env("GIT_PROTOCOL", format!("version={}", desired_version as usize)); - } - if let Some(port) = url.port { - prepare = prepare.arg(format!("-p{port}")); - } - } - ProgramKind::Plink | ProgramKind::Putty | ProgramKind::TortoisePlink => { - if *self == ProgramKind::TortoisePlink { - prepare = prepare.arg("-batch"); - } - if let Some(port) = url.port { - prepare = prepare.arg("-P"); - prepare = prepare.arg(port.to_string()); - } - } - ProgramKind::Simple => { - if url.port.is_some() { - return Err(ssh::invocation::Error::Unsupported { - command: ssh_cmd.into(), - function: "setting the port", - }); - } - } - } - - let host_maybe_with_user_as_ssh_arg = match (url.user_as_argument(), url.host_as_argument()) { - (Usable(user), Usable(host)) => format!("{user}@{host}"), - (Usable(user), Dangerous(host)) => format!("{user}@{host}"), // The `user@` makes it safe. - (Absent, Usable(host)) => host.into(), - (Dangerous(user), _) => Err(ssh::invocation::Error::AmbiguousUserName { user: user.into() })?, - (_, Dangerous(host)) => Err(ssh::invocation::Error::AmbiguousHostName { host: host.into() })?, - (_, Absent) => panic!("BUG: host should always be present in SSH URLs"), - }; - - Ok(prepare - .arg(host_maybe_with_user_as_ssh_arg) - // Try to force ssh to yield English messages (for parsing later). - .env("LANG", "C") - .env("LC_ALL", "C")) - } - - /// Note that the caller has to assure that the ssh program is launched in English by setting the locale. - pub(crate) fn line_to_err(&self, line: BString) -> Result { - let kind = match self { - ProgramKind::Ssh | ProgramKind::Simple => { - if line.contains_str(b"Permission denied") || line.contains_str(b"permission denied") { - Some(ErrorKind::PermissionDenied) - } else if line.contains_str(b"resolve hostname") { - Some(ErrorKind::ConnectionRefused) - } else if line.contains_str(b"connect to host") - || line.contains_str("Connection to ") - || line.contains_str("Connection closed by ") - { - // TODO: turn this into HostUnreachable when stable, or NetworkUnreachable in 'no route' example. - // It's important that it WON'T be considered spurious, but is considered a permanent failure. - Some(ErrorKind::NotFound) - } else { - None - } - } - ProgramKind::Plink | ProgramKind::Putty | ProgramKind::TortoisePlink => { - if line.contains_str(b"publickey") { - Some(ErrorKind::PermissionDenied) - } else { - None - } - } - }; - match kind { - Some(kind) => Ok(std::io::Error::new(kind, Vec::from(line).into_string_lossy())), - None => Err(line), - } - } -} - -impl<'a> From<&'a OsStr> for ProgramKind { - fn from(v: &'a OsStr) -> Self { - let p = std::path::Path::new(v); - match p.file_stem().and_then(OsStr::to_str) { - None => ProgramKind::Simple, - Some(stem) => { - if stem.eq_ignore_ascii_case("ssh") { - ProgramKind::Ssh - } else if stem.eq_ignore_ascii_case("plink") { - ProgramKind::Plink - } else if stem.eq_ignore_ascii_case("putty") { - ProgramKind::Putty - } else if stem.eq_ignore_ascii_case("tortoiseplink") { - ProgramKind::TortoisePlink - } else { - ProgramKind::Simple - } - } - } - } -} diff --git a/vendor/gix-transport/src/client/blocking_io/ssh/tests.rs b/vendor/gix-transport/src/client/blocking_io/ssh/tests.rs deleted file mode 100644 index 69eccc6..0000000 --- a/vendor/gix-transport/src/client/blocking_io/ssh/tests.rs +++ /dev/null @@ -1,496 +0,0 @@ -mod options { - mod ssh_command { - use crate::client::blocking_io::ssh::{ProgramKind, connect::Options}; - - #[test] - fn no_field_means_ssh() { - assert_eq!(Options::default().ssh_command(), "ssh"); - } - - #[test] - fn command_field_determines_ssh_command() { - assert_eq!( - Options { - command: Some("field-value".into()), - ..Default::default() - } - .ssh_command(), - "field-value" - ); - assert_eq!( - Options { - command: Some("field-value".into()), - kind: Some(ProgramKind::TortoisePlink), - ..Default::default() - } - .ssh_command(), - "field-value" - ); - } - - #[test] - fn kind_serves_as_fallback() { - assert_eq!( - Options { - kind: Some(ProgramKind::TortoisePlink), - ..Default::default() - } - .ssh_command(), - "tortoiseplink.exe" - ); - } - } -} - -mod program_kind { - mod from_os_str { - use std::ffi::OsStr; - - use crate::client::blocking_io::ssh::ProgramKind; - - #[test] - fn known_variants_are_derived_from_basename() { - for name_or_path in [ - "ssh", - "ssh.exe", - "SSH", - "SSH.exe", - "/bin/ssh", - "/bin/SSH", - #[cfg(windows)] - r"c:\bin\ssh.exe", - ] { - assert_eq!( - ProgramKind::from(OsStr::new(name_or_path)), - ProgramKind::Ssh, - "{name_or_path:?} could not be identified correctly" - ); - } - assert_eq!( - ProgramKind::from(OsStr::new("TortoisePlink.exe")), - ProgramKind::TortoisePlink - ); - assert_eq!(ProgramKind::from(OsStr::new("putty")), ProgramKind::Putty); - assert_eq!( - ProgramKind::from(OsStr::new("../relative/Plink.exe")), - ProgramKind::Plink - ); - } - - #[test] - fn unknown_variants_fallback_to_simple() { - assert_eq!( - ProgramKind::from(OsStr::new("something-unknown-that-does-not-exist-for-sure-foobar")), - ProgramKind::Simple, - "in theory, we could fail right here but we don't and leave non-existing programs to fail during handshake" - ); - } - - #[test] - fn ssh_disguised_within_a_script_cannot_be_detected_due_to_invocation_with_dash_g() { - assert_eq!( - ProgramKind::from(OsStr::new("ssh -VVV")), - ProgramKind::Simple, - "we don't execute the command here but assume simple, even though we could determine it's ssh if we would do what git does here" - ); - } - } - - mod prepare_invocation { - use std::ffi::OsStr; - - use crate::{ - Protocol, - client::blocking_io::ssh::{self, ProgramKind}, - }; - - #[test] - fn ssh() { - for (url, protocol, expected) in [ - ("ssh://user@host:42/p", Protocol::V1, &["ssh", "-p42", "user@host"][..]), - ("ssh://user@host/p", Protocol::V1, &["ssh", "user@host"][..]), - ("ssh://host/p", Protocol::V1, &["ssh", "host"][..]), - ( - "ssh://user@host:42/p", - Protocol::V2, - &["ssh", "-o", "SendEnv=GIT_PROTOCOL", "-p42", "user@host"][..], - ), - ( - "ssh://user@host/p", - Protocol::V2, - &["ssh", "-o", "SendEnv=GIT_PROTOCOL", "user@host"][..], - ), - ( - "ssh://host/p", - Protocol::V2, - &["ssh", "-o", "SendEnv=GIT_PROTOCOL", "host"][..], - ), - ] { - assert_eq!(call_args(ProgramKind::Ssh, url, protocol), expected); - } - } - - #[test] - fn tortoise_plink_has_batch_command() { - assert_eq!( - call_args(ProgramKind::TortoisePlink, "ssh://user@host:42/p", Protocol::V2), - ["tortoiseplink.exe", "-batch", "-P", "42", "user@host"] - ); - } - - #[test] - fn port_for_all() { - for kind in [ProgramKind::TortoisePlink, ProgramKind::Plink, ProgramKind::Putty] { - assert!(call_args(kind, "ssh://user@host:43/p", Protocol::V2).ends_with(&[ - "-P".into(), - "43".into(), - "user@host".into() - ])); - } - } - - #[test] - fn ambiguous_user_is_disallowed_explicit_ssh() { - let failure = try_call(ProgramKind::Ssh, "ssh://-arg@host/p", Protocol::V2) - .err() - .expect("the operation must fail"); - insta::assert_debug_snapshot!(gix_testtools::redact_debug_snapshot(&failure, &[]), "ambiguous user is disallowed explicit ssh", @r#" - AmbiguousUserName { - user: "-arg", - } - "#); - assert!( - matches!(failure, ssh::invocation::Error::AmbiguousUserName { user } if user == "-arg"), - "ambiguous user is disallowed explicit ssh" - ); - } - - #[test] - fn ambiguous_user_is_disallowed_implicit_ssh() { - let failure = try_call(ProgramKind::Ssh, "-arg@host:p/q", Protocol::V2) - .err() - .expect("the operation must fail"); - insta::assert_debug_snapshot!(gix_testtools::redact_debug_snapshot(&failure, &[]), "ambiguous user is disallowed implicit ssh", @r#" - AmbiguousUserName { - user: "-arg", - } - "#); - assert!( - matches!(failure, ssh::invocation::Error::AmbiguousUserName { user } if user == "-arg"), - "ambiguous user is disallowed implicit ssh" - ); - } - - #[test] - fn ambiguous_host_is_allowed_with_user_explicit_ssh() { - assert_eq!( - call_args(ProgramKind::Ssh, "ssh://user@-arg/p", Protocol::V2), - ["ssh", "-o", "SendEnv=GIT_PROTOCOL", "user@-arg"] - ); - } - - #[test] - fn ambiguous_host_is_allowed_with_user_implicit_ssh() { - assert_eq!( - call_args(ProgramKind::Ssh, "user@-arg:p/q", Protocol::V2), - ["ssh", "-o", "SendEnv=GIT_PROTOCOL", "user@-arg"] - ); - } - - #[test] - fn ambiguous_host_is_disallowed_without_user() { - let failure = try_call(ProgramKind::Ssh, "ssh://-arg/p", Protocol::V2) - .err() - .expect("the operation must fail"); - insta::assert_debug_snapshot!(gix_testtools::redact_debug_snapshot(&failure, &[]), "ambiguous host is disallowed without user", @r#" - AmbiguousHostName { - host: "-arg", - } - "#); - assert!( - matches!(failure, ssh::invocation::Error::AmbiguousHostName { host } if host == "-arg"), - "ambiguous host is disallowed without user" - ); - } - - #[test] - fn ambiguous_user_and_host_remain_disallowed_together_explicit_ssh() { - let failure = try_call(ProgramKind::Ssh, "ssh://-arg@host/p", Protocol::V2) - .err() - .expect("the operation must fail"); - insta::assert_debug_snapshot!(gix_testtools::redact_debug_snapshot(&failure, &[]), "ambiguous user and host remain disallowed together explicit ssh", @r#" - AmbiguousUserName { - user: "-arg", - } - "#); - assert!( - matches!(failure, ssh::invocation::Error::AmbiguousUserName { user } if user == "-arg"), - "ambiguous user and host remain disallowed together explicit ssh" - ); - } - - #[test] - fn ambiguous_user_and_host_remain_disallowed_together_implicit_ssh() { - let failure = try_call(ProgramKind::Ssh, "-userarg@-hostarg:p/q", Protocol::V2) - .err() - .expect("the operation must fail"); - insta::assert_debug_snapshot!(gix_testtools::redact_debug_snapshot(&failure, &[]), "ambiguous user and host remain disallowed together implicit ssh", @r#" - AmbiguousUserName { - user: "-userarg", - } - "#); - assert!( - matches!(failure, ssh::invocation::Error::AmbiguousUserName { user } if user == "-userarg"), - "ambiguous user and host remain disallowed together implicit ssh" - ); - } - - #[test] - fn simple_cannot_handle_any_arguments() { - let failure = try_call(ProgramKind::Simple, "ssh://user@host:42/p", Protocol::V2) - .err() - .expect("the operation must fail"); - insta::assert_debug_snapshot!(gix_testtools::redact_debug_snapshot(&failure, &[]), "simple cannot handle any arguments", @r#" - Unsupported { - command: "simple", - function: "setting the port", - } - "#); - assert!( - matches!(failure, ssh::invocation::Error::Unsupported { .. }), - "simple cannot handle any arguments" - ); - assert_eq!( - call_args(ProgramKind::Simple, "ssh://user@host/p", Protocol::V2), - ["simple", "user@host"], - "simple can only do simple invocations" - ); - } - - #[test] - fn ssh_env_v2() { - let prepare = call(ProgramKind::Ssh, "ssh://host/p", Protocol::V2); - assert_eq!( - prepare.env, - &[ - ("GIT_PROTOCOL".into(), "version=2".into()), - ("LANG".into(), "C".into()), - ("LC_ALL".into(), "C".into()) - ] - ); - assert!(!prepare.use_shell); - } - - #[test] - fn disallow_shell_is_honored() -> Result { - let url = gix_url::parse("ssh://host/path").expect("valid url"); - - let disallow_shell = false; - let prepare = - ProgramKind::Ssh.prepare_invocation(OsStr::new("echo hi"), &url, Protocol::V1, disallow_shell)?; - assert!(prepare.use_shell, "shells are used when needed"); - - let disallow_shell = true; - let prepare = - ProgramKind::Ssh.prepare_invocation(OsStr::new("echo hi"), &url, Protocol::V1, disallow_shell)?; - assert!( - !prepare.use_shell, - "but we can enforce it not to be used as well for historical reasons" - ); - Ok(()) - } - - fn try_call( - kind: ProgramKind, - url: &str, - version: Protocol, - ) -> std::result::Result { - let ssh_cmd = kind.exe().unwrap_or_else(|| OsStr::new("simple")); - let url = gix_url::parse(url).expect("valid url"); - kind.prepare_invocation(ssh_cmd, &url, version, false) - } - fn call(kind: ProgramKind, url: &str, version: Protocol) -> gix_command::Prepare { - try_call(kind, url, version).expect("no error") - } - fn call_args(kind: ProgramKind, url: &str, version: Protocol) -> Vec { - let prepare = call(kind, url, version); - let program = prepare.command.clone(); - let cmd = - std::process::Command::try_from(prepare).expect("command fixture can be represented by the platform"); - let expected_program = std::process::Command::try_from(gix_command::prepare(&program)) - .expect("command fixture can be represented by the platform"); - assert_eq!( - cmd.get_program(), - expected_program.get_program(), - "the selected SSH program follows the platform's command lookup" - ); - std::iter::once(program.as_os_str()) - .chain(cmd.get_args()) - .map(|arg| arg.to_string_lossy().into_owned()) - .collect() - } - - type Result = std::result::Result<(), ssh::invocation::Error>; - } - - mod line_to_err { - use std::io::ErrorKind; - - use crate::client::blocking_io::ssh::ProgramKind; - - #[test] - fn all() { - let mut diagnostics = Vec::new(); - for (kind, line, expected) in [ - ( - ProgramKind::Ssh, - "byron@github.com: Permission denied (publickey).", - ErrorKind::PermissionDenied, - ), - ( - ProgramKind::Ssh, - "ssh: Could not resolve hostname hostfoobar: nodename nor servname provided, or not known", - ErrorKind::ConnectionRefused, - ), - ( - ProgramKind::Ssh, - "ssh: connect to host example.org port 22: No route to host", - ErrorKind::NotFound, - ), - // connection closed by remote on windows - ( - ProgramKind::Ssh, - "banner exchange: Connection to 127.0.0.1 port 61024: Software caused connection abort", - ErrorKind::NotFound, - ), - // connection closed by remote on unix - ( - ProgramKind::Ssh, - "Connection closed by 127.0.0.1 port 8888", // - ErrorKind::NotFound, - ), - // this kind is basically unknown but we try our best, and simple equals ssh - ( - ProgramKind::Simple, - "something permission denied something", - ErrorKind::PermissionDenied, - ), - ( - ProgramKind::Simple, - "something resolve hostname hostfoobar: nodename nor servname something", - ErrorKind::ConnectionRefused, - ), - ( - ProgramKind::Simple, - "something connect to host something", - ErrorKind::NotFound, - ), - ] { - let err = kind.line_to_err(line.into()).expect("the SSH diagnostic is recognized"); - assert_eq!(err.kind(), expected); - diagnostics.push((kind, err)); - } - insta::assert_debug_snapshot!(diagnostics, "SSH diagnostics retain the server message alongside their I/O classification", @r#" - [ - ( - Ssh, - Custom { - kind: PermissionDenied, - error: "byron@github.com: Permission denied (publickey).", - }, - ), - ( - Ssh, - Custom { - kind: ConnectionRefused, - error: "ssh: Could not resolve hostname hostfoobar: nodename nor servname provided, or not known", - }, - ), - ( - Ssh, - Custom { - kind: NotFound, - error: "ssh: connect to host example.org port 22: No route to host", - }, - ), - ( - Ssh, - Custom { - kind: NotFound, - error: "banner exchange: Connection to 127.0.0.1 port 61024: Software caused connection abort", - }, - ), - ( - Ssh, - Custom { - kind: NotFound, - error: "Connection closed by 127.0.0.1 port 8888", - }, - ), - ( - Simple, - Custom { - kind: PermissionDenied, - error: "something permission denied something", - }, - ), - ( - Simple, - Custom { - kind: ConnectionRefused, - error: "something resolve hostname hostfoobar: nodename nor servname something", - }, - ), - ( - Simple, - Custom { - kind: NotFound, - error: "something connect to host something", - }, - ), - ] - "#); - } - - #[test] - fn tortoiseplink_putty_plink() { - let mut diagnostics = Vec::new(); - for kind in [ProgramKind::TortoisePlink, ProgramKind::Plink, ProgramKind::Putty] { - let err = kind - .line_to_err("publickey".into()) - .expect("publickey is a recognized authentication failure"); - assert_eq!( - err.kind(), - std::io::ErrorKind::PermissionDenied, - "this program pops up error messages in a window, no way to extract information from it. Maybe there is other ways to use it, 'publickey' they mention all" - ); - diagnostics.push((kind, err)); - } - insta::assert_debug_snapshot!(diagnostics, "PuTTY variants retain their public-key authentication failure", @r#" - [ - ( - TortoisePlink, - Custom { - kind: PermissionDenied, - error: "publickey", - }, - ), - ( - Plink, - Custom { - kind: PermissionDenied, - error: "publickey", - }, - ), - ( - Putty, - Custom { - kind: PermissionDenied, - error: "publickey", - }, - ), - ] - "#); - } - } -} diff --git a/vendor/gix-transport/src/client/blocking_io/traits.rs b/vendor/gix-transport/src/client/blocking_io/traits.rs deleted file mode 100644 index b38857b..0000000 --- a/vendor/gix-transport/src/client/blocking_io/traits.rs +++ /dev/null @@ -1,138 +0,0 @@ -use std::{io::Write, ops::DerefMut}; - -use bstr::BString; - -use crate::{ - Protocol, Service, - client::{ - Capabilities, Error, MessageKind, TransportWithoutIO, WriteMode, - blocking_io::{ExtendedBufRead, ReadlineBufRead, request::RequestWriter}, - }, -}; - -/// The response of the [`handshake()`][Transport::handshake()] method. -pub struct SetServiceResponse<'a> { - /// The protocol the service can provide. May be different from the requested one - pub actual_protocol: Protocol, - /// The capabilities parsed from the server response. - pub capabilities: Capabilities, - /// In protocol version one, this is set to a list of refs and their peeled counterparts. - pub refs: Option>, -} - -/// All methods provided here must be called in the correct order according to the [communication protocol][Protocol] -/// used to connect to them. -/// It does, however, know just enough to be able to provide a higher-level interface than would otherwise be possible. -/// Thus the consumer of this trait will not have to deal with packet lines at all. -/// **Note that** whenever a `Read` trait or `Write` trait is produced, it must be exhausted. -pub trait Transport: TransportWithoutIO { - /// Initiate connection to the given service and send the given `extra_parameters` along with it. - /// - /// `extra_parameters` are interpreted as `key=value` pairs if the second parameter is `Some` or as `key` - /// if it is None. - /// - /// Returns the service capabilities according to the actual [Protocol] it supports, - /// and possibly a list of refs to be obtained. - /// This means that asking for an unsupported protocol might result in a protocol downgrade to the given one - /// if [`TransportWithoutIO::supported_protocol_versions()`] includes it. - /// Exhaust the returned [`BufReader`][SetServiceResponse::refs] for a list of references in case of protocol V1 - /// before making another request. - fn handshake<'a>( - &mut self, - service: Service, - extra_parameters: &'a [(&'a str, Option<&'a str>)], - ) -> Result, Error>; - - /// Get a writer for sending data and obtaining the response. It can be configured in various ways - /// to support the task at hand. - /// `write_mode` determines how calls to the `write(…)` method are interpreted, and `on_into_read` determines - /// which message to write when the writer is turned into the response reader using [`into_read()`][RequestWriter::into_read()]. - /// If `trace` is `true`, then all packetlines written and received will be traced using facilities provided by the `gix_trace` crate. - fn request( - &mut self, - write_mode: WriteMode, - on_into_read: MessageKind, - trace: bool, - ) -> Result, Error>; -} - -// Would be nice if the box implementation could auto-forward to all implemented traits. -impl Transport for Box { - fn handshake<'a>( - &mut self, - service: Service, - extra_parameters: &'a [(&'a str, Option<&'a str>)], - ) -> Result, Error> { - self.deref_mut().handshake(service, extra_parameters) - } - - fn request( - &mut self, - write_mode: WriteMode, - on_into_read: MessageKind, - trace: bool, - ) -> Result, Error> { - self.deref_mut().request(write_mode, on_into_read, trace) - } -} - -impl Transport for &mut T { - fn handshake<'a>( - &mut self, - service: Service, - extra_parameters: &'a [(&'a str, Option<&'a str>)], - ) -> Result, Error> { - self.deref_mut().handshake(service, extra_parameters) - } - - fn request( - &mut self, - write_mode: WriteMode, - on_into_read: MessageKind, - trace: bool, - ) -> Result, Error> { - self.deref_mut().request(write_mode, on_into_read, trace) - } -} - -/// An extension trait to add more methods to everything implementing [`Transport`]. -pub trait TransportV2Ext { - /// Invoke a protocol V2 style `command` with given `capabilities` and optional command specific `arguments`. - /// The `capabilities` were communicated during the handshake. - /// If `trace` is `true`, then all packetlines written and received will be traced using facilities provided by the `gix_trace` crate. - /// - /// _Note:_ panics if [handshake][Transport::handshake()] wasn't performed beforehand. - fn invoke<'a>( - &mut self, - command: &str, - capabilities: impl Iterator>)> + 'a, - arguments: Option>, - trace: bool, - ) -> Result + Unpin + '_>, Error>; -} - -impl TransportV2Ext for T { - fn invoke<'a>( - &mut self, - command: &str, - capabilities: impl Iterator>)> + 'a, - arguments: Option>, - trace: bool, - ) -> Result + Unpin + '_>, Error> { - let mut writer = self.request(WriteMode::OneLfTerminatedLinePerWriteCall, MessageKind::Flush, trace)?; - writer.write_all(format!("command={command}").as_bytes())?; - for (name, value) in capabilities { - match value { - Some(value) => writer.write_all(format!("{name}={}", value.as_ref()).as_bytes()), - None => writer.write_all(name.as_bytes()), - }?; - } - if let Some(arguments) = arguments { - writer.write_message(MessageKind::Delimiter)?; - for argument in arguments { - writer.write_all(argument.as_ref())?; - } - } - Ok(writer.into_read()?) - } -} diff --git a/vendor/gix-transport/src/client/capabilities.rs b/vendor/gix-transport/src/client/capabilities.rs deleted file mode 100644 index 1373b44..0000000 --- a/vendor/gix-transport/src/client/capabilities.rs +++ /dev/null @@ -1,333 +0,0 @@ -use bstr::{BStr, BString, ByteSlice}; -use gix_error::Result; -use gix_error::{OptionExt, bail, corruption, message}; - -#[cfg(any(feature = "blocking-client", feature = "async-client"))] -use crate::{Protocol, client}; - -/// A structure to represent multiple [capabilities](Capability) or features supported by the server. -/// -/// ### Deviation -/// -/// As a *shortcoming*, we are unable to parse `V1` as emitted from `git-upload-pack` without a `git-daemon` or server, -/// as it will not emit any capabilities for some reason. Only `V2` and `V0` work in that context. -#[derive(Debug, Clone)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct Capabilities { - data: BString, - value_sep: u8, -} - -/// This implementation yields exactly those minimal capabilities that are required for `gix` to work, nothing more and nothing less. -/// -/// This is a bit of a hack just get tests with Protocol V0 to work, which is a good way to enforce stateful transports. -/// Of course, V1 would also do that but when calling `git-upload-pack` directly, it advertises so badly that this is easier to implement. -impl Default for Capabilities { - fn default() -> Self { - Capabilities::from_lines("version 2\nmulti_ack_detailed\nside-band-64k\n".into()) - .expect("valid format, known at compile time") - } -} - -/// The name of a single capability. -pub struct Capability<'a>(&'a BStr); - -impl<'a> Capability<'a> { - /// Returns the name of the capability. - /// - /// Most capabilities only consist of a name, making them appear like a feature toggle. - pub fn name(&self) -> &'a BStr { - self.0 - .splitn(2, |b| *b == b'=') - .next() - .expect("there is always a single item") - .as_bstr() - } - /// Returns the value associated with the capability. - /// - /// Note that the caller must know whether a single or multiple values are expected, in which - /// case [`values()`](Capability::values()) should be called. - pub fn value(&self) -> Option<&'a BStr> { - self.0.splitn(2, |b| *b == b'=').nth(1).map(ByteSlice::as_bstr) - } - /// Returns the values of a capability if its [`value()`](Capability::value()) is space separated. - pub fn values(&self) -> Option> { - self.value().map(|v| v.split(|b| *b == b' ').map(ByteSlice::as_bstr)) - } - /// Returns true if its space-separated [`value()`](Capability::value()) contains the given `want`ed capability. - pub fn supports(&self, want: impl Into<&'a BStr>) -> Option { - let want = want.into(); - self.values().map(|mut iter| iter.any(|v| v == want)) - } -} - -impl Capabilities { - /// Parse capabilities from the given `bytes`. - /// - /// Useful in case they are encoded within a `ref` behind a null byte. - pub fn from_bytes(bytes: &[u8]) -> Result<(Capabilities, usize)> { - let delimiter_pos = bytes.find_byte(0).ok_or_raise(|| { - let err = message("Capabilities were missing entirely as there was no 0 byte"); - // Explicit v1 headers reach here due to the documented V1 limitation, not malformed peer data. - if bytes == b"version 1" || bytes == b"version 1\n" { - err.unsupported() - } else { - err.corrupted() - } - })?; - if delimiter_pos + 1 == bytes.len() { - bail!(corruption("there was not a single capability behind the delimiter")); - } - let capabilities = &bytes[delimiter_pos + 1..]; - Ok(( - Capabilities { - data: capabilities.as_bstr().to_owned(), - value_sep: b' ', - }, - delimiter_pos, - )) - } - - /// Parse capabilities from the given a `lines_buf` which is expected to be all newline separated lines - /// from the server. - /// - /// Useful for parsing capabilities from a data sent from a server, and to avoid having to deal with - /// blocking and async traits for as long as possible. There is no value in parsing a few bytes - /// in a non-blocking fashion. - pub fn from_lines(lines_buf: BString) -> Result { - let mut lines = <_ as bstr::ByteSlice>::lines(lines_buf.as_slice().trim()); - let version_line = lines - .next() - .ok_or_raise(|| corruption("a version line was expected, but none was retrieved"))?; - let (name, value) = version_line.split_at( - version_line - .find(b" ") - .ok_or_raise(|| message!("expected 'version X', got {version_line:?}").corrupted())?, - ); - if name != b"version" { - bail!("expected 'version X', got {version_line:?}".corrupted()); - } - if value != b" 2" { - bail!("Got unsupported version {value:?}, expected 2".unsupported()); - } - Ok(Capabilities { - value_sep: b'\n', - data: lines.as_bytes().into(), - }) - } - - /// Returns true of the given `feature` is mentioned in this list of capabilities. - pub fn contains(&self, feature: &str) -> bool { - self.capability(feature).is_some() - } - - /// Returns the capability with `name`. - pub fn capability(&self, name: &str) -> Option> { - self.iter().find(|c| c.name() == name.as_bytes().as_bstr()) - } - - /// Returns an iterator over all capabilities. - pub fn iter(&self) -> impl Iterator> { - self.data - .split(move |b| *b == self.value_sep) - .map(|c| Capability(c.as_bstr())) - } -} - -/// internal use -#[cfg(any(feature = "blocking-client", feature = "async-client"))] -impl Capabilities { - fn extract_protocol( - capabilities_or_version: gix_packetline::TextRef<'_>, - ) -> std::result::Result { - let line = capabilities_or_version.as_bstr(); - let version = if line.starts_with_str("version ") { - if line.len() != "version X".len() { - return Err(client::Error::UnsupportedProtocolVersion(line.as_bstr().into())); - } - match line { - line if line.ends_with_str("1") => Protocol::V1, - line if line.ends_with_str("2") => Protocol::V2, - _ => return Err(client::Error::UnsupportedProtocolVersion(line.as_bstr().into())), - } - } else { - Protocol::V1 - }; - Ok(version) - } -} - -/// -#[cfg(feature = "blocking-client")] -pub mod blocking_recv { - use std::io; - - use bstr::ByteVec; - - use crate::{ - Protocol, - client::{self, Capabilities, blocking_io::ReadlineBufRead}, - packetline::blocking_io::StreamingPeekableIter, - }; - - /// The information provided by the server upon first connection. - pub struct Handshake<'a> { - /// The [`Capabilities`] the remote advertised. - pub capabilities: Capabilities, - /// The remote refs as a [`io::BufRead`]. - /// - /// This is `Some` only when protocol v1 is used. The [`io::BufRead`] must be exhausted by - /// the caller. - pub refs: Option>, - /// The [`Protocol`] the remote advertised. - pub protocol: Protocol, - } - - impl Handshake<'_> { - /// Read the capabilities and version advertisement from the given packetline reader. - /// - /// If [`Protocol::V1`] was requested, or the remote decided to downgrade, the remote refs - /// advertisement will also be included in the [`Handshake`]. - pub fn from_lines_with_version_detection( - rd: &mut StreamingPeekableIter, - ) -> Result, client::Error> { - // NOTE that this is vitally important - it is turned on and stays on for all following requests so - // we automatically abort if the server sends an ERR line anywhere. - // We are sure this can't clash with binary data when sent due to the way the PACK - // format looks like, thus there is no binary blob that could ever look like an ERR line by accident. - rd.fail_on_err_lines(true); - - Ok(match rd.peek_line() { - Some(line) => { - let line = line??.as_text().ok_or(client::Error::ExpectedLine("text"))?; - let version = Capabilities::extract_protocol(line)?; - match version { - Protocol::V0 => unreachable!("already handled in `None` case"), - Protocol::V1 => { - let (capabilities, delimiter_position) = Capabilities::from_bytes(line.0)?; - rd.peek_buffer_replace_and_truncate(delimiter_position, b'\n'); - Handshake { - capabilities, - refs: Some(Box::new(rd.as_read())), - protocol: Protocol::V1, - } - } - Protocol::V2 => Handshake { - capabilities: { - let mut rd = rd.as_read(); - let mut buf = Vec::new(); - while let Some(line) = rd.read_data_line() { - let line = line??; - match line.as_bstr() { - Some(line) => { - buf.push_str(line); - if buf.last() != Some(&b'\n') { - buf.push(b'\n'); - } - } - None => break, - } - } - Capabilities::from_lines(buf.into())? - }, - refs: None, - protocol: Protocol::V2, - }, - } - } - None => Handshake { - capabilities: Capabilities::default(), - refs: Some(Box::new(rd.as_read())), - protocol: Protocol::V0, - }, - }) - } - } -} - -/// -#[cfg(feature = "async-client")] -pub mod async_recv { - use bstr::ByteVec; - use futures_io::AsyncRead; - - use crate::{ - Protocol, - client::{self, Capabilities, async_io::ReadlineBufRead}, - packetline::async_io::StreamingPeekableIter, - }; - - /// The information provided by the server upon first connection. - pub struct Handshake<'a> { - /// The [`Capabilities`] the remote advertised. - pub capabilities: Capabilities, - /// The remote refs as an [`AsyncBufRead`][futures_io::AsyncBufRead]. - /// - /// This is `Some` only when protocol v1 is used. The [`AsyncRead`] must be exhausted by - /// the caller. - pub refs: Option>, - /// The [`Protocol`] the remote advertised. - pub protocol: Protocol, - } - - impl Handshake<'_> { - /// Read the capabilities and version advertisement from the given packetline reader. - /// - /// If [`Protocol::V1`] was requested, or the remote decided to downgrade, the remote refs - /// advertisement will also be included in the [`Handshake`]. - pub async fn from_lines_with_version_detection( - rd: &mut StreamingPeekableIter, - ) -> Result, client::Error> { - // NOTE that this is vitally important - it is turned on and stays on for all following requests so - // we automatically abort if the server sends an ERR line anywhere. - // We are sure this can't clash with binary data when sent due to the way the PACK - // format looks like, thus there is no binary blob that could ever look like an ERR line by accident. - rd.fail_on_err_lines(true); - - Ok(match rd.peek_line().await { - Some(line) => { - let line = line??.as_text().ok_or(client::Error::ExpectedLine("text"))?; - let version = Capabilities::extract_protocol(line)?; - match version { - Protocol::V0 => unreachable!("already handled in `None` case"), - Protocol::V1 => { - let (capabilities, delimiter_position) = Capabilities::from_bytes(line.0)?; - rd.peek_buffer_replace_and_truncate(delimiter_position, b'\n'); - Handshake { - capabilities, - refs: Some(Box::new(rd.as_read())), - protocol: Protocol::V1, - } - } - Protocol::V2 => Handshake { - capabilities: { - let mut rd = rd.as_read(); - let mut buf = Vec::new(); - while let Some(line) = rd.read_data_line().await { - let line = line??; - match line.as_bstr() { - Some(line) => { - buf.push_str(line); - if buf.last() != Some(&b'\n') { - buf.push(b'\n'); - } - } - None => break, - } - } - Capabilities::from_lines(buf.into())? - }, - refs: None, - protocol: Protocol::V2, - }, - } - } - None => Handshake { - capabilities: Capabilities::default(), - refs: Some(Box::new(rd.as_read())), - protocol: Protocol::V0, - }, - }) - } - } -} diff --git a/vendor/gix-transport/src/client/git/async_io.rs b/vendor/gix-transport/src/client/git/async_io.rs deleted file mode 100644 index 49c1337..0000000 --- a/vendor/gix-transport/src/client/git/async_io.rs +++ /dev/null @@ -1,200 +0,0 @@ -use gix_error::Result; -use std::borrow::Cow; - -use async_trait::async_trait; -use bstr::{BStr, BString, ByteVec}; -use futures_io::{AsyncRead, AsyncWrite}; -use futures_lite::AsyncWriteExt; - -use crate::{ - Protocol, Service, - client::{ - self, - async_io::{RequestWriter, SetServiceResponse}, - capabilities::async_recv::Handshake, - git::{self, ConnectionState}, - }, - packetline::{ - PacketLineRef, - async_io::{StreamingPeekableIter, Writer}, - }, -}; - -/// A TCP connection to either a `git` daemon or a spawned `git` process. -/// -/// When connecting to a daemon, additional context information is sent with the first line of the handshake. -pub struct Connection { - pub(in crate::client) writer: W, - pub(in crate::client) line_provider: StreamingPeekableIter, - pub(in crate::client) state: ConnectionState, -} - -impl Connection { - /// Optionally set the URL to be returned when asked for it if `Some` or calculate a default for `None`. - /// - /// The URL is required as parameter for authentication helpers which are called in transports - /// that support authentication. Even though plain git transports don't support that, this - /// may well be the case in custom transports. - pub fn custom_url(mut self, url: Option) -> Self { - self.state.custom_url = url; - self - } - - /// Return the inner reader and writer - pub fn into_inner(self) -> (R, W) { - (self.line_provider.into_inner(), self.writer) - } -} - -impl client::TransportWithoutIO for Connection -where - R: AsyncRead + Unpin, - W: AsyncWrite + Unpin, -{ - fn to_url(&self) -> Cow<'_, BStr> { - self.state.custom_url.as_ref().map_or_else( - || { - let mut possibly_lossy_url = self.state.path.clone(); - possibly_lossy_url.insert_str(0, "file://"); - Cow::Owned(possibly_lossy_url) - }, - |url| Cow::Borrowed(url.as_ref()), - ) - } - - fn connection_persists_across_multiple_requests(&self) -> bool { - true - } - - fn configure(&mut self, _config: &dyn std::any::Any) -> Result { - Ok(()) - } -} - -#[async_trait(?Send)] -impl client::async_io::Transport for Connection -where - R: AsyncRead + Unpin, - W: AsyncWrite + Unpin, -{ - async fn handshake<'a>( - &mut self, - service: Service, - extra_parameters: &'a [(&'a str, Option<&'a str>)], - ) -> std::result::Result, client::Error> { - if self.state.mode == git::ConnectMode::Daemon { - let mut line_writer = Writer::new(&mut self.writer); - line_writer.enable_binary_mode(); - line_writer - .write_all(&git::message::connect( - service, - self.state.desired_version, - &self.state.path, - self.state.virtual_host.as_ref(), - extra_parameters, - )?) - .await?; - line_writer.flush().await?; - } - - let Handshake { - capabilities, - refs, - protocol: actual_protocol, - } = Handshake::from_lines_with_version_detection(&mut self.line_provider).await?; - Ok(SetServiceResponse { - actual_protocol, - capabilities, - refs, - }) - } - - fn request( - &mut self, - write_mode: client::WriteMode, - on_into_read: client::MessageKind, - trace: bool, - ) -> std::result::Result, client::Error> { - Ok(RequestWriter::new_from_bufread( - &mut self.writer, - Box::new(self.line_provider.as_read_without_sidebands()), - write_mode, - on_into_read, - trace, - )) - } -} - -impl Connection -where - R: AsyncRead + Unpin, - W: AsyncWrite + Unpin, -{ - /// Create a connection from the given `read` and `write`, asking for `desired_version` as preferred protocol - /// and the transfer of the repository at `repository_path`. - /// - /// `virtual_host` along with a port to which to connect to, while `mode` determines the kind of endpoint to connect to. - /// If `trace` is `true`, all packetlines received or sent will be passed to the facilities of the `gix-trace` crate. - pub fn new( - read: R, - write: W, - desired_version: Protocol, - repository_path: impl Into, - virtual_host: Option<(impl Into, Option)>, - mode: git::ConnectMode, - trace: bool, - ) -> Self { - Connection { - writer: write, - line_provider: StreamingPeekableIter::new(read, &[PacketLineRef::Flush], trace), - state: ConnectionState { - path: repository_path.into(), - virtual_host: virtual_host.map(|(h, p)| (h.into(), p)), - desired_version, - custom_url: None, - mode, - }, - } - } -} - -#[cfg(feature = "async-std")] -mod async_net { - use std::time::Duration; - - use async_std::net::TcpStream; - - use crate::client::{ - Error, - git::{ConnectMode, async_io::Connection}, - }; - - impl Connection { - /// Create a new TCP connection using the `git` protocol of `desired_version`, and make a connection to `host` - /// at `port` for accessing the repository at `path` on the server side. - /// If `trace` is `true`, all packetlines received or sent will be passed to the facilities of the `gix-trace` crate. - pub async fn new_tcp( - host: &str, - port: Option, - path: bstr::BString, - desired_version: crate::Protocol, - trace: bool, - ) -> Result { - let read = async_std::io::timeout( - Duration::from_secs(5), - TcpStream::connect(&(host, port.unwrap_or(9418))), - ) - .await?; - let write = read.clone(); - Ok(Self::new( - read, - write, - desired_version, - path, - None::<(String, _)>, - ConnectMode::Daemon, - trace, - )) - } - } -} diff --git a/vendor/gix-transport/src/client/git/blocking_io.rs b/vendor/gix-transport/src/client/git/blocking_io.rs deleted file mode 100644 index 9e807b7..0000000 --- a/vendor/gix-transport/src/client/git/blocking_io.rs +++ /dev/null @@ -1,237 +0,0 @@ -use gix_error::Result; -use std::{any::Any, borrow::Cow, io::Write}; - -use bstr::{BStr, BString, ByteVec}; - -use crate::{ - Protocol, Service, - client::{ - self, - blocking_io::{RequestWriter, SetServiceResponse}, - capabilities::blocking_recv::Handshake, - git::{self, ConnectionState}, - }, - packetline::{ - PacketLineRef, - blocking_io::{StreamingPeekableIter, Writer}, - }, -}; - -/// A TCP connection to either a `git` daemon or a spawned `git` process. -/// -/// When connecting to a daemon, additional context information is sent with the first line of the handshake. Otherwise that -/// context is passed using command line arguments to a [spawned `git` process][crate::client::blocking_io::file::SpawnProcessOnDemand]. -pub struct Connection { - pub(in crate::client) writer: W, - pub(in crate::client) line_provider: StreamingPeekableIter, - pub(in crate::client) state: ConnectionState, -} - -impl Connection { - /// Optionally set the URL to be returned when asked for it if `Some` or calculate a default for `None`. - /// - /// The URL is required as parameter for authentication helpers which are called in transports - /// that support authentication. Even though plain git transports don't support that, this - /// may well be the case in custom transports. - pub fn custom_url(mut self, url: Option) -> Self { - self.state.custom_url = url; - self - } - - /// Return the inner reader and writer - pub fn into_inner(self) -> (R, W) { - (self.line_provider.into_inner(), self.writer) - } -} - -impl client::TransportWithoutIO for Connection -where - R: std::io::Read, - W: std::io::Write, -{ - fn to_url(&self) -> Cow<'_, BStr> { - self.state.custom_url.as_ref().map_or_else( - || { - let mut possibly_lossy_url = self.state.path.clone(); - possibly_lossy_url.insert_str(0, "file://"); - Cow::Owned(possibly_lossy_url) - }, - |url| Cow::Borrowed(url.as_ref()), - ) - } - - fn connection_persists_across_multiple_requests(&self) -> bool { - true - } - - fn configure(&mut self, _config: &dyn Any) -> Result { - Ok(()) - } -} - -impl client::blocking_io::Transport for Connection -where - R: std::io::Read, - W: std::io::Write, -{ - fn handshake<'a>( - &mut self, - service: Service, - extra_parameters: &'a [(&'a str, Option<&'a str>)], - ) -> std::result::Result, client::Error> { - if self.state.mode == git::ConnectMode::Daemon { - let mut line_writer = Writer::new(&mut self.writer); - line_writer.enable_binary_mode(); - line_writer.write_all(&git::message::connect( - service, - self.state.desired_version, - &self.state.path, - self.state.virtual_host.as_ref(), - extra_parameters, - )?)?; - line_writer.flush()?; - } - - let Handshake { - capabilities, - refs, - protocol: actual_protocol, - } = Handshake::from_lines_with_version_detection(&mut self.line_provider)?; - Ok(SetServiceResponse { - actual_protocol, - capabilities, - refs, - }) - } - - fn request( - &mut self, - write_mode: client::WriteMode, - on_into_read: client::MessageKind, - trace: bool, - ) -> std::result::Result, client::Error> { - Ok(RequestWriter::new_from_bufread( - &mut self.writer, - Box::new(self.line_provider.as_read_without_sidebands()), - write_mode, - on_into_read, - trace, - )) - } -} - -impl Connection -where - R: std::io::Read, - W: std::io::Write, -{ - /// Create a connection from the given `read` and `write`, asking for `desired_version` as preferred protocol - /// and the transfer of the repository at `repository_path`. - /// - /// `virtual_host` along with a port to which to connect to, while `mode` determines the kind of endpoint to connect to. - /// If `trace` is `true`, all packetlines received or sent will be passed to the facilities of the `gix-trace` crate. - pub fn new( - read: R, - write: W, - desired_version: Protocol, - repository_path: impl Into, - virtual_host: Option<(impl Into, Option)>, - mode: git::ConnectMode, - trace: bool, - ) -> Self { - Connection { - writer: write, - line_provider: StreamingPeekableIter::new(read, &[PacketLineRef::Flush], trace), - state: ConnectionState { - path: repository_path.into(), - virtual_host: virtual_host.map(|(h, p)| (h.into(), p)), - desired_version, - custom_url: None, - mode, - }, - } - } - pub(crate) fn new_for_spawned_process( - reader: R, - writer: W, - desired_version: Protocol, - repository_path: impl Into, - trace: bool, - ) -> Self { - Self::new( - reader, - writer, - desired_version, - repository_path, - None::<(&str, _)>, - git::ConnectMode::Process, - trace, - ) - } -} - -/// -pub mod connect { - use std::net::{TcpStream, ToSocketAddrs}; - - use bstr::BString; - use gix_error::{Result, ResultExt, message}; - - use super::Connection; - use crate::client::git; - - fn parse_host(input: String) -> Result<(String, Option)> { - let mut tokens = input.splitn(2, ':'); - Ok(match (tokens.next(), tokens.next()) { - (Some(host), None) => (host.to_owned(), None), - (Some(host), Some(port)) => ( - host.to_owned(), - Some(port.parse::().or_raise(|| { - gix_error::message!("Could not parse {input:?} as virtual host with format [:port]") - })?), - ), - _ => unreachable!("we expect at least one token, the original string"), - }) - } - - /// Connect to a git daemon running on `host` and optionally `port` and a repository at `path`. - /// - /// Use `desired_version` to specify a preferred protocol to use, knowing that it can be downgraded by a server not supporting it. - /// If `trace` is `true`, all packetlines received or sent will be passed to the facilities of the `gix-trace` crate. - pub fn connect( - host: &str, - path: BString, - desired_version: crate::Protocol, - port: Option, - trace: bool, - ) -> Result> { - let read = TcpStream::connect_timeout( - &(host, port.unwrap_or(9418)) - .to_socket_addrs() - .or_raise(|| message("Could not resolve git server"))? - .next() - .expect("after successful resolution there is an IP address"), - std::time::Duration::from_secs(5), - ) - .or_raise(|| message("Could not connect to git server"))?; - let write = read - .try_clone() - .or_raise(|| message("Could not clone git server connection"))?; - let vhost = std::env::var("GIT_OVERRIDE_VIRTUAL_HOST") - .ok() - .map(parse_host) - .transpose()? - .unwrap_or_else(|| (host.to_owned(), port)); - Ok(Connection::new( - read, - write, - desired_version, - path, - Some(vhost), - git::ConnectMode::Daemon, - trace, - )) - } -} - -pub use connect::connect; diff --git a/vendor/gix-transport/src/client/git/mod.rs b/vendor/gix-transport/src/client/git/mod.rs deleted file mode 100644 index 0533575..0000000 --- a/vendor/gix-transport/src/client/git/mod.rs +++ /dev/null @@ -1,206 +0,0 @@ -use bstr::BString; - -use crate::Protocol; - -/// The way to connect to a process speaking the `git` protocol. -#[derive(PartialEq, Eq, Clone, Copy)] -pub enum ConnectMode { - /// A git daemon. - Daemon, - /// A spawned `git` process to upload a pack to the client. - Process, -} - -/// Connection state shared between blocking and async connections. -pub(crate) struct ConnectionState { - pub(in crate::client) path: BString, - pub(in crate::client) virtual_host: Option<(String, Option)>, - pub(in crate::client) desired_version: Protocol, - custom_url: Option, - pub(in crate::client) mode: ConnectMode, -} - -mod message { - use bstr::{BString, ByteVec}; - - use crate::{Protocol, Service}; - - pub fn connect( - service: Service, - desired_version: Protocol, - path: &[u8], - virtual_host: Option<&(String, Option)>, - extra_parameters: &[(&str, Option<&str>)], - ) -> std::io::Result { - let path = gix_url::expand_path::for_shell(path.into()); - let is_forbidden = |byte| matches!(byte, b'\0' | b'\n'); - if path.iter().copied().any(is_forbidden) { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidInput, - "git daemon repository paths must not contain NUL or LF", - )); - } - if virtual_host.is_some_and(|(host, _)| host.bytes().any(is_forbidden)) { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidInput, - "git daemon virtual hosts must not contain NUL or LF", - )); - } - - let mut out = bstr::BString::from(service.as_str()); - out.push(b' '); - out.extend_from_slice(&path); - out.push(0); - if let Some((host, port)) = virtual_host { - out.push_str("host="); - out.extend_from_slice(host.as_bytes()); - if let Some(port) = port { - out.push_byte(b':'); - out.push_str(format!("{port}")); - } - out.push(0); - } - // We only send the version when needed, as otherwise a V2 server who is asked for V1 will respond with 'version 1' - // as extra lines in the reply, which we don't want to handle. Especially since an old server will not respond with that - // line (is what I assume, at least), so it's an optional part in the response to understand and handle. There is no value - // in that, so let's help V2 servers to respond in a way that assumes V1. - let extra_params_need_null_prefix = if desired_version != Protocol::V1 { - out.push(0); - out.push_str(format!("version={}", desired_version as usize)); - out.push(0); - false - } else { - true - }; - - if !extra_parameters.is_empty() { - if extra_params_need_null_prefix { - out.push(0); - } - for (key, value) in extra_parameters { - match value { - Some(value) => out.push_str(format!("{key}={value}")), - None => out.push_str(key), - } - out.push(0); - } - } - Ok(out) - } - #[cfg(test)] - mod tests { - use crate::{Protocol, Service, client::git}; - - #[test] - fn version_1_without_host_and_version() -> gix_testtools::TestResult { - assert_eq!( - git::message::connect(Service::UploadPack, Protocol::V1, b"hello/world", None, &[])?, - "git-upload-pack hello/world\0" - ); - Ok(()) - } - #[test] - fn version_2_without_host_and_version() -> gix_testtools::TestResult { - assert_eq!( - git::message::connect(Service::UploadPack, Protocol::V2, br"hello\world", None, &[])?, - "git-upload-pack hello\\world\0\0version=2\0" - ); - Ok(()) - } - #[test] - fn version_2_without_host_and_version_and_extra_parameters() -> gix_testtools::TestResult { - assert_eq!( - git::message::connect( - Service::UploadPack, - Protocol::V2, - b"/path/project.git", - None, - &[("key", Some("value")), ("value-only", None)] - )?, - "git-upload-pack /path/project.git\0\0version=2\0key=value\0value-only\0" - ); - Ok(()) - } - #[test] - fn with_host_without_port() -> gix_testtools::TestResult { - assert_eq!( - git::message::connect( - Service::UploadPack, - Protocol::V1, - br"hello\world", - Some(&("host".into(), None)), - &[] - )?, - "git-upload-pack hello\\world\0host=host\0" - ); - Ok(()) - } - #[test] - fn with_host_without_port_and_extra_parameters() -> gix_testtools::TestResult { - assert_eq!( - git::message::connect( - Service::UploadPack, - Protocol::V1, - br"hello\world", - Some(&("host".into(), None)), - &[("key", Some("value")), ("value-only", None)] - )?, - "git-upload-pack hello\\world\0host=host\0\0key=value\0value-only\0" - ); - Ok(()) - } - #[test] - fn with_host_with_port() -> gix_testtools::TestResult { - assert_eq!( - git::message::connect( - Service::UploadPack, - Protocol::V1, - br"hello\world", - Some(&("host".into(), Some(404))), - &[] - )?, - "git-upload-pack hello\\world\0host=host:404\0" - ); - Ok(()) - } - - #[test] - fn with_strange_host_and_port() -> gix_testtools::TestResult { - assert_eq!( - git::message::connect( - Service::UploadPack, - Protocol::V1, - b"--upload-pack=attack", - Some(&("--proxy=other-attack".into(), Some(404))), - &[] - )?, - "git-upload-pack --upload-pack=attack\0host=--proxy=other-attack:404\0", - "we explicitly allow possible `-arg` arguments to be passed to the git daemon - the remote must protect against exploitation, we don't want to prevent legitimate cases" - ); - Ok(()) - } - - #[test] - fn carriage_returns_are_allowed_like_in_git() -> gix_testtools::TestResult { - assert_eq!( - git::message::connect( - Service::UploadPack, - Protocol::V1, - b"hello\rworld", - Some(&("ho\rst".into(), None)), - &[] - )?, - "git-upload-pack hello\rworld\0host=ho\rst\0" - ); - Ok(()) - } - } -} - -/// -#[cfg(feature = "async-client")] -pub mod async_io; - -/// -#[cfg(feature = "blocking-client")] -pub mod blocking_io; diff --git a/vendor/gix-transport/src/client/mod.rs b/vendor/gix-transport/src/client/mod.rs deleted file mode 100644 index 6c70051..0000000 --- a/vendor/gix-transport/src/client/mod.rs +++ /dev/null @@ -1,23 +0,0 @@ -/// -#[cfg(feature = "async-client")] -pub mod async_io; - -mod traits; -pub use traits::TransportWithoutIO; - -/// -#[cfg(feature = "blocking-client")] -pub mod blocking_io; - -/// -pub mod capabilities; -#[doc(inline)] -pub use capabilities::Capabilities; - -mod non_io_types; -pub use gix_sec::identity::Account; -pub use non_io_types::{AuthenticationRequired, Error, MessageKind, WriteMode}; - -/// -#[cfg(any(feature = "blocking-client", feature = "async-client"))] -pub mod git; diff --git a/vendor/gix-transport/src/client/non_io_types.rs b/vendor/gix-transport/src/client/non_io_types.rs deleted file mode 100644 index f928159..0000000 --- a/vendor/gix-transport/src/client/non_io_types.rs +++ /dev/null @@ -1,498 +0,0 @@ -/// Configure how a `RequestWriter` behaves when writing bytes. -#[derive(Default, PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub enum WriteMode { - /// Each [write()][std::io::Write::write()] call writes the bytes verbatim as one or more packet lines. - /// - /// This mode also indicates to the transport that it should try to stream data as it is unbounded. This mode is typically used - /// for sending packs whose exact size is not necessarily known in advance. - Binary, - /// Each [write()][std::io::Write::write()] call assumes text in the input, assures a trailing newline and writes it as single packet line. - /// - /// This mode also indicates that the lines written fit into memory, hence the transport may chose to not stream it but to buffer it - /// instead. This is relevant for some transports, like the one for HTTP. - #[default] - OneLfTerminatedLinePerWriteCall, -} - -/// The kind of packet line to write when transforming a `RequestWriter` into an `ExtendedBufRead`. -/// -/// Both the type and the trait have different implementations for blocking vs async I/O. -#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub enum MessageKind { - /// A `flush` packet. - Flush, - /// A V2 delimiter. - Delimiter, - /// The end of a response. - ResponseEnd, - /// The given text. - Text(&'static [u8]), -} - -#[cfg(any(feature = "blocking-client", feature = "async-client"))] -pub(crate) mod connect { - /// Options for connecting to a remote. - #[derive(Debug, Default, Clone)] - pub struct Options { - /// Use `version` to set the desired protocol version to use when connecting, but note that the server may downgrade it. - pub version: crate::Protocol, - #[cfg(feature = "blocking-client")] - /// Options to use if the scheme of the URL is `ssh`. - pub ssh: crate::client::blocking_io::ssh::connect::Options, - /// If `true`, all packetlines received or sent will be passed to the facilities of the `gix-trace` crate. - pub trace: bool, - } -} - -mod error { - use gix_error::{ClassificationMarker, Exn, Message}; - use std::ffi::OsString; - - use bstr::BString; - - #[cfg(feature = "blocking-client")] - use crate::client::blocking_io::ssh; - - #[cfg(feature = "blocking-client")] - type SshInvocationError = ssh::invocation::Error; - #[cfg(not(feature = "blocking-client"))] - type SshInvocationError = std::convert::Infallible; - - /// Details carried by an HTTP authentication failure in a [`std::io::Error`] of kind - /// [`PermissionDenied`][std::io::ErrorKind::PermissionDenied]. - /// - /// Callers can downcast [`std::io::Error::get_ref()`] to this type and forward the challenges - /// to credential helpers as `wwwauth[]` attributes. - #[derive(Debug, Default)] - pub struct AuthenticationRequired { - /// HTTP `WWW-Authenticate` header values in the order supplied by the server. - pub www_authenticate: Vec, - } - - impl std::fmt::Display for AuthenticationRequired { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str("Received HTTP status 401") - } - } - - impl std::error::Error for AuthenticationRequired { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - Some(const { &ClassificationMarker::UNAUTHENTICATED }) - } - } - - /// The error used in most methods of the [`client`][crate::client] module. - /// - /// Sources preserve classifications when raised or converted to [`gix_error::Error`]. Unsafe arguments and - /// unexpected packet lines expose classification-only [`gix_error::ClassificationMarker`] sources. - /// Use [`gix_error::classify()`] or the classification predicates on [`gix_error::Exn`] and [`gix_error::Error`], - /// rather than downcasting these markers to concrete classifier errors. Use [`gix_error::classify()`] with - /// [`can_retry()`](gix_error::types::Classifications::can_retry) or - /// [`can_retry_lenient()`](gix_error::types::Classifications::can_retry_lenient) to inspect retryability directly. - #[derive(Debug)] - #[expect(missing_docs)] - pub enum Error { - MissingHandshake, - Io(std::io::Error), - Capabilities { - err: gix_error::Error, - }, - LineDecode { - err: Message, - }, - ExpectedLine(&'static str), - ExpectedDataLine, - AuthenticationUnsupported, - AuthenticationRefused(&'static str), - UnsupportedProtocolVersion(BString), - InvokeProgram { - source: std::io::Error, - command: OsString, - }, - #[cfg(feature = "http-client")] - Http(gix_error::Error), - #[cfg(not(feature = "http-client"))] - Http(std::convert::Infallible), - SshInvocation(SshInvocationError), - AmbiguousPath { - path: BString, - }, - } - - impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Error::MissingHandshake => { - f.write_str("A request was performed without performing the handshake first") - } - Error::Io(_) => f.write_str("An IO error occurred when talking to the server"), - Error::Capabilities { .. } => f.write_str("Capabilities could not be parsed"), - Error::LineDecode { .. } => f.write_str("A packet line could not be decoded"), - Error::ExpectedLine(line) => write!(f, "A {line} line was expected, but there was none"), - Error::ExpectedDataLine => f.write_str("Expected a data line, but got a delimiter"), - Error::AuthenticationUnsupported => f.write_str("The transport layer does not support authentication"), - Error::AuthenticationRefused(reason) => { - write!(f, "The transport layer refuses to use a given identity: {reason}") - } - Error::UnsupportedProtocolVersion(version) => { - write!(f, "The protocol version indicated by {version:?} is unsupported") - } - Error::InvokeProgram { command, .. } => write!(f, "Failed to invoke program {}", command.display()), - Error::Http(err) => std::fmt::Display::fmt(err, f), - Error::SshInvocation(_) => f.write_str("Failed to prepare SSH invocation"), - Error::AmbiguousPath { path } => { - write!( - f, - "The repository path \"{path}\" could be mistaken for a command-line argument" - ) - } - } - } - } - - impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Error::Io(err) => Some(err), - Error::LineDecode { err } => Some(err), - Error::InvokeProgram { source, .. } => Some(source), - Error::Capabilities { err } => Some(err), - Error::Http(err) => Some(err), - Error::SshInvocation(err) => Some(err), - Error::AmbiguousPath { .. } | Error::MissingHandshake => { - Some(const { &ClassificationMarker::VALIDATION }) - } - Error::AuthenticationUnsupported | Error::UnsupportedProtocolVersion(_) => { - Some(const { &ClassificationMarker::UNSUPPORTED }) - } - Error::ExpectedLine(_) | Error::ExpectedDataLine => Some(const { &ClassificationMarker::CORRUPTION }), - _ => None, - } - } - } - - impl From for Error { - fn from(err: std::io::Error) -> Self { - Error::Io(err) - } - } - - impl From> for Error { - fn from(err: Exn) -> Self { - Error::Capabilities { err: err.into_error() } - } - } - - impl From for Error { - fn from(err: gix_error::Error) -> Self { - Error::Capabilities { err } - } - } - - impl From for Error { - fn from(err: Message) -> Self { - Error::LineDecode { err } - } - } - - impl From for gix_error::Error { - fn from(err: Error) -> Self { - Self::from_error(err) - } - } - - #[cfg(test)] - mod tests { - #[cfg(feature = "http-client")] - use gix_error::{Class, ClassificationMarker, message}; - use gix_error::{Error, ErrorExt, classify}; - - #[test] - fn authentication_and_unsupported_errors_preserve_recovery_meaning() { - let challenge = super::AuthenticationRequired::default().raise_typed(); - assert!( - challenge.is_unauthenticated(), - "401 requests credentials rather than authorization" - ); - assert!( - challenge.downcast_any_ref::().is_some(), - "authentication challenges remain available for helpers" - ); - for cause in [ - super::Error::AuthenticationUnsupported, - super::Error::UnsupportedProtocolVersion("version 3".into()), - ] { - let err = cause.raise_typed(); - assert!(err.is_unsupported(), "switch transports or protocol implementations"); - assert!( - !err.is_corrupted(), - "unsupported capabilities are not malformed peer data" - ); - assert!( - err.downcast_any_ref::().is_some(), - "retain the typed transport error" - ); - assert!(err.into_error().is_unsupported(), "erasure preserves recovery meaning"); - } - } - - #[test] - fn io_classification_is_independent_of_conversion() { - let mut diagnostics = Vec::new(); - for kind in [ - std::io::ErrorKind::Interrupted, - std::io::ErrorKind::UnexpectedEof, - std::io::ErrorKind::TimedOut, - std::io::ErrorKind::BrokenPipe, - std::io::ErrorKind::AddrInUse, - std::io::ErrorKind::ConnectionAborted, - std::io::ErrorKind::ConnectionReset, - std::io::ErrorKind::ConnectionRefused, - std::io::ErrorKind::OutOfMemory, - std::io::ErrorKind::NotFound, - std::io::ErrorKind::PermissionDenied, - ] { - let make_error = || super::Error::Io(kind.into()); - let can_retry = classify(&make_error()).can_retry(); - let can_retry_lenient = classify(&make_error()).can_retry_lenient(); - let err = Error::from(make_error()); - assert_eq!( - err.can_retry(), - can_retry, - "the transport conversion preserves the conservative policy for {kind:?}" - ); - assert_eq!( - err.can_retry_lenient(), - can_retry_lenient, - "the transport conversion preserves the lenient policy for {kind:?}" - ); - assert!( - !err.is_retryable(), - "the transport conversion does not add an explicit retry marker for {kind:?}" - ); - assert_eq!(err.is_not_found(), kind == std::io::ErrorKind::NotFound); - assert_eq!(err.is_resource_exhausted(), kind == std::io::ErrorKind::OutOfMemory); - diagnostics.push((kind, gix_error::TestError::from(err))); - } - insta::assert_debug_snapshot!(diagnostics, "transport I/O errors retain the original cause and retry policy", @" - [ - ( - Interrupted, - An IO error occurred when talking to the server - - Caused by: - 0: operation interrupted, - ), - ( - UnexpectedEof, - An IO error occurred when talking to the server - - Caused by: - 0: unexpected end of file, - ), - ( - TimedOut, - An IO error occurred when talking to the server - - Caused by: - 0: timed out, - ), - ( - BrokenPipe, - An IO error occurred when talking to the server - - Caused by: - 0: broken pipe, - ), - ( - AddrInUse, - An IO error occurred when talking to the server - - Caused by: - 0: address in use, - ), - ( - ConnectionAborted, - An IO error occurred when talking to the server - - Caused by: - 0: connection aborted, - ), - ( - ConnectionReset, - An IO error occurred when talking to the server - - Caused by: - 0: connection reset, - ), - ( - ConnectionRefused, - An IO error occurred when talking to the server - - Caused by: - 0: connection refused, - ), - ( - OutOfMemory, - An IO error occurred when talking to the server - - Caused by: - 0: out of memory, - ), - ( - NotFound, - An IO error occurred when talking to the server - - Caused by: - 0: entity not found, - ), - ( - PermissionDenied, - An IO error occurred when talking to the server - - Caused by: - 0: permission denied, - ), - ] - "); - } - - #[cfg(feature = "http-client")] - #[test] - fn http_keeps_retryable_sources() { - let err = super::Error::Http( - std::io::Error::new(std::io::ErrorKind::BrokenPipe, "retry me").and_raise(message("HTTP failed")), - ); - insta::assert_debug_snapshot!(err, "http keeps retryable sources", @" - Http( - HTTP failed - - Caused by: - 0: I/O error (BrokenPipe) - 1: retry me, - ) - "); - - assert!(classify(&err).can_retry_lenient()); - assert!(!classify(&err).can_retry()); - let source = std::error::Error::source(&err) - .and_then(|err| err.downcast_ref::()) - .expect("HTTP errors retain their gix-error wrapper"); - assert!( - source - .iter_errors() - .any(::is::) - ); - - let explicit = super::Error::Http( - ClassificationMarker::with_source(Class::Retryable, message("retry me")) - .and_raise(message("HTTP failed")), - ); - insta::assert_debug_snapshot!(explicit, "HTTP errors retain an explicit retryable source", @" - Http( - HTTP failed - - Caused by: - 0: retry me, - ) - "); - assert!(classify(&explicit).can_retry()); - assert!(Error::from(explicit).is_retryable()); - - let out_of_memory = super::Error::Http( - std::io::Error::from(std::io::ErrorKind::OutOfMemory).and_raise(message("HTTP failed")), - ); - insta::assert_debug_snapshot!(out_of_memory, "HTTP errors retain the allocation failure as their source", @" - Http( - HTTP failed - - Caused by: - 0: out of memory, - ) - "); - assert!(!classify(&out_of_memory).can_retry()); - assert!(classify(&out_of_memory).can_retry_lenient()); - assert!(Error::from(out_of_memory).is_resource_exhausted()); - } - - #[test] - fn custom_errors_expose_classifications() { - use gix_error::Class; - - fn check( - err: impl std::error::Error + Send + Sync + 'static, - class: Class, - ) -> gix_error::Exn { - let err = err.raise_typed(); - match class { - Class::Validation => assert!(err.is_validation(), "unsafe transport arguments are invalid input"), - Class::Corruption => assert!(err.is_corrupted(), "unexpected packet lines are malformed responses"), - _ => unreachable!("only validation and corruption are exercised here"), - } - assert!( - err.downcast_any_ref::().is_some(), - "the concrete transport error remains available" - ); - assert!( - err.probable_cause().is::(), - "the concrete transport error, not its classification marker, is the probable cause" - ); - err.erased() - } - - let mut diagnostics = vec![check::( - super::Error::AmbiguousPath { path: "-arg".into() }, - Class::Validation, - )]; - for err in [super::Error::ExpectedLine("version"), super::Error::ExpectedDataLine] { - diagnostics.push(check::(err, Class::Corruption)); - } - insta::assert_debug_snapshot!(diagnostics, "unsafe paths and malformed protocol lines retain their concrete diagnostics", @" - [ - The repository path \"-arg\" could be mistaken for a command-line argument, - A version line was expected, but there was none, - Expected a data line, but got a delimiter, - ] - "); - - #[cfg(feature = "blocking-client")] - { - use crate::client::blocking_io::ssh; - - let mut diagnostics = Vec::new(); - for err in [ - ssh::invocation::Error::AmbiguousUserName { user: "-arg".into() }, - ssh::invocation::Error::AmbiguousHostName { host: "-arg".into() }, - ] { - let err = check::(super::Error::SshInvocation(err), Class::Validation); - assert!( - !err.is_corrupted(), - "SSH invocation context does not classify rejected arguments as malformed responses" - ); - diagnostics.push(err); - } - diagnostics.push(check::( - ssh::Error::AmbiguousHostName { host: "-arg".into() }, - Class::Validation, - )); - insta::assert_debug_snapshot!(diagnostics, "SSH invocation context preserves the rejected argument as its cause", @" - [ - Failed to prepare SSH invocation - - Caused by: - 0: Username '-arg' could be mistaken for a command-line argument, - Failed to prepare SSH invocation - - Caused by: - 0: Host name '-arg' could be mistaken for a command-line argument, - Host name '-arg' could be mistaken for a command-line argument, - ] - "); - } - } - } -} - -pub use error::{AuthenticationRequired, Error}; diff --git a/vendor/gix-transport/src/client/traits.rs b/vendor/gix-transport/src/client/traits.rs deleted file mode 100644 index 61a35ac..0000000 --- a/vendor/gix-transport/src/client/traits.rs +++ /dev/null @@ -1,98 +0,0 @@ -use gix_error::Result; -use std::{ - any::Any, - borrow::Cow, - ops::{Deref, DerefMut}, -}; - -use bstr::BStr; - -use crate::{Protocol, client::Error}; - -/// This trait represents all transport related functions that don't require any input/output to be done which helps -/// implementation to share more code across blocking and async programs. -pub trait TransportWithoutIO { - /// If the handshake or subsequent reads failed with [`std::io::ErrorKind::PermissionDenied`], use this method to - /// inform the transport layer about the identity to use for subsequent calls. - /// If authentication continues to fail even with an identity set, consider communicating this to the provider - /// of the identity in order to mark it as invalid. Otherwise the user might have difficulty updating obsolete - /// credentials. - /// Please note that most transport layers are unauthenticated and thus return [an error][Error::AuthenticationUnsupported] here. - fn set_identity(&mut self, _identity: gix_sec::identity::Account) -> std::result::Result<(), Error> { - Err(Error::AuthenticationUnsupported) - } - - /// Returns the canonical URL pointing to the destination of this transport. - fn to_url(&self) -> Cow<'_, BStr>; - - /// If the actually advertised server version is contained in the returned slice or it is empty, continue as normal, - /// assume the server's protocol version is desired or acceptable. - /// - /// Otherwise, abort the fetch operation with an error to avoid continuing any interaction with the transport. - /// - /// In V1 this means a potentially large list of advertised refs won't be read, instead the connection is ignored - /// leaving the server with a client who potentially unexpectedly terminated the connection. - /// - /// Note that `transport.close()` is not called explicitly. - /// - /// Custom transports can override this to prevent any use of older protocol versions. - fn supported_protocol_versions(&self) -> &[Protocol] { - &[] - } - - /// Returns true if the transport provides persistent connections across multiple requests, or false otherwise. - /// Not being persistent implies that certain information has to be resent on each 'turn' - /// of the fetch negotiation or that the end of interaction (i.e. no further request will be made) has to be indicated - /// to the server for most graceful termination of the connection. - fn connection_persists_across_multiple_requests(&self) -> bool; - - /// Pass `config` can be cast and interpreted by the implementation, as documented separately. - /// - /// The caller must know how that `config` data looks like for the intended implementation. - fn configure(&mut self, config: &dyn Any) -> Result; -} - -// Would be nice if the box implementation could auto-forward to all implemented traits. -impl TransportWithoutIO for Box { - fn set_identity(&mut self, identity: gix_sec::identity::Account) -> std::result::Result<(), Error> { - self.deref_mut().set_identity(identity) - } - - fn to_url(&self) -> Cow<'_, BStr> { - self.deref().to_url() - } - - fn supported_protocol_versions(&self) -> &[Protocol] { - self.deref().supported_protocol_versions() - } - - fn connection_persists_across_multiple_requests(&self) -> bool { - self.deref().connection_persists_across_multiple_requests() - } - - fn configure(&mut self, config: &dyn Any) -> Result { - self.deref_mut().configure(config) - } -} - -impl TransportWithoutIO for &mut T { - fn set_identity(&mut self, identity: gix_sec::identity::Account) -> std::result::Result<(), Error> { - self.deref_mut().set_identity(identity) - } - - fn to_url(&self) -> Cow<'_, BStr> { - self.deref().to_url() - } - - fn supported_protocol_versions(&self) -> &[Protocol] { - self.deref().supported_protocol_versions() - } - - fn connection_persists_across_multiple_requests(&self) -> bool { - self.deref().connection_persists_across_multiple_requests() - } - - fn configure(&mut self, config: &dyn Any) -> Result { - self.deref_mut().configure(config) - } -} diff --git a/vendor/gix-transport/src/lib.rs b/vendor/gix-transport/src/lib.rs deleted file mode 100644 index f55abd4..0000000 --- a/vendor/gix-transport/src/lib.rs +++ /dev/null @@ -1,64 +0,0 @@ -//! An implementation of the `git` transport layer, abstracting over all of its [versions][Protocol]. -//! -//! Use `client::blocking_io::connect()` or `client::async_io::connect()` to establish a connection. -//! -//! All git transports are supported, including `ssh`, `git`, `http` and `https`, as well as local repository paths. -//! ## Feature Flags -#![cfg_attr( - all(doc, feature = "document-features"), - doc = ::document_features::document_features!() -)] -#![cfg_attr(all(doc, feature = "document-features"), feature(doc_cfg))] -#![deny(missing_docs)] -#![forbid(unsafe_code)] - -#[cfg(feature = "async-trait")] -pub use async_trait; -pub use bstr; -#[cfg(feature = "futures-io")] -pub use futures_io; -pub use gix_packetline as packetline; - -/// The version of the way client and server communicate. -#[derive(Default, PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub enum Protocol { - /// Version 0 is like V1, but doesn't show capabilities at all, at least when hosted without `git-daemon`. - V0 = 0, - /// Version 1 was the first one conceived, is stateful, and our implementation was seen to cause deadlocks. Prefer V2 - V1 = 1, - /// A command-based and stateless protocol with clear semantics, and the one to use assuming the server isn't very old. - /// This is the default. - #[default] - V2 = 2, -} - -/// The kind of service to invoke on the client or the server side. -#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub enum Service { - /// The service sending packs from a server to the client. Used for fetching pack data. - UploadPack, - /// The service receiving packs produced by the client, who sends a pack to the server. - ReceivePack, -} - -impl Service { - /// Render this instance as a string recognized by the git transport layer, like `git-upload-pack`. - pub fn as_str(&self) -> &'static str { - match self { - Service::ReceivePack => "git-receive-pack", - Service::UploadPack => "git-upload-pack", - } - } - - /// Render this instance as a subcommand understood by the `git` program, like `upload-pack`. - pub fn as_git_subcommand(&self) -> &'static str { - self.as_str() - .strip_prefix("git-") - .expect("all services are 'git-*' subcommands") - } -} - -/// -pub mod client;