From 39d5ff39ac58ec2abf2b55ee69df9905a4f303c2 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 12 Sep 2022 17:35:00 +0800 Subject: [PATCH 01/68] a tiny sketch of a possible matching API (#450) It's wrong though, and it turns out that for proper matching, it would need probably need a normalization step to turn partial ref-specs into full names, and from there the matching must also provide tracking branch information or generally, the other side of the spec for later updates. --- git-refspec/src/instruction.rs | 17 +++++++++++++++++ git-refspec/tests/matches/mod.rs | 8 ++++++++ git-refspec/tests/refspec.rs | 1 + 3 files changed, 26 insertions(+) create mode 100644 git-refspec/tests/matches/mod.rs diff --git a/git-refspec/src/instruction.rs b/git-refspec/src/instruction.rs index cde4a09fc27..6dc5ac11367 100644 --- a/git-refspec/src/instruction.rs +++ b/git-refspec/src/instruction.rs @@ -63,3 +63,20 @@ pub enum Fetch<'a> { allow_non_fast_forward: bool, }, } + +/// +pub mod matches { + use crate::instruction::Fetch; + use bstr::BStr; + + impl<'a> Fetch<'a> { + /// For each name in `names`, set the corresponding byte in `matches` to `true` if the corresponding `name` matches the remote side + /// instruction (i.e. the left side of a [`fetch`][types::Mode::Fetch] refspec). + pub fn matches_remote_refs<'b>( + _names: impl Iterator + ExactSizeIterator, + _matches: &mut Vec, + ) { + todo!() + } + } +} diff --git a/git-refspec/tests/matches/mod.rs b/git-refspec/tests/matches/mod.rs new file mode 100644 index 00000000000..f1220ad888b --- /dev/null +++ b/git-refspec/tests/matches/mod.rs @@ -0,0 +1,8 @@ +use git_refspec::instruction::Fetch; + +#[test] +fn fetch_only() { + let _spec = Fetch::Only { + src: "refs/heads/main".into(), + }; +} diff --git a/git-refspec/tests/refspec.rs b/git-refspec/tests/refspec.rs index ee35817f145..2c0383fa6ed 100644 --- a/git-refspec/tests/refspec.rs +++ b/git-refspec/tests/refspec.rs @@ -1,2 +1,3 @@ mod impls; +mod matches; mod parse; From 1b15fe80817d600f39090848c7d144ff94ac398c Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 12 Sep 2022 18:30:43 +0800 Subject: [PATCH 02/68] get more clarity about `git ls-remote` and `git fetch` (#450) Both differ as pattern on the command-line of ls-remote are just that, without needing to be parsed as ref-spec. Thus many pattenrs there seem to work when they should fail (if they were a ref-spec). It's worth noting though that patterns which are source-only can technically work just like a pattern, which would certainly make them more flexible. --- .../fixtures/generated-archives/make_baseline.tar.xz | 4 ++-- git-refspec/tests/fixtures/make_baseline.sh | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/git-refspec/tests/fixtures/generated-archives/make_baseline.tar.xz b/git-refspec/tests/fixtures/generated-archives/make_baseline.tar.xz index aa3836355df..b60886792c8 100644 --- a/git-refspec/tests/fixtures/generated-archives/make_baseline.tar.xz +++ b/git-refspec/tests/fixtures/generated-archives/make_baseline.tar.xz @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9d52f5fe25601c545187b0ac19b004f30488937b6907e8206333d2ba923c7b29 -size 9372 +oid sha256:f7083749e364f85c416925dc304785da9c64b6b77f69be31bd22a0ad1d1bdd89 +size 9404 diff --git a/git-refspec/tests/fixtures/make_baseline.sh b/git-refspec/tests/fixtures/make_baseline.sh index 3e78ce9788a..7f27e0454a9 100644 --- a/git-refspec/tests/fixtures/make_baseline.sh +++ b/git-refspec/tests/fixtures/make_baseline.sh @@ -68,6 +68,9 @@ 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 '+:' @@ -81,6 +84,7 @@ 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 +97,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 '+@' From 095a09918dc080ba7794c6ff13db0ef0ead20d0d Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 12 Sep 2022 21:08:14 +0800 Subject: [PATCH 03/68] more examples using fully spelled out object names as fetch destination (#450) --- git-refspec/src/instruction.rs | 8 +++++--- .../fixtures/generated-archives/make_baseline.tar.xz | 4 ++-- git-refspec/tests/fixtures/make_baseline.sh | 2 ++ 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/git-refspec/src/instruction.rs b/git-refspec/src/instruction.rs index 6dc5ac11367..381fa11ade9 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. @@ -72,8 +73,9 @@ pub mod matches { impl<'a> Fetch<'a> { /// For each name in `names`, set the corresponding byte in `matches` to `true` if the corresponding `name` matches the remote side /// instruction (i.e. the left side of a [`fetch`][types::Mode::Fetch] refspec). + /// Note that `name` is expected to be the full name of a reference. pub fn matches_remote_refs<'b>( - _names: impl Iterator + ExactSizeIterator, + _names: impl Iterator + ExactSizeIterator, _matches: &mut Vec, ) { todo!() diff --git a/git-refspec/tests/fixtures/generated-archives/make_baseline.tar.xz b/git-refspec/tests/fixtures/generated-archives/make_baseline.tar.xz index b60886792c8..f2aed7658a7 100644 --- a/git-refspec/tests/fixtures/generated-archives/make_baseline.tar.xz +++ b/git-refspec/tests/fixtures/generated-archives/make_baseline.tar.xz @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f7083749e364f85c416925dc304785da9c64b6b77f69be31bd22a0ad1d1bdd89 -size 9404 +oid sha256:5ac4c314e26592cf73f7f9b3aefd63cfc5a2c595d22b9f8069321f0edefd6b75 +size 9448 diff --git a/git-refspec/tests/fixtures/make_baseline.sh b/git-refspec/tests/fixtures/make_baseline.sh index 7f27e0454a9..cf87060ab6c 100644 --- a/git-refspec/tests/fixtures/make_baseline.sh +++ b/git-refspec/tests/fixtures/make_baseline.sh @@ -75,11 +75,13 @@ 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/*' From f9aeda88554fb852dd21ae8402000e0da742e2b2 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 12 Sep 2022 21:32:00 +0800 Subject: [PATCH 04/68] frame for `git-fetchhead` crate (#450) Even though not immediately required for baselines, it will be useful to have fetchhead related types and writing capabilities. --- Cargo.lock | 4 ++++ Cargo.toml | 1 + README.md | 1 + crate-status.md | 4 ++++ etc/check-package-size.sh | 1 + git-fetchhead/Cargo.toml | 13 +++++++++++++ git-fetchhead/src/lib.rs | 2 ++ 7 files changed, 26 insertions(+) create mode 100644 git-fetchhead/Cargo.toml create mode 100644 git-fetchhead/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 6fe19ef74ea..7fa306a4e53 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1292,6 +1292,10 @@ dependencies = [ "walkdir", ] +[[package]] +name = "git-fetchhead" +version = "0.0.0" + [[package]] name = "git-filter" version = "0.0.0" 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/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)] From 7aff0856b76043f1e9021fff72ce96d4f69d255e Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 12 Sep 2022 21:33:22 +0800 Subject: [PATCH 05/68] prepare changelog for initial `git-fetchhead` release (#450) --- git-fetchhead/CHANGELOG.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 git-fetchhead/CHANGELOG.md diff --git a/git-fetchhead/CHANGELOG.md b/git-fetchhead/CHANGELOG.md new file mode 100644 index 00000000000..36a29010e49 --- /dev/null +++ b/git-fetchhead/CHANGELOG.md @@ -0,0 +1,29 @@ +# 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). + +## Unreleased + +The initial release to reserve the name. + +### Commit Statistics + + + + - 1 commit 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)** + - frame for `git-fetchhead` crate ([`f9aeda8`](https://github.com/Byron/gitoxide/commit/f9aeda88554fb852dd21ae8402000e0da742e2b2)) +
+ From 48d3e1608ff091aeab39a2eca8029f397c290b75 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 12 Sep 2022 21:33:55 +0800 Subject: [PATCH 06/68] Release git-fetchhead v0.0.0 --- git-fetchhead/CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/git-fetchhead/CHANGELOG.md b/git-fetchhead/CHANGELOG.md index 36a29010e49..a22b3a1589e 100644 --- a/git-fetchhead/CHANGELOG.md +++ b/git-fetchhead/CHANGELOG.md @@ -5,7 +5,7 @@ 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). -## Unreleased +## 0.0.0 (2022-09-12) The initial release to reserve the name. @@ -13,7 +13,7 @@ The initial release to reserve the name. - - 1 commit contributed to the release. + - 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) @@ -24,6 +24,7 @@ The initial release to reserve the name.
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))
From 73b405fe70cf7d53e5e011cf69ea654f4bd96dd2 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 12 Sep 2022 21:35:28 +0800 Subject: [PATCH 07/68] thanks clippy --- git-refspec/src/instruction.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/git-refspec/src/instruction.rs b/git-refspec/src/instruction.rs index 381fa11ade9..eb4d73a39d1 100644 --- a/git-refspec/src/instruction.rs +++ b/git-refspec/src/instruction.rs @@ -72,11 +72,11 @@ pub mod matches { impl<'a> Fetch<'a> { /// For each name in `names`, set the corresponding byte in `matches` to `true` if the corresponding `name` matches the remote side - /// instruction (i.e. the left side of a [`fetch`][types::Mode::Fetch] refspec). + /// instruction (i.e. the left side of a [`fetch`][crate::parse::Operation::Fetch] refspec). /// Note that `name` is expected to be the full name of a reference. pub fn matches_remote_refs<'b>( _names: impl Iterator + ExactSizeIterator, - _matches: &mut Vec, + _matches: (), // TODO: the actual type to write which would contain the tracking branch. ) { todo!() } From 0887e2e0b7ebdcad30606a2633794ac8ff586091 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 12 Sep 2022 21:55:06 +0800 Subject: [PATCH 08/68] prefer to represent instructions with Matchers (#450) --- git-refspec/src/instruction.rs | 10 ++++------ git-refspec/src/lib.rs | 16 +++++++++++++++- git-refspec/src/types.rs | 3 +++ 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/git-refspec/src/instruction.rs b/git-refspec/src/instruction.rs index eb4d73a39d1..55e06f38edd 100644 --- a/git-refspec/src/instruction.rs +++ b/git-refspec/src/instruction.rs @@ -66,18 +66,16 @@ pub enum Fetch<'a> { } /// -pub mod matches { +mod matcher { use crate::instruction::Fetch; - use bstr::BStr; + use crate::Matcher; impl<'a> Fetch<'a> { /// For each name in `names`, set the corresponding byte in `matches` to `true` if the corresponding `name` matches the remote side /// instruction (i.e. the left side of a [`fetch`][crate::parse::Operation::Fetch] refspec). /// Note that `name` is expected to be the full name of a reference. - pub fn matches_remote_refs<'b>( - _names: impl Iterator + ExactSizeIterator, - _matches: (), // TODO: the actual type to write which would contain the tracking branch. - ) { + // TODO: move docs to Matcher, adjust these. + pub fn to_matcher<'b>(&self) -> Matcher { todo!() } } diff --git a/git-refspec/src/lib.rs b/git-refspec/src/lib.rs index f53fdcdc013..f044d73de6f 100644 --- a/git-refspec/src/lib.rs +++ b/git-refspec/src/lib.rs @@ -29,5 +29,19 @@ pub struct RefSpec { mod spec; +/// +pub mod matcher { + use bstr::BStr; + use git_hash::oid; + + /// An item to match + pub enum Item<'a> { + /// An object id + Oid(&'a oid), + /// The full name of a reference. + FullRefName(&'a BStr), + } +} + mod types; -pub use types::Instruction; +pub use types::{Instruction, Matcher}; diff --git a/git-refspec/src/types.rs b/git-refspec/src/types.rs index 0a0e24e36cc..5197f4a1bf4 100644 --- a/git-refspec/src/types.rs +++ b/git-refspec/src/types.rs @@ -19,3 +19,6 @@ pub enum Instruction<'a> { /// An instruction for fetching. Fetch(instruction::Fetch<'a>), } + +/// A type keeping enough information about a ref-spec to be able to efficiently match it against matcher items. +pub struct Matcher {} From 99905bacace8aed42b16d43f0f04cae996cb971c Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 12 Sep 2022 22:02:46 +0800 Subject: [PATCH 09/68] change!: upgrade `bstr` to `1.0.1` (#450) --- .github/workflows/msrv.yml | 2 +- Cargo.lock | 66 ++++++++++++++++++++-------------- etc/msrv-badge.svg | 2 +- git-actor/Cargo.toml | 4 +-- git-attributes/Cargo.toml | 4 +-- git-commitgraph/Cargo.toml | 4 +-- git-config-value/Cargo.toml | 4 +-- git-config/Cargo.toml | 4 +-- git-credentials/Cargo.toml | 4 +-- git-date/Cargo.toml | 4 +-- git-discover/Cargo.toml | 2 +- git-glob/Cargo.toml | 4 +-- git-index/Cargo.toml | 2 +- git-mailmap/Cargo.toml | 4 +-- git-object/Cargo.toml | 4 +-- git-pack/Cargo.toml | 2 +- git-packetline/Cargo.toml | 4 +-- git-path/Cargo.toml | 2 +- git-pathspec/Cargo.toml | 2 +- git-protocol/Cargo.toml | 4 +-- git-quote/Cargo.toml | 2 +- git-refspec/Cargo.toml | 2 +- git-refspec/src/instruction.rs | 2 +- git-repository/Cargo.toml | 2 +- git-revision/Cargo.toml | 2 +- git-revision/fuzz/Cargo.toml | 2 +- git-transport/Cargo.toml | 2 +- git-url/Cargo.toml | 4 +-- git-validate/Cargo.toml | 2 +- git-worktree/Cargo.toml | 4 +-- tests/tools/Cargo.toml | 2 +- 31 files changed, 83 insertions(+), 71 deletions(-) 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 7fa306a4e53..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", @@ -1305,7 +1317,7 @@ name = "git-glob" version = "0.3.2" dependencies = [ "bitflags", - "bstr", + "bstr 1.0.1", "document-features", "git-testtools", "serde", @@ -1328,7 +1340,7 @@ version = "0.4.3" dependencies = [ "atoi", "bitflags", - "bstr", + "bstr 1.0.1", "document-features", "filetime", "git-bitmap", @@ -1361,7 +1373,7 @@ dependencies = [ name = "git-mailmap" version = "0.3.2" dependencies = [ - "bstr", + "bstr 1.0.1", "document-features", "git-actor", "git-testtools", @@ -1377,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", @@ -1421,7 +1433,7 @@ dependencies = [ name = "git-pack" version = "0.22.0" dependencies = [ - "bstr", + "bstr 1.0.1", "bytesize", "clru", "dashmap", @@ -1452,7 +1464,7 @@ name = "git-packetline" version = "0.12.7" dependencies = [ "async-std", - "bstr", + "bstr 1.0.1", "document-features", "futures-io", "futures-lite", @@ -1469,7 +1481,7 @@ dependencies = [ name = "git-path" version = "0.4.2" dependencies = [ - "bstr", + "bstr 1.0.1", "tempfile", "thiserror", ] @@ -1479,7 +1491,7 @@ name = "git-pathspec" version = "0.0.0" dependencies = [ "bitflags", - "bstr", + "bstr 1.0.1", "git-attributes", "git-glob", "git-testtools", @@ -1507,7 +1519,7 @@ version = "0.20.0" dependencies = [ "async-std", "async-trait", - "bstr", + "bstr 1.0.1", "btoi", "document-features", "futures-io", @@ -1528,7 +1540,7 @@ dependencies = [ name = "git-quote" version = "0.2.1" dependencies = [ - "bstr", + "bstr 1.0.1", "btoi", "quick-error", ] @@ -1564,7 +1576,7 @@ dependencies = [ name = "git-refspec" version = "0.1.1" dependencies = [ - "bstr", + "bstr 1.0.1", "git-hash", "git-revision", "git-testtools", @@ -1630,7 +1642,7 @@ dependencies = [ name = "git-revision" version = "0.4.4" dependencies = [ - "bstr", + "bstr 1.0.1", "document-features", "git-date", "git-hash", @@ -1680,7 +1692,7 @@ dependencies = [ name = "git-testtools" version = "0.8.0" dependencies = [ - "bstr", + "bstr 1.0.1", "crc", "fs_extra", "git-attributes", @@ -1710,7 +1722,7 @@ dependencies = [ "async-trait", "base64", "blocking", - "bstr", + "bstr 1.0.1", "curl", "document-features", "futures-io", @@ -1747,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", @@ -1761,7 +1773,7 @@ dependencies = [ name = "git-validate" version = "0.5.5" dependencies = [ - "bstr", + "bstr 1.0.1", "git-testtools", "thiserror", ] @@ -1770,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/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-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-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 55e06f38edd..0f8941823f2 100644 --- a/git-refspec/src/instruction.rs +++ b/git-refspec/src/instruction.rs @@ -75,7 +75,7 @@ mod matcher { /// instruction (i.e. the left side of a [`fetch`][crate::parse::Operation::Fetch] refspec). /// Note that `name` is expected to be the full name of a reference. // TODO: move docs to Matcher, adjust these. - pub fn to_matcher<'b>(&self) -> Matcher { + pub fn to_matcher(&self) -> Matcher { todo!() } } 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-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" From 2569da5988a055372a1b85660f93185603900dbe Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 13 Sep 2022 09:08:07 +0800 Subject: [PATCH 10/68] frame for baseline for fetch-matching (#450) --- git-glob/tests/pattern/matching.rs | 2 +- .../fixtures/generated-archives/.gitignore | 1 + .../generated-archives/make_baseline.tar.xz | 3 -- .../generated-archives/parse_baseline.tar.xz | 3 ++ git-refspec/tests/fixtures/match_baseline.sh | 36 +++++++++++++++++++ .../{make_baseline.sh => parse_baseline.sh} | 0 git-refspec/tests/matcher/mod.rs | 16 +++++++++ git-refspec/tests/matches/mod.rs | 8 ----- git-refspec/tests/parse/mod.rs | 2 +- git-refspec/tests/refspec.rs | 4 ++- 10 files changed, 61 insertions(+), 14 deletions(-) create mode 100644 git-refspec/tests/fixtures/generated-archives/.gitignore delete mode 100644 git-refspec/tests/fixtures/generated-archives/make_baseline.tar.xz create mode 100644 git-refspec/tests/fixtures/generated-archives/parse_baseline.tar.xz create mode 100644 git-refspec/tests/fixtures/match_baseline.sh rename git-refspec/tests/fixtures/{make_baseline.sh => parse_baseline.sh} (100%) create mode 100644 git-refspec/tests/matcher/mod.rs delete mode 100644 git-refspec/tests/matches/mod.rs diff --git a/git-glob/tests/pattern/matching.rs b/git-glob/tests/pattern/matching.rs index 4dddf804ecf..fef11a27bee 100644 --- a/git-glob/tests/pattern/matching.rs +++ b/git-glob/tests/pattern/matching.rs @@ -43,7 +43,7 @@ impl<'a> Baseline<'a> { #[test] fn compare_baseline_with_ours() { - let dir = git_testtools::scripted_fixture_repo_read_only("make_baseline.sh").unwrap(); + let dir = git_testtools::scripted_fixture_repo_read_only("parse_baseline.sh").unwrap(); let (mut total_matches, mut total_correct, mut panics) = (0, 0, 0); let mut mismatches = Vec::new(); for (input_file, expected_matches, case) in &[ 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 f2aed7658a7..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:5ac4c314e26592cf73f7f9b3aefd63cfc5a2c595d22b9f8069321f0edefd6b75 -size 9448 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..cf9a3a05dda --- /dev/null +++ b/git-refspec/tests/fixtures/match_baseline.sh @@ -0,0 +1,36 @@ +#!/bin/bash +set -eu -o pipefail + +git init; + +function baseline() { + { + git fetch -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 clone --shared ./base clone +(cd clone + baseline "refs/heads/main" + baseline "heads/main" + baseline "main" +) + diff --git a/git-refspec/tests/fixtures/make_baseline.sh b/git-refspec/tests/fixtures/parse_baseline.sh similarity index 100% rename from git-refspec/tests/fixtures/make_baseline.sh rename to git-refspec/tests/fixtures/parse_baseline.sh diff --git a/git-refspec/tests/matcher/mod.rs b/git-refspec/tests/matcher/mod.rs new file mode 100644 index 00000000000..4b3bdcc724b --- /dev/null +++ b/git-refspec/tests/matcher/mod.rs @@ -0,0 +1,16 @@ +use git_refspec::instruction::Fetch; + +#[test] +fn fetch_only() { + baseline::parse().unwrap(); + let _spec = Fetch::Only { + src: "refs/heads/main".into(), + }; +} + +mod baseline { + pub fn parse() -> crate::Result { + let _ = git_testtools::scripted_fixture_repo_read_only("match_baseline.sh")?; + Ok(()) + } +} diff --git a/git-refspec/tests/matches/mod.rs b/git-refspec/tests/matches/mod.rs deleted file mode 100644 index f1220ad888b..00000000000 --- a/git-refspec/tests/matches/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -use git_refspec::instruction::Fetch; - -#[test] -fn fetch_only() { - let _spec = Fetch::Only { - src: "refs/heads/main".into(), - }; -} diff --git a/git-refspec/tests/parse/mod.rs b/git-refspec/tests/parse/mod.rs index d6c022f6d8e..d0bcdd54a7e 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; diff --git a/git-refspec/tests/refspec.rs b/git-refspec/tests/refspec.rs index 2c0383fa6ed..fded8b9f18d 100644 --- a/git-refspec/tests/refspec.rs +++ b/git-refspec/tests/refspec.rs @@ -1,3 +1,5 @@ +use git_testtools::Result; + mod impls; -mod matches; +mod matcher; mod parse; From fce877f8d2112fafdb71208784104a66b2313a40 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 13 Sep 2022 09:55:58 +0800 Subject: [PATCH 11/68] parse baseline reflist which serves as input to the matcher (#450) --- git-refspec/src/lib.rs | 12 ++++--- git-refspec/tests/fixtures/match_baseline.sh | 5 ++- git-refspec/tests/matcher/mod.rs | 35 ++++++++++++++++++++ 3 files changed, 46 insertions(+), 6 deletions(-) diff --git a/git-refspec/src/lib.rs b/git-refspec/src/lib.rs index f044d73de6f..9b509c419f9 100644 --- a/git-refspec/src/lib.rs +++ b/git-refspec/src/lib.rs @@ -35,11 +35,13 @@ pub mod matcher { use git_hash::oid; /// An item to match - pub enum Item<'a> { - /// An object id - Oid(&'a oid), - /// The full name of a reference. - FullRefName(&'a BStr), + 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>, } } diff --git a/git-refspec/tests/fixtures/match_baseline.sh b/git-refspec/tests/fixtures/match_baseline.sh index cf9a3a05dda..2d612ab442e 100644 --- a/git-refspec/tests/fixtures/match_baseline.sh +++ b/git-refspec/tests/fixtures/match_baseline.sh @@ -5,7 +5,7 @@ git init; function baseline() { { - git fetch -v origin "$@" 2>&1 + git fetch -v origin "$@" 2>&1 | tail +2 echo specs: "$@" } >> baseline.git } @@ -29,8 +29,11 @@ mkdir base git clone --shared ./base clone (cd clone + git ls-remote 2>&1 | tail +2 > remote-refs.list baseline "refs/heads/main" baseline "heads/main" baseline "main" + baseline "78b1c1be9421b33a49a7a8176d93eeeafa112da1" + baseline "9d2fab1a0ba3585d0bc50922bfdd04ebb59361df" ) diff --git a/git-refspec/tests/matcher/mod.rs b/git-refspec/tests/matcher/mod.rs index 4b3bdcc724b..1984d179a02 100644 --- a/git-refspec/tests/matcher/mod.rs +++ b/git-refspec/tests/matcher/mod.rs @@ -2,6 +2,7 @@ use git_refspec::instruction::Fetch; #[test] fn fetch_only() { + baseline::parse_input().unwrap(); baseline::parse().unwrap(); let _spec = Fetch::Only { src: "refs/heads/main".into(), @@ -9,6 +10,40 @@ fn fetch_only() { } mod baseline { + use bstr::{BString, ByteSlice}; + use git_hash::ObjectId; + + #[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, + } + + pub 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() { + 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 fn parse() -> crate::Result { let _ = git_testtools::scripted_fixture_repo_read_only("match_baseline.sh")?; Ok(()) From 3000a14c1eed4a543fdef2fd8bcbacba2742aece Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 13 Sep 2022 10:12:33 +0800 Subject: [PATCH 12/68] parse basline mapping (#450) --- git-refspec/tests/fixtures/match_baseline.sh | 7 +++- git-refspec/tests/matcher/mod.rs | 42 ++++++++++++++++++-- 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/git-refspec/tests/fixtures/match_baseline.sh b/git-refspec/tests/fixtures/match_baseline.sh index 2d612ab442e..62e553c48e6 100644 --- a/git-refspec/tests/fixtures/match_baseline.sh +++ b/git-refspec/tests/fixtures/match_baseline.sh @@ -5,7 +5,7 @@ git init; function baseline() { { - git fetch -v origin "$@" 2>&1 | tail +2 + git fetch --dry-run -v origin "$@" 2>&1 | tail +2 echo specs: "$@" } >> baseline.git } @@ -33,6 +33,11 @@ git clone --shared ./base clone baseline "refs/heads/main" baseline "heads/main" baseline "main" + baseline "+refs/heads/*:refs/remotes/origin/*" + baseline "refs/heads/*:refs/remotes/origin/*" "^main" + 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 "78b1c1be9421b33a49a7a8176d93eeeafa112da1" baseline "9d2fab1a0ba3585d0bc50922bfdd04ebb59361df" ) diff --git a/git-refspec/tests/matcher/mod.rs b/git-refspec/tests/matcher/mod.rs index 1984d179a02..948c1f75753 100644 --- a/git-refspec/tests/matcher/mod.rs +++ b/git-refspec/tests/matcher/mod.rs @@ -12,6 +12,7 @@ fn fetch_only() { mod baseline { use bstr::{BString, ByteSlice}; use git_hash::ObjectId; + use std::collections::HashMap; #[derive(Debug)] pub struct Ref { @@ -21,6 +22,12 @@ mod baseline { pub tag: Option, } + #[derive(Debug)] + pub struct Mapping { + pub remote: BString, + pub local: BString, + } + pub 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"))?; @@ -44,8 +51,37 @@ mod baseline { } Ok(out) } - pub fn parse() -> crate::Result { - let _ = git_testtools::scripted_fixture_repo_read_only("match_baseline.sh")?; - Ok(()) + + pub fn parse() -> crate::Result, Vec>> { + 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(); + for line in buf.lines() { + if line.ends_with(b"FETCH_HEAD") { + continue; + } + match line.strip_prefix(b"specs: ") { + Some(specs) => { + let key: Vec<_> = specs.split(|b| *b == b' ').map(BString::from).collect(); + map.insert(key, std::mem::take(&mut mappings)); + } + None => { + let past_note = line.splitn(2, |b| *b == b']').nth(1).unwrap(); + let mut tokens = past_note.split(|b| *b == b' ').filter(|t| !t.is_empty()); + + let lhs = tokens.next().unwrap().trim(); + drop(tokens.next()); + let rhs = tokens.next().unwrap().trim(); + mappings.push(Mapping { + remote: lhs.into(), + local: rhs.into(), + }) + } + } + } + + Ok(map) } } From f6124db39dc0e828801a59310265d95a755ea46a Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 13 Sep 2022 10:48:35 +0800 Subject: [PATCH 13/68] restore full ref names for baseline (#450) --- git-refspec/tests/matcher/mod.rs | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/git-refspec/tests/matcher/mod.rs b/git-refspec/tests/matcher/mod.rs index 948c1f75753..774121c5d9a 100644 --- a/git-refspec/tests/matcher/mod.rs +++ b/git-refspec/tests/matcher/mod.rs @@ -10,7 +10,7 @@ fn fetch_only() { } mod baseline { - use bstr::{BString, ByteSlice}; + use bstr::{BString, ByteSlice, ByteVec}; use git_hash::ObjectId; use std::collections::HashMap; @@ -75,8 +75,8 @@ mod baseline { drop(tokens.next()); let rhs = tokens.next().unwrap().trim(); mappings.push(Mapping { - remote: lhs.into(), - local: rhs.into(), + remote: full_remote_ref(lhs.into()), + local: full_tracking_ref(rhs.into()), }) } } @@ -84,4 +84,18 @@ mod baseline { Ok(map) } + + fn full_remote_ref(mut name: BString) -> BString { + if !name.contains(&b'/') { + name.insert_str(0, b"refs/heads/"); + } + name + } + + fn full_tracking_ref(mut name: BString) -> BString { + if name.starts_with_str(b"origin/") { + name.insert_str(0, b"refs/remotes/"); + } + name + } } From 44228a0b9c057bcc915bc0ade43b4ccb3cb916f2 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 13 Sep 2022 11:15:27 +0800 Subject: [PATCH 14/68] pares FETCH_HEAD (as specs without local sides); sketch `Match` type (#450) --- git-refspec/src/instruction.rs | 2 +- git-refspec/src/lib.rs | 17 ++--------- git-refspec/src/matcher.rs | 32 ++++++++++++++++++++ git-refspec/src/types.rs | 17 +++++++++-- git-refspec/tests/fixtures/match_baseline.sh | 2 +- git-refspec/tests/matcher/mod.rs | 25 ++++++++++----- git-refspec/tests/refspec.rs | 2 ++ 7 files changed, 71 insertions(+), 26 deletions(-) create mode 100644 git-refspec/src/matcher.rs diff --git a/git-refspec/src/instruction.rs b/git-refspec/src/instruction.rs index 0f8941823f2..66077de8ac3 100644 --- a/git-refspec/src/instruction.rs +++ b/git-refspec/src/instruction.rs @@ -75,7 +75,7 @@ mod matcher { /// instruction (i.e. the left side of a [`fetch`][crate::parse::Operation::Fetch] refspec). /// Note that `name` is expected to be the full name of a reference. // TODO: move docs to Matcher, adjust these. - pub fn to_matcher(&self) -> Matcher { + pub fn to_matcher(&self) -> Matcher<'_> { todo!() } } diff --git a/git-refspec/src/lib.rs b/git-refspec/src/lib.rs index 9b509c419f9..bd2356196e8 100644 --- a/git-refspec/src/lib.rs +++ b/git-refspec/src/lib.rs @@ -30,20 +30,7 @@ pub struct RefSpec { mod spec; /// -pub mod matcher { - use bstr::BStr; - use git_hash::oid; - - /// An item to match - 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>, - } -} +pub mod matcher; mod types; -pub use types::{Instruction, Matcher}; +pub use types::{Instruction, Match, Matcher}; diff --git a/git-refspec/src/matcher.rs b/git-refspec/src/matcher.rs new file mode 100644 index 00000000000..9cdaa90d79b --- /dev/null +++ b/git-refspec/src/matcher.rs @@ -0,0 +1,32 @@ +use crate::Match; +use bstr::BStr; +use git_hash::oid; + +/// An item to match +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>, +} + +impl Match<'_> { + /// Return true if we are representing an actual match + pub fn matched(&self) -> bool { + self.lhs.is_some() + } + + /// Return the remote side (i.e. left side) of the fetch ref-spec that matched, or `None` if it didn't match. + pub fn remote(&self) -> Option<&BStr> { + self.lhs + } + + /// Return the local side (i.e. right side) of the fetch ref-spec that matched, or `None` if it didn't match. + /// + /// This is also called a tracking ref. + pub fn local(&self) -> Option<&BStr> { + self.rhs + } +} diff --git a/git-refspec/src/types.rs b/git-refspec/src/types.rs index 5197f4a1bf4..8f80989a4b9 100644 --- a/git-refspec/src/types.rs +++ b/git-refspec/src/types.rs @@ -20,5 +20,18 @@ pub enum Instruction<'a> { Fetch(instruction::Fetch<'a>), } -/// A type keeping enough information about a ref-spec to be able to efficiently match it against matcher items. -pub struct Matcher {} +/// A type keeping enough information about a ref-spec to be able to efficiently match it against multiple matcher items. +#[allow(dead_code)] +pub struct Matcher<'a> { + /// How to interpret our lefthand-side and right-hand side ref-specs + op: crate::parse::Operation, + lhs: &'a bstr::BStr, +} + +/// The result of a match operation. +#[derive(Default)] +#[allow(dead_code)] +pub struct Match<'a> { + pub(crate) lhs: Option<&'a bstr::BStr>, + pub(crate) rhs: Option<&'a bstr::BStr>, +} diff --git a/git-refspec/tests/fixtures/match_baseline.sh b/git-refspec/tests/fixtures/match_baseline.sh index 62e553c48e6..c5fabdf0b3d 100644 --- a/git-refspec/tests/fixtures/match_baseline.sh +++ b/git-refspec/tests/fixtures/match_baseline.sh @@ -5,7 +5,7 @@ git init; function baseline() { { - git fetch --dry-run -v origin "$@" 2>&1 | tail +2 + git fetch --refmap= --dry-run -v origin "$@" 2>&1 | tail +2 echo specs: "$@" } >> baseline.git } diff --git a/git-refspec/tests/matcher/mod.rs b/git-refspec/tests/matcher/mod.rs index 774121c5d9a..f4b03cf3dd2 100644 --- a/git-refspec/tests/matcher/mod.rs +++ b/git-refspec/tests/matcher/mod.rs @@ -1,5 +1,14 @@ use git_refspec::instruction::Fetch; +mod match_ { + use git_refspec::Match; + + #[test] + fn default_is_not_matched() { + assert!(!Match::default().matched()) + } +} + #[test] fn fetch_only() { baseline::parse_input().unwrap(); @@ -25,7 +34,8 @@ mod baseline { #[derive(Debug)] pub struct Mapping { pub remote: BString, - pub local: BString, + /// `None` if there is no destination/tracking branch + pub local: Option, } pub fn parse_input() -> crate::Result> { @@ -59,24 +69,25 @@ mod baseline { let mut map = HashMap::new(); let mut mappings = Vec::new(); for line in buf.lines() { - if line.ends_with(b"FETCH_HEAD") { - continue; - } match line.strip_prefix(b"specs: ") { Some(specs) => { let key: Vec<_> = specs.split(|b| *b == b' ').map(BString::from).collect(); map.insert(key, std::mem::take(&mut mappings)); } None => { - let past_note = line.splitn(2, |b| *b == b']').nth(1).unwrap(); + let past_note = line + .splitn(2, |b| *b == b']') + .nth(1) + .or_else(|| line.strip_prefix(b" * branch ")) + .unwrap(); let mut tokens = past_note.split(|b| *b == b' ').filter(|t| !t.is_empty()); let lhs = tokens.next().unwrap().trim(); - drop(tokens.next()); + tokens.next(); let rhs = tokens.next().unwrap().trim(); mappings.push(Mapping { remote: full_remote_ref(lhs.into()), - local: full_tracking_ref(rhs.into()), + local: (rhs != b"FETCH_HEAD").then(|| full_tracking_ref(rhs.into())), }) } } diff --git a/git-refspec/tests/refspec.rs b/git-refspec/tests/refspec.rs index fded8b9f18d..8b4366e250e 100644 --- a/git-refspec/tests/refspec.rs +++ b/git-refspec/tests/refspec.rs @@ -1,3 +1,5 @@ +extern crate core; + use git_testtools::Result; mod impls; From 0c17681602ef74cb5e9329d489df6fd7a07d546d Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 13 Sep 2022 14:22:22 +0800 Subject: [PATCH 15/68] fix git-glob tests (#450) --- git-glob/tests/pattern/matching.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git-glob/tests/pattern/matching.rs b/git-glob/tests/pattern/matching.rs index fef11a27bee..4dddf804ecf 100644 --- a/git-glob/tests/pattern/matching.rs +++ b/git-glob/tests/pattern/matching.rs @@ -43,7 +43,7 @@ impl<'a> Baseline<'a> { #[test] fn compare_baseline_with_ours() { - let dir = git_testtools::scripted_fixture_repo_read_only("parse_baseline.sh").unwrap(); + let dir = git_testtools::scripted_fixture_repo_read_only("make_baseline.sh").unwrap(); let (mut total_matches, mut total_correct, mut panics) = (0, 0, 0); let mut mismatches = Vec::new(); for (input_file, expected_matches, case) in &[ From 54ca267138a5116aa2215109b4abe00a64518feb Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 13 Sep 2022 15:15:20 +0800 Subject: [PATCH 16/68] more robust baseline tests on windows (#450) Turns out that `tail +2` or channel redirection may reorder lines, or the timing is very different to cause certain output to end up at the end of the stream. Maibe `tail +2` just works differently. --- git-refspec/tests/fixtures/match_baseline.sh | 4 ++-- git-refspec/tests/matcher/mod.rs | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/git-refspec/tests/fixtures/match_baseline.sh b/git-refspec/tests/fixtures/match_baseline.sh index c5fabdf0b3d..141b165319a 100644 --- a/git-refspec/tests/fixtures/match_baseline.sh +++ b/git-refspec/tests/fixtures/match_baseline.sh @@ -5,7 +5,7 @@ git init; function baseline() { { - git fetch --refmap= --dry-run -v origin "$@" 2>&1 | tail +2 + git fetch --refmap= --dry-run -v origin "$@" 2>&1 echo specs: "$@" } >> baseline.git } @@ -29,7 +29,7 @@ mkdir base git clone --shared ./base clone (cd clone - git ls-remote 2>&1 | tail +2 > remote-refs.list + git ls-remote 2>&1 > remote-refs.list baseline "refs/heads/main" baseline "heads/main" baseline "main" diff --git a/git-refspec/tests/matcher/mod.rs b/git-refspec/tests/matcher/mod.rs index f4b03cf3dd2..6af560f6e46 100644 --- a/git-refspec/tests/matcher/mod.rs +++ b/git-refspec/tests/matcher/mod.rs @@ -43,6 +43,9 @@ mod baseline { 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"); @@ -69,6 +72,9 @@ mod baseline { let mut map = HashMap::new(); let mut mappings = Vec::new(); 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(); From dd1d8244c8708bbc3583cc0f3f42ad967d5ad524 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 13 Sep 2022 15:17:23 +0800 Subject: [PATCH 17/68] a more realistic sketch for `Matcher`, which will need a surrounding `MatchGroup` (#450) The group can handle things like ambiguity which can later lead to rejection and error message generation. --- git-refspec/src/instruction.rs | 16 ---------------- git-refspec/src/matcher.rs | 16 +++++++++++++++- git-refspec/src/spec.rs | 20 ++++++++++++++++++++ git-refspec/src/types.rs | 8 +++++--- 4 files changed, 40 insertions(+), 20 deletions(-) diff --git a/git-refspec/src/instruction.rs b/git-refspec/src/instruction.rs index 66077de8ac3..c37ceebcd35 100644 --- a/git-refspec/src/instruction.rs +++ b/git-refspec/src/instruction.rs @@ -64,19 +64,3 @@ pub enum Fetch<'a> { allow_non_fast_forward: bool, }, } - -/// -mod matcher { - use crate::instruction::Fetch; - use crate::Matcher; - - impl<'a> Fetch<'a> { - /// For each name in `names`, set the corresponding byte in `matches` to `true` if the corresponding `name` matches the remote side - /// instruction (i.e. the left side of a [`fetch`][crate::parse::Operation::Fetch] refspec). - /// Note that `name` is expected to be the full name of a reference. - // TODO: move docs to Matcher, adjust these. - pub fn to_matcher(&self) -> Matcher<'_> { - todo!() - } - } -} diff --git a/git-refspec/src/matcher.rs b/git-refspec/src/matcher.rs index 9cdaa90d79b..dd6d94ad4f2 100644 --- a/git-refspec/src/matcher.rs +++ b/git-refspec/src/matcher.rs @@ -1,6 +1,20 @@ use crate::Match; use bstr::BStr; -use git_hash::oid; +use git_hash::{oid, ObjectId}; + +#[allow(dead_code)] +pub(crate) enum Needle<'a> { + FullName(&'a BStr), + PartialName(&'a BStr), + Glob { glob: &'a BStr, asterisk_pos: usize }, + Object(ObjectId), +} + +impl<'a> From<&'a BStr> for Needle<'a> { + fn from(_v: &'a BStr) -> Self { + todo!() + } +} /// An item to match pub struct Item<'a> { diff --git a/git-refspec/src/spec.rs b/git-refspec/src/spec.rs index f99ead73250..26bb9751f84 100644 --- a/git-refspec/src/spec.rs +++ b/git-refspec/src/spec.rs @@ -129,6 +129,26 @@ impl RefSpecRef<'_> { } } +/// +mod matcher { + use crate::{Matcher, RefSpecRef}; + + impl<'a> RefSpecRef<'a> { + /// For each name in `names`, set the corresponding byte in `matches` to `true` if the corresponding `name` matches the remote side + /// instruction (i.e. the left side of a [`fetch`][crate::parse::Operation::Fetch] refspec). + /// Note that `name` is expected to be the full name of a reference. + // TODO: move docs to Matcher, adjust these. + pub fn to_matcher(&self) -> Matcher<'_> { + Matcher { + op: self.op, + mode: self.mode, + src: self.src.map(Into::into), + dst: self.dst.map(Into::into), + } + } + } +} + /// Conversion impl RefSpecRef<'_> { /// Convert this ref into a standalone, owned copy. diff --git a/git-refspec/src/types.rs b/git-refspec/src/types.rs index 8f80989a4b9..7a231cafebf 100644 --- a/git-refspec/src/types.rs +++ b/git-refspec/src/types.rs @@ -1,4 +1,4 @@ -use crate::instruction; +use crate::{instruction, matcher}; /// The way to interpret a refspec. #[derive(PartialOrd, Ord, PartialEq, Eq, Copy, Clone, Hash, Debug)] @@ -24,8 +24,10 @@ pub enum Instruction<'a> { #[allow(dead_code)] pub struct Matcher<'a> { /// How to interpret our lefthand-side and right-hand side ref-specs - op: crate::parse::Operation, - lhs: &'a bstr::BStr, + pub(crate) mode: crate::types::Mode, + pub(crate) op: crate::parse::Operation, + pub(crate) src: Option>, + pub(crate) dst: Option>, } /// The result of a match operation. From 192d4f78ba611f090dafda7ef5014efb900d2115 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 13 Sep 2022 15:36:10 +0800 Subject: [PATCH 18/68] more tests to investigate conflict handling (#450) --- git-refspec/tests/fixtures/match_baseline.sh | 15 +++++-- git-refspec/tests/matcher/mod.rs | 44 ++++++++++++-------- 2 files changed, 39 insertions(+), 20 deletions(-) diff --git a/git-refspec/tests/fixtures/match_baseline.sh b/git-refspec/tests/fixtures/match_baseline.sh index 141b165319a..f3776feec4e 100644 --- a/git-refspec/tests/fixtures/match_baseline.sh +++ b/git-refspec/tests/fixtures/match_baseline.sh @@ -5,7 +5,7 @@ git init; function baseline() { { - git fetch --refmap= --dry-run -v origin "$@" 2>&1 + git fetch --refmap= --dry-run -v origin "$@" 2>&1 || : echo specs: "$@" } >> baseline.git } @@ -25,6 +25,10 @@ mkdir base 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 @@ -33,12 +37,17 @@ git clone --shared ./base clone baseline "refs/heads/main" baseline "heads/main" baseline "main" + baseline "78b1c1be9421b33a49a7a8176d93eeeafa112da1" + baseline "9d2fab1a0ba3585d0bc50922bfdd04ebb59361df" baseline "+refs/heads/*:refs/remotes/origin/*" baseline "refs/heads/*:refs/remotes/origin/*" "^main" 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 "78b1c1be9421b33a49a7a8176d93eeeafa112da1" - baseline "9d2fab1a0ba3585d0bc50922bfdd04ebb59361df" + 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/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 ) diff --git a/git-refspec/tests/matcher/mod.rs b/git-refspec/tests/matcher/mod.rs index 6af560f6e46..cfa562e5d12 100644 --- a/git-refspec/tests/matcher/mod.rs +++ b/git-refspec/tests/matcher/mod.rs @@ -65,12 +65,13 @@ mod baseline { Ok(out) } - pub fn parse() -> crate::Result, Vec>> { + pub fn parse() -> crate::Result, Result, BString>>> { 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; @@ -78,24 +79,33 @@ mod baseline { match line.strip_prefix(b"specs: ") { Some(specs) => { let key: Vec<_> = specs.split(|b| *b == b' ').map(BString::from).collect(); - map.insert(key, std::mem::take(&mut mappings)); + let value = match fatal.take() { + Some(message) => Err(message), + None => Ok(std::mem::take(&mut mappings)), + }; + map.insert(key, value); } - None => { - let past_note = line - .splitn(2, |b| *b == b']') - .nth(1) - .or_else(|| line.strip_prefix(b" * branch ")) - .unwrap(); - let mut tokens = past_note.split(|b| *b == b' ').filter(|t| !t.is_empty()); + None => match line.strip_prefix(b"fatal: ") { + Some(message) => { + fatal = Some(message.into()); + } + None => { + let past_note = line + .splitn(2, |b| *b == b']') + .nth(1) + .or_else(|| line.strip_prefix(b" * branch ")) + .unwrap(); + let mut tokens = past_note.split(|b| *b == b' ').filter(|t| !t.is_empty()); - let lhs = tokens.next().unwrap().trim(); - tokens.next(); - let rhs = tokens.next().unwrap().trim(); - mappings.push(Mapping { - remote: full_remote_ref(lhs.into()), - local: (rhs != b"FETCH_HEAD").then(|| full_tracking_ref(rhs.into())), - }) - } + let lhs = tokens.next().unwrap().trim(); + tokens.next(); + let rhs = tokens.next().unwrap().trim(); + mappings.push(Mapping { + remote: full_remote_ref(lhs.into()), + local: (rhs != b"FETCH_HEAD").then(|| full_tracking_ref(rhs.into())), + }) + } + }, } } From b8ac13e5074fa08111fcef1092432ed3a2326c6e Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 13 Sep 2022 15:38:18 +0800 Subject: [PATCH 19/68] thanks clippy --- git-refspec/tests/matcher/mod.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/git-refspec/tests/matcher/mod.rs b/git-refspec/tests/matcher/mod.rs index cfa562e5d12..d6ac82a00d8 100644 --- a/git-refspec/tests/matcher/mod.rs +++ b/git-refspec/tests/matcher/mod.rs @@ -38,6 +38,8 @@ mod baseline { pub local: Option, } + type Baseline = HashMap, Result, BString>>; + pub 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"))?; @@ -65,7 +67,7 @@ mod baseline { Ok(out) } - pub fn parse() -> crate::Result, Result, BString>>> { + pub 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"))?; From 4f354852e15b469260bd3553e4f615f9612fabcc Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 13 Sep 2022 15:57:11 +0800 Subject: [PATCH 20/68] tag specific tests (#450) --- git-refspec/tests/fixtures/match_baseline.sh | 2 ++ git-refspec/tests/matcher/mod.rs | 14 ++++++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/git-refspec/tests/fixtures/match_baseline.sh b/git-refspec/tests/fixtures/match_baseline.sh index f3776feec4e..21ef9789c71 100644 --- a/git-refspec/tests/fixtures/match_baseline.sh +++ b/git-refspec/tests/fixtures/match_baseline.sh @@ -49,5 +49,7 @@ git clone --shared ./base clone baseline "refs/heads/f1:refs/remotes/origin/conflict" "refs/heads/f2:refs/remotes/origin/conflict" baseline "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/*" ) diff --git a/git-refspec/tests/matcher/mod.rs b/git-refspec/tests/matcher/mod.rs index d6ac82a00d8..82438f56bc0 100644 --- a/git-refspec/tests/matcher/mod.rs +++ b/git-refspec/tests/matcher/mod.rs @@ -12,7 +12,7 @@ mod match_ { #[test] fn fetch_only() { baseline::parse_input().unwrap(); - baseline::parse().unwrap(); + dbg!(baseline::parse().unwrap()); let _spec = Fetch::Only { src: "refs/heads/main".into(), }; @@ -114,9 +114,17 @@ mod baseline { Ok(map) } + fn looks_like_tag(name: &BString) -> bool { + name.starts_with(b"v0.") + } + fn full_remote_ref(mut name: BString) -> BString { if !name.contains(&b'/') { - name.insert_str(0, b"refs/heads/"); + if looks_like_tag(&name) { + name.insert_str(0, b"refs/tags/"); + } else { + name.insert_str(0, b"refs/heads/"); + } } name } @@ -124,6 +132,8 @@ mod baseline { fn full_tracking_ref(mut name: BString) -> BString { if name.starts_with_str(b"origin/") { name.insert_str(0, b"refs/remotes/"); + } else if looks_like_tag(&name) { + name.insert_str(0, b"refs/tags/"); } name } From 509764c95978115da129b8bb9baeb304634fa10c Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 13 Sep 2022 16:28:03 +0800 Subject: [PATCH 21/68] ground work for matcher tests (#450) These are single-refspec matches without anything added to them. We might just go straight to matchgroups actually as it it make handling types easier. Let's see. --- git-refspec/src/lib.rs | 2 +- git-refspec/src/matcher.rs | 32 ++++++- git-refspec/src/spec.rs | 5 +- git-refspec/src/types.rs | 8 -- git-refspec/tests/matcher/mod.rs | 137 ++------------------------- git-refspec/tests/matching/mod.rs | 152 ++++++++++++++++++++++++++++++ git-refspec/tests/refspec.rs | 1 + 7 files changed, 193 insertions(+), 144 deletions(-) create mode 100644 git-refspec/tests/matching/mod.rs diff --git a/git-refspec/src/lib.rs b/git-refspec/src/lib.rs index bd2356196e8..b8bfc89b613 100644 --- a/git-refspec/src/lib.rs +++ b/git-refspec/src/lib.rs @@ -33,4 +33,4 @@ mod spec; pub mod matcher; mod types; -pub use types::{Instruction, Match, Matcher}; +pub use types::{Instruction, Matcher}; diff --git a/git-refspec/src/matcher.rs b/git-refspec/src/matcher.rs index dd6d94ad4f2..a9fef07eb08 100644 --- a/git-refspec/src/matcher.rs +++ b/git-refspec/src/matcher.rs @@ -1,4 +1,4 @@ -use crate::Match; +use crate::Matcher; use bstr::BStr; use git_hash::{oid, ObjectId}; @@ -11,12 +11,30 @@ pub(crate) enum Needle<'a> { } impl<'a> From<&'a BStr> for Needle<'a> { - fn from(_v: &'a BStr) -> Self { + fn from(v: &'a BStr) -> Self { + if v.starts_with(b"refs/") { + Needle::FullName(v) + } else { + todo!() + } + } +} + +impl<'a> Matcher<'a> { + /// For each name in `names`, set the corresponding byte in `matches` to `true` if the corresponding `name` matches the remote side + /// instruction (i.e. the left side of a [`fetch`][crate::parse::Operation::Fetch] refspec). + /// Note that `name` is expected to be the full name of a reference. + pub fn match_remotes<'b>( + &self, + _names: impl Iterator> + ExactSizeIterator, + _matches: &mut Vec>, + ) { todo!() } } -/// An item to match +/// 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, @@ -44,3 +62,11 @@ impl Match<'_> { self.rhs } } + +/// The result of a match operation. +#[derive(Default)] +#[allow(dead_code)] +pub struct Match<'a> { + pub(crate) lhs: Option<&'a bstr::BStr>, + pub(crate) rhs: Option<&'a bstr::BStr>, +} diff --git a/git-refspec/src/spec.rs b/git-refspec/src/spec.rs index 26bb9751f84..75910e61f13 100644 --- a/git-refspec/src/spec.rs +++ b/git-refspec/src/spec.rs @@ -134,10 +134,7 @@ mod matcher { use crate::{Matcher, RefSpecRef}; impl<'a> RefSpecRef<'a> { - /// For each name in `names`, set the corresponding byte in `matches` to `true` if the corresponding `name` matches the remote side - /// instruction (i.e. the left side of a [`fetch`][crate::parse::Operation::Fetch] refspec). - /// Note that `name` is expected to be the full name of a reference. - // TODO: move docs to Matcher, adjust these. + /// Create a matcher to perform matching operations against this refspec. pub fn to_matcher(&self) -> Matcher<'_> { Matcher { op: self.op, diff --git a/git-refspec/src/types.rs b/git-refspec/src/types.rs index 7a231cafebf..5d4f04897a0 100644 --- a/git-refspec/src/types.rs +++ b/git-refspec/src/types.rs @@ -29,11 +29,3 @@ pub struct Matcher<'a> { pub(crate) src: Option>, pub(crate) dst: Option>, } - -/// The result of a match operation. -#[derive(Default)] -#[allow(dead_code)] -pub struct Match<'a> { - pub(crate) lhs: Option<&'a bstr::BStr>, - pub(crate) rhs: Option<&'a bstr::BStr>, -} diff --git a/git-refspec/tests/matcher/mod.rs b/git-refspec/tests/matcher/mod.rs index 82438f56bc0..f0b47eca0ad 100644 --- a/git-refspec/tests/matcher/mod.rs +++ b/git-refspec/tests/matcher/mod.rs @@ -1,7 +1,8 @@ -use git_refspec::instruction::Fetch; +use crate::matching; +use git_refspec::parse::Operation; mod match_ { - use git_refspec::Match; + use git_refspec::matcher::Match; #[test] fn default_is_not_matched() { @@ -10,131 +11,11 @@ mod match_ { } #[test] +#[ignore] fn fetch_only() { - baseline::parse_input().unwrap(); - dbg!(baseline::parse().unwrap()); - let _spec = Fetch::Only { - src: "refs/heads/main".into(), - }; -} - -mod baseline { - use bstr::{BString, ByteSlice, ByteVec}; - use git_hash::ObjectId; - 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, - } - - #[derive(Debug)] - pub struct Mapping { - pub remote: BString, - /// `None` if there is no destination/tracking branch - pub local: Option, - } - - type Baseline = HashMap, Result, BString>>; - - pub 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 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 => { - let past_note = line - .splitn(2, |b| *b == b']') - .nth(1) - .or_else(|| line.strip_prefix(b" * branch ")) - .unwrap(); - let mut tokens = past_note.split(|b| *b == b' ').filter(|t| !t.is_empty()); - - let lhs = tokens.next().unwrap().trim(); - tokens.next(); - let rhs = tokens.next().unwrap().trim(); - mappings.push(Mapping { - remote: full_remote_ref(lhs.into()), - local: (rhs != b"FETCH_HEAD").then(|| full_tracking_ref(rhs.into())), - }) - } - }, - } - } - - Ok(map) - } - - fn looks_like_tag(name: &BString) -> bool { - name.starts_with(b"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 { - name.insert_str(0, b"refs/heads/"); - } - } - name - } - - fn full_tracking_ref(mut name: BString) -> BString { - if name.starts_with_str(b"origin/") { - name.insert_str(0, b"refs/remotes/"); - } else if looks_like_tag(&name) { - name.insert_str(0, b"refs/tags/"); - } - name - } + let spec = git_refspec::parse("refs/heads/main".into(), Operation::Fetch).unwrap(); + let matcher = spec.to_matcher(); + let mut out = Vec::new(); + matcher.match_remotes(matching::baseline::input(), &mut out); + let _expected = matching::baseline::single(spec); } diff --git a/git-refspec/tests/matching/mod.rs b/git-refspec/tests/matching/mod.rs new file mode 100644 index 00000000000..e387580a72b --- /dev/null +++ b/git-refspec/tests/matching/mod.rs @@ -0,0 +1,152 @@ +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::RefSpecRef; + 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::matcher::Item<'_> { + git_refspec::matcher::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 { + INPUT.iter().map(Ref::to_item) + } + + pub fn single(spec: RefSpecRef<'_>) -> &Result, BString> { + // let key = spec.to_string().into(); + let key = "TODO: tostring".into(); + BASELINE + .get(&vec![key]) + .unwrap_or_else(|| panic!("BUG: Need {:?} added to the baseline", spec)) + } + + 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 => { + let past_note = line + .splitn(2, |b| *b == b']') + .nth(1) + .or_else(|| line.strip_prefix(b" * branch ")) + .unwrap(); + let mut tokens = past_note.split(|b| *b == b' ').filter(|t| !t.is_empty()); + + let lhs = tokens.next().unwrap().trim(); + tokens.next(); + let rhs = tokens.next().unwrap().trim(); + mappings.push(Mapping { + remote: full_remote_ref(lhs.into()), + local: (rhs != b"FETCH_HEAD").then(|| full_tracking_ref(rhs.into())), + }) + } + }, + } + } + + Ok(map) + } + + fn looks_like_tag(name: &BString) -> bool { + name.starts_with(b"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 { + name.insert_str(0, b"refs/heads/"); + } + } + name + } + + fn full_tracking_ref(mut name: BString) -> BString { + if name.starts_with_str(b"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/refspec.rs b/git-refspec/tests/refspec.rs index 8b4366e250e..3ec8b34358b 100644 --- a/git-refspec/tests/refspec.rs +++ b/git-refspec/tests/refspec.rs @@ -4,4 +4,5 @@ use git_testtools::Result; mod impls; mod matcher; +mod matching; mod parse; From abdf83f494e2a9fba4a8d9fcb776f2c84baebd3e Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 13 Sep 2022 16:46:11 +0800 Subject: [PATCH 22/68] feat: Simple serialization for `Instruction` and `RefSpecRef` type. (#450) It's also a way to normalize input strings as there is only one way to serialize instructions, which themselves are already normalized towards what's possible. --- git-refspec/src/lib.rs | 2 + git-refspec/src/write.rs | 71 ++++++++++++++++++++++++ git-refspec/tests/refspec.rs | 1 + git-refspec/tests/write/mod.rs | 98 ++++++++++++++++++++++++++++++++++ 4 files changed, 172 insertions(+) create mode 100644 git-refspec/src/write.rs create mode 100644 git-refspec/tests/write/mod.rs diff --git a/git-refspec/src/lib.rs b/git-refspec/src/lib.rs index b8bfc89b613..0176bc73846 100644 --- a/git-refspec/src/lib.rs +++ b/git-refspec/src/lib.rs @@ -29,6 +29,8 @@ pub struct RefSpec { mod spec; +mod write; + /// pub mod matcher; 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/refspec.rs b/git-refspec/tests/refspec.rs index 3ec8b34358b..6f568401d93 100644 --- a/git-refspec/tests/refspec.rs +++ b/git-refspec/tests/refspec.rs @@ -6,3 +6,4 @@ mod impls; mod matcher; 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" + ); + } +} From cbbdf59290d6c3fb4936b31e3b7836becb126ce4 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 13 Sep 2022 17:02:10 +0800 Subject: [PATCH 23/68] Get to the point where the matcher is invoked (#450) still need to check expectations though. --- git-refspec/src/matcher.rs | 15 ++++++--------- git-refspec/tests/matcher/mod.rs | 5 ++++- git-refspec/tests/matching/mod.rs | 4 +--- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/git-refspec/src/matcher.rs b/git-refspec/src/matcher.rs index a9fef07eb08..d191c3743d8 100644 --- a/git-refspec/src/matcher.rs +++ b/git-refspec/src/matcher.rs @@ -1,6 +1,7 @@ use crate::Matcher; use bstr::BStr; use git_hash::{oid, ObjectId}; +use std::borrow::BorrowMut; #[allow(dead_code)] pub(crate) enum Needle<'a> { @@ -21,14 +22,10 @@ impl<'a> From<&'a BStr> for Needle<'a> { } impl<'a> Matcher<'a> { - /// For each name in `names`, set the corresponding byte in `matches` to `true` if the corresponding `name` matches the remote side - /// instruction (i.e. the left side of a [`fetch`][crate::parse::Operation::Fetch] refspec). - /// Note that `name` is expected to be the full name of a reference. - pub fn match_remotes<'b>( - &self, - _names: impl Iterator> + ExactSizeIterator, - _matches: &mut Vec>, - ) { + /// For each item in `names`, fill the accompanying `Match` structure to represent whether or not a match was found based + /// on all included information to match against. + /// Note that each _name_ in `names` is expected to be the full name of a reference. + pub fn match_remotes<'b>(&self, _names: impl Iterator, impl BorrowMut>)>) { todo!() } } @@ -64,7 +61,7 @@ impl Match<'_> { } /// The result of a match operation. -#[derive(Default)] +#[derive(Default, Clone)] #[allow(dead_code)] pub struct Match<'a> { pub(crate) lhs: Option<&'a bstr::BStr>, diff --git a/git-refspec/tests/matcher/mod.rs b/git-refspec/tests/matcher/mod.rs index f0b47eca0ad..e7f7a3b7974 100644 --- a/git-refspec/tests/matcher/mod.rs +++ b/git-refspec/tests/matcher/mod.rs @@ -1,4 +1,5 @@ use crate::matching; +use git_refspec::matcher::Match; use git_refspec::parse::Operation; mod match_ { @@ -16,6 +17,8 @@ fn fetch_only() { let spec = git_refspec::parse("refs/heads/main".into(), Operation::Fetch).unwrap(); let matcher = spec.to_matcher(); let mut out = Vec::new(); - matcher.match_remotes(matching::baseline::input(), &mut out); + out.extend(std::iter::repeat(Match::default()).take(matching::baseline::input().len())); + + matcher.match_remotes(matching::baseline::input().zip(out.iter_mut())); let _expected = matching::baseline::single(spec); } diff --git a/git-refspec/tests/matching/mod.rs b/git-refspec/tests/matching/mod.rs index e387580a72b..249ae7c5d06 100644 --- a/git-refspec/tests/matching/mod.rs +++ b/git-refspec/tests/matching/mod.rs @@ -45,10 +45,8 @@ pub mod baseline { } pub fn single(spec: RefSpecRef<'_>) -> &Result, BString> { - // let key = spec.to_string().into(); - let key = "TODO: tostring".into(); BASELINE - .get(&vec![key]) + .get(&vec![spec.to_bstring()]) .unwrap_or_else(|| panic!("BUG: Need {:?} added to the baseline", spec)) } From cec69057585796ec7bc69f5a6295b97cddb8cb4f Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 13 Sep 2022 19:14:54 +0800 Subject: [PATCH 24/68] actual expectation for first simple test (#450) --- git-refspec/src/matcher.rs | 2 +- git-refspec/tests/matcher/mod.rs | 31 +++++++++++++++++++++++++++---- git-refspec/tests/matching/mod.rs | 3 ++- 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/git-refspec/src/matcher.rs b/git-refspec/src/matcher.rs index d191c3743d8..d0312bed7ad 100644 --- a/git-refspec/src/matcher.rs +++ b/git-refspec/src/matcher.rs @@ -61,7 +61,7 @@ impl Match<'_> { } /// The result of a match operation. -#[derive(Default, Clone)] +#[derive(Debug, Default, Clone)] #[allow(dead_code)] pub struct Match<'a> { pub(crate) lhs: Option<&'a bstr::BStr>, diff --git a/git-refspec/tests/matcher/mod.rs b/git-refspec/tests/matcher/mod.rs index e7f7a3b7974..f82e35475d2 100644 --- a/git-refspec/tests/matcher/mod.rs +++ b/git-refspec/tests/matcher/mod.rs @@ -16,9 +16,32 @@ mod match_ { fn fetch_only() { let spec = git_refspec::parse("refs/heads/main".into(), Operation::Fetch).unwrap(); let matcher = spec.to_matcher(); - let mut out = Vec::new(); - out.extend(std::iter::repeat(Match::default()).take(matching::baseline::input().len())); - matcher.match_remotes(matching::baseline::input().zip(out.iter_mut())); - let _expected = matching::baseline::single(spec); + let mut actual = Vec::new(); + actual.extend(std::iter::repeat(Match::default()).take(matching::baseline::input().len())); + matcher.match_remotes(matching::baseline::input().zip(actual.iter_mut())); + actual.retain(|m| m.matched()); + + let expected = matching::baseline::single(spec).unwrap(); + 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!( + actual.remote().expect("local matched"), + &expected.remote, + "{}: remote mismatch", + idx + ); + if let Some(expected) = expected.local.as_ref() { + match actual.local() { + None => panic!("{}: Expected local ref to be {}, got none", idx, expected), + Some(actual) => assert_eq!(actual, expected, "{}: mismatched local ref", idx), + } + } + } } diff --git a/git-refspec/tests/matching/mod.rs b/git-refspec/tests/matching/mod.rs index 249ae7c5d06..b38ba144c1f 100644 --- a/git-refspec/tests/matching/mod.rs +++ b/git-refspec/tests/matching/mod.rs @@ -44,10 +44,11 @@ pub mod baseline { INPUT.iter().map(Ref::to_item) } - pub fn single(spec: RefSpecRef<'_>) -> &Result, BString> { + pub fn single(spec: RefSpecRef<'_>) -> Result<&Vec, &BString> { BASELINE .get(&vec![spec.to_bstring()]) .unwrap_or_else(|| panic!("BUG: Need {:?} added to the baseline", spec)) + .as_ref() } fn parse_input() -> crate::Result> { From 7f3bc300dfb980d6e6aa72f8c22edd58fa9351fb Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 13 Sep 2022 19:38:49 +0800 Subject: [PATCH 25/68] not using a matchgroup right away seems like the wrong approach (#450) --- git-refspec/src/matcher.rs | 13 +++++++++++-- git-refspec/src/spec.rs | 1 - git-refspec/src/types.rs | 1 - 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/git-refspec/src/matcher.rs b/git-refspec/src/matcher.rs index d0312bed7ad..2bc125b4f10 100644 --- a/git-refspec/src/matcher.rs +++ b/git-refspec/src/matcher.rs @@ -1,9 +1,11 @@ +use crate::parse::Operation; use crate::Matcher; use bstr::BStr; use git_hash::{oid, ObjectId}; use std::borrow::BorrowMut; #[allow(dead_code)] +#[derive(Debug, Copy, Clone)] pub(crate) enum Needle<'a> { FullName(&'a BStr), PartialName(&'a BStr), @@ -25,8 +27,15 @@ impl<'a> Matcher<'a> { /// For each item in `names`, fill the accompanying `Match` structure to represent whether or not a match was found based /// on all included information to match against. /// Note that each _name_ in `names` is expected to be the full name of a reference. - pub fn match_remotes<'b>(&self, _names: impl Iterator, impl BorrowMut>)>) { - todo!() + pub fn match_remotes<'b>(&self, items: impl Iterator, impl BorrowMut>)>) { + assert_eq!(self.op, Operation::Fetch, "Cannot yet handle push operation"); + for (_item, mut m) in items { + let _m = m.borrow_mut(); + match (self.src.as_ref(), self.dst) { + (Some(_src), None) => todo!(), + _ => todo!(), + } + } } } diff --git a/git-refspec/src/spec.rs b/git-refspec/src/spec.rs index 75910e61f13..29c780af8a0 100644 --- a/git-refspec/src/spec.rs +++ b/git-refspec/src/spec.rs @@ -138,7 +138,6 @@ mod matcher { pub fn to_matcher(&self) -> Matcher<'_> { Matcher { op: self.op, - mode: self.mode, src: self.src.map(Into::into), dst: self.dst.map(Into::into), } diff --git a/git-refspec/src/types.rs b/git-refspec/src/types.rs index 5d4f04897a0..9ac97553a8f 100644 --- a/git-refspec/src/types.rs +++ b/git-refspec/src/types.rs @@ -24,7 +24,6 @@ pub enum Instruction<'a> { #[allow(dead_code)] pub struct Matcher<'a> { /// How to interpret our lefthand-side and right-hand side ref-specs - pub(crate) mode: crate::types::Mode, pub(crate) op: crate::parse::Operation, pub(crate) src: Option>, pub(crate) dst: Option>, From 4ba31c55b57f644361e18e5d31d5df514cddd58a Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 13 Sep 2022 20:49:13 +0800 Subject: [PATCH 26/68] refactor to use a match-group instead. (#450) The new data model makes it easier to apply negations and handle a set of edges prior to dealing with conflicts. This makes it easier to test and cleaner as well. --- git-refspec/src/lib.rs | 4 +- git-refspec/src/match_group.rs | 98 +++++++++++++++++++ git-refspec/src/matcher.rs | 78 --------------- git-refspec/src/spec.rs | 16 --- git-refspec/src/types.rs | 12 +-- .../tests/{matcher => match_group}/mod.rs | 27 +---- git-refspec/tests/matching/mod.rs | 6 +- git-refspec/tests/refspec.rs | 2 +- 8 files changed, 113 insertions(+), 130 deletions(-) create mode 100644 git-refspec/src/match_group.rs delete mode 100644 git-refspec/src/matcher.rs rename git-refspec/tests/{matcher => match_group}/mod.rs (53%) diff --git a/git-refspec/src/lib.rs b/git-refspec/src/lib.rs index 0176bc73846..fdb83d0ef7e 100644 --- a/git-refspec/src/lib.rs +++ b/git-refspec/src/lib.rs @@ -32,7 +32,7 @@ mod spec; mod write; /// -pub mod matcher; +pub mod match_group; mod types; -pub use types::{Instruction, Matcher}; +pub use types::{Instruction, MatchGroup}; diff --git a/git-refspec/src/match_group.rs b/git-refspec/src/match_group.rs new file mode 100644 index 00000000000..21fb3863a2d --- /dev/null +++ b/git-refspec/src/match_group.rs @@ -0,0 +1,98 @@ +use crate::parse::Operation; +use crate::types::MatchGroup; +use crate::RefSpecRef; +use bstr::BStr; +use git_hash::oid; +use git_hash::ObjectId; +use std::borrow::Cow; + +/// 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>, +} + +/// 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. + /// 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. + pub fn match_remotes<'item>(&self, _items: impl Iterator>) -> Vec> { + let _matchers: Vec> = self.specs.iter().copied().map(Into::into).collect(); + todo!() + } +} + +/// 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, Default, Clone)] +#[allow(dead_code)] +pub struct Mapping<'a> { + /// The index of the matched ref-spec as seen from the match group. + group_spec_index: usize, + /// The remote side for fetches or the local one for pushes. + pub lhs: &'a BStr, + /// The local side for fetches or the remote one for pushes. + pub rhs: Option<&'a BStr>, +} + +/// A type keeping enough information about a ref-spec to be able to efficiently match it against multiple matcher items. +#[allow(dead_code)] +struct Matcher<'a> { + lhs: Option>, + rhs: Option>, +} + +impl<'a> Matcher<'a> { + /// Match `item` against this spec and return `Some` to gain the other side of the match as configured. + /// This may involve resolving a glob with an allocation, as the destination is built using the matching portion of a glob. + #[allow(dead_code)] + pub fn matches_lhs(&self, _item: Item<'_>) -> Option> { + todo!() + } +} + +#[allow(dead_code)] +#[derive(Debug, Copy, Clone)] +pub(crate) enum Needle<'a> { + FullName(&'a BStr), + PartialName(&'a BStr), + Glob { glob: &'a BStr, asterisk_pos: usize }, + Object(ObjectId), +} + +impl<'a> From<&'a BStr> for Needle<'a> { + fn from(v: &'a BStr) -> Self { + if v.starts_with(b"refs/") { + Needle::FullName(v) + } else { + todo!() + } + } +} + +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/matcher.rs b/git-refspec/src/matcher.rs deleted file mode 100644 index 2bc125b4f10..00000000000 --- a/git-refspec/src/matcher.rs +++ /dev/null @@ -1,78 +0,0 @@ -use crate::parse::Operation; -use crate::Matcher; -use bstr::BStr; -use git_hash::{oid, ObjectId}; -use std::borrow::BorrowMut; - -#[allow(dead_code)] -#[derive(Debug, Copy, Clone)] -pub(crate) enum Needle<'a> { - FullName(&'a BStr), - PartialName(&'a BStr), - Glob { glob: &'a BStr, asterisk_pos: usize }, - Object(ObjectId), -} - -impl<'a> From<&'a BStr> for Needle<'a> { - fn from(v: &'a BStr) -> Self { - if v.starts_with(b"refs/") { - Needle::FullName(v) - } else { - todo!() - } - } -} - -impl<'a> Matcher<'a> { - /// For each item in `names`, fill the accompanying `Match` structure to represent whether or not a match was found based - /// on all included information to match against. - /// Note that each _name_ in `names` is expected to be the full name of a reference. - pub fn match_remotes<'b>(&self, items: impl Iterator, impl BorrowMut>)>) { - assert_eq!(self.op, Operation::Fetch, "Cannot yet handle push operation"); - for (_item, mut m) in items { - let _m = m.borrow_mut(); - match (self.src.as_ref(), self.dst) { - (Some(_src), None) => todo!(), - _ => todo!(), - } - } - } -} - -/// 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>, -} - -impl Match<'_> { - /// Return true if we are representing an actual match - pub fn matched(&self) -> bool { - self.lhs.is_some() - } - - /// Return the remote side (i.e. left side) of the fetch ref-spec that matched, or `None` if it didn't match. - pub fn remote(&self) -> Option<&BStr> { - self.lhs - } - - /// Return the local side (i.e. right side) of the fetch ref-spec that matched, or `None` if it didn't match. - /// - /// This is also called a tracking ref. - pub fn local(&self) -> Option<&BStr> { - self.rhs - } -} - -/// The result of a match operation. -#[derive(Debug, Default, Clone)] -#[allow(dead_code)] -pub struct Match<'a> { - pub(crate) lhs: Option<&'a bstr::BStr>, - pub(crate) rhs: Option<&'a bstr::BStr>, -} diff --git a/git-refspec/src/spec.rs b/git-refspec/src/spec.rs index 29c780af8a0..f99ead73250 100644 --- a/git-refspec/src/spec.rs +++ b/git-refspec/src/spec.rs @@ -129,22 +129,6 @@ impl RefSpecRef<'_> { } } -/// -mod matcher { - use crate::{Matcher, RefSpecRef}; - - impl<'a> RefSpecRef<'a> { - /// Create a matcher to perform matching operations against this refspec. - pub fn to_matcher(&self) -> Matcher<'_> { - Matcher { - op: self.op, - src: self.src.map(Into::into), - dst: self.dst.map(Into::into), - } - } - } -} - /// Conversion impl RefSpecRef<'_> { /// Convert this ref into a standalone, owned copy. diff --git a/git-refspec/src/types.rs b/git-refspec/src/types.rs index 9ac97553a8f..3b27383d4f1 100644 --- a/git-refspec/src/types.rs +++ b/git-refspec/src/types.rs @@ -1,4 +1,4 @@ -use crate::{instruction, matcher}; +use crate::{instruction, RefSpecRef}; /// The way to interpret a refspec. #[derive(PartialOrd, Ord, PartialEq, Eq, Copy, Clone, Hash, Debug)] @@ -20,11 +20,7 @@ pub enum Instruction<'a> { Fetch(instruction::Fetch<'a>), } -/// A type keeping enough information about a ref-spec to be able to efficiently match it against multiple matcher items. -#[allow(dead_code)] -pub struct Matcher<'a> { - /// How to interpret our lefthand-side and right-hand side ref-specs - pub(crate) op: crate::parse::Operation, - pub(crate) src: Option>, - pub(crate) dst: Option>, +/// A match group is able to match a list of ref specs in order while handling negation, conflicts and one to many mappings. +pub struct MatchGroup<'a> { + pub(crate) specs: Vec>, } diff --git a/git-refspec/tests/matcher/mod.rs b/git-refspec/tests/match_group/mod.rs similarity index 53% rename from git-refspec/tests/matcher/mod.rs rename to git-refspec/tests/match_group/mod.rs index f82e35475d2..e6906547c59 100644 --- a/git-refspec/tests/matcher/mod.rs +++ b/git-refspec/tests/match_group/mod.rs @@ -1,26 +1,14 @@ use crate::matching; -use git_refspec::matcher::Match; use git_refspec::parse::Operation; - -mod match_ { - use git_refspec::matcher::Match; - - #[test] - fn default_is_not_matched() { - assert!(!Match::default().matched()) - } -} +use git_refspec::MatchGroup; #[test] #[ignore] fn fetch_only() { let spec = git_refspec::parse("refs/heads/main".into(), Operation::Fetch).unwrap(); - let matcher = spec.to_matcher(); + let match_group = MatchGroup::from_fetch_specs(Some(spec)); - let mut actual = Vec::new(); - actual.extend(std::iter::repeat(Match::default()).take(matching::baseline::input().len())); - matcher.match_remotes(matching::baseline::input().zip(actual.iter_mut())); - actual.retain(|m| m.matched()); + let actual = match_group.match_remotes(matching::baseline::input()); let expected = matching::baseline::single(spec).unwrap(); assert_eq!( @@ -31,14 +19,9 @@ fn fetch_only() { expected ); for (idx, (actual, expected)) in actual.iter().zip(expected).enumerate() { - assert_eq!( - actual.remote().expect("local matched"), - &expected.remote, - "{}: remote mismatch", - idx - ); + assert_eq!(actual.lhs, &expected.remote, "{}: remote mismatch", idx); if let Some(expected) = expected.local.as_ref() { - match actual.local() { + match actual.rhs { None => panic!("{}: Expected local ref to be {}, got none", idx, expected), Some(actual) => assert_eq!(actual, expected, "{}: mismatched local ref", idx), } diff --git a/git-refspec/tests/matching/mod.rs b/git-refspec/tests/matching/mod.rs index b38ba144c1f..ede4639d35e 100644 --- a/git-refspec/tests/matching/mod.rs +++ b/git-refspec/tests/matching/mod.rs @@ -20,8 +20,8 @@ pub mod baseline { } impl Ref { - pub fn to_item(&self) -> git_refspec::matcher::Item<'_> { - git_refspec::matcher::Item { + 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(), @@ -40,7 +40,7 @@ pub mod baseline { pub local: Option, } - pub fn input() -> impl Iterator> + ExactSizeIterator { + pub fn input() -> impl Iterator> + ExactSizeIterator { INPUT.iter().map(Ref::to_item) } diff --git a/git-refspec/tests/refspec.rs b/git-refspec/tests/refspec.rs index 6f568401d93..5768dc5c1f9 100644 --- a/git-refspec/tests/refspec.rs +++ b/git-refspec/tests/refspec.rs @@ -3,7 +3,7 @@ extern crate core; use git_testtools::Result; mod impls; -mod matcher; +mod match_group; mod matching; mod parse; mod write; From c915a5f5f0d771e704b108b9442a605d62f0945e Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 13 Sep 2022 21:21:13 +0800 Subject: [PATCH 27/68] top-level match-group loop without negation (#450) --- git-refspec/src/match_group.rs | 51 +++++++++++++++++++++------- git-refspec/tests/match_group/mod.rs | 4 +-- git-refspec/tests/matching/mod.rs | 2 +- 3 files changed, 42 insertions(+), 15 deletions(-) diff --git a/git-refspec/src/match_group.rs b/git-refspec/src/match_group.rs index 21fb3863a2d..12aa98f7c78 100644 --- a/git-refspec/src/match_group.rs +++ b/git-refspec/src/match_group.rs @@ -1,5 +1,5 @@ use crate::parse::Operation; -use crate::types::MatchGroup; +use crate::types::{MatchGroup, Mode}; use crate::RefSpecRef; use bstr::BStr; use git_hash::oid; @@ -33,9 +33,32 @@ impl<'a> MatchGroup<'a> { /// 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. - pub fn match_remotes<'item>(&self, _items: impl Iterator>) -> Vec> { - let _matchers: Vec> = self.specs.iter().copied().map(Into::into).collect(); - todo!() + pub fn match_remotes<'item>(&self, items: impl Iterator> + Clone) -> Vec> { + let mut matchers: Vec> = self.specs.iter().copied().map(Into::into).collect(); + let mut out = Vec::new(); + 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 { + continue; + } + let (matched, rhs) = matcher.matches_lhs(item); + if matched { + out.push(Mapping { + item_index, + lhs: item.full_ref_name, + rhs, + spec_index, + }) + } + } + } + // TODO: negation subtracts from the entire set, order doesn't matter. + out + } + + /// Return the spec that produced the given `mapping`. + pub fn spec_by_mapping(&self, mapping: &Mapping<'_, '_>) -> RefSpecRef<'a> { + self.specs[mapping.spec_index] } } @@ -44,13 +67,15 @@ impl<'a> MatchGroup<'a> { /// Mappings are like edges in a graph, initially without any constraints. #[derive(Debug, Default, Clone)] #[allow(dead_code)] -pub struct Mapping<'a> { - /// The index of the matched ref-spec as seen from the match group. - group_spec_index: usize, - /// The remote side for fetches or the local one for pushes. +pub struct Mapping<'a, 'b> { + /// The index into the initial `items` list. + pub item_index: usize, + /// The name of the remote side for fetches or the local one for pushes that matched. pub lhs: &'a BStr, - /// The local side for fetches or the remote one for pushes. - pub rhs: Option<&'a BStr>, + /// 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. + spec_index: usize, } /// A type keeping enough information about a ref-spec to be able to efficiently match it against multiple matcher items. @@ -61,10 +86,12 @@ struct Matcher<'a> { } impl<'a> Matcher<'a> { - /// Match `item` against this spec and return `Some` to gain the other side of the match as configured. + /// 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. #[allow(dead_code)] - pub fn matches_lhs(&self, _item: Item<'_>) -> Option> { + pub fn matches_lhs(&self, _item: Item<'_>) -> (bool, Option>) { todo!() } } diff --git a/git-refspec/tests/match_group/mod.rs b/git-refspec/tests/match_group/mod.rs index e6906547c59..b1e01f58b19 100644 --- a/git-refspec/tests/match_group/mod.rs +++ b/git-refspec/tests/match_group/mod.rs @@ -21,9 +21,9 @@ fn fetch_only() { for (idx, (actual, expected)) in actual.iter().zip(expected).enumerate() { assert_eq!(actual.lhs, &expected.remote, "{}: remote mismatch", idx); if let Some(expected) = expected.local.as_ref() { - match actual.rhs { + match actual.rhs.as_ref() { None => panic!("{}: Expected local ref to be {}, got none", idx, expected), - Some(actual) => assert_eq!(actual, expected, "{}: mismatched local ref", idx), + Some(actual) => assert_eq!(actual.as_ref(), expected, "{}: mismatched local ref", idx), } } } diff --git a/git-refspec/tests/matching/mod.rs b/git-refspec/tests/matching/mod.rs index ede4639d35e..8dd05042c75 100644 --- a/git-refspec/tests/matching/mod.rs +++ b/git-refspec/tests/matching/mod.rs @@ -40,7 +40,7 @@ pub mod baseline { pub local: Option, } - pub fn input() -> impl Iterator> + ExactSizeIterator { + pub fn input() -> impl Iterator> + ExactSizeIterator + Clone { INPUT.iter().map(Ref::to_item) } From 3625d5a0abb109270b046e2dc206d6f870164306 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 13 Sep 2022 21:37:40 +0800 Subject: [PATCH 28/68] first successful test (#450) --- git-refspec/src/match_group.rs | 22 ++++++++++++++++++++-- git-refspec/tests/match_group/mod.rs | 1 - 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/git-refspec/src/match_group.rs b/git-refspec/src/match_group.rs index 12aa98f7c78..7bcd31cc96a 100644 --- a/git-refspec/src/match_group.rs +++ b/git-refspec/src/match_group.rs @@ -91,8 +91,11 @@ impl<'a> Matcher<'a> { /// /// This may involve resolving a glob with an allocation, as the destination is built using the matching portion of a glob. #[allow(dead_code)] - pub fn matches_lhs(&self, _item: Item<'_>) -> (bool, Option>) { - todo!() + pub fn matches_lhs(&self, item: Item<'_>) -> (bool, Option>) { + match (self.lhs, self.rhs) { + (Some(lhs), None) => (lhs.matches(item), None), + _ => todo!(), + } } } @@ -105,6 +108,21 @@ pub(crate) enum Needle<'a> { Object(ObjectId), } +impl Needle<'_> { + #[inline] + fn matches(&self, item: Item<'_>) -> bool { + match self { + Needle::FullName(name) => *name == item.full_ref_name, + Needle::PartialName(_name) => todo!("partial name"), + Needle::Glob { + glob: _, + asterisk_pos: _, + } => todo!("glob"), + Needle::Object(_) => todo!("object check"), + } + } +} + impl<'a> From<&'a BStr> for Needle<'a> { fn from(v: &'a BStr) -> Self { if v.starts_with(b"refs/") { diff --git a/git-refspec/tests/match_group/mod.rs b/git-refspec/tests/match_group/mod.rs index b1e01f58b19..d45f6d69f03 100644 --- a/git-refspec/tests/match_group/mod.rs +++ b/git-refspec/tests/match_group/mod.rs @@ -3,7 +3,6 @@ use git_refspec::parse::Operation; use git_refspec::MatchGroup; #[test] -#[ignore] fn fetch_only() { let spec = git_refspec::parse("refs/heads/main".into(), Operation::Fetch).unwrap(); let match_group = MatchGroup::from_fetch_specs(Some(spec)); From b752e48b4201c1f26401af39de0a7312b158607b Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 13 Sep 2022 21:47:21 +0800 Subject: [PATCH 29/68] generalize baseline assertion to support multiple input specs (#450) --- git-refspec/tests/match_group/mod.rs | 27 ++------------------ git-refspec/tests/matching/mod.rs | 38 ++++++++++++++++++++++++---- 2 files changed, 35 insertions(+), 30 deletions(-) diff --git a/git-refspec/tests/match_group/mod.rs b/git-refspec/tests/match_group/mod.rs index d45f6d69f03..bbb3045a34d 100644 --- a/git-refspec/tests/match_group/mod.rs +++ b/git-refspec/tests/match_group/mod.rs @@ -1,29 +1,6 @@ -use crate::matching; -use git_refspec::parse::Operation; -use git_refspec::MatchGroup; +use crate::matching::baseline; #[test] fn fetch_only() { - let spec = git_refspec::parse("refs/heads/main".into(), Operation::Fetch).unwrap(); - let match_group = MatchGroup::from_fetch_specs(Some(spec)); - - let actual = match_group.match_remotes(matching::baseline::input()); - - let expected = matching::baseline::single(spec).unwrap(); - 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!(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), - } - } - } + baseline::agrees_with_fetch_specs(Some("refs/heads/main")); } diff --git a/git-refspec/tests/matching/mod.rs b/git-refspec/tests/matching/mod.rs index 8dd05042c75..7dbd607bd3b 100644 --- a/git-refspec/tests/matching/mod.rs +++ b/git-refspec/tests/matching/mod.rs @@ -6,7 +6,8 @@ pub mod baseline { use crate::matching::BASELINE; use bstr::{BString, ByteSlice, ByteVec}; use git_hash::ObjectId; - use git_refspec::RefSpecRef; + use git_refspec::parse::Operation; + use git_refspec::MatchGroup; use git_testtools::once_cell::sync::Lazy; use std::borrow::Borrow; use std::collections::HashMap; @@ -44,11 +45,38 @@ pub mod baseline { INPUT.iter().map(Ref::to_item) } - pub fn single(spec: RefSpecRef<'_>) -> Result<&Vec, &BString> { - BASELINE - .get(&vec![spec.to_bstring()]) - .unwrap_or_else(|| panic!("BUG: Need {:?} added to the baseline", spec)) + pub fn agrees_with_fetch_specs<'a>(specs: impl IntoIterator + Clone) { + 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() + .expect("no error"); + + let actual = match_group.match_remotes(input()); + 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!(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 parse_input() -> crate::Result> { From 8e46f547f510a6453d0304a8a41eaf9bbf3b17a3 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 14 Sep 2022 09:09:28 +0800 Subject: [PATCH 30/68] More standard derives for `Change` type --- git-diff/src/tree/visit.rs | 1 + 1 file changed, 1 insertion(+) 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 { From fb396089495518c45104cc8d62ee4d6221c6b76a Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 14 Sep 2022 11:08:26 +0800 Subject: [PATCH 31/68] use Rust credential implementation (#450) This effectively saves one program invocation, all else being the same. Note that it also requires the `git_binary` built in configuration source to work as expected. Otherwise it's better to set the credentials function to `git_credentials::builtin()`. --- .../src/remote/connection/list_refs.rs | 31 +++++++++++-------- git-repository/src/remote/connection/mod.rs | 11 ++++--- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/git-repository/src/remote/connection/list_refs.rs b/git-repository/src/remote/connection/list_refs.rs index aa07edead5a..b75177a4349 100644 --- a/git-repository/src/remote/connection/list_refs.rs +++ b/git-repository/src/remote/connection/list_refs.rs @@ -12,6 +12,10 @@ 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), + #[error("No fetch url could be obtained to configure credentials")] + MissingFetchUrlForConfiguringCredentials, } } pub use error::Error; @@ -34,19 +38,20 @@ 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) + .ok_or(Error::MissingFetchUrlForConfiguringCredentials)? + .to_owned(); + 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> { From fac87d0dc4d4524713dbd7ef1cfc6acae54f9748 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 14 Sep 2022 11:16:32 +0800 Subject: [PATCH 32/68] Extract URL from transport to support custom remotes better and avoid error case (#450) --- git-repository/src/remote/connection/list_refs.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/git-repository/src/remote/connection/list_refs.rs b/git-repository/src/remote/connection/list_refs.rs index b75177a4349..f497ebdbfc7 100644 --- a/git-repository/src/remote/connection/list_refs.rs +++ b/git-repository/src/remote/connection/list_refs.rs @@ -14,8 +14,6 @@ mod error { Transport(#[from] git_protocol::transport::client::Error), #[error(transparent)] ConfigureCredentials(#[from] crate::config::credential_helpers::Error), - #[error("No fetch url could be obtained to configure credentials")] - MissingFetchUrlForConfiguringCredentials, } } pub use error::Error; @@ -44,8 +42,11 @@ where let url = self .remote .url(Direction::Fetch) - .ok_or(Error::MissingFetchUrlForConfiguringCredentials)? - .to_owned(); + .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 } From dc7f1620cb6d00af60cf78e02b4c2949a3e260e4 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 14 Sep 2022 11:39:19 +0800 Subject: [PATCH 33/68] handle partial names as well (#450) --- git-refspec/src/match_group.rs | 37 +++++++++++++++++--- git-refspec/tests/fixtures/match_baseline.sh | 2 ++ git-refspec/tests/match_group/mod.rs | 4 +++ git-refspec/tests/matching/mod.rs | 3 +- 4 files changed, 40 insertions(+), 6 deletions(-) diff --git a/git-refspec/src/match_group.rs b/git-refspec/src/match_group.rs index 7bcd31cc96a..6b3c61ea667 100644 --- a/git-refspec/src/match_group.rs +++ b/git-refspec/src/match_group.rs @@ -1,7 +1,7 @@ use crate::parse::Operation; use crate::types::{MatchGroup, Mode}; use crate::RefSpecRef; -use bstr::BStr; +use bstr::{BStr, BString, ByteSlice, ByteVec}; use git_hash::oid; use git_hash::ObjectId; use std::borrow::Cow; @@ -104,7 +104,7 @@ impl<'a> Matcher<'a> { pub(crate) enum Needle<'a> { FullName(&'a BStr), PartialName(&'a BStr), - Glob { glob: &'a BStr, asterisk_pos: usize }, + Glob { name: &'a BStr, asterisk_pos: usize }, Object(ObjectId), } @@ -113,9 +113,29 @@ impl Needle<'_> { fn matches(&self, item: Item<'_>) -> bool { match self { Needle::FullName(name) => *name == item.full_ref_name, - Needle::PartialName(_name) => todo!("partial name"), + Needle::PartialName(name) => { + let mut buf = BString::from(Vec::with_capacity(128)); + for (base, append_head) in [ + ("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 true; + } + } + false + } Needle::Glob { - glob: _, + name: _, asterisk_pos: _, } => todo!("glob"), Needle::Object(_) => todo!("object check"), @@ -127,8 +147,15 @@ impl<'a> From<&'a BStr> for Needle<'a> { fn from(v: &'a BStr) -> Self { if v.starts_with(b"refs/") { Needle::FullName(v) + } else if let Some(pos) = v.find_byte(b'*') { + Needle::Glob { + name: v, + asterisk_pos: pos, + } + } else if let Ok(id) = git_hash::ObjectId::from_hex(v) { + Needle::Object(id) } else { - todo!() + Needle::PartialName(v) } } } diff --git a/git-refspec/tests/fixtures/match_baseline.sh b/git-refspec/tests/fixtures/match_baseline.sh index 21ef9789c71..59618ef7dc7 100644 --- a/git-refspec/tests/fixtures/match_baseline.sh +++ b/git-refspec/tests/fixtures/match_baseline.sh @@ -37,6 +37,8 @@ git clone --shared ./base clone baseline "refs/heads/main" baseline "heads/main" baseline "main" + baseline "v0.0-f1" + baseline "tags/v0.0-f2" baseline "78b1c1be9421b33a49a7a8176d93eeeafa112da1" baseline "9d2fab1a0ba3585d0bc50922bfdd04ebb59361df" baseline "+refs/heads/*:refs/remotes/origin/*" diff --git a/git-refspec/tests/match_group/mod.rs b/git-refspec/tests/match_group/mod.rs index bbb3045a34d..8b21a862729 100644 --- a/git-refspec/tests/match_group/mod.rs +++ b/git-refspec/tests/match_group/mod.rs @@ -3,4 +3,8 @@ 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")); } diff --git a/git-refspec/tests/matching/mod.rs b/git-refspec/tests/matching/mod.rs index 7dbd607bd3b..c0dd0bad808 100644 --- a/git-refspec/tests/matching/mod.rs +++ b/git-refspec/tests/matching/mod.rs @@ -135,7 +135,8 @@ pub mod baseline { .splitn(2, |b| *b == b']') .nth(1) .or_else(|| line.strip_prefix(b" * branch ")) - .unwrap(); + .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 lhs = tokens.next().unwrap().trim(); From 426107fea911a2f75d3b624a1c7279cac4edc12e Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 14 Sep 2022 11:50:36 +0800 Subject: [PATCH 34/68] preliminary matching of refs by name (#450) --- git-refspec/src/match_group.rs | 11 ++++++++++- git-refspec/tests/match_group/mod.rs | 2 ++ git-refspec/tests/matching/mod.rs | 13 ++++++++++++- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/git-refspec/src/match_group.rs b/git-refspec/src/match_group.rs index 6b3c61ea667..212e904c501 100644 --- a/git-refspec/src/match_group.rs +++ b/git-refspec/src/match_group.rs @@ -138,7 +138,16 @@ impl Needle<'_> { name: _, asterisk_pos: _, } => todo!("glob"), - Needle::Object(_) => todo!("object check"), + Needle::Object(id) => { + if *id == item.target { + return true; + } + if let Some(tag) = item.tag { + *id == tag + } else { + false + } + } } } } diff --git a/git-refspec/tests/match_group/mod.rs b/git-refspec/tests/match_group/mod.rs index 8b21a862729..677030b8610 100644 --- a/git-refspec/tests/match_group/mod.rs +++ b/git-refspec/tests/match_group/mod.rs @@ -7,4 +7,6 @@ fn fetch_only() { 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::agrees_with_fetch_specs(Some("78b1c1be9421b33a49a7a8176d93eeeafa112da1")); + // baseline::agrees_with_fetch_specs(Some("9d2fab1a0ba3585d0bc50922bfdd04ebb59361df")); // TODO: figure out how not to match 'main' here } diff --git a/git-refspec/tests/matching/mod.rs b/git-refspec/tests/matching/mod.rs index c0dd0bad808..7334ecc1947 100644 --- a/git-refspec/tests/matching/mod.rs +++ b/git-refspec/tests/matching/mod.rs @@ -69,7 +69,16 @@ pub mod baseline { expected ); for (idx, (actual, expected)) in actual.iter().zip(expected).enumerate() { - assert_eq!(actual.lhs, &expected.remote, "{}: remote mismatch", idx); + if let Ok(expected) = git_hash::ObjectId::from_hex(expected.remote.as_ref()) { + assert!( + actual.lhs.starts_with_str("refs/tags/"), + "{}: remote (by object name) mismatch {}", + idx, + expected, + ); + } else { + assert_eq!(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), @@ -162,6 +171,8 @@ pub mod baseline { 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 { name.insert_str(0, b"refs/heads/"); } From bb61c49a9b6a3a109d7af3ddde43fc98bb712ec7 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 14 Sep 2022 13:48:31 +0800 Subject: [PATCH 35/68] support testing source-only object names (#450) --- git-refspec/tests/match_group/mod.rs | 10 ++++-- git-refspec/tests/matching/mod.rs | 51 ++++++++++++++++++++++------ 2 files changed, 48 insertions(+), 13 deletions(-) diff --git a/git-refspec/tests/match_group/mod.rs b/git-refspec/tests/match_group/mod.rs index 677030b8610..065a111954d 100644 --- a/git-refspec/tests/match_group/mod.rs +++ b/git-refspec/tests/match_group/mod.rs @@ -7,6 +7,12 @@ fn fetch_only() { 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::agrees_with_fetch_specs(Some("78b1c1be9421b33a49a7a8176d93eeeafa112da1")); - // baseline::agrees_with_fetch_specs(Some("9d2fab1a0ba3585d0bc50922bfdd04ebb59361df")); // TODO: figure out how not to match 'main' here + baseline::provides_does_not_actually_match_object_names( + Some("78b1c1be9421b33a49a7a8176d93eeeafa112da1"), + ["refs/tags/annotated-v0.0"], + ); + baseline::provides_does_not_actually_match_object_names( + Some("9d2fab1a0ba3585d0bc50922bfdd04ebb59361df"), + ["refs/heads/main", "refs/tags/annotated-v0.0"], + ); } diff --git a/git-refspec/tests/matching/mod.rs b/git-refspec/tests/matching/mod.rs index 7334ecc1947..3bc532c15be 100644 --- a/git-refspec/tests/matching/mod.rs +++ b/git-refspec/tests/matching/mod.rs @@ -4,7 +4,7 @@ static BASELINE: Lazy = Lazy::new(|| baseline::parse().unwra pub mod baseline { use crate::matching::BASELINE; - use bstr::{BString, ByteSlice, ByteVec}; + use bstr::{BStr, BString, ByteSlice, ByteVec}; use git_hash::ObjectId; use git_refspec::parse::Operation; use git_refspec::MatchGroup; @@ -45,7 +45,28 @@ pub mod baseline { INPUT.iter().map(Ref::to_item) } + pub fn provides_does_not_actually_match_object_names<'a, 'b>( + specs: impl IntoIterator + Clone, + expected: impl IntoIterator, + ) { + check_fetch_remote( + specs, + Mode::ObjectHashSource { + expected: expected.into_iter().map(|s| s.into()).collect(), + }, + ) + } + pub fn agrees_with_fetch_specs<'a>(specs: impl IntoIterator + Clone) { + check_fetch_remote(specs, Mode::Normal) + } + + enum Mode<'a> { + Normal, + ObjectHashSource { expected: Vec<&'a BStr> }, + } + + fn check_fetch_remote<'a>(specs: impl IntoIterator + Clone, mode: Mode) { let match_group = MatchGroup::from_fetch_specs( specs .clone() @@ -63,21 +84,21 @@ pub mod baseline { let actual = match_group.match_remotes(input()); assert_eq!( actual.len(), - expected.len(), + match &mode { + Mode::Normal => expected.len(), + Mode::ObjectHashSource { expected } => expected.len(), + }, "got a different amount of mappings: {:?} != {:?}", actual, expected ); + for (idx, (actual, expected)) in actual.iter().zip(expected).enumerate() { - if let Ok(expected) = git_hash::ObjectId::from_hex(expected.remote.as_ref()) { - assert!( - actual.lhs.starts_with_str("refs/tags/"), - "{}: remote (by object name) mismatch {}", - idx, - expected, - ); - } else { - assert_eq!(actual.lhs, &expected.remote, "{}: remote mismatch", idx); + match &mode { + Mode::ObjectHashSource { .. } => {} + Mode::Normal => { + assert_eq!(actual.lhs, &expected.remote, "{}: remote mismatch", idx); + } } if let Some(expected) = expected.local.as_ref() { match actual.rhs.as_ref() { @@ -86,6 +107,14 @@ pub mod baseline { } } } + + if let Mode::ObjectHashSource { expected } = &mode { + assert_eq!( + actual.iter().map(|a| a.lhs).collect::>(), + *expected, + "object hash expectations should be aligned in code" + ); + } } fn parse_input() -> crate::Result> { From 579e89188679942508f9da107d856ab782a512a1 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 14 Sep 2022 13:53:48 +0800 Subject: [PATCH 36/68] refactor (#450) --- git-refspec/tests/matching/mod.rs | 38 +++++++++++++------------------ 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/git-refspec/tests/matching/mod.rs b/git-refspec/tests/matching/mod.rs index 3bc532c15be..20f1a29e2a8 100644 --- a/git-refspec/tests/matching/mod.rs +++ b/git-refspec/tests/matching/mod.rs @@ -4,7 +4,7 @@ static BASELINE: Lazy = Lazy::new(|| baseline::parse().unwra pub mod baseline { use crate::matching::BASELINE; - use bstr::{BStr, BString, ByteSlice, ByteVec}; + use bstr::{BString, ByteSlice, ByteVec}; use git_hash::ObjectId; use git_refspec::parse::Operation; use git_refspec::MatchGroup; @@ -52,7 +52,13 @@ pub mod baseline { check_fetch_remote( specs, Mode::ObjectHashSource { - expected: expected.into_iter().map(|s| s.into()).collect(), + expected: expected + .into_iter() + .map(|s| Mapping { + remote: s.into(), + local: None, + }) + .collect(), }, ) } @@ -61,9 +67,9 @@ pub mod baseline { check_fetch_remote(specs, Mode::Normal) } - enum Mode<'a> { + enum Mode { Normal, - ObjectHashSource { expected: Vec<&'a BStr> }, + ObjectHashSource { expected: Vec }, } fn check_fetch_remote<'a>(specs: impl IntoIterator + Clone, mode: Mode) { @@ -82,24 +88,20 @@ pub mod baseline { .expect("no error"); let actual = match_group.match_remotes(input()); + let expected = match &mode { + Mode::Normal => expected, + Mode::ObjectHashSource { expected } => expected, + }; assert_eq!( actual.len(), - match &mode { - Mode::Normal => expected.len(), - Mode::ObjectHashSource { expected } => expected.len(), - }, + expected.len(), "got a different amount of mappings: {:?} != {:?}", actual, expected ); for (idx, (actual, expected)) in actual.iter().zip(expected).enumerate() { - match &mode { - Mode::ObjectHashSource { .. } => {} - Mode::Normal => { - assert_eq!(actual.lhs, &expected.remote, "{}: remote mismatch", idx); - } - } + assert_eq!(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), @@ -107,14 +109,6 @@ pub mod baseline { } } } - - if let Mode::ObjectHashSource { expected } = &mode { - assert_eq!( - actual.iter().map(|a| a.lhs).collect::>(), - *expected, - "object hash expectations should be aligned in code" - ); - } } fn parse_input() -> crate::Result> { From 7368fe4ee38bbd34bd811310afa8eeb78c475fda Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 14 Sep 2022 14:23:51 +0800 Subject: [PATCH 37/68] prepare for dual-sided ref mapping to realize that it needs a special case. (#450) The left-hand can never match an lhs in the list, but already is a match in itself. It may, however, serve as automatic mapping that is always present. --- git-refspec/src/match_group.rs | 17 +++++++++++- git-refspec/src/spec.rs | 17 ++++++++++++ git-refspec/tests/fixtures/match_baseline.sh | 3 +++ git-refspec/tests/match_group/mod.rs | 13 ++++++++++ git-refspec/tests/matching/mod.rs | 27 +++++++++++++++++--- 5 files changed, 73 insertions(+), 4 deletions(-) diff --git a/git-refspec/src/match_group.rs b/git-refspec/src/match_group.rs index 212e904c501..01d7cbdf673 100644 --- a/git-refspec/src/match_group.rs +++ b/git-refspec/src/match_group.rs @@ -94,6 +94,7 @@ impl<'a> Matcher<'a> { pub fn matches_lhs(&self, item: Item<'_>) -> (bool, Option>) { match (self.lhs, self.rhs) { (Some(lhs), None) => (lhs.matches(item), None), + (Some(lhs), Some(rhs)) => (lhs.matches(item), Some(rhs.to_bstr())), _ => todo!(), } } @@ -108,7 +109,7 @@ pub(crate) enum Needle<'a> { Object(ObjectId), } -impl Needle<'_> { +impl<'a> Needle<'a> { #[inline] fn matches(&self, item: Item<'_>) -> bool { match self { @@ -150,6 +151,20 @@ impl Needle<'_> { } } } + + fn to_bstr(&self) -> Cow<'a, BStr> { + match self { + Needle::FullName(name) => Cow::Borrowed(name), + Needle::PartialName(name) => Cow::Owned({ + let mut base: BString = "refs/".into(); + base.push_str("heads/"); + base.push_str(name); + base + }), + Needle::Glob { .. } => todo!("resolve glob with replacement string"), + Needle::Object(_) => unreachable!("The right side can never be an object name"), + } + } } impl<'a> From<&'a BStr> for Needle<'a> { diff --git a/git-refspec/src/spec.rs b/git-refspec/src/spec.rs index f99ead73250..8c033c2b6dd 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,22 @@ 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 + } + /// 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/tests/fixtures/match_baseline.sh b/git-refspec/tests/fixtures/match_baseline.sh index 59618ef7dc7..4fb22ecee27 100644 --- a/git-refspec/tests/fixtures/match_baseline.sh +++ b/git-refspec/tests/fixtures/match_baseline.sh @@ -41,6 +41,9 @@ git clone --shared ./base clone baseline "tags/v0.0-f2" baseline "78b1c1be9421b33a49a7a8176d93eeeafa112da1" baseline "9d2fab1a0ba3585d0bc50922bfdd04ebb59361df" + baseline "78b1c1be9421b33a49a7a8176d93eeeafa112da1:special" + baseline "9d2fab1a0ba3585d0bc50922bfdd04ebb59361df:tags/special" + baseline "9d2fab1a0ba3585d0bc50922bfdd04ebb59361df:refs/tags/special" baseline "+refs/heads/*:refs/remotes/origin/*" baseline "refs/heads/*:refs/remotes/origin/*" "^main" baseline "^main" "refs/heads/*:refs/remotes/origin/*" diff --git a/git-refspec/tests/match_group/mod.rs b/git-refspec/tests/match_group/mod.rs index 065a111954d..2edf77baba3 100644 --- a/git-refspec/tests/match_group/mod.rs +++ b/git-refspec/tests/match_group/mod.rs @@ -16,3 +16,16 @@ fn fetch_only() { ["refs/heads/main", "refs/tags/annotated-v0.0"], ); } + +#[test] +#[ignore] +fn fetch_and_update() { + baseline::provides_does_not_actually_match_object_names_and_specific_local( + Some("78b1c1be9421b33a49a7a8176d93eeeafa112da1:special"), + ["refs/tags/annotated-v0.0:refs/heads/special"], + ); + baseline::provides_does_not_actually_match_object_names_and_specific_local( + Some("9d2fab1a0ba3585d0bc50922bfdd04ebb59361df:tags/special"), + ["refs/tags/annotated-v0.0:refs/tags/special"], + ); +} diff --git a/git-refspec/tests/matching/mod.rs b/git-refspec/tests/matching/mod.rs index 20f1a29e2a8..3396e59c661 100644 --- a/git-refspec/tests/matching/mod.rs +++ b/git-refspec/tests/matching/mod.rs @@ -45,13 +45,34 @@ pub mod baseline { INPUT.iter().map(Ref::to_item) } + pub fn provides_does_not_actually_match_object_names_and_specific_local<'a, 'b>( + specs: impl IntoIterator + Clone, + 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(), + }, + ) + } + pub fn provides_does_not_actually_match_object_names<'a, 'b>( specs: impl IntoIterator + Clone, expected: impl IntoIterator, ) { check_fetch_remote( specs, - Mode::ObjectHashSource { + Mode::Custom { expected: expected .into_iter() .map(|s| Mapping { @@ -69,7 +90,7 @@ pub mod baseline { enum Mode { Normal, - ObjectHashSource { expected: Vec }, + Custom { expected: Vec }, } fn check_fetch_remote<'a>(specs: impl IntoIterator + Clone, mode: Mode) { @@ -90,7 +111,7 @@ pub mod baseline { let actual = match_group.match_remotes(input()); let expected = match &mode { Mode::Normal => expected, - Mode::ObjectHashSource { expected } => expected, + Mode::Custom { expected } => expected, }; assert_eq!( actual.len(), From 016cd1f70a536ac95eaa8b80958110caa096d875 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 14 Sep 2022 14:26:04 +0800 Subject: [PATCH 38/68] thanks clippy --- git-refspec/src/match_group.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git-refspec/src/match_group.rs b/git-refspec/src/match_group.rs index 01d7cbdf673..0ab3d189f59 100644 --- a/git-refspec/src/match_group.rs +++ b/git-refspec/src/match_group.rs @@ -152,7 +152,7 @@ impl<'a> Needle<'a> { } } - fn to_bstr(&self) -> Cow<'a, BStr> { + fn to_bstr(self) -> Cow<'a, BStr> { match self { Needle::FullName(name) => Cow::Borrowed(name), Needle::PartialName(name) => Cow::Owned({ From 413051d03c843c9c99dbc67f4a5f48d6f2b1aeb2 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 14 Sep 2022 17:14:41 +0800 Subject: [PATCH 39/68] make object-ids in the source position type-safe(#450) --- git-refspec/src/match_group.rs | 62 +++++++++++++++++++++------- git-refspec/tests/match_group/mod.rs | 10 +---- git-refspec/tests/matching/mod.rs | 31 +++++++------- 3 files changed, 66 insertions(+), 37 deletions(-) diff --git a/git-refspec/src/match_group.rs b/git-refspec/src/match_group.rs index 0ab3d189f59..020ffb47820 100644 --- a/git-refspec/src/match_group.rs +++ b/git-refspec/src/match_group.rs @@ -34,21 +34,42 @@ impl<'a> MatchGroup<'a> { /// /// Note that negative matches are not part of the return value, so they are not observable. pub fn match_remotes<'item>(&self, items: impl Iterator> + Clone) -> Vec> { - let mut matchers: Vec> = self.specs.iter().copied().map(Into::into).collect(); let mut out = Vec::new(); + let mut matchers: Vec>> = self + .specs + .iter() + .copied() + .map(Matcher::from) + .enumerate() + .map(|(idx, m)| match m.lhs { + Some(Needle::Object(id)) => { + out.push(Mapping { + item_index: None, + lhs: Source::ObjectId(id), + rhs: m.rhs.map(|n| n.to_bstr()), + spec_index: idx, + }); + None + } + _ => Some(m), + }) + .collect(); + 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 { continue; } - let (matched, rhs) = matcher.matches_lhs(item); - if matched { - out.push(Mapping { - item_index, - lhs: item.full_ref_name, - rhs, - spec_index, - }) + if let Some(matcher) = matcher { + let (matched, rhs) = matcher.matches_lhs(item); + if matched { + out.push(Mapping { + item_index: Some(item_index), + lhs: Source::FullName(item.full_ref_name), + rhs, + spec_index, + }) + } } } } @@ -62,16 +83,29 @@ impl<'a> MatchGroup<'a> { } } +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +/// The source (or left-hand) side of a mapping. +pub enum Source<'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), +} + /// 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, Default, Clone)] -#[allow(dead_code)] +#[derive(Debug, Clone)] pub struct Mapping<'a, 'b> { - /// The index into the initial `items` list. - pub item_index: usize, + /// 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: &'a BStr, + pub lhs: Source<'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. diff --git a/git-refspec/tests/match_group/mod.rs b/git-refspec/tests/match_group/mod.rs index 2edf77baba3..1bfe71b2536 100644 --- a/git-refspec/tests/match_group/mod.rs +++ b/git-refspec/tests/match_group/mod.rs @@ -7,14 +7,8 @@ fn fetch_only() { 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::provides_does_not_actually_match_object_names( - Some("78b1c1be9421b33a49a7a8176d93eeeafa112da1"), - ["refs/tags/annotated-v0.0"], - ); - baseline::provides_does_not_actually_match_object_names( - Some("9d2fab1a0ba3585d0bc50922bfdd04ebb59361df"), - ["refs/heads/main", "refs/tags/annotated-v0.0"], - ); + 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] diff --git a/git-refspec/tests/matching/mod.rs b/git-refspec/tests/matching/mod.rs index 3396e59c661..c1e433cadac 100644 --- a/git-refspec/tests/matching/mod.rs +++ b/git-refspec/tests/matching/mod.rs @@ -6,6 +6,7 @@ pub mod baseline { use crate::matching::BASELINE; use bstr::{BString, ByteSlice, ByteVec}; use git_hash::ObjectId; + use git_refspec::match_group::Source; use git_refspec::parse::Operation; use git_refspec::MatchGroup; use git_testtools::once_cell::sync::Lazy; @@ -66,22 +67,10 @@ pub mod baseline { ) } - pub fn provides_does_not_actually_match_object_names<'a, 'b>( + pub fn of_objects_always_matches_if_the_server_has_the_object<'a, 'b>( specs: impl IntoIterator + Clone, - expected: impl IntoIterator, ) { - check_fetch_remote( - specs, - Mode::Custom { - expected: expected - .into_iter() - .map(|s| Mapping { - remote: s.into(), - local: None, - }) - .collect(), - }, - ) + check_fetch_remote(specs, Mode::Normal) } pub fn agrees_with_fetch_specs<'a>(specs: impl IntoIterator + Clone) { @@ -122,7 +111,12 @@ pub mod baseline { ); for (idx, (actual, expected)) in actual.iter().zip(expected).enumerate() { - assert_eq!(actual.lhs, &expected.remote, "{}: remote mismatch", idx); + 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), @@ -132,6 +126,13 @@ pub mod baseline { } } + fn source_to_bstring(source: Source) -> BString { + match source { + Source::FullName(name) => name.into(), + Source::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"))?; From 6668c3f418663ed6f2ed56efd3d7e78d27124296 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 14 Sep 2022 18:35:59 +0800 Subject: [PATCH 40/68] Don't reject object-id like heads on the receiving side. (#450) --- git-refspec/src/match_group.rs | 6 +++++- git-refspec/src/parse.rs | 3 --- git-refspec/tests/fixtures/match_baseline.sh | 1 + git-refspec/tests/match_group/mod.rs | 15 +++++++++------ git-refspec/tests/matching/mod.rs | 2 +- git-refspec/tests/parse/fetch.rs | 14 +++++++++----- 6 files changed, 25 insertions(+), 16 deletions(-) diff --git a/git-refspec/src/match_group.rs b/git-refspec/src/match_group.rs index 020ffb47820..22516c245c0 100644 --- a/git-refspec/src/match_group.rs +++ b/git-refspec/src/match_group.rs @@ -196,7 +196,11 @@ impl<'a> Needle<'a> { base }), Needle::Glob { .. } => todo!("resolve glob with replacement string"), - Needle::Object(_) => unreachable!("The right side can never be an object name"), + Needle::Object(id) => { + let mut name = id.to_string(); + name.insert_str(0, "refs/heads/"); + Cow::Owned(name.into()) + } } } } diff --git a/git-refspec/src/parse.rs b/git-refspec/src/parse.rs index 02cb5dc10c7..a5405e24814 100644 --- a/git-refspec/src/parse.rs +++ b/git-refspec/src/parse.rs @@ -130,9 +130,6 @@ 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); } diff --git a/git-refspec/tests/fixtures/match_baseline.sh b/git-refspec/tests/fixtures/match_baseline.sh index 4fb22ecee27..263e4a143ad 100644 --- a/git-refspec/tests/fixtures/match_baseline.sh +++ b/git-refspec/tests/fixtures/match_baseline.sh @@ -42,6 +42,7 @@ git clone --shared ./base clone baseline "78b1c1be9421b33a49a7a8176d93eeeafa112da1" baseline "9d2fab1a0ba3585d0bc50922bfdd04ebb59361df" baseline "78b1c1be9421b33a49a7a8176d93eeeafa112da1:special" + baseline "78b1c1be9421b33a49a7a8176d93eeeafa112da1:1111111111111111111111111111111111111111" baseline "9d2fab1a0ba3585d0bc50922bfdd04ebb59361df:tags/special" baseline "9d2fab1a0ba3585d0bc50922bfdd04ebb59361df:refs/tags/special" baseline "+refs/heads/*:refs/remotes/origin/*" diff --git a/git-refspec/tests/match_group/mod.rs b/git-refspec/tests/match_group/mod.rs index 1bfe71b2536..b1cc0d82f3d 100644 --- a/git-refspec/tests/match_group/mod.rs +++ b/git-refspec/tests/match_group/mod.rs @@ -12,14 +12,17 @@ fn fetch_only() { } #[test] -#[ignore] fn fetch_and_update() { - baseline::provides_does_not_actually_match_object_names_and_specific_local( + baseline::of_objects_with_destinations_are_written_into_given_local_branches( Some("78b1c1be9421b33a49a7a8176d93eeeafa112da1:special"), - ["refs/tags/annotated-v0.0:refs/heads/special"], + ["78b1c1be9421b33a49a7a8176d93eeeafa112da1:refs/heads/special"], ); - baseline::provides_does_not_actually_match_object_names_and_specific_local( - Some("9d2fab1a0ba3585d0bc50922bfdd04ebb59361df:tags/special"), - ["refs/tags/annotated-v0.0:refs/tags/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"], + // ); } diff --git a/git-refspec/tests/matching/mod.rs b/git-refspec/tests/matching/mod.rs index c1e433cadac..808ec5d3497 100644 --- a/git-refspec/tests/matching/mod.rs +++ b/git-refspec/tests/matching/mod.rs @@ -46,7 +46,7 @@ pub mod baseline { INPUT.iter().map(Ref::to_item) } - pub fn provides_does_not_actually_match_object_names_and_specific_local<'a, 'b>( + pub fn of_objects_with_destinations_are_written_into_given_local_branches<'a, 'b>( specs: impl IntoIterator + Clone, expected: impl IntoIterator, ) { diff --git a/git-refspec/tests/parse/fetch.rs b/git-refspec/tests/parse/fetch.rs index b83d7d1ed30..9928b65bef2 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] From 67506b1b1997c2b5951f0e1320b0459eac1366e2 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 14 Sep 2022 18:41:16 +0800 Subject: [PATCH 41/68] Make it easy to obtain the local and remote sides of RefSpecs (#450) --- git-refspec/src/spec.rs | 16 ++++++++++++++++ git-refspec/tests/parse/mod.rs | 12 ++++++++++++ 2 files changed, 28 insertions(+) diff --git a/git-refspec/src/spec.rs b/git-refspec/src/spec.rs index 8c033c2b6dd..4ac70fc0566 100644 --- a/git-refspec/src/spec.rs +++ b/git-refspec/src/spec.rs @@ -100,6 +100,22 @@ impl RefSpecRef<'_> { 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/tests/parse/mod.rs b/git-refspec/tests/parse/mod.rs index d0bcdd54a7e..da33009e447 100644 --- a/git-refspec/tests/parse/mod.rs +++ b/git-refspec/tests/parse/mod.rs @@ -57,6 +57,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; From 74de83cbea30b84136bfa191f471e137ae7af5c3 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 14 Sep 2022 19:11:57 +0800 Subject: [PATCH 42/68] more tests for simple 1:1 fetch and update specs (#450) --- git-refspec/src/match_group.rs | 4 +++- git-refspec/tests/fixtures/match_baseline.sh | 3 +++ git-refspec/tests/match_group/mod.rs | 19 +++++++++++++++---- git-refspec/tests/matching/mod.rs | 8 ++++++++ 4 files changed, 29 insertions(+), 5 deletions(-) diff --git a/git-refspec/src/match_group.rs b/git-refspec/src/match_group.rs index 22516c245c0..f45020453f4 100644 --- a/git-refspec/src/match_group.rs +++ b/git-refspec/src/match_group.rs @@ -191,7 +191,9 @@ impl<'a> Needle<'a> { Needle::FullName(name) => Cow::Borrowed(name), Needle::PartialName(name) => Cow::Owned({ let mut base: BString = "refs/".into(); - base.push_str("heads/"); + if !(name.starts_with(b"tags/") || name.starts_with(b"remotes/")) { + base.push_str("heads/"); + } base.push_str(name); base }), diff --git a/git-refspec/tests/fixtures/match_baseline.sh b/git-refspec/tests/fixtures/match_baseline.sh index 263e4a143ad..ab82772894d 100644 --- a/git-refspec/tests/fixtures/match_baseline.sh +++ b/git-refspec/tests/fixtures/match_baseline.sh @@ -45,6 +45,9 @@ git clone --shared ./base clone 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/*:refs/remotes/origin/*" "^main" baseline "^main" "refs/heads/*:refs/remotes/origin/*" diff --git a/git-refspec/tests/match_group/mod.rs b/git-refspec/tests/match_group/mod.rs index b1cc0d82f3d..24040a0ea35 100644 --- a/git-refspec/tests/match_group/mod.rs +++ b/git-refspec/tests/match_group/mod.rs @@ -21,8 +21,19 @@ fn fetch_and_update() { 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: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"]); } diff --git a/git-refspec/tests/matching/mod.rs b/git-refspec/tests/matching/mod.rs index 808ec5d3497..95b88e3e08a 100644 --- a/git-refspec/tests/matching/mod.rs +++ b/git-refspec/tests/matching/mod.rs @@ -77,6 +77,14 @@ pub mod baseline { check_fetch_remote(specs, Mode::Normal) } + /// 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, Custom { expected: Vec }, From 4b73d11a4f0bef8db374cde567547a9ba7097719 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 14 Sep 2022 19:17:32 +0800 Subject: [PATCH 43/68] type-system supprots glob matching (#450) --- git-refspec/src/match_group.rs | 55 +++++++++++++++++++--------- git-refspec/tests/match_group/mod.rs | 1 + 2 files changed, 38 insertions(+), 18 deletions(-) diff --git a/git-refspec/src/match_group.rs b/git-refspec/src/match_group.rs index f45020453f4..0e48937db61 100644 --- a/git-refspec/src/match_group.rs +++ b/git-refspec/src/match_group.rs @@ -5,6 +5,7 @@ use bstr::{BStr, BString, ByteSlice, ByteVec}; use git_hash::oid; use git_hash::ObjectId; use std::borrow::Cow; +use std::ops::Range; /// An item to match, input to various matching operations. #[derive(Debug, Copy, Clone)] @@ -127,8 +128,14 @@ impl<'a> Matcher<'a> { #[allow(dead_code)] pub fn matches_lhs(&self, item: Item<'_>) -> (bool, Option>) { match (self.lhs, self.rhs) { - (Some(lhs), None) => (lhs.matches(item), None), - (Some(lhs), Some(rhs)) => (lhs.matches(item), Some(rhs.to_bstr())), + (Some(lhs), None) => (lhs.matches(item).is_some(), None), + (Some(lhs), Some(rhs)) => { + let m = lhs.matches(item); + match m { + Some(m) => (true, Some(rhs.to_bstr_replace(m.glob_range.map(|r| (r, item))))), + None => (false, None), + } + } _ => todo!(), } } @@ -143,11 +150,17 @@ pub(crate) enum Needle<'a> { Object(ObjectId), } +#[derive(Default)] +struct Match { + /// The range of text to copy from the originating item name + glob_range: Option>, +} + impl<'a> Needle<'a> { #[inline] - fn matches(&self, item: Item<'_>) -> bool { + fn matches(&self, item: Item<'_>) -> Option { match self { - Needle::FullName(name) => *name == item.full_ref_name, + Needle::FullName(name) => (*name == item.full_ref_name).then(Match::default), Needle::PartialName(name) => { let mut buf = BString::from(Vec::with_capacity(128)); for (base, append_head) in [ @@ -164,10 +177,10 @@ impl<'a> Needle<'a> { buf.push_str("/HEAD"); } if buf == item.full_ref_name { - return true; + return Some(Match::default()); } } - false + None } Needle::Glob { name: _, @@ -175,21 +188,21 @@ impl<'a> Needle<'a> { } => todo!("glob"), Needle::Object(id) => { if *id == item.target { - return true; + return Some(Match::default()); } if let Some(tag) = item.tag { - *id == tag + (*id == tag).then(Match::default) } else { - false + None } } } } - fn to_bstr(self) -> Cow<'a, BStr> { - match self { - Needle::FullName(name) => Cow::Borrowed(name), - Needle::PartialName(name) => Cow::Owned({ + 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/"); @@ -197,25 +210,31 @@ impl<'a> Needle<'a> { base.push_str(name); base }), - Needle::Glob { .. } => todo!("resolve glob with replacement string"), - Needle::Object(id) => { + (Needle::Glob { .. }, Some(_range)) => todo!("resolve glob with replacement string"), + (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."), } } + + 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 v.starts_with(b"refs/") { - Needle::FullName(v) - } else if let Some(pos) = v.find_byte(b'*') { + 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 { diff --git a/git-refspec/tests/match_group/mod.rs b/git-refspec/tests/match_group/mod.rs index 24040a0ea35..ca612aefdfd 100644 --- a/git-refspec/tests/match_group/mod.rs +++ b/git-refspec/tests/match_group/mod.rs @@ -36,4 +36,5 @@ fn fetch_and_update() { ["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/*")) } From a93628cb404987c498779b35994db0a05b3dbc0a Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 14 Sep 2022 19:45:35 +0800 Subject: [PATCH 44/68] basic glob matching. (#450) And most importantly, it seems to integrate nicely with everything that's already there. --- git-refspec/src/match_group.rs | 29 ++++++++++++++++++---------- git-refspec/tests/match_group/mod.rs | 2 +- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/git-refspec/src/match_group.rs b/git-refspec/src/match_group.rs index 0e48937db61..5e338a77bec 100644 --- a/git-refspec/src/match_group.rs +++ b/git-refspec/src/match_group.rs @@ -182,19 +182,22 @@ impl<'a> Needle<'a> { } None } - Needle::Glob { - name: _, - asterisk_pos: _, - } => todo!("glob"), + Needle::Glob { name, asterisk_pos } => { + if &name[..*asterisk_pos] != &item.full_ref_name.get(..*asterisk_pos)? { + return None; + } + let end = item.full_ref_name[*asterisk_pos..] + .find_byte(b'/') + .unwrap_or(item.full_ref_name.len()); + Some(Match { + glob_range: Some(*asterisk_pos..end), + }) + } Needle::Object(id) => { if *id == item.target { return Some(Match::default()); } - if let Some(tag) = item.tag { - (*id == tag).then(Match::default) - } else { - None - } + (*id == item.tag?).then(Match::default) } } } @@ -210,7 +213,13 @@ impl<'a> Needle<'a> { base.push_str(name); base }), - (Needle::Glob { .. }, Some(_range)) => todo!("resolve glob with replacement string"), + (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/"); diff --git a/git-refspec/tests/match_group/mod.rs b/git-refspec/tests/match_group/mod.rs index ca612aefdfd..9529c02f989 100644 --- a/git-refspec/tests/match_group/mod.rs +++ b/git-refspec/tests/match_group/mod.rs @@ -36,5 +36,5 @@ fn fetch_and_update() { ["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/*:refs/remotes/origin/*")) } From 74a5f2262154c5cb5434c1ef2854c4ec3d839f89 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 14 Sep 2022 19:46:26 +0800 Subject: [PATCH 45/68] thanks clippy --- git-refspec/src/match_group.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git-refspec/src/match_group.rs b/git-refspec/src/match_group.rs index 5e338a77bec..1aafa75def7 100644 --- a/git-refspec/src/match_group.rs +++ b/git-refspec/src/match_group.rs @@ -183,7 +183,7 @@ impl<'a> Needle<'a> { None } Needle::Glob { name, asterisk_pos } => { - if &name[..*asterisk_pos] != &item.full_ref_name.get(..*asterisk_pos)? { + if name[..*asterisk_pos] != item.full_ref_name.get(..*asterisk_pos)? { return None; } let end = item.full_ref_name[*asterisk_pos..] From eaf36e7d0336be8398d0b1d9414d3ad73afbb393 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 14 Sep 2022 21:28:55 +0800 Subject: [PATCH 46/68] improved glob matching (#450) Now it seems en-par with the git implementation, which also manages without `git-glob`. --- git-refspec/src/match_group.rs | 9 ++++++--- git-refspec/tests/fixtures/match_baseline.sh | 2 ++ git-refspec/tests/match_group/mod.rs | 4 +++- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/git-refspec/src/match_group.rs b/git-refspec/src/match_group.rs index 1aafa75def7..3384a06adac 100644 --- a/git-refspec/src/match_group.rs +++ b/git-refspec/src/match_group.rs @@ -186,9 +186,12 @@ impl<'a> Needle<'a> { if name[..*asterisk_pos] != item.full_ref_name.get(..*asterisk_pos)? { return None; } - let end = item.full_ref_name[*asterisk_pos..] - .find_byte(b'/') - .unwrap_or(item.full_ref_name.len()); + let tail = &name[*asterisk_pos + 1..]; + if !item.full_ref_name.ends_with(tail) { + return 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); Some(Match { glob_range: Some(*asterisk_pos..end), }) diff --git a/git-refspec/tests/fixtures/match_baseline.sh b/git-refspec/tests/fixtures/match_baseline.sh index ab82772894d..a225a28b3eb 100644 --- a/git-refspec/tests/fixtures/match_baseline.sh +++ b/git-refspec/tests/fixtures/match_baseline.sh @@ -49,6 +49,8 @@ git clone --shared ./base clone 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 "refs/heads/*:refs/remotes/origin/*" "^main" baseline "^main" "refs/heads/*:refs/remotes/origin/*" baseline "^refs/heads/main" "refs/heads/*:refs/remotes/origin/*" diff --git a/git-refspec/tests/match_group/mod.rs b/git-refspec/tests/match_group/mod.rs index 9529c02f989..c34446dcc28 100644 --- a/git-refspec/tests/match_group/mod.rs +++ b/git-refspec/tests/match_group/mod.rs @@ -36,5 +36,7 @@ fn fetch_and_update() { ["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/*: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")); } From 00401bef4279d4b8152ea4c149a00ddf50f518e3 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 15 Sep 2022 09:47:10 +0800 Subject: [PATCH 47/68] refactor (#450) --- git-refspec/src/match_group.rs | 41 +++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/git-refspec/src/match_group.rs b/git-refspec/src/match_group.rs index 3384a06adac..38d5cdaf3b6 100644 --- a/git-refspec/src/match_group.rs +++ b/git-refspec/src/match_group.rs @@ -129,19 +129,15 @@ impl<'a> Matcher<'a> { pub fn matches_lhs(&self, item: Item<'_>) -> (bool, Option>) { match (self.lhs, self.rhs) { (Some(lhs), None) => (lhs.matches(item).is_some(), None), - (Some(lhs), Some(rhs)) => { - let m = lhs.matches(item); - match m { - Some(m) => (true, Some(rhs.to_bstr_replace(m.glob_range.map(|r| (r, item))))), - None => (false, None), - } - } + (Some(lhs), Some(rhs)) => lhs + .matches(item) + .map(|m| m.into_match_outcome(rhs, item)) + .unwrap_or_default(), _ => todo!(), } } } -#[allow(dead_code)] #[derive(Debug, Copy, Clone)] pub(crate) enum Needle<'a> { FullName(&'a BStr), @@ -150,17 +146,28 @@ pub(crate) enum Needle<'a> { Object(ObjectId), } -#[derive(Default)] -struct Match { +enum Match { + /// No additional data is provided as part of the match. + Normal, /// The range of text to copy from the originating item name - glob_range: Option>, + GlobRange(Range), +} + +impl Match { + fn into_match_outcome<'a>(self, destination: Needle<'a>, item: Item<'_>) -> (bool, Option>) { + let arg = match self { + 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<'_>) -> Option { match self { - Needle::FullName(name) => (*name == item.full_ref_name).then(Match::default), + Needle::FullName(name) => (*name == item.full_ref_name).then(|| Match::Normal), Needle::PartialName(name) => { let mut buf = BString::from(Vec::with_capacity(128)); for (base, append_head) in [ @@ -177,7 +184,7 @@ impl<'a> Needle<'a> { buf.push_str("/HEAD"); } if buf == item.full_ref_name { - return Some(Match::default()); + return Some(Match::Normal); } } None @@ -192,15 +199,13 @@ impl<'a> Needle<'a> { } let end = item.full_ref_name.len() - tail.len(); let end = item.full_ref_name[*asterisk_pos..end].find_byte(b'/').unwrap_or(end); - Some(Match { - glob_range: Some(*asterisk_pos..end), - }) + Some(Match::GlobRange(*asterisk_pos..end)) } Needle::Object(id) => { if *id == item.target { - return Some(Match::default()); + return Some(Match::Normal); } - (*id == item.tag?).then(Match::default) + (*id == item.tag?).then(|| Match::Normal) } } } From 4c73a19ae4b044df816e95a4fc19dc6481222a4c Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 15 Sep 2022 09:57:36 +0800 Subject: [PATCH 48/68] refactor (#450) --- git-refspec/src/match_group.rs | 48 ++++++++++++++++++++++------------ 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/git-refspec/src/match_group.rs b/git-refspec/src/match_group.rs index 38d5cdaf3b6..69a3af11dd5 100644 --- a/git-refspec/src/match_group.rs +++ b/git-refspec/src/match_group.rs @@ -128,11 +128,8 @@ impl<'a> Matcher<'a> { #[allow(dead_code)] pub fn matches_lhs(&self, item: Item<'_>) -> (bool, Option>) { match (self.lhs, self.rhs) { - (Some(lhs), None) => (lhs.matches(item).is_some(), None), - (Some(lhs), Some(rhs)) => lhs - .matches(item) - .map(|m| m.into_match_outcome(rhs, item)) - .unwrap_or_default(), + (Some(lhs), None) => (lhs.matches(item).is_match(), None), + (Some(lhs), Some(rhs)) => lhs.matches(item).into_match_outcome(rhs, item), _ => todo!(), } } @@ -147,6 +144,8 @@ pub(crate) enum Needle<'a> { } 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 @@ -154,8 +153,12 @@ enum Match { } 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)), }; @@ -165,9 +168,15 @@ impl Match { impl<'a> Needle<'a> { #[inline] - fn matches(&self, item: Item<'_>) -> Option { + fn matches(&self, item: Item<'_>) -> Match { match self { - Needle::FullName(name) => (*name == item.full_ref_name).then(|| Match::Normal), + 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 [ @@ -184,28 +193,35 @@ impl<'a> Needle<'a> { buf.push_str("/HEAD"); } if buf == item.full_ref_name { - return Some(Match::Normal); + return Match::Normal; } } - None + Match::None } Needle::Glob { name, asterisk_pos } => { - if name[..*asterisk_pos] != item.full_ref_name.get(..*asterisk_pos)? { - return None; - } + 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 None; + 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); - Some(Match::GlobRange(*asterisk_pos..end)) + Match::GlobRange(*asterisk_pos..end) } Needle::Object(id) => { if *id == item.target { - return Some(Match::Normal); + return Match::Normal; + } + match item.tag { + Some(tag) if tag == *id => Match::Normal, + _ => Match::None, } - (*id == item.tag?).then(|| Match::Normal) } } } From 77db1127a8ccd71c75670b5d803cabcf93cbcedc Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 15 Sep 2022 10:32:04 +0800 Subject: [PATCH 49/68] first tests for multiple refspecs (#450) --- git-refspec/tests/fixtures/match_baseline.sh | 5 ++ git-refspec/tests/match_group/mod.rs | 93 ++++++++++++-------- 2 files changed, 61 insertions(+), 37 deletions(-) diff --git a/git-refspec/tests/fixtures/match_baseline.sh b/git-refspec/tests/fixtures/match_baseline.sh index a225a28b3eb..ed81fcf0796 100644 --- a/git-refspec/tests/fixtures/match_baseline.sh +++ b/git-refspec/tests/fixtures/match_baseline.sh @@ -51,7 +51,12 @@ git clone --shared ./base clone 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 "^main" "refs/heads/*:refs/remotes/origin/*" baseline "^refs/heads/main" "refs/heads/*:refs/remotes/origin/*" baseline "refs/heads/*:refs/remotes/origin/*" "^refs/heads/main" diff --git a/git-refspec/tests/match_group/mod.rs b/git-refspec/tests/match_group/mod.rs index c34446dcc28..aa34ad1e56a 100644 --- a/git-refspec/tests/match_group/mod.rs +++ b/git-refspec/tests/match_group/mod.rs @@ -1,42 +1,61 @@ -use crate::matching::baseline; +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_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")); + } } -#[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"], - ); +mod multiple { + use crate::matching::baseline; - 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")); + #[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"]); + } } From e4931d0205c9b8e8e859e8ea940b67483e62a07e Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 15 Sep 2022 11:13:44 +0800 Subject: [PATCH 50/68] basic negation implementation along with first failure. (#450) Our matching is too broad in case of negations, which requires full ref names. This is something we could reject early actually. --- git-refspec/src/match_group.rs | 30 ++++++++++++++++++-- git-refspec/tests/fixtures/match_baseline.sh | 4 +++ git-refspec/tests/match_group/mod.rs | 9 ++++++ 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/git-refspec/src/match_group.rs b/git-refspec/src/match_group.rs index 69a3af11dd5..cd512fd8094 100644 --- a/git-refspec/src/match_group.rs +++ b/git-refspec/src/match_group.rs @@ -34,7 +34,10 @@ impl<'a> MatchGroup<'a> { /// 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. - pub fn match_remotes<'item>(&self, items: impl Iterator> + Clone) -> Vec> { + pub fn match_remotes<'item>( + &self, + mut items: impl Iterator> + Clone, + ) -> Vec> { let mut out = Vec::new(); let mut matchers: Vec>> = self .specs @@ -56,9 +59,11 @@ impl<'a> MatchGroup<'a> { }) .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 { @@ -74,7 +79,28 @@ impl<'a> MatchGroup<'a> { } } } - // TODO: negation subtracts from the entire set, order doesn't matter. + + 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 { + Source::ObjectId(_) => true, + Source::FullName(name) => { + !matcher + .matches_lhs(Item { + full_ref_name: name, + target: &null_id, + tag: None, + }) + .0 + } + }); + } + } out } diff --git a/git-refspec/tests/fixtures/match_baseline.sh b/git-refspec/tests/fixtures/match_baseline.sh index ed81fcf0796..4a645eab27b 100644 --- a/git-refspec/tests/fixtures/match_baseline.sh +++ b/git-refspec/tests/fixtures/match_baseline.sh @@ -57,6 +57,10 @@ git clone --shared ./base clone 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 "^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" diff --git a/git-refspec/tests/match_group/mod.rs b/git-refspec/tests/match_group/mod.rs index aa34ad1e56a..cb8baead78f 100644 --- a/git-refspec/tests/match_group/mod.rs +++ b/git-refspec/tests/match_group/mod.rs @@ -58,4 +58,13 @@ mod multiple { baseline::agrees_with_fetch_specs(["heads/f1", "f2", "refs/heads/f3", "heads/main"]); baseline::agrees_with_fetch_specs(["f*:a*", "refs/heads/main"]); } + + #[test] + #[ignore] + fn fetch_only_and_negations() { + baseline::agrees_with_fetch_specs(["refs/heads/f*:refs/remotes/origin/a*", "^f1"]); + baseline::agrees_with_fetch_specs(["refs/heads/f*:refs/remotes/origin/a*", "^refs/heads/f1"]); + baseline::agrees_with_fetch_specs(["^heads/f2", "refs/heads/f*:refs/remotes/origin/a*"]); + baseline::agrees_with_fetch_specs(["^refs/heads/f2", "refs/heads/f*:refs/remotes/origin/a*"]); + } } From 4c4f82170d08b910a7f64482431c99956b1a04c3 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 15 Sep 2022 11:40:26 +0800 Subject: [PATCH 51/68] change!: reject all invalid negative refspec patterns. (#450) Git is more lenient, but will then fail to match against such patterns which seems like avoidable surprising behaviour. --- git-refspec/src/parse.rs | 31 +++++++++++++------- git-refspec/tests/fixtures/match_baseline.sh | 1 + git-refspec/tests/match_group/mod.rs | 1 + git-refspec/tests/parse/fetch.rs | 14 +++++++-- git-refspec/tests/parse/mod.rs | 17 +++++++++-- 5 files changed, 48 insertions(+), 16 deletions(-) diff --git a/git-refspec/src/parse.rs b/git-refspec/src/parse.rs index a5405e24814..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(); @@ -133,6 +126,22 @@ pub(crate) mod function { 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/tests/fixtures/match_baseline.sh b/git-refspec/tests/fixtures/match_baseline.sh index 4a645eab27b..019caee9bfb 100644 --- a/git-refspec/tests/fixtures/match_baseline.sh +++ b/git-refspec/tests/fixtures/match_baseline.sh @@ -60,6 +60,7 @@ git clone --shared ./base clone 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/*" diff --git a/git-refspec/tests/match_group/mod.rs b/git-refspec/tests/match_group/mod.rs index cb8baead78f..0ac222d9022 100644 --- a/git-refspec/tests/match_group/mod.rs +++ b/git-refspec/tests/match_group/mod.rs @@ -63,6 +63,7 @@ mod multiple { #[ignore] fn fetch_only_and_negations() { baseline::agrees_with_fetch_specs(["refs/heads/f*:refs/remotes/origin/a*", "^f1"]); + baseline::agrees_with_fetch_specs(["heads/f2", "^refs/heads/f*:refs/remotes/origin/a*"]); baseline::agrees_with_fetch_specs(["refs/heads/f*:refs/remotes/origin/a*", "^refs/heads/f1"]); baseline::agrees_with_fetch_specs(["^heads/f2", "refs/heads/f*:refs/remotes/origin/a*"]); baseline::agrees_with_fetch_specs(["^refs/heads/f2", "refs/heads/f*:refs/remotes/origin/a*"]); diff --git a/git-refspec/tests/parse/fetch.rs b/git-refspec/tests/parse/fetch.rs index 9928b65bef2..4f5dcf78ee5 100644 --- a/git-refspec/tests/parse/fetch.rs +++ b/git-refspec/tests/parse/fetch.rs @@ -66,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] diff --git a/git-refspec/tests/parse/mod.rs b/git-refspec/tests/parse/mod.rs index da33009e447..f7c0218f985 100644 --- a/git-refspec/tests/parse/mod.rs +++ b/git-refspec/tests/parse/mod.rs @@ -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(_) => { From 6e1b19b7f07050c3fcb70187a4d6a4e4210d3343 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 15 Sep 2022 11:58:39 +0800 Subject: [PATCH 52/68] adjust expectations to make first exclusion tests work (#450) --- git-refspec/tests/match_group/mod.rs | 17 +++++++++++++---- git-refspec/tests/matching/mod.rs | 14 ++++++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/git-refspec/tests/match_group/mod.rs b/git-refspec/tests/match_group/mod.rs index 0ac222d9022..277a8044667 100644 --- a/git-refspec/tests/match_group/mod.rs +++ b/git-refspec/tests/match_group/mod.rs @@ -49,6 +49,7 @@ mod single { mod multiple { use crate::matching::baseline; + use git_refspec::parse::Error; #[test] fn fetch_only() { @@ -60,12 +61,20 @@ mod multiple { } #[test] - #[ignore] fn fetch_only_and_negations() { - baseline::agrees_with_fetch_specs(["refs/heads/f*:refs/remotes/origin/a*", "^f1"]); - baseline::agrees_with_fetch_specs(["heads/f2", "^refs/heads/f*:refs/remotes/origin/a*"]); + 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::agrees_with_fetch_specs(["^heads/f2", "refs/heads/f*:refs/remotes/origin/a*"]); + 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*"]); } } diff --git a/git-refspec/tests/matching/mod.rs b/git-refspec/tests/matching/mod.rs index 95b88e3e08a..798a56e28f7 100644 --- a/git-refspec/tests/matching/mod.rs +++ b/git-refspec/tests/matching/mod.rs @@ -77,6 +77,20 @@ pub mod baseline { check_fetch_remote(specs, Mode::Normal) } + 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(); + for spec in specs { + match git_refspec::parse(spec.into(), Operation::Fetch) { + Ok(_) => {} + Err(e) if e.to_string() == err => {} + Err(err) => panic!("Unexpected parse error: {:?}", 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, From 8ed5d01a75ceb03083b2bddc58b1e9dc26a66cd0 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 15 Sep 2022 16:33:52 +0800 Subject: [PATCH 53/68] just-in-time deduplication of mappings (#450) --- git-refspec/src/match_group.rs | 31 +++++++++++++++++++++++----- git-refspec/tests/match_group/mod.rs | 20 +++++++++++++++++- git-refspec/tests/matching/mod.rs | 2 +- 3 files changed, 46 insertions(+), 7 deletions(-) diff --git a/git-refspec/src/match_group.rs b/git-refspec/src/match_group.rs index cd512fd8094..b77e4709a6c 100644 --- a/git-refspec/src/match_group.rs +++ b/git-refspec/src/match_group.rs @@ -5,6 +5,7 @@ use bstr::{BStr, BString, ByteSlice, ByteVec}; use git_hash::oid; use git_hash::ObjectId; use std::borrow::Cow; +use std::collections::BTreeSet; use std::ops::Range; /// An item to match, input to various matching operations. @@ -30,15 +31,21 @@ impl<'a> MatchGroup<'a> { /// Matching impl<'a> MatchGroup<'a> { - /// Match all `items` against all fetch specs present in this group. + /// 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. + /// 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, ) -> Vec> { 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() @@ -47,7 +54,7 @@ impl<'a> MatchGroup<'a> { .enumerate() .map(|(idx, m)| match m.lhs { Some(Needle::Object(id)) => { - out.push(Mapping { + push_unique(Mapping { item_index: None, lhs: Source::ObjectId(id), rhs: m.rhs.map(|n| n.to_bstr()), @@ -69,7 +76,7 @@ impl<'a> MatchGroup<'a> { if let Some(matcher) = matcher { let (matched, rhs) = matcher.matches_lhs(item); if matched { - out.push(Mapping { + push_unique(Mapping { item_index: Some(item_index), lhs: Source::FullName(item.full_ref_name), rhs, @@ -110,7 +117,14 @@ impl<'a> MatchGroup<'a> { } } -#[derive(Debug, Copy, Clone, PartialEq, Eq)] +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() +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] /// The source (or left-hand) side of a mapping. pub enum Source<'a> { /// A full reference name, which is expected to be valid. @@ -139,6 +153,13 @@ pub struct Mapping<'a, 'b> { spec_index: usize, } +impl std::hash::Hash for Mapping<'_, '_> { + fn hash(&self, state: &mut H) { + self.lhs.hash(state); + self.rhs.hash(state); + } +} + /// A type keeping enough information about a ref-spec to be able to efficiently match it against multiple matcher items. #[allow(dead_code)] struct Matcher<'a> { diff --git a/git-refspec/tests/match_group/mod.rs b/git-refspec/tests/match_group/mod.rs index 277a8044667..8e0bd4d0bf8 100644 --- a/git-refspec/tests/match_group/mod.rs +++ b/git-refspec/tests/match_group/mod.rs @@ -61,7 +61,7 @@ mod multiple { } #[test] - fn fetch_only_and_negations() { + 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, @@ -76,5 +76,23 @@ mod multiple { 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_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. + ]); } } diff --git a/git-refspec/tests/matching/mod.rs b/git-refspec/tests/matching/mod.rs index 798a56e28f7..f1be7260560 100644 --- a/git-refspec/tests/matching/mod.rs +++ b/git-refspec/tests/matching/mod.rs @@ -248,7 +248,7 @@ pub mod baseline { } fn full_tracking_ref(mut name: BString) -> BString { - if name.starts_with_str(b"origin/") { + 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/"); From 508a33a5f279c9a6f29e98f560fcd54cea1ed77d Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 15 Sep 2022 16:47:14 +0800 Subject: [PATCH 54/68] prepare first test for conflicts and validation (#450) Just have to figure out a decent API for that. --- git-refspec/tests/match_group/mod.rs | 12 ++++++++++++ git-refspec/tests/matching/mod.rs | 26 ++++++++++++++++++++------ 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/git-refspec/tests/match_group/mod.rs b/git-refspec/tests/match_group/mod.rs index 8e0bd4d0bf8..010fb664413 100644 --- a/git-refspec/tests/match_group/mod.rs +++ b/git-refspec/tests/match_group/mod.rs @@ -95,4 +95,16 @@ mod multiple { "refs/heads/main:refs/remotes/origin/main", // duplicates are removed immediately. ]); } + + #[test] + #[ignore] + 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", + ], + "TBD", + ); + } } diff --git a/git-refspec/tests/matching/mod.rs b/git-refspec/tests/matching/mod.rs index f1be7260560..889882f54a5 100644 --- a/git-refspec/tests/matching/mod.rs +++ b/git-refspec/tests/matching/mod.rs @@ -70,11 +70,23 @@ pub mod baseline { pub fn of_objects_always_matches_if_the_server_has_the_object<'a, 'b>( specs: impl IntoIterator + Clone, ) { - check_fetch_remote(specs, Mode::Normal) + 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) + 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>( @@ -100,7 +112,7 @@ pub mod baseline { } enum Mode { - Normal, + Normal { validate_err: Option }, Custom { expected: Vec }, } @@ -116,12 +128,14 @@ pub mod baseline { let expected = BASELINE .get(&key) .unwrap_or_else(|| panic!("BUG: Need {:?} added to the baseline", key)) - .as_ref() - .expect("no error"); + .as_ref(); let actual = match_group.match_remotes(input()); let expected = match &mode { - Mode::Normal => expected, + Mode::Normal { validate_err } => match validate_err { + Some(_err_message) => todo!("validation and error comparison"), + None => expected.expect("no error"), + }, Mode::Custom { expected } => expected, }; assert_eq!( From 53e17c10f663bc3c389a13cdfec3716da34dd311 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 15 Sep 2022 16:57:07 +0800 Subject: [PATCH 55/68] sketch `Outcome` type which can be used for later sanitization and validation. (#450) --- git-refspec/src/match_group.rs | 21 ++++++++++++++++----- git-refspec/src/types.rs | 1 + git-refspec/tests/matching/mod.rs | 2 +- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/git-refspec/src/match_group.rs b/git-refspec/src/match_group.rs index b77e4709a6c..7294e9715df 100644 --- a/git-refspec/src/match_group.rs +++ b/git-refspec/src/match_group.rs @@ -8,6 +8,17 @@ use std::borrow::Cow; use std::collections::BTreeSet; use std::ops::Range; +/// 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> { @@ -35,10 +46,7 @@ impl<'a> MatchGroup<'a> { /// 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, - ) -> Vec> { + 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| { @@ -108,7 +116,10 @@ impl<'a> MatchGroup<'a> { }); } } - out + Outcome { + group: self, + mappings: out, + } } /// Return the spec that produced the given `mapping`. diff --git a/git-refspec/src/types.rs b/git-refspec/src/types.rs index 3b27383d4f1..349cea584f9 100644 --- a/git-refspec/src/types.rs +++ b/git-refspec/src/types.rs @@ -21,6 +21,7 @@ pub enum Instruction<'a> { } /// 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>, } diff --git a/git-refspec/tests/matching/mod.rs b/git-refspec/tests/matching/mod.rs index 889882f54a5..dbc1adacc22 100644 --- a/git-refspec/tests/matching/mod.rs +++ b/git-refspec/tests/matching/mod.rs @@ -130,7 +130,7 @@ pub mod baseline { .unwrap_or_else(|| panic!("BUG: Need {:?} added to the baseline", key)) .as_ref(); - let actual = match_group.match_remotes(input()); + let actual = match_group.match_remotes(input()).mappings; let expected = match &mode { Mode::Normal { validate_err } => match validate_err { Some(_err_message) => todo!("validation and error comparison"), From 547129e98dfcac32ebc83e743f9aee05d038629b Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 15 Sep 2022 17:03:42 +0800 Subject: [PATCH 56/68] refactor (#450) --- git-refspec/src/lib.rs | 3 +- git-refspec/src/match_group.rs | 344 --------------------------- git-refspec/src/match_group/mod.rs | 122 ++++++++++ git-refspec/src/match_group/types.rs | 61 +++++ git-refspec/src/match_group/util.rs | 180 ++++++++++++++ git-refspec/src/types.rs | 8 +- 6 files changed, 366 insertions(+), 352 deletions(-) delete mode 100644 git-refspec/src/match_group.rs create mode 100644 git-refspec/src/match_group/mod.rs create mode 100644 git-refspec/src/match_group/types.rs create mode 100644 git-refspec/src/match_group/util.rs diff --git a/git-refspec/src/lib.rs b/git-refspec/src/lib.rs index fdb83d0ef7e..54d5f3057a9 100644 --- a/git-refspec/src/lib.rs +++ b/git-refspec/src/lib.rs @@ -33,6 +33,7 @@ mod write; /// pub mod match_group; +pub use match_group::types::MatchGroup; mod types; -pub use types::{Instruction, MatchGroup}; +pub use types::Instruction; diff --git a/git-refspec/src/match_group.rs b/git-refspec/src/match_group.rs deleted file mode 100644 index 7294e9715df..00000000000 --- a/git-refspec/src/match_group.rs +++ /dev/null @@ -1,344 +0,0 @@ -use crate::parse::Operation; -use crate::types::{MatchGroup, Mode}; -use crate::RefSpecRef; -use bstr::{BStr, BString, ByteSlice, ByteVec}; -use git_hash::oid; -use git_hash::ObjectId; -use std::borrow::Cow; -use std::collections::BTreeSet; -use std::ops::Range; - -/// 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>, -} - -/// 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: Source::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: Source::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 { - Source::ObjectId(_) => true, - Source::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() -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -/// The source (or left-hand) side of a mapping. -pub enum Source<'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), -} - -/// 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: Source<'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. - spec_index: usize, -} - -impl std::hash::Hash for Mapping<'_, '_> { - fn hash(&self, state: &mut H) { - self.lhs.hash(state); - self.rhs.hash(state); - } -} - -/// A type keeping enough information about a ref-spec to be able to efficiently match it against multiple matcher items. -#[allow(dead_code)] -struct Matcher<'a> { - lhs: Option>, - 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. - #[allow(dead_code)] - 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), - _ => todo!(), - } - } -} - -#[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 [ - ("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."), - } - } - - 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/mod.rs b/git-refspec/src/match_group/mod.rs new file mode 100644 index 00000000000..6ee25a5a2a6 --- /dev/null +++ b/git-refspec/src/match_group/mod.rs @@ -0,0 +1,122 @@ +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}; + +/// 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: Source::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: Source::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 { + Source::ObjectId(_) => true, + Source::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() +} + +impl std::hash::Hash for Mapping<'_, '_> { + fn hash(&self, state: &mut H) { + self.lhs.hash(state); + self.rhs.hash(state); + } +} + +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..21cb9d117fc --- /dev/null +++ b/git-refspec/src/match_group/types.rs @@ -0,0 +1,61 @@ +use crate::RefSpecRef; +use bstr::BStr; +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. +pub enum Source<'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), +} + +/// 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: Source<'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, +} diff --git a/git-refspec/src/match_group/util.rs b/git-refspec/src/match_group/util.rs new file mode 100644 index 00000000000..80ccc80ade1 --- /dev/null +++ b/git-refspec/src/match_group/util.rs @@ -0,0 +1,180 @@ +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. +#[allow(dead_code)] +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. + #[allow(dead_code)] + 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), + _ => todo!(), + } + } +} + +#[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 [ + ("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/types.rs b/git-refspec/src/types.rs index 349cea584f9..0a0e24e36cc 100644 --- a/git-refspec/src/types.rs +++ b/git-refspec/src/types.rs @@ -1,4 +1,4 @@ -use crate::{instruction, RefSpecRef}; +use crate::instruction; /// The way to interpret a refspec. #[derive(PartialOrd, Ord, PartialEq, Eq, Copy, Clone, Hash, Debug)] @@ -19,9 +19,3 @@ pub enum Instruction<'a> { /// An instruction for fetching. Fetch(instruction::Fetch<'a>), } - -/// 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>, -} From 70a765e295295f87f8550453452d2ffe95b177be Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 15 Sep 2022 18:46:58 +0800 Subject: [PATCH 57/68] sketch of validation API along with test suite integration (#450) --- git-refspec/src/match_group/mod.rs | 24 ++++++++++++++++++++++++ git-refspec/tests/matching/mod.rs | 13 ++++++++----- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/git-refspec/src/match_group/mod.rs b/git-refspec/src/match_group/mod.rs index 6ee25a5a2a6..3759c1507d0 100644 --- a/git-refspec/src/match_group/mod.rs +++ b/git-refspec/src/match_group/mod.rs @@ -6,6 +6,30 @@ use std::collections::BTreeSet; pub(crate) mod types; pub use types::{Item, Mapping, Outcome, Source}; +/// +pub mod validate { + use crate::match_group::Outcome; + + /// All possible issues found while validating matched mappings. + #[derive(Debug)] + pub enum Issue {} + + /// The error returned [outcome validation][Outcome::validated()]. + #[derive(Debug, thiserror::Error)] + #[error("TBD")] + pub struct Error { + /// All issues discovered during validation. + pub issues: Vec, + } + + impl<'spec, 'item> Outcome<'spec, 'item> { + /// Validate all mappings or dissolve them into an error stating the discovered issues. + pub fn validated(self) -> Result { + Ok(self) + } + } +} + /// Initialization impl<'a> MatchGroup<'a> { /// Take all the fetch ref specs from `specs` get a match group ready. diff --git a/git-refspec/tests/matching/mod.rs b/git-refspec/tests/matching/mod.rs index dbc1adacc22..c0b2b4e8bc7 100644 --- a/git-refspec/tests/matching/mod.rs +++ b/git-refspec/tests/matching/mod.rs @@ -130,13 +130,16 @@ pub mod baseline { .unwrap_or_else(|| panic!("BUG: Need {:?} added to the baseline", key)) .as_ref(); - let actual = match_group.match_remotes(input()).mappings; - let expected = match &mode { + let actual = match_group.match_remotes(input()).validated(); + let (actual, expected) = match &mode { Mode::Normal { validate_err } => match validate_err { - Some(_err_message) => todo!("validation and error comparison"), - None => expected.expect("no error"), + Some(err_message) => { + assert_eq!(actual.unwrap_err().to_string(), *err_message); + return; + } + None => (actual.unwrap().mappings, expected.expect("no error")), }, - Mode::Custom { expected } => expected, + Mode::Custom { expected } => (actual.unwrap().mappings, expected), }; assert_eq!( actual.len(), From aef0a464811ce98e81d44d1417098c9adef035f5 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 16 Sep 2022 08:57:12 +0800 Subject: [PATCH 58/68] the first test to validate conflict reporting (#450) --- git-refspec/src/match_group/mod.rs | 122 ++++++++++++++++++++++----- git-refspec/src/match_group/types.rs | 48 ++++++++++- git-refspec/tests/match_group/mod.rs | 3 +- git-refspec/tests/matching/mod.rs | 28 ++++-- 4 files changed, 170 insertions(+), 31 deletions(-) diff --git a/git-refspec/src/match_group/mod.rs b/git-refspec/src/match_group/mod.rs index 3759c1507d0..62b4057345a 100644 --- a/git-refspec/src/match_group/mod.rs +++ b/git-refspec/src/match_group/mod.rs @@ -4,28 +4,119 @@ use crate::{MatchGroup, RefSpecRef}; use std::collections::BTreeSet; pub(crate) mod types; -pub use types::{Item, Mapping, Outcome, Source}; +pub use types::{Item, Mapping, Outcome, Source, SourceRef}; /// pub mod validate { - use crate::match_group::Outcome; + use crate::match_group::{Outcome, Source}; + use bstr::BString; + use std::collections::BTreeMap; /// All possible issues found while validating matched mappings. - #[derive(Debug)] - pub enum Issue {} + #[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 {} /// The error returned [outcome validation][Outcome::validated()]. - #[derive(Debug, thiserror::Error)] - #[error("TBD")] + #[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. - pub fn validated(self) -> Result { - Ok(self) + /// 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(self) -> Result<(Self, Vec), Error> { + let fixed = Vec::new(); + 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 { + Ok((self, fixed)) + } } } } @@ -64,7 +155,7 @@ impl<'a> MatchGroup<'a> { Some(Needle::Object(id)) => { push_unique(Mapping { item_index: None, - lhs: Source::ObjectId(id), + lhs: SourceRef::ObjectId(id), rhs: m.rhs.map(|n| n.to_bstr()), spec_index: idx, }); @@ -86,7 +177,7 @@ impl<'a> MatchGroup<'a> { if matched { push_unique(Mapping { item_index: Some(item_index), - lhs: Source::FullName(item.full_ref_name), + lhs: SourceRef::FullName(item.full_ref_name), rhs, spec_index, }) @@ -103,8 +194,8 @@ impl<'a> MatchGroup<'a> { .filter_map(|(m, spec)| m.and_then(|m| (spec.mode == Mode::Negative).then(|| m))) { out.retain(|m| match m.lhs { - Source::ObjectId(_) => true, - Source::FullName(name) => { + SourceRef::ObjectId(_) => true, + SourceRef::FullName(name) => { !matcher .matches_lhs(Item { full_ref_name: name, @@ -135,12 +226,5 @@ fn calculate_hash(t: &T) -> u64 { s.finish() } -impl std::hash::Hash for Mapping<'_, '_> { - fn hash(&self, state: &mut H) { - self.lhs.hash(state); - self.rhs.hash(state); - } -} - mod util; use util::{Matcher, Needle}; diff --git a/git-refspec/src/match_group/types.rs b/git-refspec/src/match_group/types.rs index 21cb9d117fc..61711252d9c 100644 --- a/git-refspec/src/match_group/types.rs +++ b/git-refspec/src/match_group/types.rs @@ -1,5 +1,5 @@ use crate::RefSpecRef; -use bstr::BStr; +use bstr::{BStr, BString}; use git_hash::oid; use std::borrow::Cow; @@ -32,8 +32,8 @@ pub struct Item<'a> { } #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -/// The source (or left-hand) side of a mapping. -pub enum Source<'a> { +/// 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. @@ -45,6 +45,39 @@ pub enum Source<'a> { 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. @@ -53,9 +86,16 @@ 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: Source<'a>, + 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/tests/match_group/mod.rs b/git-refspec/tests/match_group/mod.rs index 010fb664413..0683c554f25 100644 --- a/git-refspec/tests/match_group/mod.rs +++ b/git-refspec/tests/match_group/mod.rs @@ -97,14 +97,13 @@ mod multiple { } #[test] - #[ignore] 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", ], - "TBD", + "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\")", ); } } diff --git a/git-refspec/tests/matching/mod.rs b/git-refspec/tests/matching/mod.rs index c0b2b4e8bc7..607635cb59c 100644 --- a/git-refspec/tests/matching/mod.rs +++ b/git-refspec/tests/matching/mod.rs @@ -6,7 +6,7 @@ pub mod baseline { use crate::matching::BASELINE; use bstr::{BString, ByteSlice, ByteVec}; use git_hash::ObjectId; - use git_refspec::match_group::Source; + use git_refspec::match_group::SourceRef; use git_refspec::parse::Operation; use git_refspec::MatchGroup; use git_testtools::once_cell::sync::Lazy; @@ -137,9 +137,25 @@ pub mod baseline { assert_eq!(actual.unwrap_err().to_string(), *err_message); return; } - None => (actual.unwrap().mappings, expected.expect("no error")), + 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 } => (actual.unwrap().mappings, expected), + Mode::Custom { expected } => { + let (actual, fixed) = actual.unwrap(); + assert_eq!( + fixed, + Vec::::new(), + "we don't expect any issues to be fixed here" + ); + (actual.mappings, expected) + } }; assert_eq!( actual.len(), @@ -165,10 +181,10 @@ pub mod baseline { } } - fn source_to_bstring(source: Source) -> BString { + fn source_to_bstring(source: SourceRef) -> BString { match source { - Source::FullName(name) => name.into(), - Source::ObjectId(id) => id.to_string().into(), + SourceRef::FullName(name) => name.into(), + SourceRef::ObjectId(id) => id.to_string().into(), } } From afc0a3da864362ec7a0ab243f72daba4713db569 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 16 Sep 2022 09:07:17 +0800 Subject: [PATCH 59/68] all baseline specs are tested and pass (#450) --- git-refspec/tests/fixtures/match_baseline.sh | 2 +- git-refspec/tests/match_group/mod.rs | 22 ++++++++++++++++++++ git-refspec/tests/matching/mod.rs | 2 +- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/git-refspec/tests/fixtures/match_baseline.sh b/git-refspec/tests/fixtures/match_baseline.sh index 019caee9bfb..0a5e2e79ca6 100644 --- a/git-refspec/tests/fixtures/match_baseline.sh +++ b/git-refspec/tests/fixtures/match_baseline.sh @@ -68,7 +68,7 @@ git clone --shared ./base clone 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/conflict" "refs/heads/f2:refs/remotes/origin/conflict" "refs/heads/f3: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/*" diff --git a/git-refspec/tests/match_group/mod.rs b/git-refspec/tests/match_group/mod.rs index 0683c554f25..ea3285a52f7 100644 --- a/git-refspec/tests/match_group/mod.rs +++ b/git-refspec/tests/match_group/mod.rs @@ -58,6 +58,11 @@ mod multiple { 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] @@ -105,5 +110,22 @@ mod multiple { ], "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\")", + ); } } diff --git a/git-refspec/tests/matching/mod.rs b/git-refspec/tests/matching/mod.rs index 607635cb59c..16a29cbfc7a 100644 --- a/git-refspec/tests/matching/mod.rs +++ b/git-refspec/tests/matching/mod.rs @@ -264,7 +264,7 @@ pub mod baseline { } fn looks_like_tag(name: &BString) -> bool { - name.starts_with(b"v0.") + name.starts_with(b"v0.") || name.starts_with(b"annotated-v0.") } fn full_remote_ref(mut name: BString) -> BString { From d37fd044df2cc5355735121e63fcc3c54b8ea4cb Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 16 Sep 2022 11:17:30 +0800 Subject: [PATCH 60/68] refactor (#450) --- git-refspec/src/match_group/mod.rs | 114 +----------------------- git-refspec/src/match_group/validate.rs | 111 +++++++++++++++++++++++ 2 files changed, 112 insertions(+), 113 deletions(-) create mode 100644 git-refspec/src/match_group/validate.rs diff --git a/git-refspec/src/match_group/mod.rs b/git-refspec/src/match_group/mod.rs index 62b4057345a..5b10ee32cc4 100644 --- a/git-refspec/src/match_group/mod.rs +++ b/git-refspec/src/match_group/mod.rs @@ -7,119 +7,7 @@ pub(crate) mod types; pub use types::{Item, Mapping, Outcome, Source, SourceRef}; /// -pub mod validate { - 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 {} - - /// 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(self) -> Result<(Self, Vec), Error> { - let fixed = Vec::new(); - 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 { - Ok((self, fixed)) - } - } - } -} +pub mod validate; /// Initialization impl<'a> MatchGroup<'a> { diff --git a/git-refspec/src/match_group/validate.rs b/git-refspec/src/match_group/validate.rs new file mode 100644 index 00000000000..ce65d184488 --- /dev/null +++ b/git-refspec/src/match_group/validate.rs @@ -0,0 +1,111 @@ +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 {} + +/// 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(self) -> Result<(Self, Vec), Error> { + let fixed = Vec::new(); + 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 { + Ok((self, fixed)) + } + } +} From 91481020c87cfa0cae9dd497fb87e7fb9dd33c8a Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 16 Sep 2022 11:37:58 +0800 Subject: [PATCH 61/68] frame for testing of fixes (#450) --- git-refspec/src/match_group/validate.rs | 5 +- git-refspec/tests/fixtures/match_baseline.sh | 1 + git-refspec/tests/match_group/mod.rs | 11 ++++ git-refspec/tests/matching/mod.rs | 60 ++++++++++++-------- 4 files changed, 52 insertions(+), 25 deletions(-) diff --git a/git-refspec/src/match_group/validate.rs b/git-refspec/src/match_group/validate.rs index ce65d184488..37e8f205341 100644 --- a/git-refspec/src/match_group/validate.rs +++ b/git-refspec/src/match_group/validate.rs @@ -44,7 +44,10 @@ impl std::fmt::Display for Issue { /// All possible fixes corrected while validating matched mappings. #[derive(Debug, PartialEq, Eq)] -pub enum Fix {} +pub enum Fix { + /// TODO + InvalidRefName, +} /// The error returned [outcome validation][Outcome::validated()]. #[derive(Debug)] diff --git a/git-refspec/tests/fixtures/match_baseline.sh b/git-refspec/tests/fixtures/match_baseline.sh index 0a5e2e79ca6..8b1ac592979 100644 --- a/git-refspec/tests/fixtures/match_baseline.sh +++ b/git-refspec/tests/fixtures/match_baseline.sh @@ -72,5 +72,6 @@ git clone --shared ./base clone 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' ) diff --git a/git-refspec/tests/match_group/mod.rs b/git-refspec/tests/match_group/mod.rs index ea3285a52f7..696d97351f0 100644 --- a/git-refspec/tests/match_group/mod.rs +++ b/git-refspec/tests/match_group/mod.rs @@ -49,6 +49,7 @@ mod single { mod multiple { use crate::matching::baseline; + use git_refspec::match_group::validate::Fix; use git_refspec::parse::Error; #[test] @@ -128,4 +129,14 @@ mod multiple { "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\")", ); } + + #[test] + #[ignore] + fn fetch_and_update_with_fixes() { + baseline::agrees_and_applies_fixes( + ["refs/heads/f*:foo/f*", "f1:f1"], + [Fix::InvalidRefName], + ["refs/heads/f1:refs/heads/f1"], + ) + } } diff --git a/git-refspec/tests/matching/mod.rs b/git-refspec/tests/matching/mod.rs index 16a29cbfc7a..cf38212f841 100644 --- a/git-refspec/tests/matching/mod.rs +++ b/git-refspec/tests/matching/mod.rs @@ -6,6 +6,7 @@ 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; @@ -49,6 +50,14 @@ pub mod baseline { 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, @@ -63,6 +72,7 @@ pub mod baseline { } }) .collect(), + fixes: fixes.into_iter().collect(), }, ) } @@ -113,7 +123,7 @@ pub mod baseline { enum Mode { Normal { validate_err: Option }, - Custom { expected: Vec }, + Custom { expected: Vec, fixes: Vec }, } fn check_fetch_remote<'a>(specs: impl IntoIterator + Clone, mode: Mode) { @@ -147,13 +157,12 @@ pub mod baseline { (actual.mappings, expected.expect("no error")) } }, - Mode::Custom { expected } => { - let (actual, fixed) = actual.unwrap(); - assert_eq!( - fixed, - Vec::::new(), - "we don't expect any issues to be fixed here" - ); + Mode::Custom { + expected, + fixes: expected_fixes, + } => { + let (actual, actual_fixes) = actual.unwrap(); + assert_eq!(&actual_fixes, expected_fixes,); (actual.mappings, expected) } }; @@ -239,23 +248,26 @@ pub mod baseline { Some(message) => { fatal = Some(message.into()); } - 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()); + 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 lhs = tokens.next().unwrap().trim(); - tokens.next(); - let rhs = tokens.next().unwrap().trim(); - mappings.push(Mapping { - remote: full_remote_ref(lhs.into()), - local: (rhs != b"FETCH_HEAD").then(|| full_tracking_ref(rhs.into())), - }) - } + let lhs = tokens.next().unwrap().trim(); + tokens.next(); + let rhs = tokens.next().unwrap().trim(); + mappings.push(Mapping { + remote: full_remote_ref(lhs.into()), + local: (rhs != b"FETCH_HEAD").then(|| full_tracking_ref(rhs.into())), + }) + } + }, }, } } From c81e418ae7f90b674ad005e4b42816c35332a417 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 16 Sep 2022 12:16:34 +0800 Subject: [PATCH 62/68] A first version of the 'funny name' sanitization (#450) however, it's not actually correct as git ignores these entirely. --- git-refspec/src/match_group/validate.rs | 25 ++++++++++++++++++++---- git-refspec/tests/match_group/mod.rs | 26 +++++++++++++++++++++---- 2 files changed, 43 insertions(+), 8 deletions(-) diff --git a/git-refspec/src/match_group/validate.rs b/git-refspec/src/match_group/validate.rs index 37e8f205341..ced5b0ec404 100644 --- a/git-refspec/src/match_group/validate.rs +++ b/git-refspec/src/match_group/validate.rs @@ -45,8 +45,13 @@ impl std::fmt::Display for Issue { /// All possible fixes corrected while validating matched mappings. #[derive(Debug, PartialEq, Eq)] pub enum Fix { - /// TODO - InvalidRefName, + /// 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()]. @@ -81,8 +86,7 @@ impl<'spec, 'item> Outcome<'spec, 'item> { /// 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(self) -> Result<(Self, Vec), Error> { - let fixed = Vec::new(); + pub fn validated(mut self) -> Result<(Self, Vec), Error> { let mut sources_by_destinations = BTreeMap::new(); for (dst, (spec_index, src)) in self .mappings @@ -108,6 +112,19 @@ impl<'spec, 'item> Outcome<'spec, 'item> { if !issues.is_empty() { Err(Error { issues }) } else { + let mut fixed = Vec::new(); + for mapping in self.mappings.iter_mut() { + if let Some(dst) = mapping.rhs.as_ref() { + if dst.starts_with(b"refs/") { + continue; + } + fixed.push(Fix::MappingWithPartialDestinationRemoved { + name: dst.as_ref().to_owned(), + spec: self.group.specs[mapping.spec_index].to_bstring(), + }); + mapping.rhs = None; + } + } Ok((self, fixed)) } } diff --git a/git-refspec/tests/match_group/mod.rs b/git-refspec/tests/match_group/mod.rs index 696d97351f0..15e6af0f0d3 100644 --- a/git-refspec/tests/match_group/mod.rs +++ b/git-refspec/tests/match_group/mod.rs @@ -131,12 +131,30 @@ mod multiple { } #[test] - #[ignore] fn fetch_and_update_with_fixes() { + let glob_spec = "refs/heads/f*:foo/f*"; baseline::agrees_and_applies_fixes( - ["refs/heads/f*:foo/f*", "f1:f1"], - [Fix::InvalidRefName], - ["refs/heads/f1:refs/heads/f1"], + [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/f2", + "refs/heads/f3", + "refs/heads/f1:refs/heads/f1", + ], ) } } From f137d6010610d98de32edb4501053d7786181217 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 16 Sep 2022 12:52:11 +0800 Subject: [PATCH 63/68] fully drop 'funny' names (#450) Still we don't handle (or trigger) any santiization. --- git-refspec/src/match_group/validate.rs | 21 ++++++++++++--------- git-refspec/tests/match_group/mod.rs | 7 +------ 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/git-refspec/src/match_group/validate.rs b/git-refspec/src/match_group/validate.rs index ced5b0ec404..538470c38cb 100644 --- a/git-refspec/src/match_group/validate.rs +++ b/git-refspec/src/match_group/validate.rs @@ -113,18 +113,21 @@ impl<'spec, 'item> Outcome<'spec, 'item> { Err(Error { issues }) } else { let mut fixed = Vec::new(); - for mapping in self.mappings.iter_mut() { - if let Some(dst) = mapping.rhs.as_ref() { + let group = &self.group; + self.mappings.retain(|m| match m.rhs.as_ref() { + Some(dst) => { if dst.starts_with(b"refs/") { - continue; + true + } else { + fixed.push(Fix::MappingWithPartialDestinationRemoved { + name: dst.as_ref().to_owned(), + spec: group.specs[m.spec_index].to_bstring(), + }); + false } - fixed.push(Fix::MappingWithPartialDestinationRemoved { - name: dst.as_ref().to_owned(), - spec: self.group.specs[mapping.spec_index].to_bstring(), - }); - mapping.rhs = None; } - } + None => true, + }); Ok((self, fixed)) } } diff --git a/git-refspec/tests/match_group/mod.rs b/git-refspec/tests/match_group/mod.rs index 15e6af0f0d3..4526b11a6e3 100644 --- a/git-refspec/tests/match_group/mod.rs +++ b/git-refspec/tests/match_group/mod.rs @@ -149,12 +149,7 @@ mod multiple { spec: glob_spec.into(), }, ], - [ - "refs/heads/f1", - "refs/heads/f2", - "refs/heads/f3", - "refs/heads/f1:refs/heads/f1", - ], + ["refs/heads/f1:refs/heads/f1"], ) } } From 4f48095566fc1e2b440d542ef2c5118c3e37fddd Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 16 Sep 2022 13:20:36 +0800 Subject: [PATCH 64/68] another test which doesn't manage to trigger a certain message from git. (#450) Looked for "warning: refs/remotes/origin/branch1 usually tracks refs/heads/branch1, not refs/heads/branch2"" but it seems to take more to get it. Ultimately it's about multiple destinations, and that's caught either way. --- git-refspec/tests/fixtures/match_baseline.sh | 1 + git-refspec/tests/match_group/mod.rs | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/git-refspec/tests/fixtures/match_baseline.sh b/git-refspec/tests/fixtures/match_baseline.sh index 8b1ac592979..c4f832a6b4e 100644 --- a/git-refspec/tests/fixtures/match_baseline.sh +++ b/git-refspec/tests/fixtures/match_baseline.sh @@ -73,5 +73,6 @@ git clone --shared ./base clone 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" ) diff --git a/git-refspec/tests/match_group/mod.rs b/git-refspec/tests/match_group/mod.rs index 4526b11a6e3..ca8f3d0a974 100644 --- a/git-refspec/tests/match_group/mod.rs +++ b/git-refspec/tests/match_group/mod.rs @@ -128,6 +128,14 @@ mod multiple { ], "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] From 2fdec7315a65117095f909b4d7d57a91ba666a43 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 16 Sep 2022 14:05:53 +0800 Subject: [PATCH 65/68] tests to show that empty remotes actually work (#450) Even though the test still fails. --- git-refspec/tests/fixtures/match_baseline.sh | 4 +++ git-refspec/tests/match_group/mod.rs | 9 +++++++ git-refspec/tests/matching/mod.rs | 27 ++++++++++++++------ git-refspec/tests/parse/fetch.rs | 13 ++++++++++ 4 files changed, 45 insertions(+), 8 deletions(-) diff --git a/git-refspec/tests/fixtures/match_baseline.sh b/git-refspec/tests/fixtures/match_baseline.sh index c4f832a6b4e..6e528376c39 100644 --- a/git-refspec/tests/fixtures/match_baseline.sh +++ b/git-refspec/tests/fixtures/match_baseline.sh @@ -74,5 +74,9 @@ git clone --shared ./base clone 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 '@:origin' + baseline '@:HEAD' ) diff --git a/git-refspec/tests/match_group/mod.rs b/git-refspec/tests/match_group/mod.rs index ca8f3d0a974..31e887ff4d2 100644 --- a/git-refspec/tests/match_group/mod.rs +++ b/git-refspec/tests/match_group/mod.rs @@ -90,6 +90,15 @@ mod multiple { baseline::agrees_with_fetch_specs(["refs/heads/*:refs/remotes/origin/*", "^refs/heads/main"]); } + #[test] + #[ignore] + fn fetch_and_update_with_empty_lhs() { + baseline::agrees_with_fetch_specs([":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"]); + baseline::agrees_but_observable_refs_are_vague(["@:HEAD"], ["HEAD:HEAD"]); + } + #[test] fn fetch_and_update_multiple_destinations() { baseline::agrees_with_fetch_specs([ diff --git a/git-refspec/tests/matching/mod.rs b/git-refspec/tests/matching/mod.rs index cf38212f841..bf88bb68d63 100644 --- a/git-refspec/tests/matching/mod.rs +++ b/git-refspec/tests/matching/mod.rs @@ -104,13 +104,17 @@ pub mod baseline { 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 => {} + 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 @@ -259,13 +263,20 @@ pub mod baseline { .unwrap_or_else(|| panic!("line unhandled: {:?}", line.as_bstr())); let mut tokens = past_note.split(|b| *b == b' ').filter(|t| !t.is_empty()); - let lhs = tokens.next().unwrap().trim(); - tokens.next(); + let mut lhs = tokens.next().unwrap().trim(); + if lhs.as_bstr() == "->" { + lhs = "HEAD".as_bytes(); + } else { + tokens.next(); + }; let rhs = tokens.next().unwrap().trim(); - mappings.push(Mapping { - remote: full_remote_ref(lhs.into()), - local: (rhs != b"FETCH_HEAD").then(|| full_tracking_ref(rhs.into())), - }) + 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, + }) + } } }, }, @@ -285,7 +296,7 @@ pub mod baseline { name.insert_str(0, b"refs/tags/"); } else if let Ok(_id) = git_hash::ObjectId::from_hex(name.as_ref()) { // keep as is - } else { + } else if name != "HEAD" { name.insert_str(0, b"refs/heads/"); } } diff --git a/git-refspec/tests/parse/fetch.rs b/git-refspec/tests/parse/fetch.rs index 4f5dcf78ee5..dbe801ee903 100644 --- a/git-refspec/tests/parse/fetch.rs +++ b/git-refspec/tests/parse/fetch.rs @@ -157,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") })); From 7432a2bb5d11e9991ed5f9d1b29ecf79b10c676a Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 16 Sep 2022 16:21:26 +0800 Subject: [PATCH 66/68] Allow 'HEAD' based refspecs to match correctly (#450) --- git-refspec/src/match_group/util.rs | 4 +--- git-refspec/src/match_group/validate.rs | 2 +- git-refspec/tests/fixtures/match_baseline.sh | 2 +- git-refspec/tests/match_group/mod.rs | 3 +-- 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/git-refspec/src/match_group/util.rs b/git-refspec/src/match_group/util.rs index 80ccc80ade1..01372186937 100644 --- a/git-refspec/src/match_group/util.rs +++ b/git-refspec/src/match_group/util.rs @@ -6,7 +6,6 @@ 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. -#[allow(dead_code)] pub struct Matcher<'a> { pub(crate) lhs: Option>, pub(crate) rhs: Option>, @@ -17,7 +16,6 @@ impl<'a> Matcher<'a> { /// 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. - #[allow(dead_code)] pub fn matches_lhs(&self, item: Item<'_>) -> (bool, Option>) { match (self.lhs, self.rhs) { (Some(lhs), None) => (lhs.matches(item).is_match(), None), @@ -160,7 +158,7 @@ impl<'a> From<&'a BStr> for Needle<'a> { name: v, asterisk_pos: pos, } - } else if v.starts_with(b"refs/") { + } else if v.starts_with(b"refs/") || v == "HEAD" { Needle::FullName(v) } else if let Ok(id) = git_hash::ObjectId::from_hex(v) { Needle::Object(id) diff --git a/git-refspec/src/match_group/validate.rs b/git-refspec/src/match_group/validate.rs index 538470c38cb..0443a47b3cf 100644 --- a/git-refspec/src/match_group/validate.rs +++ b/git-refspec/src/match_group/validate.rs @@ -116,7 +116,7 @@ impl<'spec, 'item> Outcome<'spec, 'item> { let group = &self.group; self.mappings.retain(|m| match m.rhs.as_ref() { Some(dst) => { - if dst.starts_with(b"refs/") { + if dst.starts_with(b"refs/") || dst.as_ref() == "HEAD" { true } else { fixed.push(Fix::MappingWithPartialDestinationRemoved { diff --git a/git-refspec/tests/fixtures/match_baseline.sh b/git-refspec/tests/fixtures/match_baseline.sh index 6e528376c39..d07530b2c20 100644 --- a/git-refspec/tests/fixtures/match_baseline.sh +++ b/git-refspec/tests/fixtures/match_baseline.sh @@ -76,7 +76,7 @@ git clone --shared ./base clone 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 '@:origin' + baseline '@:f1' baseline '@:HEAD' ) diff --git a/git-refspec/tests/match_group/mod.rs b/git-refspec/tests/match_group/mod.rs index 31e887ff4d2..b7ce1ffdf69 100644 --- a/git-refspec/tests/match_group/mod.rs +++ b/git-refspec/tests/match_group/mod.rs @@ -91,9 +91,8 @@ mod multiple { } #[test] - #[ignore] fn fetch_and_update_with_empty_lhs() { - baseline::agrees_with_fetch_specs([":refs/heads/f1"]); + 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"]); baseline::agrees_but_observable_refs_are_vague(["@:HEAD"], ["HEAD:HEAD"]); From 13356184c735d72edb891d64b3de1bb5c981a6ad Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 16 Sep 2022 16:31:04 +0800 Subject: [PATCH 67/68] Actually assure we don't try to write into the HEAD ref, which git avoids as well (#450) --- git-refspec/src/match_group/util.rs | 3 ++- git-refspec/tests/fixtures/match_baseline.sh | 3 +++ git-refspec/tests/match_group/mod.rs | 13 ++++++++++++- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/git-refspec/src/match_group/util.rs b/git-refspec/src/match_group/util.rs index 01372186937..469c9b2de9b 100644 --- a/git-refspec/src/match_group/util.rs +++ b/git-refspec/src/match_group/util.rs @@ -70,6 +70,7 @@ impl<'a> Needle<'a> { 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), @@ -158,7 +159,7 @@ impl<'a> From<&'a BStr> for Needle<'a> { name: v, asterisk_pos: pos, } - } else if v.starts_with(b"refs/") || v == "HEAD" { + } else if v.starts_with(b"refs/") { Needle::FullName(v) } else if let Ok(id) = git_hash::ObjectId::from_hex(v) { Needle::Object(id) diff --git a/git-refspec/tests/fixtures/match_baseline.sh b/git-refspec/tests/fixtures/match_baseline.sh index d07530b2c20..3c2bdff940d 100644 --- a/git-refspec/tests/fixtures/match_baseline.sh +++ b/git-refspec/tests/fixtures/match_baseline.sh @@ -76,6 +76,9 @@ git clone --shared ./base clone 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/match_group/mod.rs b/git-refspec/tests/match_group/mod.rs index b7ce1ffdf69..fade345bb04 100644 --- a/git-refspec/tests/match_group/mod.rs +++ b/git-refspec/tests/match_group/mod.rs @@ -95,7 +95,18 @@ mod multiple { 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"]); - baseline::agrees_but_observable_refs_are_vague(["@:HEAD"], ["HEAD:HEAD"]); + } + + #[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] From 9dc7a3f40dbf3d4802ed095fc21dfb3da67acfea Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 16 Sep 2022 16:34:12 +0800 Subject: [PATCH 68/68] remote todo with note about our current understanding (#450) --- git-refspec/src/match_group/util.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/git-refspec/src/match_group/util.rs b/git-refspec/src/match_group/util.rs index 469c9b2de9b..42a31c03acb 100644 --- a/git-refspec/src/match_group/util.rs +++ b/git-refspec/src/match_group/util.rs @@ -20,7 +20,9 @@ impl<'a> Matcher<'a> { 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), - _ => todo!(), + (None, None) | (None, Some(_)) => { + unreachable!("For all we know, the lefthand side is never empty. Push specs might change that.") + } } } }