diff --git a/.github/workflows/msrv.yml b/.github/workflows/msrv.yml index 4d0fb9613df..f16da43f3a8 100644 --- a/.github/workflows/msrv.yml +++ b/.github/workflows/msrv.yml @@ -20,7 +20,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v3 - - uses: dtolnay/rust-toolchain@1.59.0 # dictated by `windows` crates effectively, IMPORTANT: adjust etc/msrv-badge.svg as well + - uses: dtolnay/rust-toolchain@1.60.0 # dictated by `windows` crates effectively, IMPORTANT: adjust etc/msrv-badge.svg as well - run: make check-msrv-on-ci continue-on-error: true # TODO: turn this off once the toolchain gets updated. There is a strange error preventing cargo to select the correct libgit2 version # like it doesn't exist. diff --git a/Cargo.lock b/Cargo.lock index 6fe19ef74ea..660f997149d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -258,6 +258,18 @@ dependencies = [ "serde", ] +[[package]] +name = "bstr" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fca0852af221f458706eb0725c03e4ed6c46af9ac98e6a689d5e634215d594dd" +dependencies = [ + "memchr", + "once_cell", + "regex-automata", + "serde", +] + [[package]] name = "btoi" version = "0.4.2" @@ -736,7 +748,7 @@ version = "1.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22813a6dc45b335f9bade10bf7271dc477e81113e89eb251a0bc2a8a81c536e1" dependencies = [ - "bstr", + "bstr 0.2.17", "csv-core", "itoa 0.4.8", "ryu", @@ -1092,7 +1104,7 @@ dependencies = [ name = "git-actor" version = "0.11.4" dependencies = [ - "bstr", + "bstr 1.0.1", "btoi", "document-features", "git-date", @@ -1109,7 +1121,7 @@ dependencies = [ name = "git-attributes" version = "0.3.3" dependencies = [ - "bstr", + "bstr 1.0.1", "compact_str", "document-features", "git-features", @@ -1148,7 +1160,7 @@ dependencies = [ name = "git-commitgraph" version = "0.8.2" dependencies = [ - "bstr", + "bstr 1.0.1", "document-features", "git-chunk", "git-features", @@ -1163,7 +1175,7 @@ dependencies = [ name = "git-config" version = "0.7.1" dependencies = [ - "bstr", + "bstr 1.0.1", "criterion", "document-features", "git-config-value", @@ -1190,7 +1202,7 @@ name = "git-config-value" version = "0.7.0" dependencies = [ "bitflags", - "bstr", + "bstr 1.0.1", "document-features", "git-path", "libc", @@ -1213,7 +1225,7 @@ dependencies = [ name = "git-credentials" version = "0.4.0" dependencies = [ - "bstr", + "bstr 1.0.1", "document-features", "git-command", "git-config-value", @@ -1230,7 +1242,7 @@ dependencies = [ name = "git-date" version = "0.1.0" dependencies = [ - "bstr", + "bstr 1.0.1", "document-features", "git-testtools", "itoa 1.0.3", @@ -1256,7 +1268,7 @@ dependencies = [ name = "git-discover" version = "0.4.2" dependencies = [ - "bstr", + "bstr 1.0.1", "defer", "git-hash", "git-path", @@ -1272,7 +1284,7 @@ dependencies = [ name = "git-features" version = "0.22.4" dependencies = [ - "bstr", + "bstr 0.2.17", "bytes", "crc32fast", "crossbeam-channel", @@ -1292,6 +1304,10 @@ dependencies = [ "walkdir", ] +[[package]] +name = "git-fetchhead" +version = "0.0.0" + [[package]] name = "git-filter" version = "0.0.0" @@ -1301,7 +1317,7 @@ name = "git-glob" version = "0.3.2" dependencies = [ "bitflags", - "bstr", + "bstr 1.0.1", "document-features", "git-testtools", "serde", @@ -1324,7 +1340,7 @@ version = "0.4.3" dependencies = [ "atoi", "bitflags", - "bstr", + "bstr 1.0.1", "document-features", "filetime", "git-bitmap", @@ -1357,7 +1373,7 @@ dependencies = [ name = "git-mailmap" version = "0.3.2" dependencies = [ - "bstr", + "bstr 1.0.1", "document-features", "git-actor", "git-testtools", @@ -1373,7 +1389,7 @@ version = "0.0.0" name = "git-object" version = "0.20.3" dependencies = [ - "bstr", + "bstr 1.0.1", "btoi", "document-features", "git-actor", @@ -1417,7 +1433,7 @@ dependencies = [ name = "git-pack" version = "0.22.0" dependencies = [ - "bstr", + "bstr 1.0.1", "bytesize", "clru", "dashmap", @@ -1448,7 +1464,7 @@ name = "git-packetline" version = "0.12.7" dependencies = [ "async-std", - "bstr", + "bstr 1.0.1", "document-features", "futures-io", "futures-lite", @@ -1465,7 +1481,7 @@ dependencies = [ name = "git-path" version = "0.4.2" dependencies = [ - "bstr", + "bstr 1.0.1", "tempfile", "thiserror", ] @@ -1475,7 +1491,7 @@ name = "git-pathspec" version = "0.0.0" dependencies = [ "bitflags", - "bstr", + "bstr 1.0.1", "git-attributes", "git-glob", "git-testtools", @@ -1503,7 +1519,7 @@ version = "0.20.0" dependencies = [ "async-std", "async-trait", - "bstr", + "bstr 1.0.1", "btoi", "document-features", "futures-io", @@ -1524,7 +1540,7 @@ dependencies = [ name = "git-quote" version = "0.2.1" dependencies = [ - "bstr", + "bstr 1.0.1", "btoi", "quick-error", ] @@ -1560,7 +1576,7 @@ dependencies = [ name = "git-refspec" version = "0.1.1" dependencies = [ - "bstr", + "bstr 1.0.1", "git-hash", "git-revision", "git-testtools", @@ -1626,7 +1642,7 @@ dependencies = [ name = "git-revision" version = "0.4.4" dependencies = [ - "bstr", + "bstr 1.0.1", "document-features", "git-date", "git-hash", @@ -1676,7 +1692,7 @@ dependencies = [ name = "git-testtools" version = "0.8.0" dependencies = [ - "bstr", + "bstr 1.0.1", "crc", "fs_extra", "git-attributes", @@ -1706,7 +1722,7 @@ dependencies = [ "async-trait", "base64", "blocking", - "bstr", + "bstr 1.0.1", "curl", "document-features", "futures-io", @@ -1743,7 +1759,7 @@ version = "0.0.0" name = "git-url" version = "0.8.0" dependencies = [ - "bstr", + "bstr 1.0.1", "document-features", "git-features", "git-path", @@ -1757,7 +1773,7 @@ dependencies = [ name = "git-validate" version = "0.5.5" dependencies = [ - "bstr", + "bstr 1.0.1", "git-testtools", "thiserror", ] @@ -1766,7 +1782,7 @@ dependencies = [ name = "git-worktree" version = "0.4.3" dependencies = [ - "bstr", + "bstr 1.0.1", "document-features", "git-attributes", "git-features", diff --git a/Cargo.toml b/Cargo.toml index 17f287e4bc1..fc82ce0ad02 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -146,6 +146,7 @@ members = [ "git-packetline", "git-mailmap", "git-note", + "git-fetchhead", "git-prompt", "git-filter", "git-sec", diff --git a/README.md b/README.md index f18bf132b96..d064cc3d831 100644 --- a/README.md +++ b/README.md @@ -149,6 +149,7 @@ is usable to some extend. * [git-date](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-date) * **idea** _(just a name placeholder)_ * [git-note](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-note) + * [git-fetchhead](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-fetchhead) * [git-filter](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-filter) * [git-lfs](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-lfs) * [git-rebase](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-rebase) diff --git a/crate-status.md b/crate-status.md index f9a38016f00..78f282293a5 100644 --- a/crate-status.md +++ b/crate-status.md @@ -259,6 +259,10 @@ Check out the [performance discussion][git-traverse-performance] as well. A mechanism to associate metadata with any object, and keep revisions of it using git itself. * [ ] CRUD for git notes + +### git-fetchhead +* [ ] parse `FETCH_HEAD` information back entirely +* [ ] write typical fetch-head lines ### git-discover diff --git a/etc/check-package-size.sh b/etc/check-package-size.sh index 52501746588..ce2eb789ffe 100755 --- a/etc/check-package-size.sh +++ b/etc/check-package-size.sh @@ -46,6 +46,7 @@ echo "in root: gitoxide CLI" (enter git-filter && indent cargo diet -n --package-size-limit 5KB) (enter git-lfs && indent cargo diet -n --package-size-limit 5KB) (enter git-note && indent cargo diet -n --package-size-limit 5KB) +(enter git-fetchhead && indent cargo diet -n --package-size-limit 5KB) (enter git-sec && indent cargo diet -n --package-size-limit 15KB) (enter git-tix && indent cargo diet -n --package-size-limit 5KB) (enter git-credentials && indent cargo diet -n --package-size-limit 20KB) diff --git a/etc/msrv-badge.svg b/etc/msrv-badge.svg index e3e639ade1e..4e327611bf4 100644 --- a/etc/msrv-badge.svg +++ b/etc/msrv-badge.svg @@ -1 +1 @@ -rustc: 1.59.0+rustc1.59.0+ +rustc: 1.59.0+rustc1.60.0+ diff --git a/git-actor/Cargo.toml b/git-actor/Cargo.toml index a807f4fffe9..5f09075d7c3 100644 --- a/git-actor/Cargo.toml +++ b/git-actor/Cargo.toml @@ -13,7 +13,7 @@ doctest = false [features] ## Data structures implement `serde::Serialize` and `serde::Deserialize`. -serde1 = ["serde", "bstr/serde1", "git-date/serde1"] +serde1 = ["serde", "bstr/serde", "git-date/serde1"] [dependencies] git-features = { version = "^0.22.4", path = "../git-features", optional = true } @@ -21,7 +21,7 @@ git-date = { version = "^0.1.0", path = "../git-date" } quick-error = "2.0.0" btoi = "0.4.2" -bstr = { version = "0.2.13", default-features = false, features = ["std", "unicode"]} +bstr = { version = "1.0.1", default-features = false, features = ["std", "unicode"]} nom = { version = "7", default-features = false, features = ["std"]} itoa = "1.0.1" serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"]} diff --git a/git-attributes/Cargo.toml b/git-attributes/Cargo.toml index fc5ce6bdb19..6cf4ab23444 100644 --- a/git-attributes/Cargo.toml +++ b/git-attributes/Cargo.toml @@ -13,7 +13,7 @@ doctest = false [features] ## Data structures implement `serde::Serialize` and `serde::Deserialize`. -serde1 = ["serde", "bstr/serde1", "git-glob/serde1", "compact_str/serde"] +serde1 = ["serde", "bstr/serde", "git-glob/serde1", "compact_str/serde"] [dependencies] git-features = { version = "^0.22.4", path = "../git-features" } @@ -21,7 +21,7 @@ git-path = { version = "^0.4.2", path = "../git-path" } git-quote = { version = "^0.2.1", path = "../git-quote" } git-glob = { version = "^0.3.2", path = "../git-glob" } -bstr = { version = "0.2.13", default-features = false, features = ["std"]} +bstr = { version = "1.0.1", default-features = false, features = ["std", "unicode"]} unicode-bom = "1.1.4" thiserror = "1.0.26" serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"]} diff --git a/git-commitgraph/Cargo.toml b/git-commitgraph/Cargo.toml index 32a043d106c..c94cbc68472 100644 --- a/git-commitgraph/Cargo.toml +++ b/git-commitgraph/Cargo.toml @@ -14,14 +14,14 @@ doctest = false [features] ## Data structures implement `serde::Serialize` and `serde::Deserialize` -serde1 = ["serde", "git-hash/serde1", "bstr/serde1"] +serde1 = ["serde", "git-hash/serde1", "bstr/serde"] [dependencies] git-features = { version = "^0.22.4", path = "../git-features", features = ["rustsha1"] } git-hash = { version = "^0.9.9", path = "../git-hash" } git-chunk = { version = "^0.3.1", path = "../git-chunk" } -bstr = { version = "0.2.13", default-features = false, features = ["std"] } +bstr = { version = "1.0.1", default-features = false, features = ["std"] } memmap2 = "0.5.0" serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"] } thiserror = "1.0.26" diff --git a/git-config-value/Cargo.toml b/git-config-value/Cargo.toml index 8b5ab3050db..5ad16633b8a 100644 --- a/git-config-value/Cargo.toml +++ b/git-config-value/Cargo.toml @@ -12,13 +12,13 @@ doctest = false [features] ## Data structures implement `serde::Serialize` and `serde::Deserialize`. -serde1 = ["serde", "bstr/serde1"] +serde1 = ["serde", "bstr/serde"] [dependencies] git-path = { version = "^0.4.2", path = "../git-path" } thiserror = "1.0.32" -bstr = "0.2.17" +bstr = "1.0.1" serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"]} bitflags = "1.3.2" diff --git a/git-config/Cargo.toml b/git-config/Cargo.toml index 598b390a1f4..a5afbd107bf 100644 --- a/git-config/Cargo.toml +++ b/git-config/Cargo.toml @@ -12,7 +12,7 @@ include = ["src/**/*", "LICENSE-*", "README.md", "CHANGELOG.md"] [features] ## Data structures implement `serde::Serialize` and `serde::Deserialize`. -serde1 = ["serde", "bstr/serde1", "git-sec/serde1", "git-ref/serde1", "git-glob/serde1", "git-config-value/serde1"] +serde1 = ["serde", "bstr/serde", "git-sec/serde1", "git-ref/serde1", "git-glob/serde1", "git-config-value/serde1"] [dependencies] git-features = { version = "^0.22.4", path = "../git-features"} @@ -26,7 +26,7 @@ nom = { version = "7", default_features = false, features = [ "std" ] } memchr = "2" thiserror = "1.0.26" unicode-bom = "1.1.4" -bstr = { version = "0.2.13", default-features = false, features = ["std"] } +bstr = { version = "1.0.1", default-features = false, features = ["std"] } serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"]} smallvec = "1.9.0" diff --git a/git-credentials/Cargo.toml b/git-credentials/Cargo.toml index e89e862ae3c..9f89370c526 100644 --- a/git-credentials/Cargo.toml +++ b/git-credentials/Cargo.toml @@ -12,7 +12,7 @@ doctest = false [features] ## Data structures implement `serde::Serialize` and `serde::Deserialize`. -serde1 = ["serde", "bstr/serde1", "git-sec/serde1"] +serde1 = ["serde", "bstr/serde", "git-sec/serde1"] [dependencies] git-sec = { version = "^0.3.1", path = "../git-sec" } @@ -24,7 +24,7 @@ git-prompt = { version = "0.1.0", path = "../git-prompt" } thiserror = "1.0.32" serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"] } -bstr = { version = "0.2.13", default-features = false, features = ["std"]} +bstr = { version = "1.0.1", default-features = false, features = ["std"]} diff --git a/git-date/Cargo.toml b/git-date/Cargo.toml index 093a5764174..1106f0fad3b 100644 --- a/git-date/Cargo.toml +++ b/git-date/Cargo.toml @@ -13,10 +13,10 @@ doctest = false [features] ## Data structures implement `serde::Serialize` and `serde::Deserialize`. -serde1 = ["serde", "bstr/serde1"] +serde1 = ["serde", "bstr/serde"] [dependencies] -bstr = { version = "0.2.13", default-features = false, features = ["std"]} +bstr = { version = "1.0.1", default-features = false, features = ["std"]} serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"]} itoa = "1.0.1" time = { version = "0.3.2", default-features = false, features = ["local-offset", "formatting", "macros", "parsing"] } diff --git a/git-diff/src/tree/visit.rs b/git-diff/src/tree/visit.rs index d5c3f6b4348..b66e47b37ce 100644 --- a/git-diff/src/tree/visit.rs +++ b/git-diff/src/tree/visit.rs @@ -2,6 +2,7 @@ use git_hash::ObjectId; use git_object::{bstr::BStr, tree}; /// Represents any possible change in order to turn one tree into another. +#[derive(Debug, Clone, PartialOrd, PartialEq, Ord, Eq, Hash)] pub enum Change { /// An entry was added, like the addition of a file or directory. Addition { diff --git a/git-discover/Cargo.toml b/git-discover/Cargo.toml index 817a5d0e1f3..c24a1b31d28 100644 --- a/git-discover/Cargo.toml +++ b/git-discover/Cargo.toml @@ -17,7 +17,7 @@ git-path = { version = "^0.4.2", path = "../git-path" } git-ref = { version = "^0.15.2", path = "../git-ref" } git-hash = { version = "^0.9.9", path = "../git-hash" } -bstr = { version = "0.2.13", default-features = false, features = ["std", "unicode"] } +bstr = { version = "1.0.1", default-features = false, features = ["std", "unicode"] } thiserror = "1.0.26" [dev-dependencies] diff --git a/git-fetchhead/CHANGELOG.md b/git-fetchhead/CHANGELOG.md new file mode 100644 index 00000000000..a22b3a1589e --- /dev/null +++ b/git-fetchhead/CHANGELOG.md @@ -0,0 +1,30 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## 0.0.0 (2022-09-12) + +The initial release to reserve the name. + +### Commit Statistics + + + + - 2 commits contributed to the release. + - 0 commits were understood as [conventional](https://www.conventionalcommits.org). + - 1 unique issue was worked on: [#450](https://github.com/Byron/gitoxide/issues/450) + +### Commit Details + + + +
view details + + * **[#450](https://github.com/Byron/gitoxide/issues/450)** + - prepare changelog for initial `git-fetchhead` release ([`7aff085`](https://github.com/Byron/gitoxide/commit/7aff0856b76043f1e9021fff72ce96d4f69d255e)) + - frame for `git-fetchhead` crate ([`f9aeda8`](https://github.com/Byron/gitoxide/commit/f9aeda88554fb852dd21ae8402000e0da742e2b2)) +
+ diff --git a/git-fetchhead/Cargo.toml b/git-fetchhead/Cargo.toml new file mode 100644 index 00000000000..70d8a0a6fcb --- /dev/null +++ b/git-fetchhead/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "git-fetchhead" +version = "0.0.0" +repository = "https://github.com/Byron/gitoxide" +license = "MIT/Apache-2.0" +description = "A WIP crate of the gitoxide project to read and write .git/FETCH_HEAD" +authors = ["Sebastian Thiel "] +edition = "2018" + +[lib] +doctest = false + +[dependencies] diff --git a/git-fetchhead/src/lib.rs b/git-fetchhead/src/lib.rs new file mode 100644 index 00000000000..3a6cd994a53 --- /dev/null +++ b/git-fetchhead/src/lib.rs @@ -0,0 +1,2 @@ +#![deny(rust_2018_idioms)] +#![forbid(unsafe_code)] diff --git a/git-glob/Cargo.toml b/git-glob/Cargo.toml index c901a098588..d3a904d45db 100644 --- a/git-glob/Cargo.toml +++ b/git-glob/Cargo.toml @@ -12,10 +12,10 @@ doctest = false [features] ## Data structures implement `serde::Serialize` and `serde::Deserialize`. -serde1 = ["serde", "bstr/serde1"] +serde1 = ["serde", "bstr/serde"] [dependencies] -bstr = { version = "0.2.13", default-features = false, features = ["std"]} +bstr = { version = "1.0.1", default-features = false, features = ["std"]} bitflags = "1.3.2" serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"]} diff --git a/git-index/Cargo.toml b/git-index/Cargo.toml index ba3c450943b..ef47e49af9a 100644 --- a/git-index/Cargo.toml +++ b/git-index/Cargo.toml @@ -38,7 +38,7 @@ git-object = { version = "^0.20.2", path = "../git-object" } thiserror = "1.0.32" memmap2 = "0.5.0" filetime = "0.2.15" -bstr = { version = "0.2.13", default-features = false } +bstr = { version = "1.0.1", default-features = false } serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"] } smallvec = "1.7.0" diff --git a/git-mailmap/Cargo.toml b/git-mailmap/Cargo.toml index 88a0400972f..9f229435a4f 100644 --- a/git-mailmap/Cargo.toml +++ b/git-mailmap/Cargo.toml @@ -12,11 +12,11 @@ doctest = false [features] ## Data structures implement `serde::Serialize` and `serde::Deserialize`. -serde1 = ["serde", "bstr/serde1", "git-actor/serde1"] +serde1 = ["serde", "bstr/serde", "git-actor/serde1"] [dependencies] git-actor = { version = "^0.11.3", path = "../git-actor" } -bstr = { version = "0.2.13", default-features = false, features = ["std", "unicode"]} +bstr = { version = "1.0.1", default-features = false, features = ["std", "unicode"]} quick-error = "2.0.0" serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"]} diff --git a/git-object/Cargo.toml b/git-object/Cargo.toml index 5b23f0d470b..5565ab7b9f0 100644 --- a/git-object/Cargo.toml +++ b/git-object/Cargo.toml @@ -13,7 +13,7 @@ doctest = false [features] ## Data structures implement `serde::Serialize` and `serde::Deserialize`. -serde1 = ["serde", "bstr/serde1", "smallvec/serde", "git-hash/serde1", "git-actor/serde1"] +serde1 = ["serde", "bstr/serde", "smallvec/serde", "git-hash/serde1", "git-actor/serde1"] ## When parsing objects by default errors will only be available on the granularity of success or failure, and with the above flag enabled ## details information about the error location will be collected. ## Use it in applications which expect broken or invalid objects or for debugging purposes. Incorrectly formatted objects aren't at all @@ -30,7 +30,7 @@ btoi = "0.4.2" itoa = "1.0.1" thiserror = "1.0.34" hex = "0.4.2" -bstr = { version = "0.2.13", default-features = false, features = ["std", "unicode"] } +bstr = { version = "1.0.1", default-features = false, features = ["std", "unicode"] } nom = { version = "7", default-features = false, features = ["std"]} smallvec = { version = "1.4.0", features = ["write"] } serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"]} diff --git a/git-pack/Cargo.toml b/git-pack/Cargo.toml index 70b6c9d5a68..0ec79073ff2 100644 --- a/git-pack/Cargo.toml +++ b/git-pack/Cargo.toml @@ -63,7 +63,7 @@ document-features = { version = "0.2.0", optional = true } git-testtools = { path = "../tests/tools"} git-odb = { path = "../git-odb" } tempfile = "3.1.0" -bstr = { version = "0.2.13", default-features = false, features = ["std"] } +bstr = { version = "1.0.1", default-features = false, features = ["std"] } maplit = "1.0.2" [package.metadata.docs.rs] diff --git a/git-packetline/Cargo.toml b/git-packetline/Cargo.toml index a7981dd3e5a..5f2c6c24b0d 100644 --- a/git-packetline/Cargo.toml +++ b/git-packetline/Cargo.toml @@ -25,7 +25,7 @@ async-io = ["futures-io", "futures-lite", "pin-project-lite"] #! ### Other ## Data structures implement `serde::Serialize` and `serde::Deserialize`. -serde1 = ["serde", "bstr/serde1"] +serde1 = ["serde", "bstr/serde"] [[test]] name = "async-packetline" @@ -41,7 +41,7 @@ required-features = ["blocking-io", "maybe-async/is_sync"] serde = { version = "1.0.114", optional = true, default-features = false, features = ["std", "derive"]} thiserror = "1.0.34" hex = "0.4.2" -bstr = { version = "0.2.13", default-features = false, features = ["std"] } +bstr = { version = "1.0.1", default-features = false, features = ["std"] } # async support futures-io = { version = "0.3.16", optional = true } futures-lite = { version = "1.12.0", optional = true } diff --git a/git-path/Cargo.toml b/git-path/Cargo.toml index d771a5b87db..c10d334d4df 100644 --- a/git-path/Cargo.toml +++ b/git-path/Cargo.toml @@ -12,7 +12,7 @@ include = ["src/**/*", "CHANGELOG.md"] doctest = false [dependencies] -bstr = { version = "0.2.17", default-features = false, features = ["std"] } +bstr = { version = "1.0.1", default-features = false, features = ["std"] } thiserror = "1.0.26" [dev-dependencies] diff --git a/git-pathspec/Cargo.toml b/git-pathspec/Cargo.toml index 47878786254..97ed19abca8 100644 --- a/git-pathspec/Cargo.toml +++ b/git-pathspec/Cargo.toml @@ -14,7 +14,7 @@ doctest = false git-glob = { version = "^0.3.2", path = "../git-glob" } git-attributes = { version = "^0.3.3", path = "../git-attributes" } -bstr = { version = "0.2.13", default-features = false, features = ["std"]} +bstr = { version = "1.0.1", default-features = false, features = ["std"]} bitflags = "1.3.2" thiserror = "1.0.26" diff --git a/git-protocol/Cargo.toml b/git-protocol/Cargo.toml index d780c384d1f..e263117f841 100644 --- a/git-protocol/Cargo.toml +++ b/git-protocol/Cargo.toml @@ -26,7 +26,7 @@ async-client = ["git-transport/async-client", "async-trait", "futures-io", "futu #! ### Other ## Data structures implement `serde::Serialize` and `serde::Deserialize`. -serde1 = ["serde", "bstr/serde1", "git-transport/serde1", "git-hash/serde1"] +serde1 = ["serde", "bstr/serde", "git-transport/serde1", "git-hash/serde1"] [[test]] name = "blocking-client-protocol" @@ -46,7 +46,7 @@ git-credentials = { version = "^0.4.0", path = "../git-credentials" } thiserror = "1.0.32" serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"]} -bstr = { version = "0.2.13", default-features = false, features = ["std"] } +bstr = { version = "1.0.1", default-features = false, features = ["std"] } nom = { version = "7", default-features = false, features = ["std"]} btoi = "0.4.2" diff --git a/git-quote/Cargo.toml b/git-quote/Cargo.toml index 829647d1276..d96de1cae1f 100644 --- a/git-quote/Cargo.toml +++ b/git-quote/Cargo.toml @@ -11,6 +11,6 @@ edition = "2018" doctest = false [dependencies] -bstr = { version = "0.2.13", default-features = false, features = ["std"]} +bstr = { version = "1.0.1", default-features = false, features = ["std"]} quick-error = "2.0.0" btoi = "0.4.2" diff --git a/git-refspec/Cargo.toml b/git-refspec/Cargo.toml index 2897bed7e29..9ea3722f848 100644 --- a/git-refspec/Cargo.toml +++ b/git-refspec/Cargo.toml @@ -16,7 +16,7 @@ git-revision = { version = "^0.4.4", path = "../git-revision" } git-validate = { version = "^0.5.5", path = "../git-validate" } git-hash = { version = "^0.9.9", path = "../git-hash" } -bstr = { version = "0.2.13", default-features = false, features = ["std"]} +bstr = { version = "1.0.1", default-features = false, features = ["std"]} thiserror = "1.0.26" smallvec = "1.9.0" diff --git a/git-refspec/src/instruction.rs b/git-refspec/src/instruction.rs index cde4a09fc27..c37ceebcd35 100644 --- a/git-refspec/src/instruction.rs +++ b/git-refspec/src/instruction.rs @@ -42,9 +42,10 @@ pub enum Push<'a> { /// Destinations can only be a partial or full ref-names on the local side. #[derive(PartialOrd, Ord, PartialEq, Eq, Copy, Clone, Hash, Debug)] pub enum Fetch<'a> { - /// Fetch a ref or refs and write the result into the `FETCH_HEAD` without updating local branches. + /// Fetch a ref or refs, without updating local branches. Only { - /// The ref name to fetch on the remote side, without updating the local side. This will write the result into `FETCH_HEAD`. + /// The partial or full ref name to fetch on the remote side or the full object hex-name, without updating the local side. + /// Note that this may not be a glob pattern, as those need to be matched by a destination which isn't present here. src: &'a BStr, }, /// Exclude a single ref. diff --git a/git-refspec/src/lib.rs b/git-refspec/src/lib.rs index f53fdcdc013..54d5f3057a9 100644 --- a/git-refspec/src/lib.rs +++ b/git-refspec/src/lib.rs @@ -29,5 +29,11 @@ pub struct RefSpec { mod spec; +mod write; + +/// +pub mod match_group; +pub use match_group::types::MatchGroup; + mod types; pub use types::Instruction; diff --git a/git-refspec/src/match_group/mod.rs b/git-refspec/src/match_group/mod.rs new file mode 100644 index 00000000000..5b10ee32cc4 --- /dev/null +++ b/git-refspec/src/match_group/mod.rs @@ -0,0 +1,118 @@ +use crate::parse::Operation; +use crate::types::Mode; +use crate::{MatchGroup, RefSpecRef}; +use std::collections::BTreeSet; + +pub(crate) mod types; +pub use types::{Item, Mapping, Outcome, Source, SourceRef}; + +/// +pub mod validate; + +/// Initialization +impl<'a> MatchGroup<'a> { + /// Take all the fetch ref specs from `specs` get a match group ready. + pub fn from_fetch_specs(specs: impl IntoIterator>) -> Self { + MatchGroup { + specs: specs.into_iter().filter(|s| s.op == Operation::Fetch).collect(), + } + } +} + +/// Matching +impl<'a> MatchGroup<'a> { + /// Match all `items` against all fetch specs present in this group, returning deduplicated mappings from source to destination. + /// Note that this method only makes sense if the specs are indeed fetch specs and may panic otherwise. + /// + /// Note that negative matches are not part of the return value, so they are not observable but will be used to remove mappings. + pub fn match_remotes<'item>(self, mut items: impl Iterator> + Clone) -> Outcome<'a, 'item> { + let mut out = Vec::new(); + let mut seen = BTreeSet::default(); + let mut push_unique = |mapping| { + if seen.insert(calculate_hash(&mapping)) { + out.push(mapping); + } + }; + let mut matchers: Vec>> = self + .specs + .iter() + .copied() + .map(Matcher::from) + .enumerate() + .map(|(idx, m)| match m.lhs { + Some(Needle::Object(id)) => { + push_unique(Mapping { + item_index: None, + lhs: SourceRef::ObjectId(id), + rhs: m.rhs.map(|n| n.to_bstr()), + spec_index: idx, + }); + None + } + _ => Some(m), + }) + .collect(); + + let mut has_negation = false; + for (spec_index, (spec, matcher)) in self.specs.iter().zip(matchers.iter_mut()).enumerate() { + for (item_index, item) in items.clone().enumerate() { + if spec.mode == Mode::Negative { + has_negation = true; + continue; + } + if let Some(matcher) = matcher { + let (matched, rhs) = matcher.matches_lhs(item); + if matched { + push_unique(Mapping { + item_index: Some(item_index), + lhs: SourceRef::FullName(item.full_ref_name), + rhs, + spec_index, + }) + } + } + } + } + + if let Some(id) = has_negation.then(|| items.next().map(|i| i.target)).flatten() { + let null_id = git_hash::ObjectId::null(id.kind()); + for matcher in matchers + .into_iter() + .zip(self.specs.iter()) + .filter_map(|(m, spec)| m.and_then(|m| (spec.mode == Mode::Negative).then(|| m))) + { + out.retain(|m| match m.lhs { + SourceRef::ObjectId(_) => true, + SourceRef::FullName(name) => { + !matcher + .matches_lhs(Item { + full_ref_name: name, + target: &null_id, + tag: None, + }) + .0 + } + }); + } + } + Outcome { + group: self, + mappings: out, + } + } + + /// Return the spec that produced the given `mapping`. + pub fn spec_by_mapping(&self, mapping: &Mapping<'_, '_>) -> RefSpecRef<'a> { + self.specs[mapping.spec_index] + } +} + +fn calculate_hash(t: &T) -> u64 { + use std::hash::Hasher; + let mut s = std::collections::hash_map::DefaultHasher::new(); + t.hash(&mut s); + s.finish() +} + +mod util; +use util::{Matcher, Needle}; diff --git a/git-refspec/src/match_group/types.rs b/git-refspec/src/match_group/types.rs new file mode 100644 index 00000000000..61711252d9c --- /dev/null +++ b/git-refspec/src/match_group/types.rs @@ -0,0 +1,101 @@ +use crate::RefSpecRef; +use bstr::{BStr, BString}; +use git_hash::oid; +use std::borrow::Cow; + +/// A match group is able to match a list of ref specs in order while handling negation, conflicts and one to many mappings. +#[derive(Debug, Clone)] +pub struct MatchGroup<'a> { + pub(crate) specs: Vec>, +} + +/// The outcome of any matching operation of a [`MatchGroup`]. +/// +/// It's used to validate and process the contained [mappings][Mapping]. +#[derive(Debug, Clone)] +pub struct Outcome<'spec, 'item> { + /// The match group that produced this outcome. + pub group: MatchGroup<'spec>, + /// The mappings derived from matching [items][Item]. + pub mappings: Vec>, +} + +/// An item to match, input to various matching operations. +#[derive(Debug, Copy, Clone)] +pub struct Item<'a> { + /// The full name of the references, like `refs/heads/main` + pub full_ref_name: &'a BStr, + /// The peeled id it points to that we should match against. + pub target: &'a oid, + /// The tag object's id if this is a tag + pub tag: Option<&'a oid>, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +/// The source (or left-hand) side of a mapping, which references its name. +pub enum SourceRef<'a> { + /// A full reference name, which is expected to be valid. + /// + /// Validity, however, is not enforced here. + FullName(&'a BStr), + /// The name of an object that is expected to exist on the remote side. + /// Note that it might not be advertised by the remote but part of the object graph, + /// and thus gets sent in the pack. The server is expected to fail unless the desired + /// object is present but at some time it is merely a request by the user. + ObjectId(git_hash::ObjectId), +} + +impl SourceRef<'_> { + /// Create a fully owned instance from this one. + pub fn to_owned(&self) -> Source { + match self { + SourceRef::ObjectId(id) => Source::ObjectId(*id), + SourceRef::FullName(name) => Source::FullName((*name).to_owned()), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +/// The source (or left-hand) side of a mapping, which owns its name. +pub enum Source { + /// A full reference name, which is expected to be valid. + /// + /// Validity, however, is not enforced here. + FullName(BString), + /// The name of an object that is expected to exist on the remote side. + /// Note that it might not be advertised by the remote but part of the object graph, + /// and thus gets sent in the pack. The server is expected to fail unless the desired + /// object is present but at some time it is merely a request by the user. + ObjectId(git_hash::ObjectId), +} + +impl std::fmt::Display for Source { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Source::FullName(name) => name.fmt(f), + Source::ObjectId(id) => id.fmt(f), + } + } +} + +/// A mapping from a remote to a local refs for fetches or local to remote refs for pushes. +/// +/// Mappings are like edges in a graph, initially without any constraints. +#[derive(Debug, Clone)] +pub struct Mapping<'a, 'b> { + /// The index into the initial `items` list that matched against a spec. + pub item_index: Option, + /// The name of the remote side for fetches or the local one for pushes that matched. + pub lhs: SourceRef<'a>, + /// The name of the local side for fetches or the remote one for pushes that corresponds to `lhs`, if available. + pub rhs: Option>, + /// The index of the matched ref-spec as seen from the match group. + pub(crate) spec_index: usize, +} + +impl std::hash::Hash for Mapping<'_, '_> { + fn hash(&self, state: &mut H) { + self.lhs.hash(state); + self.rhs.hash(state); + } +} diff --git a/git-refspec/src/match_group/util.rs b/git-refspec/src/match_group/util.rs new file mode 100644 index 00000000000..42a31c03acb --- /dev/null +++ b/git-refspec/src/match_group/util.rs @@ -0,0 +1,181 @@ +use crate::match_group::Item; +use crate::RefSpecRef; +use bstr::{BStr, BString, ByteSlice, ByteVec}; +use git_hash::ObjectId; +use std::borrow::Cow; +use std::ops::Range; + +/// A type keeping enough information about a ref-spec to be able to efficiently match it against multiple matcher items. +pub struct Matcher<'a> { + pub(crate) lhs: Option>, + pub(crate) rhs: Option>, +} + +impl<'a> Matcher<'a> { + /// Match `item` against this spec and return `(true, Some)` to gain the other side of the match as configured, or `(true, None)` + /// if there was no `rhs`. + /// + /// This may involve resolving a glob with an allocation, as the destination is built using the matching portion of a glob. + pub fn matches_lhs(&self, item: Item<'_>) -> (bool, Option>) { + match (self.lhs, self.rhs) { + (Some(lhs), None) => (lhs.matches(item).is_match(), None), + (Some(lhs), Some(rhs)) => lhs.matches(item).into_match_outcome(rhs, item), + (None, None) | (None, Some(_)) => { + unreachable!("For all we know, the lefthand side is never empty. Push specs might change that.") + } + } + } +} + +#[derive(Debug, Copy, Clone)] +pub(crate) enum Needle<'a> { + FullName(&'a BStr), + PartialName(&'a BStr), + Glob { name: &'a BStr, asterisk_pos: usize }, + Object(ObjectId), +} + +enum Match { + /// There was no match. + None, + /// No additional data is provided as part of the match. + Normal, + /// The range of text to copy from the originating item name + GlobRange(Range), +} + +impl Match { + fn is_match(&self) -> bool { + !matches!(self, Match::None) + } + fn into_match_outcome<'a>(self, destination: Needle<'a>, item: Item<'_>) -> (bool, Option>) { + let arg = match self { + Match::None => return (false, None), + Match::Normal => None, + Match::GlobRange(range) => Some((range, item)), + }; + (true, destination.to_bstr_replace(arg).into()) + } +} + +impl<'a> Needle<'a> { + #[inline] + fn matches(&self, item: Item<'_>) -> Match { + match self { + Needle::FullName(name) => { + if *name == item.full_ref_name { + Match::Normal + } else { + Match::None + } + } + Needle::PartialName(name) => { + let mut buf = BString::from(Vec::with_capacity(128)); + for (base, append_head) in [ + ("", false), + ("refs/", false), + ("refs/tags/", false), + ("refs/heads/", false), + ("refs/remotes/", false), + ("refs/remotes/", true), + ] { + buf.clear(); + buf.push_str(base); + buf.push_str(name); + if append_head { + buf.push_str("/HEAD"); + } + if buf == item.full_ref_name { + return Match::Normal; + } + } + Match::None + } + Needle::Glob { name, asterisk_pos } => { + match item.full_ref_name.get(..*asterisk_pos) { + Some(full_name_portion) if full_name_portion != name[..*asterisk_pos] => { + return Match::None; + } + None => return Match::None, + _ => {} + }; + let tail = &name[*asterisk_pos + 1..]; + if !item.full_ref_name.ends_with(tail) { + return Match::None; + } + let end = item.full_ref_name.len() - tail.len(); + let end = item.full_ref_name[*asterisk_pos..end].find_byte(b'/').unwrap_or(end); + Match::GlobRange(*asterisk_pos..end) + } + Needle::Object(id) => { + if *id == item.target { + return Match::Normal; + } + match item.tag { + Some(tag) if tag == *id => Match::Normal, + _ => Match::None, + } + } + } + } + + fn to_bstr_replace(self, range: Option<(Range, Item<'_>)>) -> Cow<'a, BStr> { + match (self, range) { + (Needle::FullName(name), None) => Cow::Borrowed(name), + (Needle::PartialName(name), None) => Cow::Owned({ + let mut base: BString = "refs/".into(); + if !(name.starts_with(b"tags/") || name.starts_with(b"remotes/")) { + base.push_str("heads/"); + } + base.push_str(name); + base + }), + (Needle::Glob { name, asterisk_pos }, Some((range, item))) => { + let mut buf = Vec::with_capacity(name.len() + range.len() - 1); + buf.push_str(&name[..asterisk_pos]); + buf.push_str(&item.full_ref_name[range]); + buf.push_str(&name[asterisk_pos + 1..]); + Cow::Owned(buf.into()) + } + (Needle::Object(id), None) => { + let mut name = id.to_string(); + name.insert_str(0, "refs/heads/"); + Cow::Owned(name.into()) + } + (Needle::Glob { .. }, None) => unreachable!("BUG: no range provided for glob pattern"), + (_, Some(_)) => { + unreachable!("BUG: range provided even though needle wasn't a glob. Globs are symmetric.") + } + } + } + + pub fn to_bstr(self) -> Cow<'a, BStr> { + self.to_bstr_replace(None) + } +} + +impl<'a> From<&'a BStr> for Needle<'a> { + fn from(v: &'a BStr) -> Self { + if let Some(pos) = v.find_byte(b'*') { + Needle::Glob { + name: v, + asterisk_pos: pos, + } + } else if v.starts_with(b"refs/") { + Needle::FullName(v) + } else if let Ok(id) = git_hash::ObjectId::from_hex(v) { + Needle::Object(id) + } else { + Needle::PartialName(v) + } + } +} + +impl<'a> From> for Matcher<'a> { + fn from(v: RefSpecRef<'a>) -> Self { + Matcher { + lhs: v.src.map(Into::into), + rhs: v.dst.map(Into::into), + } + } +} diff --git a/git-refspec/src/match_group/validate.rs b/git-refspec/src/match_group/validate.rs new file mode 100644 index 00000000000..0443a47b3cf --- /dev/null +++ b/git-refspec/src/match_group/validate.rs @@ -0,0 +1,134 @@ +use crate::match_group::{Outcome, Source}; +use bstr::BString; +use std::collections::BTreeMap; + +/// All possible issues found while validating matched mappings. +#[derive(Debug, PartialEq, Eq)] +pub enum Issue { + /// Multiple sources try to write the same destination. + /// + /// Note that this issue doesn't take into consideration that these sources might contain the same object behind a reference. + Conflict { + /// The unenforced full name of the reference to be written. + destination_full_ref_name: BString, + /// The list of sources that map to this destination. + sources: Vec, + /// The list of specs that caused the mapping conflict, each matching the respective one in `sources` to allow both + /// `sources` and `specs` to be zipped together. + specs: Vec, + }, +} + +impl std::fmt::Display for Issue { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Issue::Conflict { + destination_full_ref_name, + sources, + specs, + } => { + write!( + f, + "Conflicting destination {destination_full_ref_name:?} would be written by {}", + sources + .iter() + .zip(specs.iter()) + .map(|(src, spec)| format!("{src} ({spec:?})")) + .collect::>() + .join(", ") + ) + } + } + } +} + +/// All possible fixes corrected while validating matched mappings. +#[derive(Debug, PartialEq, Eq)] +pub enum Fix { + /// Removed a mapping that contained a partial destination entirely. + MappingWithPartialDestinationRemoved { + /// The destination ref name that was ignored. + name: BString, + /// The spec that defined the mapping + spec: BString, + }, +} + +/// The error returned [outcome validation][Outcome::validated()]. +#[derive(Debug)] +pub struct Error { + /// All issues discovered during validation. + pub issues: Vec, +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "Found {} {} the refspec mapping to be used: \n\t{}", + self.issues.len(), + (self.issues.len() == 1) + .then(|| "issue that prevents") + .unwrap_or("issues that prevent"), + self.issues + .iter() + .map(|issue| issue.to_string()) + .collect::>() + .join("\n\t") + ) + } +} + +impl std::error::Error for Error {} + +impl<'spec, 'item> Outcome<'spec, 'item> { + /// Validate all mappings or dissolve them into an error stating the discovered issues. + /// Return `(modified self, issues)` providing a fixed-up set of mappings in `self` with the fixed `issues` + /// provided as part of it. + /// Terminal issues are communicated using the [`Error`] type accordingly. + pub fn validated(mut self) -> Result<(Self, Vec), Error> { + let mut sources_by_destinations = BTreeMap::new(); + for (dst, (spec_index, src)) in self + .mappings + .iter() + .filter_map(|m| m.rhs.as_ref().map(|dst| (dst.as_ref(), (m.spec_index, &m.lhs)))) + { + let sources = sources_by_destinations.entry(dst).or_insert_with(Vec::new); + if !sources.iter().any(|(_, lhs)| lhs == &src) { + sources.push((spec_index, src)) + } + } + let mut issues = Vec::new(); + for (dst, conflicting_sources) in sources_by_destinations.into_iter().filter(|(_, v)| v.len() > 1) { + issues.push(Issue::Conflict { + destination_full_ref_name: dst.to_owned(), + specs: conflicting_sources + .iter() + .map(|(spec_idx, _)| self.group.specs[*spec_idx].to_bstring()) + .collect(), + sources: conflicting_sources.into_iter().map(|(_, src)| src.to_owned()).collect(), + }) + } + if !issues.is_empty() { + Err(Error { issues }) + } else { + let mut fixed = Vec::new(); + let group = &self.group; + self.mappings.retain(|m| match m.rhs.as_ref() { + Some(dst) => { + if dst.starts_with(b"refs/") || dst.as_ref() == "HEAD" { + true + } else { + fixed.push(Fix::MappingWithPartialDestinationRemoved { + name: dst.as_ref().to_owned(), + spec: group.specs[m.spec_index].to_bstring(), + }); + false + } + } + None => true, + }); + Ok((self, fixed)) + } + } +} diff --git a/git-refspec/src/parse.rs b/git-refspec/src/parse.rs index 02cb5dc10c7..93c43d3064e 100644 --- a/git-refspec/src/parse.rs +++ b/git-refspec/src/parse.rs @@ -12,6 +12,10 @@ pub enum Error { NegativeUnsupported, #[error("Negative specs must be object hashes")] NegativeObjectHash, + #[error("Negative specs must be full ref names, starting with \"refs/\"")] + NegativePartialName, + #[error("Negative glob patterns are not allowed")] + NegativeGlobPattern, #[error("Fetch destinations must be ref-names, like 'HEAD:refs/heads/branch'")] InvalidFetchDestination, #[error("Cannot push into an empty destination")] @@ -112,17 +116,6 @@ pub(crate) mod function { } }; - if mode == Mode::Negative { - match src { - Some(spec) => { - if looks_like_object_hash(spec) { - return Err(Error::NegativeObjectHash); - } - } - None => return Err(Error::NegativeEmpty), - } - } - if let Some(spec) = src.as_mut() { if *spec == "@" { *spec = "HEAD".into(); @@ -130,12 +123,25 @@ pub(crate) mod function { } let (src, src_had_pattern) = validated(src, operation == Operation::Push && dst.is_some())?; let (dst, dst_had_pattern) = validated(dst, false)?; - if !dst_had_pattern && looks_like_object_hash(dst.unwrap_or_default()) { - return Err(Error::InvalidFetchDestination); - } if mode != Mode::Negative && src_had_pattern != dst_had_pattern { return Err(Error::PatternUnbalanced); } + + if mode == Mode::Negative { + match src { + Some(spec) => { + if src_had_pattern { + return Err(Error::NegativeGlobPattern); + } else if looks_like_object_hash(spec) { + return Err(Error::NegativeObjectHash); + } else if !spec.starts_with(b"refs/") && spec != "HEAD" { + return Err(Error::NegativePartialName); + } + } + None => return Err(Error::NegativeEmpty), + } + } + Ok(RefSpecRef { op: operation, mode, diff --git a/git-refspec/src/spec.rs b/git-refspec/src/spec.rs index f99ead73250..4ac70fc0566 100644 --- a/git-refspec/src/spec.rs +++ b/git-refspec/src/spec.rs @@ -4,6 +4,7 @@ use crate::{ types::Mode, Instruction, RefSpec, RefSpecRef, }; +use bstr::BStr; /// Conversion. Use the [RefSpecRef][RefSpec::to_ref()] type for more usage options. impl RefSpec { @@ -83,6 +84,38 @@ mod impls { /// Access impl RefSpecRef<'_> { + /// Return the left-hand side of the spec, typically the source. + /// It takes many different forms so don't rely on this being a ref name. + /// + /// It's not present in case of deletions. + pub fn source(&self) -> Option<&BStr> { + self.src + } + + /// Return the right-hand side of the spec, typically the destination. + /// It takes many different forms so don't rely on this being a ref name. + /// + /// It's not present in case of source-only specs. + pub fn destination(&self) -> Option<&BStr> { + self.dst + } + + /// Always returns the remote side, whose actual side in the refspec depends on how it was parsed. + pub fn remote(&self) -> Option<&BStr> { + match self.op { + Operation::Push => self.dst, + Operation::Fetch => self.src, + } + } + + /// Always returns the local side, whose actual side in the refspec depends on how it was parsed. + pub fn local(&self) -> Option<&BStr> { + match self.op { + Operation::Push => self.src, + Operation::Fetch => self.dst, + } + } + /// Transform the state of the refspec into an instruction making clear what to do with it. pub fn instruction(&self) -> Instruction<'_> { match self.op { diff --git a/git-refspec/src/write.rs b/git-refspec/src/write.rs new file mode 100644 index 00000000000..b33c5ec539c --- /dev/null +++ b/git-refspec/src/write.rs @@ -0,0 +1,71 @@ +use crate::instruction::{Fetch, Push}; +use crate::{Instruction, RefSpecRef}; +use bstr::BString; + +impl RefSpecRef<'_> { + /// Reproduce ourselves in parseable form. + pub fn to_bstring(&self) -> BString { + let mut buf = Vec::with_capacity(128); + self.write_to(&mut buf).expect("no io error"); + buf.into() + } + + /// Serialize ourselves in a parseable format to `out`. + pub fn write_to(&self, out: impl std::io::Write) -> std::io::Result<()> { + self.instruction().write_to(out) + } +} + +impl Instruction<'_> { + /// Reproduce ourselves in parseable form. + pub fn to_bstring(&self) -> BString { + let mut buf = Vec::with_capacity(128); + self.write_to(&mut buf).expect("no io error"); + buf.into() + } + + /// Serialize ourselves in a parseable format to `out`. + pub fn write_to(&self, mut out: impl std::io::Write) -> std::io::Result<()> { + match self { + Instruction::Push(Push::Matching { + src, + dst, + allow_non_fast_forward, + }) => { + if *allow_non_fast_forward { + out.write_all(&[b'+'])?; + } + out.write_all(src)?; + out.write_all(&[b':'])?; + out.write_all(dst) + } + Instruction::Push(Push::AllMatchingBranches { allow_non_fast_forward }) => { + if *allow_non_fast_forward { + out.write_all(&[b'+'])?; + } + out.write_all(&[b':']) + } + Instruction::Push(Push::Delete { ref_or_pattern }) => { + out.write_all(&[b':'])?; + out.write_all(ref_or_pattern) + } + Instruction::Fetch(Fetch::Only { src }) => out.write_all(src), + Instruction::Fetch(Fetch::Exclude { src }) => { + out.write_all(&[b'^'])?; + out.write_all(src) + } + Instruction::Fetch(Fetch::AndUpdate { + src, + dst, + allow_non_fast_forward, + }) => { + if *allow_non_fast_forward { + out.write_all(&[b'+'])?; + } + out.write_all(src)?; + out.write_all(&[b':'])?; + out.write_all(dst) + } + } + } +} diff --git a/git-refspec/tests/fixtures/generated-archives/.gitignore b/git-refspec/tests/fixtures/generated-archives/.gitignore new file mode 100644 index 00000000000..6d9f4282261 --- /dev/null +++ b/git-refspec/tests/fixtures/generated-archives/.gitignore @@ -0,0 +1 @@ +/match_baseline.tar.xz diff --git a/git-refspec/tests/fixtures/generated-archives/make_baseline.tar.xz b/git-refspec/tests/fixtures/generated-archives/make_baseline.tar.xz deleted file mode 100644 index aa3836355df..00000000000 --- a/git-refspec/tests/fixtures/generated-archives/make_baseline.tar.xz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9d52f5fe25601c545187b0ac19b004f30488937b6907e8206333d2ba923c7b29 -size 9372 diff --git a/git-refspec/tests/fixtures/generated-archives/parse_baseline.tar.xz b/git-refspec/tests/fixtures/generated-archives/parse_baseline.tar.xz new file mode 100644 index 00000000000..66105e8679e --- /dev/null +++ b/git-refspec/tests/fixtures/generated-archives/parse_baseline.tar.xz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:583804a5796540f5f5cbba69001e9746bf23a0fe6d085d7ff643fa4bd9c3e2d9 +size 9448 diff --git a/git-refspec/tests/fixtures/match_baseline.sh b/git-refspec/tests/fixtures/match_baseline.sh new file mode 100644 index 00000000000..3c2bdff940d --- /dev/null +++ b/git-refspec/tests/fixtures/match_baseline.sh @@ -0,0 +1,85 @@ +#!/bin/bash +set -eu -o pipefail + +git init; + +function baseline() { + { + git fetch --refmap= --dry-run -v origin "$@" 2>&1 || : + echo specs: "$@" + } >> baseline.git +} + +mkdir base +(cd base + git init + touch file + git add file + git commit -m "initial commit" + git tag -m "message" annotated-v0.0 + + git checkout -b f1 + git commit -m "f1" --allow-empty + git tag v0.0-f1 + + git checkout -b f2 main + git commit -m "f2" --allow-empty + git tag v0.0-f2 + + git checkout -b f3 main + git commit -m "f3" --allow-empty + git tag v0.0-f3 +) + +git clone --shared ./base clone +(cd clone + git ls-remote 2>&1 > remote-refs.list + baseline "refs/heads/main" + baseline "heads/main" + baseline "main" + baseline "v0.0-f1" + baseline "tags/v0.0-f2" + baseline "78b1c1be9421b33a49a7a8176d93eeeafa112da1" + baseline "9d2fab1a0ba3585d0bc50922bfdd04ebb59361df" + baseline "78b1c1be9421b33a49a7a8176d93eeeafa112da1:special" + baseline "78b1c1be9421b33a49a7a8176d93eeeafa112da1:1111111111111111111111111111111111111111" + baseline "9d2fab1a0ba3585d0bc50922bfdd04ebb59361df:tags/special" + baseline "9d2fab1a0ba3585d0bc50922bfdd04ebb59361df:refs/tags/special" + baseline "f1:origin/f1" + baseline "f1:remotes/origin/f1" + baseline "f1:notes/f1" + baseline "+refs/heads/*:refs/remotes/origin/*" + baseline "refs/heads/*1:refs/remotes/origin/*1" + baseline "refs/heads/f*:refs/remotes/origin/a*" + baseline "main" "f1" + baseline "heads/main" "heads/f1" + baseline "refs/heads/main" "refs/heads/f1" + baseline "refs/heads/*:refs/remotes/origin/*" "^main" + baseline "heads/f1" "f2" "refs/heads/f3" "heads/main" + baseline "f*:a*" "refs/heads/main" + baseline "refs/heads/f*:refs/remotes/origin/a*" "^f1" + baseline "refs/heads/f*:refs/remotes/origin/a*" "^refs/heads/f1" + baseline "^heads/f2" "refs/heads/f*:refs/remotes/origin/a*" + baseline "heads/f2" "^refs/heads/f*:refs/remotes/origin/a*" + baseline "^refs/heads/f2" "refs/heads/f*:refs/remotes/origin/a*" + baseline "^main" "refs/heads/*:refs/remotes/origin/*" + baseline "^refs/heads/main" "refs/heads/*:refs/remotes/origin/*" + baseline "refs/heads/*:refs/remotes/origin/*" "^refs/heads/main" + baseline "refs/heads/*:refs/remotes/origin/*" "refs/heads/main:refs/remotes/new-origin/main" + baseline "refs/heads/*:refs/remotes/origin/*" "refs/heads/main:refs/remotes/origin/main" + baseline "refs/heads/f1:refs/remotes/origin/conflict" "refs/heads/f2:refs/remotes/origin/conflict" + baseline "refs/heads/f1:refs/remotes/origin/conflict2" "refs/heads/f2:refs/remotes/origin/conflict2" "refs/heads/f1:refs/remotes/origin/conflict" "refs/heads/f2:refs/remotes/origin/conflict" "refs/heads/f3:refs/remotes/origin/conflict" + baseline "refs/heads/f1:refs/remotes/origin/same" "refs/tags/v0.0-f1:refs/remotes/origin/same" # same object, not technically a problem but git flags it anyway + baseline "refs/tags/*:refs/remotes/origin/*" "refs/heads/*:refs/remotes/origin/*" + baseline "refs/tags/*:refs/tags/*" + baseline 'refs/heads/f*:foo/f*' 'f1:f1' + baseline "+refs/heads/*:refs/remotes/origin/*" "refs/heads/f1:refs/remotes/origin/f2" "refs/heads/f2:refs/remotes/origin/f1" + baseline ':refs/heads/f1' + baseline ':f1' + baseline ':' + baseline 'HEAD:' + baseline '@:' + baseline '@:f1' + baseline '@:HEAD' +) + diff --git a/git-refspec/tests/fixtures/make_baseline.sh b/git-refspec/tests/fixtures/parse_baseline.sh similarity index 89% rename from git-refspec/tests/fixtures/make_baseline.sh rename to git-refspec/tests/fixtures/parse_baseline.sh index 3e78ce9788a..cf87060ab6c 100644 --- a/git-refspec/tests/fixtures/make_baseline.sh +++ b/git-refspec/tests/fixtures/parse_baseline.sh @@ -68,19 +68,25 @@ baseline fetch 'refs/heads/*g*/for-linus:refs/remotes/mine/*' baseline push 'refs/heads/*g*/for-linus:refs/remotes/mine/*' bad=$(printf '\011tab') baseline fetch "refs/heads/${bad}" +baseline fetch 'refs/*/*' +baseline fetch 'refs/heads/*' +baseline fetch '^refs/*/*' # valid baseline push '+:' baseline push ':' +baseline fetch 55e825ebe8fd2ff78cad3826afb696b96b576a7e baseline fetch '' baseline fetch ':' baseline fetch '+' baseline push 'refs/heads/main:refs/remotes/frotz/xyzzy' +baseline fetch '55e825ebe8fd2ff78cad3826afb696b96b576a7e:refs/heads/main' baseline push 'refs/heads/*:refs/remotes/frotz/*' baseline fetch 'refs/heads/*:refs/remotes/frotz/*' +baseline fetch 'heads/main' baseline fetch 'refs/heads/main:refs/remotes/frotz/xyzzy' baseline push 'main~1:refs/remotes/frotz/backup' @@ -93,6 +99,10 @@ baseline fetch '@' baseline push '^@' fail baseline fetch '^@' +baseline fetch '^refs/heads/main' +baseline fetch '^refs/heads/*' +baseline fetch '^heads/main' +baseline fetch '^heads/*' baseline push '+@' baseline fetch '+@' diff --git a/git-refspec/tests/match_group/mod.rs b/git-refspec/tests/match_group/mod.rs new file mode 100644 index 00000000000..fade345bb04 --- /dev/null +++ b/git-refspec/tests/match_group/mod.rs @@ -0,0 +1,182 @@ +mod single { + use crate::matching::baseline; + + #[test] + fn fetch_only() { + baseline::agrees_with_fetch_specs(Some("refs/heads/main")); + baseline::agrees_with_fetch_specs(Some("heads/main")); + baseline::agrees_with_fetch_specs(Some("main")); + baseline::agrees_with_fetch_specs(Some("v0.0-f1")); + baseline::agrees_with_fetch_specs(Some("tags/v0.0-f2")); + baseline::of_objects_always_matches_if_the_server_has_the_object(Some( + "78b1c1be9421b33a49a7a8176d93eeeafa112da1", + )); + baseline::of_objects_always_matches_if_the_server_has_the_object(Some( + "9d2fab1a0ba3585d0bc50922bfdd04ebb59361df", + )); + } + + #[test] + fn fetch_and_update() { + baseline::of_objects_with_destinations_are_written_into_given_local_branches( + Some("78b1c1be9421b33a49a7a8176d93eeeafa112da1:special"), + ["78b1c1be9421b33a49a7a8176d93eeeafa112da1:refs/heads/special"], + ); + baseline::of_objects_with_destinations_are_written_into_given_local_branches( + Some("78b1c1be9421b33a49a7a8176d93eeeafa112da1:1111111111111111111111111111111111111111"), + ["78b1c1be9421b33a49a7a8176d93eeeafa112da1:refs/heads/1111111111111111111111111111111111111111"], + ); + baseline::of_objects_with_destinations_are_written_into_given_local_branches( + Some("9d2fab1a0ba3585d0bc50922bfdd04ebb59361df:tags/special"), + ["9d2fab1a0ba3585d0bc50922bfdd04ebb59361df:refs/tags/special"], + ); + baseline::of_objects_with_destinations_are_written_into_given_local_branches( + Some("9d2fab1a0ba3585d0bc50922bfdd04ebb59361df:refs/tags/special"), + ["9d2fab1a0ba3585d0bc50922bfdd04ebb59361df:refs/tags/special"], + ); + + baseline::agrees_but_observable_refs_are_vague(Some("f1:origin/f1"), ["refs/heads/f1:refs/heads/origin/f1"]); + baseline::agrees_but_observable_refs_are_vague( + Some("f1:remotes/origin/f1"), + ["refs/heads/f1:refs/remotes/origin/f1"], + ); + baseline::agrees_but_observable_refs_are_vague(Some("f1:notes/f1"), ["refs/heads/f1:refs/heads/notes/f1"]); + baseline::agrees_with_fetch_specs(Some("+refs/heads/*:refs/remotes/origin/*")); + baseline::agrees_with_fetch_specs(Some("refs/heads/f*:refs/remotes/origin/a*")); + baseline::agrees_with_fetch_specs(Some("refs/heads/*1:refs/remotes/origin/*1")); + } +} + +mod multiple { + use crate::matching::baseline; + use git_refspec::match_group::validate::Fix; + use git_refspec::parse::Error; + + #[test] + fn fetch_only() { + baseline::agrees_with_fetch_specs(["main", "f1"]); + baseline::agrees_with_fetch_specs(["heads/main", "heads/f1"]); + baseline::agrees_with_fetch_specs(["refs/heads/main", "refs/heads/f1"]); + baseline::agrees_with_fetch_specs(["heads/f1", "f2", "refs/heads/f3", "heads/main"]); + baseline::agrees_with_fetch_specs(["f*:a*", "refs/heads/main"]); + baseline::agrees_with_fetch_specs([ + "refs/tags/*:refs/remotes/origin/*", + "refs/heads/*:refs/remotes/origin/*", + ]); + baseline::agrees_with_fetch_specs(["refs/tags/*:refs/tags/*"]); + } + + #[test] + fn fetch_and_update_and_negations() { + baseline::invalid_specs_fail_to_parse_where_git_shows_surprising_behaviour( + ["refs/heads/f*:refs/remotes/origin/a*", "^f1"], + Error::NegativePartialName, + ); + baseline::invalid_specs_fail_to_parse_where_git_shows_surprising_behaviour( + ["heads/f2", "^refs/heads/f*:refs/remotes/origin/a*"], + Error::NegativeWithDestination, + ); + baseline::agrees_with_fetch_specs(["refs/heads/f*:refs/remotes/origin/a*", "^refs/heads/f1"]); + baseline::invalid_specs_fail_to_parse_where_git_shows_surprising_behaviour( + ["^heads/f2", "refs/heads/f*:refs/remotes/origin/a*"], + Error::NegativePartialName, + ); + baseline::agrees_with_fetch_specs(["^refs/heads/f2", "refs/heads/f*:refs/remotes/origin/a*"]); + baseline::invalid_specs_fail_to_parse_where_git_shows_surprising_behaviour( + ["^main", "refs/heads/*:refs/remotes/origin/*"], + Error::NegativePartialName, + ); + baseline::agrees_with_fetch_specs(["^refs/heads/main", "refs/heads/*:refs/remotes/origin/*"]); + baseline::agrees_with_fetch_specs(["refs/heads/*:refs/remotes/origin/*", "^refs/heads/main"]); + } + + #[test] + fn fetch_and_update_with_empty_lhs() { + baseline::agrees_but_observable_refs_are_vague([":refs/heads/f1"], ["HEAD:refs/heads/f1"]); + baseline::agrees_but_observable_refs_are_vague([":f1"], ["HEAD:refs/heads/f1"]); + baseline::agrees_but_observable_refs_are_vague(["@:f1"], ["HEAD:refs/heads/f1"]); + } + + #[test] + fn fetch_and_update_head_to_head_never_updates_actual_head_ref() { + baseline::agrees_but_observable_refs_are_vague(["@:HEAD"], ["HEAD:refs/heads/HEAD"]); + } + + #[test] + fn fetch_and_update_head_with_empty_rhs() { + baseline::agrees_but_observable_refs_are_vague([":"], ["HEAD:"]); + baseline::agrees_but_observable_refs_are_vague(["HEAD:"], ["HEAD:"]); + baseline::agrees_but_observable_refs_are_vague(["@:"], ["HEAD:"]); + } + + #[test] + fn fetch_and_update_multiple_destinations() { + baseline::agrees_with_fetch_specs([ + "refs/heads/*:refs/remotes/origin/*", + "refs/heads/main:refs/remotes/new-origin/main", + ]); + baseline::agrees_with_fetch_specs([ + "refs/heads/*:refs/remotes/origin/*", + "refs/heads/main:refs/remotes/origin/main", // duplicates are removed immediately. + ]); + } + + #[test] + fn fetch_and_update_with_conflicts() { + baseline::agrees_with_fetch_specs_validation_error( + [ + "refs/heads/f1:refs/remotes/origin/conflict", + "refs/heads/f2:refs/remotes/origin/conflict", + ], + "Found 1 issue that prevents the refspec mapping to be used: \n\tConflicting destination \"refs/remotes/origin/conflict\" would be written by refs/heads/f1 (\"refs/heads/f1:refs/remotes/origin/conflict\"), refs/heads/f2 (\"refs/heads/f2:refs/remotes/origin/conflict\")", + ); + baseline::agrees_with_fetch_specs_validation_error( + [ + "refs/heads/f1:refs/remotes/origin/conflict2", + "refs/heads/f2:refs/remotes/origin/conflict2", + "refs/heads/f1:refs/remotes/origin/conflict", + "refs/heads/f2:refs/remotes/origin/conflict", + "refs/heads/f3:refs/remotes/origin/conflict", + ], + "Found 2 issues that prevent the refspec mapping to be used: \n\tConflicting destination \"refs/remotes/origin/conflict\" would be written by refs/heads/f1 (\"refs/heads/f1:refs/remotes/origin/conflict\"), refs/heads/f2 (\"refs/heads/f2:refs/remotes/origin/conflict\"), refs/heads/f3 (\"refs/heads/f3:refs/remotes/origin/conflict\")\n\tConflicting destination \"refs/remotes/origin/conflict2\" would be written by refs/heads/f1 (\"refs/heads/f1:refs/remotes/origin/conflict2\"), refs/heads/f2 (\"refs/heads/f2:refs/remotes/origin/conflict2\")", + ); + baseline::agrees_with_fetch_specs_validation_error( + [ + "refs/heads/f1:refs/remotes/origin/same", + "refs/tags/v0.0-f1:refs/remotes/origin/same", + ], + "Found 1 issue that prevents the refspec mapping to be used: \n\tConflicting destination \"refs/remotes/origin/same\" would be written by refs/heads/f1 (\"refs/heads/f1:refs/remotes/origin/same\"), refs/tags/v0.0-f1 (\"refs/tags/v0.0-f1:refs/remotes/origin/same\")", + ); + baseline::agrees_with_fetch_specs_validation_error( + [ + "+refs/heads/*:refs/remotes/origin/*", + "refs/heads/f1:refs/remotes/origin/f2", + "refs/heads/f2:refs/remotes/origin/f1", + ], + "Found 2 issues that prevent the refspec mapping to be used: \n\tConflicting destination \"refs/remotes/origin/f1\" would be written by refs/heads/f1 (\"+refs/heads/*:refs/remotes/origin/*\"), refs/heads/f2 (\"refs/heads/f2:refs/remotes/origin/f1\")\n\tConflicting destination \"refs/remotes/origin/f2\" would be written by refs/heads/f2 (\"+refs/heads/*:refs/remotes/origin/*\"), refs/heads/f1 (\"refs/heads/f1:refs/remotes/origin/f2\")", + ); + } + + #[test] + fn fetch_and_update_with_fixes() { + let glob_spec = "refs/heads/f*:foo/f*"; + baseline::agrees_and_applies_fixes( + [glob_spec, "f1:f1"], + [ + Fix::MappingWithPartialDestinationRemoved { + name: "foo/f1".into(), + spec: glob_spec.into(), + }, + Fix::MappingWithPartialDestinationRemoved { + name: "foo/f2".into(), + spec: glob_spec.into(), + }, + Fix::MappingWithPartialDestinationRemoved { + name: "foo/f3".into(), + spec: glob_spec.into(), + }, + ], + ["refs/heads/f1:refs/heads/f1"], + ) + } +} diff --git a/git-refspec/tests/matching/mod.rs b/git-refspec/tests/matching/mod.rs new file mode 100644 index 00000000000..bf88bb68d63 --- /dev/null +++ b/git-refspec/tests/matching/mod.rs @@ -0,0 +1,314 @@ +use git_testtools::once_cell::sync::Lazy; + +static BASELINE: Lazy = Lazy::new(|| baseline::parse().unwrap()); + +pub mod baseline { + use crate::matching::BASELINE; + use bstr::{BString, ByteSlice, ByteVec}; + use git_hash::ObjectId; + use git_refspec::match_group::validate::Fix; + use git_refspec::match_group::SourceRef; + use git_refspec::parse::Operation; + use git_refspec::MatchGroup; + use git_testtools::once_cell::sync::Lazy; + use std::borrow::Borrow; + use std::collections::HashMap; + + #[derive(Debug)] + pub struct Ref { + pub name: BString, + pub target: ObjectId, + /// Set if this is a tag, pointing to the tag object itself + pub tag: Option, + } + + impl Ref { + pub fn to_item(&self) -> git_refspec::match_group::Item<'_> { + git_refspec::match_group::Item { + full_ref_name: self.name.borrow(), + target: &self.target, + tag: self.tag.as_deref(), + } + } + } + + static INPUT: Lazy> = Lazy::new(|| parse_input().unwrap()); + + pub type Baseline = HashMap, Result, BString>>; + + #[derive(Debug)] + pub struct Mapping { + pub remote: BString, + /// `None` if there is no destination/tracking branch + pub local: Option, + } + + pub fn input() -> impl Iterator> + ExactSizeIterator + Clone { + INPUT.iter().map(Ref::to_item) + } + + pub fn of_objects_with_destinations_are_written_into_given_local_branches<'a, 'b>( + specs: impl IntoIterator + Clone, + expected: impl IntoIterator, + ) { + agrees_and_applies_fixes(specs, Vec::new(), expected) + } + + pub fn agrees_and_applies_fixes<'a, 'b>( + specs: impl IntoIterator + Clone, + fixes: impl IntoIterator, + expected: impl IntoIterator, + ) { + check_fetch_remote( + specs, + Mode::Custom { + expected: expected + .into_iter() + .map(|s| { + let spec = git_refspec::parse(s.into(), Operation::Fetch).expect("valid spec"); + Mapping { + remote: spec.source().unwrap().into(), + local: spec.destination().map(ToOwned::to_owned), + } + }) + .collect(), + fixes: fixes.into_iter().collect(), + }, + ) + } + + pub fn of_objects_always_matches_if_the_server_has_the_object<'a, 'b>( + specs: impl IntoIterator + Clone, + ) { + check_fetch_remote(specs, Mode::Normal { validate_err: None }) + } + + pub fn agrees_with_fetch_specs<'a>(specs: impl IntoIterator + Clone) { + check_fetch_remote(specs, Mode::Normal { validate_err: None }) + } + + pub fn agrees_with_fetch_specs_validation_error<'a>( + specs: impl IntoIterator + Clone, + validate_err: impl Into, + ) { + check_fetch_remote( + specs, + Mode::Normal { + validate_err: Some(validate_err.into()), + }, + ) + } + + pub fn invalid_specs_fail_to_parse_where_git_shows_surprising_behaviour<'a>( + specs: impl IntoIterator, + err: git_refspec::parse::Error, + ) { + let err = err.to_string(); + let mut saw_err = false; + for spec in specs { + match git_refspec::parse(spec.into(), Operation::Fetch) { + Ok(_) => {} + Err(e) if e.to_string() == err => { + saw_err = true; + } + Err(err) => panic!("Unexpected parse error: {:?}", err), + } + } + assert!(saw_err, "Failed to see error when parsing specs: {:?}", err) + } + + /// Here we checked by hand which refs are actually written with a particular refspec + pub fn agrees_but_observable_refs_are_vague<'a, 'b>( + specs: impl IntoIterator + Clone, + expected: impl IntoIterator, + ) { + of_objects_with_destinations_are_written_into_given_local_branches(specs, expected) + } + + enum Mode { + Normal { validate_err: Option }, + Custom { expected: Vec, fixes: Vec }, + } + + fn check_fetch_remote<'a>(specs: impl IntoIterator + Clone, mode: Mode) { + let match_group = MatchGroup::from_fetch_specs( + specs + .clone() + .into_iter() + .map(|spec| git_refspec::parse(spec.into(), Operation::Fetch).unwrap()), + ); + + let key: Vec<_> = specs.into_iter().map(BString::from).collect(); + let expected = BASELINE + .get(&key) + .unwrap_or_else(|| panic!("BUG: Need {:?} added to the baseline", key)) + .as_ref(); + + let actual = match_group.match_remotes(input()).validated(); + let (actual, expected) = match &mode { + Mode::Normal { validate_err } => match validate_err { + Some(err_message) => { + assert_eq!(actual.unwrap_err().to_string(), *err_message); + return; + } + None => { + let (actual, fixed) = actual.unwrap(); + assert_eq!( + fixed, + Vec::::new(), + "we don't expect any issues to be fixed here" + ); + (actual.mappings, expected.expect("no error")) + } + }, + Mode::Custom { + expected, + fixes: expected_fixes, + } => { + let (actual, actual_fixes) = actual.unwrap(); + assert_eq!(&actual_fixes, expected_fixes,); + (actual.mappings, expected) + } + }; + assert_eq!( + actual.len(), + expected.len(), + "got a different amount of mappings: {:?} != {:?}", + actual, + expected + ); + + for (idx, (actual, expected)) in actual.iter().zip(expected).enumerate() { + assert_eq!( + source_to_bstring(actual.lhs), + expected.remote, + "{}: remote mismatch", + idx + ); + if let Some(expected) = expected.local.as_ref() { + match actual.rhs.as_ref() { + None => panic!("{}: Expected local ref to be {}, got none", idx, expected), + Some(actual) => assert_eq!(actual.as_ref(), expected, "{}: mismatched local ref", idx), + } + } + } + } + + fn source_to_bstring(source: SourceRef) -> BString { + match source { + SourceRef::FullName(name) => name.into(), + SourceRef::ObjectId(id) => id.to_string().into(), + } + } + + fn parse_input() -> crate::Result> { + let dir = git_testtools::scripted_fixture_repo_read_only("match_baseline.sh")?; + let refs_buf = std::fs::read(dir.join("clone").join("remote-refs.list"))?; + let mut out = Vec::new(); + for line in refs_buf.lines() { + if line.starts_with(b"From ") { + continue; + } + let mut tokens = line.splitn(2, |b| *b == b'\t'); + let target = ObjectId::from_hex(tokens.next().expect("hex-sha"))?; + let name = tokens.next().expect("name"); + if !name.ends_with(b"^{}") { + out.push(Ref { + name: name.into(), + target, + tag: None, + }) + } else { + let last = out.last_mut().unwrap(); + let tag = last.target; + last.target = target; + last.tag = Some(tag); + } + } + Ok(out) + } + + pub(crate) fn parse() -> crate::Result { + let dir = git_testtools::scripted_fixture_repo_read_only("match_baseline.sh")?; + let buf = std::fs::read(dir.join("clone").join("baseline.git"))?; + + let mut map = HashMap::new(); + let mut mappings = Vec::new(); + let mut fatal = None; + for line in buf.lines() { + if line.starts_with(b"From ") { + continue; + } + match line.strip_prefix(b"specs: ") { + Some(specs) => { + let key: Vec<_> = specs.split(|b| *b == b' ').map(BString::from).collect(); + let value = match fatal.take() { + Some(message) => Err(message), + None => Ok(std::mem::take(&mut mappings)), + }; + map.insert(key, value); + } + None => match line.strip_prefix(b"fatal: ") { + Some(message) => { + fatal = Some(message.into()); + } + None => match line.strip_prefix(b"error: * Ignoring funny ref") { + Some(_) => continue, + None => { + let past_note = line + .splitn(2, |b| *b == b']') + .nth(1) + .or_else(|| line.strip_prefix(b" * branch ")) + .or_else(|| line.strip_prefix(b" * tag ")) + .unwrap_or_else(|| panic!("line unhandled: {:?}", line.as_bstr())); + let mut tokens = past_note.split(|b| *b == b' ').filter(|t| !t.is_empty()); + + let mut lhs = tokens.next().unwrap().trim(); + if lhs.as_bstr() == "->" { + lhs = "HEAD".as_bytes(); + } else { + tokens.next(); + }; + let rhs = tokens.next().unwrap().trim(); + let local = (rhs != b"FETCH_HEAD").then(|| full_tracking_ref(rhs.into())); + if !(lhs.as_bstr() == "HEAD" && local.is_none()) { + mappings.push(Mapping { + remote: full_remote_ref(lhs.into()), + local, + }) + } + } + }, + }, + } + } + + Ok(map) + } + + fn looks_like_tag(name: &BString) -> bool { + name.starts_with(b"v0.") || name.starts_with(b"annotated-v0.") + } + + fn full_remote_ref(mut name: BString) -> BString { + if !name.contains(&b'/') { + if looks_like_tag(&name) { + name.insert_str(0, b"refs/tags/"); + } else if let Ok(_id) = git_hash::ObjectId::from_hex(name.as_ref()) { + // keep as is + } else if name != "HEAD" { + name.insert_str(0, b"refs/heads/"); + } + } + name + } + + fn full_tracking_ref(mut name: BString) -> BString { + if name.starts_with_str(b"origin/") || name.starts_with_str("new-origin/") { + name.insert_str(0, b"refs/remotes/"); + } else if looks_like_tag(&name) { + name.insert_str(0, b"refs/tags/"); + } + name + } +} diff --git a/git-refspec/tests/parse/fetch.rs b/git-refspec/tests/parse/fetch.rs index b83d7d1ed30..dbe801ee903 100644 --- a/git-refspec/tests/parse/fetch.rs +++ b/git-refspec/tests/parse/fetch.rs @@ -27,11 +27,15 @@ fn object_hash_as_source() { } #[test] -fn object_hash_destination_is_invalid() { - assert!(matches!( - try_parse("a:e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", Operation::Fetch).unwrap_err(), - Error::InvalidFetchDestination - )); +fn object_hash_destination_are_valid_as_they_might_be_a_strange_partial_branch_name() { + assert_parse( + "a:e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", + Instruction::Fetch(Fetch::AndUpdate { + src: b("a"), + dst: b("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"), + allow_non_fast_forward: false, + }), + ); } #[test] @@ -62,8 +66,18 @@ fn negative_with_destination() { #[test] fn exclude() { - assert_parse("^a", Instruction::Fetch(Fetch::Exclude { src: b("a") })); - assert_parse("^a*", Instruction::Fetch(Fetch::Exclude { src: b("a*") })); + assert!(matches!( + try_parse("^a", Operation::Fetch).unwrap_err(), + Error::NegativePartialName + )); + assert!(matches!( + try_parse("^a*", Operation::Fetch).unwrap_err(), + Error::NegativeGlobPattern + )); + assert_parse( + "^refs/heads/a", + Instruction::Fetch(Fetch::Exclude { src: b("refs/heads/a") }), + ); } #[test] @@ -143,6 +157,19 @@ fn colon_alone_is_for_fetching_head_into_fetchhead() { assert_parse("+:", Instruction::Fetch(Fetch::Only { src: b("HEAD") })); } +#[test] +fn ampersand_on_left_hand_side_is_head() { + assert_parse("@:", Instruction::Fetch(Fetch::Only { src: b("HEAD") })); + assert_parse( + "@:HEAD", + Instruction::Fetch(Fetch::AndUpdate { + src: b("HEAD"), + dst: b("HEAD"), + allow_non_fast_forward: false, + }), + ); +} + #[test] fn empty_refspec_is_enough_for_fetching_head_into_fetchhead() { assert_parse("", Instruction::Fetch(Fetch::Only { src: b("HEAD") })); diff --git a/git-refspec/tests/parse/mod.rs b/git-refspec/tests/parse/mod.rs index d6c022f6d8e..f7c0218f985 100644 --- a/git-refspec/tests/parse/mod.rs +++ b/git-refspec/tests/parse/mod.rs @@ -6,7 +6,7 @@ use git_testtools::scripted_fixture_repo_read_only; #[test] fn baseline() { - let dir = scripted_fixture_repo_read_only("make_baseline.sh").unwrap(); + let dir = scripted_fixture_repo_read_only("parse_baseline.sh").unwrap(); let baseline = std::fs::read(dir.join("baseline.git")).unwrap(); let mut lines = baseline.lines(); let mut panics = 0; @@ -29,7 +29,7 @@ fn baseline() { _ => unreachable!("{} unexpected", kind.as_bstr()), }; let res = catch_unwind(|| try_parse(spec.to_str().unwrap(), op)); - match res { + match &res { Ok(res) => match (res.is_ok(), err_code == 0) { (true, true) | (false, false) => { if let Ok(spec) = res { @@ -37,8 +37,19 @@ fn baseline() { } } _ => { - eprintln!("{err_code} {res:?} {} {:?}", kind.as_bstr(), spec.as_bstr()); - mismatch += 1; + match (res.as_ref().err(), err_code == 0) { + ( + Some( + git_refspec::parse::Error::NegativePartialName + | git_refspec::parse::Error::NegativeGlobPattern, + ), + true, + ) => {} // we prefer failing fast, git let's it pass + _ => { + eprintln!("{err_code} {res:?} {} {:?}", kind.as_bstr(), spec.as_bstr()); + mismatch += 1; + } + } } }, Err(_) => { @@ -57,6 +68,18 @@ fn baseline() { } } +#[test] +fn local_and_remote() -> crate::Result { + let spec = git_refspec::parse("remote:local".into(), Operation::Fetch)?; + assert_eq!(spec.remote(), spec.source()); + assert_eq!(spec.local(), spec.destination()); + + let spec = git_refspec::parse("local:remote".into(), Operation::Push)?; + assert_eq!(spec.local(), spec.source()); + assert_eq!(spec.remote(), spec.destination()); + Ok(()) +} + mod fetch; mod invalid; mod push; diff --git a/git-refspec/tests/refspec.rs b/git-refspec/tests/refspec.rs index ee35817f145..5768dc5c1f9 100644 --- a/git-refspec/tests/refspec.rs +++ b/git-refspec/tests/refspec.rs @@ -1,2 +1,9 @@ +extern crate core; + +use git_testtools::Result; + mod impls; +mod match_group; +mod matching; mod parse; +mod write; diff --git a/git-refspec/tests/write/mod.rs b/git-refspec/tests/write/mod.rs new file mode 100644 index 00000000000..a93ca1d4b8b --- /dev/null +++ b/git-refspec/tests/write/mod.rs @@ -0,0 +1,98 @@ +mod push { + use git_refspec::{instruction, Instruction}; + + #[test] + fn all_matching_branches() { + assert_eq!( + Instruction::Push(instruction::Push::AllMatchingBranches { + allow_non_fast_forward: false + }) + .to_bstring(), + ":" + ); + assert_eq!( + Instruction::Push(instruction::Push::AllMatchingBranches { + allow_non_fast_forward: true + }) + .to_bstring(), + "+:" + ); + } + + #[test] + fn delete() { + assert_eq!( + Instruction::Push(instruction::Push::Delete { + ref_or_pattern: "for-deletion".into(), + }) + .to_bstring(), + ":for-deletion" + ); + } + + #[test] + fn matching() { + assert_eq!( + Instruction::Push(instruction::Push::Matching { + src: "from".into(), + dst: "to".into(), + allow_non_fast_forward: false + }) + .to_bstring(), + "from:to" + ); + assert_eq!( + Instruction::Push(instruction::Push::Matching { + src: "from".into(), + dst: "to".into(), + allow_non_fast_forward: true + }) + .to_bstring(), + "+from:to" + ); + } +} + +mod fetch { + use git_refspec::{instruction, Instruction}; + #[test] + fn only() { + assert_eq!( + Instruction::Fetch(instruction::Fetch::Only { + src: "refs/heads/main".into(), + }) + .to_bstring(), + "refs/heads/main" + ); + } + + #[test] + fn exclude() { + assert_eq!( + Instruction::Fetch(instruction::Fetch::Exclude { src: "excluded".into() }).to_bstring(), + "^excluded" + ); + } + + #[test] + fn and_update() { + assert_eq!( + Instruction::Fetch(instruction::Fetch::AndUpdate { + src: "from".into(), + dst: "to".into(), + allow_non_fast_forward: false + }) + .to_bstring(), + "from:to" + ); + assert_eq!( + Instruction::Fetch(instruction::Fetch::AndUpdate { + src: "from".into(), + dst: "to".into(), + allow_non_fast_forward: true + }) + .to_bstring(), + "+from:to" + ); + } +} diff --git a/git-repository/Cargo.toml b/git-repository/Cargo.toml index f184b8a6c09..81d4454bd5b 100644 --- a/git-repository/Cargo.toml +++ b/git-repository/Cargo.toml @@ -7,7 +7,7 @@ version = "0.24.0" authors = ["Sebastian Thiel "] edition = "2018" include = ["src/**/*", "CHANGELOG.md"] -rust-version = "1.59" +rust-version = "1.60" [lib] doctest = false diff --git a/git-repository/src/remote/connection/list_refs.rs b/git-repository/src/remote/connection/list_refs.rs index aa07edead5a..f497ebdbfc7 100644 --- a/git-repository/src/remote/connection/list_refs.rs +++ b/git-repository/src/remote/connection/list_refs.rs @@ -12,6 +12,8 @@ mod error { ListRefs(#[from] git_protocol::fetch::refs::Error), #[error(transparent)] Transport(#[from] git_protocol::transport::client::Error), + #[error(transparent)] + ConfigureCredentials(#[from] crate::config::credential_helpers::Error), } } pub use error::Error; @@ -34,19 +36,23 @@ where #[git_protocol::maybe_async::maybe_async] async fn fetch_refs(&mut self) -> Result { let mut credentials_storage; - let mut outcome = git_protocol::fetch::handshake( - &mut self.transport, - match self.credentials.as_mut() { - Some(f) => f, - None => { - credentials_storage = Self::configured_credentials(); - &mut credentials_storage - } - }, - Vec::new(), - &mut self.progress, - ) - .await?; + let authenticate = match self.credentials.as_mut() { + Some(f) => f, + None => { + let url = self + .remote + .url(Direction::Fetch) + .map(ToOwned::to_owned) + .unwrap_or_else(|| { + git_url::parse(self.transport.to_url().as_bytes().into()) + .expect("valid URL to be provided by transport") + }); + credentials_storage = self.configured_credentials(url)?; + &mut credentials_storage + } + }; + let mut outcome = + git_protocol::fetch::handshake(&mut self.transport, authenticate, Vec::new(), &mut self.progress).await?; let refs = match outcome.refs.take() { Some(refs) => refs, None => { diff --git a/git-repository/src/remote/connection/mod.rs b/git-repository/src/remote/connection/mod.rs index 53dc83ef558..8e3e4e449e0 100644 --- a/git-repository/src/remote/connection/mod.rs +++ b/git-repository/src/remote/connection/mod.rs @@ -50,10 +50,13 @@ mod access { /// /// It's meant to be used by users of the [`with_credentials()`][Self::with_credentials()] builder to gain access to the /// default way of handling credentials, which they can call as fallback. - // TODO: take url as parameter - pub fn configured_credentials() -> CredentialsFn<'a> { - // TODO: actually fetch this from configuration - Box::new(git_protocol::credentials::builtin) as CredentialsFn<'_> + pub fn configured_credentials( + &self, + url: git_url::Url, + ) -> Result, crate::config::credential_helpers::Error> { + let (mut cascade, _action_with_normalized_url, prompt_opts) = + self.remote.repo.config_snapshot().credential_helpers(url)?; + Ok(Box::new(move |action| cascade.invoke(action, prompt_opts.clone())) as CredentialsFn<'_>) } /// Drop the transport and additional state to regain the original remote. pub fn remote(&self) -> &Remote<'repo> { diff --git a/git-revision/Cargo.toml b/git-revision/Cargo.toml index a9f59668674..f0005dce016 100644 --- a/git-revision/Cargo.toml +++ b/git-revision/Cargo.toml @@ -20,7 +20,7 @@ git-hash = { version = "^0.9.9", path = "../git-hash" } git-object = { version = "^0.20.2", path = "../git-object" } git-date = { version = "^0.1.0", path = "../git-date" } -bstr = { version = "0.2.13", default-features = false, features = ["std"]} +bstr = { version = "1.0.1", default-features = false, features = ["std"]} hash_hasher = "2.0.3" thiserror = "1.0.26" serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"] } diff --git a/git-revision/fuzz/Cargo.toml b/git-revision/fuzz/Cargo.toml index 8b8edef771f..e1faa8ec7ac 100644 --- a/git-revision/fuzz/Cargo.toml +++ b/git-revision/fuzz/Cargo.toml @@ -11,7 +11,7 @@ cargo-fuzz = true [dependencies] libfuzzer-sys = "0.4" git-hash = { path = "../../git-hash" } -bstr = "0.2.15" +bstr = "1.0.1" [dependencies.git-revision] path = ".." diff --git a/git-transport/Cargo.toml b/git-transport/Cargo.toml index 874f09afbc4..943fbec5a46 100644 --- a/git-transport/Cargo.toml +++ b/git-transport/Cargo.toml @@ -54,7 +54,7 @@ git-sec = { version = "^0.3.1", path = "../git-sec" } git-packetline = { version = "^0.12.7", path = "../git-packetline" } serde = { version = "1.0.114", optional = true, default-features = false, features = ["std", "derive"]} -bstr = { version = "0.2.13", default-features = false, features = ["std"] } +bstr = { version = "1.0.1", default-features = false, features = ["std"] } # for async-client async-trait = { version = "0.1.51", optional = true } diff --git a/git-url/Cargo.toml b/git-url/Cargo.toml index ae6c12ae77d..e7820045713 100644 --- a/git-url/Cargo.toml +++ b/git-url/Cargo.toml @@ -13,7 +13,7 @@ doctest = false [features] ## Data structures implement `serde::Serialize` and `serde::Deserialize`. -serde1 = ["serde", "bstr/serde1"] +serde1 = ["serde", "bstr/serde"] [dependencies] git-features = { version = "^0.22.4", path = "../git-features" } @@ -22,7 +22,7 @@ git-path = { version = "^0.4.2", path = "../git-path" } serde = { version = "1.0.114", optional = true, default-features = false, features = ["std", "derive"]} thiserror = "1.0.32" url = "2.1.1" -bstr = { version = "0.2.13", default-features = false, features = ["std"] } +bstr = { version = "1.0.1", default-features = false, features = ["std"] } home = "0.5.3" document-features = { version = "0.2.0", optional = true } diff --git a/git-validate/Cargo.toml b/git-validate/Cargo.toml index cc68ae8bc7f..1227ef717bd 100644 --- a/git-validate/Cargo.toml +++ b/git-validate/Cargo.toml @@ -14,7 +14,7 @@ test = true [dependencies] thiserror = "1.0.34" -bstr = { version = "0.2.13", default-features = false, features = ["std"] } +bstr = { version = "1.0.1", default-features = false, features = ["std"] } [dev-dependencies] git-testtools = { path = "../tests/tools" } diff --git a/git-worktree/Cargo.toml b/git-worktree/Cargo.toml index 2f9e717add1..15aa3452e0d 100644 --- a/git-worktree/Cargo.toml +++ b/git-worktree/Cargo.toml @@ -23,7 +23,7 @@ required-features = ["internal-testing-to-avoid-being-run-by-cargo-test-all"] [features] ## Data structures implement `serde::Serialize` and `serde::Deserialize`. -serde1 = [ "serde", "bstr/serde1", "git-index/serde1", "git-hash/serde1", "git-object/serde1" ] +serde1 = [ "serde", "bstr/serde", "git-index/serde1", "git-hash/serde1", "git-object/serde1" ] internal-testing-git-features-parallel = ["git-features/parallel"] internal-testing-to-avoid-being-run-by-cargo-test-all = [] @@ -40,7 +40,7 @@ git-features = { version = "^0.22.4", path = "../git-features" } serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"]} thiserror = "1.0.26" -bstr = { version = "0.2.13", default-features = false } +bstr = { version = "1.0.1", default-features = false } document-features = { version = "0.2.0", optional = true } io-close = "0.3.7" diff --git a/tests/tools/Cargo.toml b/tests/tools/Cargo.toml index c8a8c694f4a..7ba363252af 100644 --- a/tests/tools/Cargo.toml +++ b/tests/tools/Cargo.toml @@ -21,7 +21,7 @@ git-attributes = { version = "^0.3.3", path = "../../git-attributes" } git-worktree = { version = "^0.4.3", path = "../../git-worktree" } nom = { version = "7", default-features = false, features = ["std"]} -bstr = "0.2.15" +bstr = "1.0.1" crc = "3.0.0" once_cell = "1.8.0" tempfile = "3.2.0"