diff --git a/Cargo.lock b/Cargo.lock
index 83cf0debbe0..0d853e295ec 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1484,6 +1484,19 @@ dependencies = [
"tempfile",
]
+[[package]]
+name = "git-refspec"
+version = "0.0.0"
+dependencies = [
+ "bstr",
+ "git-hash",
+ "git-revision",
+ "git-testtools",
+ "git-validate",
+ "smallvec",
+ "thiserror",
+]
+
[[package]]
name = "git-repository"
version = "0.20.0"
diff --git a/Cargo.toml b/Cargo.toml
index 145aebfa155..0b55bfb6423 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -162,6 +162,7 @@ members = [
"git-lock",
"git-attributes",
"git-pathspec",
+ "git-refspec",
"git-path",
"git-repository",
"gitoxide-core",
diff --git a/README.md b/README.md
index 01eec38a247..c9ce701f6c5 100644
--- a/README.md
+++ b/README.md
@@ -132,6 +132,7 @@ is usable to some extend.
* [git-repository](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-repository)
* [git-attributes](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-attributes)
* [git-pathspec](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-pathspec)
+ * [git-refspec](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-refspec)
* `gitoxide-core`
* **very early** _(possibly without any documentation and many rough edges)_
* [git-index](https://github.com/Byron/gitoxide/blob/main/crate-status.md#git-index)
diff --git a/crate-status.md b/crate-status.md
index 3f412ee8561..15fcc568563 100644
--- a/crate-status.md
+++ b/crate-status.md
@@ -232,7 +232,11 @@ Check out the [performance discussion][git-traverse-performance] as well.
### git-pathspec
* [x] parse
-* [ ] check for match
+* [ ] matching of paths
+
+### git-refspec
+* [x] parse
+* [ ] matching of references and object names
### git-note
diff --git a/etc/check-package-size.sh b/etc/check-package-size.sh
index a6117e9b250..aff6a2fd162 100755
--- a/etc/check-package-size.sh
+++ b/etc/check-package-size.sh
@@ -18,6 +18,7 @@ echo "in root: gitoxide CLI"
(enter cargo-smart-release && indent cargo diet -n --package-size-limit 95KB)
(enter git-actor && indent cargo diet -n --package-size-limit 5KB)
(enter git-pathspec && indent cargo diet -n --package-size-limit 25KB)
+(enter git-refspec && indent cargo diet -n --package-size-limit 15KB)
(enter git-path && indent cargo diet -n --package-size-limit 15KB)
(enter git-attributes && indent cargo diet -n --package-size-limit 15KB)
(enter git-discover && indent cargo diet -n --package-size-limit 20KB)
diff --git a/git-refspec/CHANGELOG.md b/git-refspec/CHANGELOG.md
new file mode 100644
index 00000000000..64f6c97c7bc
--- /dev/null
+++ b/git-refspec/CHANGELOG.md
@@ -0,0 +1,30 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## 0.0.0 (2022-08-05)
+
+Initial release for name reservation.
+
+### Commit Statistics
+
+
+
+ - 2 commits contributed to the release.
+ - 0 commits where understood as [conventional](https://www.conventionalcommits.org).
+ - 1 unique issue was worked on: [#450](https://github.com/Byron/gitoxide/issues/450)
+
+### Commit Details
+
+
+
+view details
+
+ * **[#450](https://github.com/Byron/gitoxide/issues/450)**
+ - prepare git-refspec changelog prior to release ([`3383408`](https://github.com/Byron/gitoxide/commit/3383408ce22ca9c7502ad2d1fab51cf12dc5ee72))
+ - empty `git-refspec` crate for name reservation prior to implementation ([`871a3c0`](https://github.com/Byron/gitoxide/commit/871a3c054d4fe6c1e92b6f2e260b19463404509f))
+
+
diff --git a/git-refspec/Cargo.toml b/git-refspec/Cargo.toml
new file mode 100644
index 00000000000..aa8b8cbe3bf
--- /dev/null
+++ b/git-refspec/Cargo.toml
@@ -0,0 +1,26 @@
+[package]
+name = "git-refspec"
+version = "0.0.0"
+repository = "https://github.com/Byron/gitoxide"
+license = "MIT/Apache-2.0"
+description = "A WIP crate of the gitoxide project for parsing and representing refspecs"
+authors = ["Sebastian Thiel "]
+edition = "2018"
+include = ["src/**/*", "CHANGELOG.md", "README.md"]
+
+[lib]
+doctest = false
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+git-revision = { version = "^0.3.0", path = "../git-revision" }
+git-validate = { version = "^0.5.4", path = "../git-validate" }
+git-hash = { version = "^0.9.6", path = "../git-hash" }
+
+bstr = { version = "0.2.13", default-features = false, features = ["std"]}
+thiserror = "1.0.26"
+smallvec = "1.9.0"
+
+[dev-dependencies]
+git-testtools = { path = "../tests/tools" }
diff --git a/git-refspec/README.md b/git-refspec/README.md
new file mode 100644
index 00000000000..022495e4f27
--- /dev/null
+++ b/git-refspec/README.md
@@ -0,0 +1,11 @@
+# `git-refspec`
+
+### Testing
+
+#### Fuzzing
+
+`cargo fuzz` is used for fuzzing, installable with `cargo install cargo-fuzz`.
+
+Targets can be listed with `cargo fuzz list` and executed via `cargo +nightly fuzz run `,
+where `` can be `parse` for example.
+
diff --git a/git-refspec/fuzz/.gitignore b/git-refspec/fuzz/.gitignore
new file mode 100644
index 00000000000..a0925114d61
--- /dev/null
+++ b/git-refspec/fuzz/.gitignore
@@ -0,0 +1,3 @@
+target
+corpus
+artifacts
diff --git a/git-refspec/fuzz/Cargo.lock b/git-refspec/fuzz/Cargo.lock
new file mode 100644
index 00000000000..a5e6eb61092
--- /dev/null
+++ b/git-refspec/fuzz/Cargo.lock
@@ -0,0 +1,397 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "arbitrary"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a7924531f38b1970ff630f03eb20a2fde69db5c590c93b0f3482e95dcc5fd60"
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "bstr"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223"
+dependencies = [
+ "lazy_static",
+ "memchr",
+ "regex-automata",
+]
+
+[[package]]
+name = "btoi"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97c0869a9faa81f8bbf8102371105d6d0a7b79167a04c340b04ab16892246a11"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3"
+
+[[package]]
+name = "cc"
+version = "1.0.73"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "git-actor"
+version = "0.11.0"
+dependencies = [
+ "bstr",
+ "btoi",
+ "git-date",
+ "itoa",
+ "nom",
+ "quick-error",
+]
+
+[[package]]
+name = "git-date"
+version = "0.0.2"
+dependencies = [
+ "bstr",
+ "itoa",
+ "time",
+]
+
+[[package]]
+name = "git-features"
+version = "0.22.0"
+dependencies = [
+ "git-hash",
+ "libc",
+ "sha1_smol",
+]
+
+[[package]]
+name = "git-hash"
+version = "0.9.6"
+dependencies = [
+ "hex",
+ "quick-error",
+]
+
+[[package]]
+name = "git-object"
+version = "0.20.0"
+dependencies = [
+ "bstr",
+ "btoi",
+ "git-actor",
+ "git-features",
+ "git-hash",
+ "git-validate",
+ "hex",
+ "itoa",
+ "nom",
+ "quick-error",
+ "smallvec",
+]
+
+[[package]]
+name = "git-refspec"
+version = "0.0.0"
+dependencies = [
+ "bstr",
+ "git-hash",
+ "git-revision",
+ "git-validate",
+ "smallvec",
+ "thiserror",
+]
+
+[[package]]
+name = "git-refspec-fuzz"
+version = "0.0.0"
+dependencies = [
+ "git-refspec",
+ "libfuzzer-sys",
+]
+
+[[package]]
+name = "git-revision"
+version = "0.3.0"
+dependencies = [
+ "bstr",
+ "git-date",
+ "git-hash",
+ "git-object",
+ "hash_hasher",
+ "thiserror",
+]
+
+[[package]]
+name = "git-validate"
+version = "0.5.4"
+dependencies = [
+ "bstr",
+ "quick-error",
+]
+
+[[package]]
+name = "hash_hasher"
+version = "2.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74721d007512d0cb3338cd20f0654ac913920061a4c4d0d8708edb3f2a698c0c"
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "itoa"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754"
+
+[[package]]
+name = "js-sys"
+version = "0.3.59"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "258451ab10b34f8af53416d1fdab72c22e805f0c92a1136d59470ec0b11138b2"
+dependencies = [
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "libc"
+version = "0.2.127"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "505e71a4706fa491e9b1b55f51b95d4037d0821ee40131190475f692b35b009b"
+
+[[package]]
+name = "libfuzzer-sys"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "336244aaeab6a12df46480dc585802aa743a72d66b11937844c61bbca84c991d"
+dependencies = [
+ "arbitrary",
+ "cc",
+ "once_cell",
+]
+
+[[package]]
+name = "log"
+version = "0.4.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "memchr"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
+
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
+name = "nom"
+version = "7.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "num_threads"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quick-error"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
+
+[[package]]
+name = "quote"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
+
+[[package]]
+name = "sha1_smol"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012"
+
+[[package]]
+name = "smallvec"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1"
+
+[[package]]
+name = "syn"
+version = "1.0.99"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f5f6586b7f764adc0231f4c79be7b920e766bb2f3e51b3661cdb263828f19994"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12bafc5b54507e0149cdf1b145a5d80ab80a90bcd9275df43d4fff68460f6c21"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "time"
+version = "0.3.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74b7cc93fc23ba97fde84f7eea56c55d1ba183f495c6715defdfc7b9cb8c870f"
+dependencies = [
+ "js-sys",
+ "libc",
+ "num_threads",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.82"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc7652e3f6c4706c8d9cd54832c4a4ccb9b5336e2c3bd154d5cccfbf1c1f5f7d"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.82"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "662cd44805586bd52971b9586b1df85cdbbd9112e4ef4d8f41559c334dc6ac3f"
+dependencies = [
+ "bumpalo",
+ "log",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.82"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b260f13d3012071dfb1512849c033b1925038373aea48ced3012c09df952c602"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.82"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5be8e654bdd9b79216c2929ab90721aa82faf65c48cdf08bdc4e7f51357b80da"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.82"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6598dd0bd3c7d51095ff6531a5b23e02acdc81804e30d8f07afb77b7215a140a"
diff --git a/git-refspec/fuzz/Cargo.toml b/git-refspec/fuzz/Cargo.toml
new file mode 100644
index 00000000000..bcbabbb425a
--- /dev/null
+++ b/git-refspec/fuzz/Cargo.toml
@@ -0,0 +1,25 @@
+[package]
+name = "git-refspec-fuzz"
+version = "0.0.0"
+authors = ["Automatically generated"]
+publish = false
+edition = "2018"
+
+[package.metadata]
+cargo-fuzz = true
+
+[dependencies]
+libfuzzer-sys = "0.4"
+
+[dependencies.git-refspec]
+path = ".."
+
+# Prevent this from interfering with workspaces
+[workspace]
+members = ["."]
+
+[[bin]]
+name = "parse"
+path = "fuzz_targets/parse.rs"
+test = false
+doc = false
diff --git a/git-refspec/fuzz/fuzz_targets/parse.rs b/git-refspec/fuzz/fuzz_targets/parse.rs
new file mode 100644
index 00000000000..c319b20f35e
--- /dev/null
+++ b/git-refspec/fuzz/fuzz_targets/parse.rs
@@ -0,0 +1,7 @@
+#![no_main]
+use libfuzzer_sys::fuzz_target;
+
+fuzz_target!(|data: &[u8]| {
+ drop(git_refspec::parse(data.into(), git_refspec::Operation::Push));
+ drop(git_refspec::parse(data.into(), git_refspec::Operation::Fetch));
+});
diff --git a/git-refspec/src/instruction.rs b/git-refspec/src/instruction.rs
new file mode 100644
index 00000000000..ceceb9db79d
--- /dev/null
+++ b/git-refspec/src/instruction.rs
@@ -0,0 +1,64 @@
+use crate::{parse::Operation, Instruction};
+use bstr::BStr;
+
+impl Instruction<'_> {
+ /// Derive the mode of operation from this instruction.
+ pub fn operation(&self) -> Operation {
+ match self {
+ Instruction::Push(_) => Operation::Push,
+ Instruction::Fetch(_) => Operation::Fetch,
+ }
+ }
+}
+
+/// Note that all sources can either be a ref-name, partial or full, or a rev-spec, unless specified otherwise, on the local side.
+/// Destinations can only be a partial or full ref names on the remote side.
+#[derive(PartialEq, Eq, Copy, Clone, Hash, Debug)]
+pub enum Push<'a> {
+ /// Push all local branches to the matching destination on the remote, which has to exist to be updated.
+ AllMatchingBranches {
+ /// If true, allow non-fast-forward updates of the matched destination branch.
+ allow_non_fast_forward: bool,
+ },
+ /// Delete the destination ref or glob pattern, with only a single `*` allowed.
+ Delete {
+ /// The reference or pattern to delete on the remote.
+ ref_or_pattern: &'a BStr,
+ },
+ /// Push a single ref or refspec to a known destination ref.
+ Matching {
+ /// The source ref or refspec to push. If pattern, it contains a single `*`.
+ src: &'a BStr,
+ /// The ref to update with the object from `src`. If `src` is a pattern, this is a pattern too.
+ dst: &'a BStr,
+ /// If true, allow non-fast-forward updates of `dest`.
+ allow_non_fast_forward: bool,
+ },
+}
+
+/// Any source can either be a ref name (full or partial) or a fully spelled out hex-sha for an object, on the remote side.
+///
+/// Destinations can only be a partial or full ref-names on the local side.
+#[derive(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.
+ Only {
+ /// The ref name to fetch on the remote side, without updating the local side. This will write the result into `FETCH_HEAD`.
+ src: &'a BStr,
+ },
+ /// Exclude a single ref.
+ Exclude {
+ /// A single partial or full ref name to exclude on the remote, or a pattern with a single `*`. It cannot be a spelled out object hash.
+ src: &'a BStr,
+ },
+ /// Fetch from `src` and update the corresponding destination branches in `dst` accordingly.
+ AndUpdate {
+ /// The ref name to fetch on the remote side, or a pattern with a single `*` to match against.
+ src: &'a BStr,
+ /// The local destination to update with what was fetched, or a pattern whose single `*` will be replaced with the matching portion
+ /// of the `*` from `src`.
+ dst: &'a BStr,
+ /// If true, allow non-fast-forward updates of `dest`.
+ allow_non_fast_forward: bool,
+ },
+}
diff --git a/git-refspec/src/lib.rs b/git-refspec/src/lib.rs
new file mode 100644
index 00000000000..5f25a43c497
--- /dev/null
+++ b/git-refspec/src/lib.rs
@@ -0,0 +1,33 @@
+//! Parse git ref-specs and represent them.
+#![forbid(unsafe_code, rust_2018_idioms)]
+#![deny(missing_docs)]
+
+///
+pub mod parse;
+pub use parse::function::parse;
+
+///
+pub mod instruction;
+
+/// A refspec with references to the memory it was parsed from.
+#[derive(PartialEq, Eq, Copy, Clone, Hash, Debug)]
+pub struct RefSpecRef<'a> {
+ mode: types::Mode,
+ op: parse::Operation,
+ src: Option<&'a bstr::BStr>,
+ dst: Option<&'a bstr::BStr>,
+}
+
+/// An owned refspec.
+#[derive(PartialEq, Eq, Clone, Hash, Debug)]
+pub struct RefSpec {
+ mode: types::Mode,
+ op: parse::Operation,
+ src: Option,
+ dst: Option,
+}
+
+mod spec;
+
+mod types;
+pub use types::Instruction;
diff --git a/git-refspec/src/parse.rs b/git-refspec/src/parse.rs
new file mode 100644
index 00000000000..496ef202bca
--- /dev/null
+++ b/git-refspec/src/parse.rs
@@ -0,0 +1,245 @@
+/// The error returned by the [`parse()`][crate::parse()] function.
+#[derive(Debug, thiserror::Error)]
+#[allow(missing_docs)]
+pub enum Error {
+ #[error("Empty refspecs are invalid")]
+ Empty,
+ #[error("Negative refspecs cannot have destinations as they exclude sources")]
+ NegativeWithDestination,
+ #[error("Negative specs must not be empty")]
+ NegativeEmpty,
+ #[error("Negative specs are only supported when fetching")]
+ NegativeUnsupported,
+ #[error("Negative specs must be object hashes")]
+ NegativeObjectHash,
+ #[error("Fetch destinations must be ref-names, like 'HEAD:refs/heads/branch'")]
+ InvalidFetchDestination,
+ #[error("Cannot push into an empty destination")]
+ PushToEmpty,
+ #[error("glob patterns may only involved a single '*' character, found {pattern:?}")]
+ PatternUnsupported { pattern: bstr::BString },
+ #[error("Both sides of the specification need a pattern, like 'a/*:b/*'")]
+ PatternUnbalanced,
+ #[error(transparent)]
+ ReferenceName(#[from] git_validate::refname::Error),
+ #[error(transparent)]
+ RevSpec(#[from] git_revision::spec::parse::Error),
+}
+
+/// Define how the parsed refspec should be used.
+#[derive(PartialEq, Eq, Copy, Clone, Hash, Debug)]
+pub enum Operation {
+ /// The `src` side is local and the `dst` side is remote.
+ Push,
+ /// The `src` side is remote and the `dst` side is local.
+ Fetch,
+}
+
+pub(crate) mod function {
+ use crate::parse::Error;
+ use crate::{parse::Operation, types::Mode, RefSpecRef};
+ use bstr::{BStr, ByteSlice};
+
+ /// Parse `spec` for use in `operation` and return it if it is valid.
+ pub fn parse(mut spec: &BStr, operation: Operation) -> Result, Error> {
+ fn fetch_head_only(mode: Mode) -> RefSpecRef<'static> {
+ RefSpecRef {
+ mode,
+ op: Operation::Fetch,
+ src: Some("HEAD".into()),
+ dst: None,
+ }
+ }
+
+ let mode = match spec.get(0) {
+ Some(&b'^') => {
+ spec = &spec[1..];
+ if operation == Operation::Push {
+ return Err(Error::NegativeUnsupported);
+ }
+ Mode::Negative
+ }
+ Some(&b'+') => {
+ spec = &spec[1..];
+ Mode::Force
+ }
+ Some(_) => Mode::Normal,
+ None => {
+ return match operation {
+ Operation::Push => Err(Error::Empty),
+ Operation::Fetch => Ok(fetch_head_only(Mode::Normal)),
+ }
+ }
+ };
+
+ let (mut src, dst) = match spec.find_byte(b':') {
+ Some(pos) => {
+ if mode == Mode::Negative {
+ return Err(Error::NegativeWithDestination);
+ }
+
+ let (src, dst) = spec.split_at(pos);
+ let dst = &dst[1..];
+ let src = (!src.is_empty()).then(|| src.as_bstr());
+ let dst = (!dst.is_empty()).then(|| dst.as_bstr());
+ match (src, dst) {
+ (None, None) => match operation {
+ Operation::Push => (None, None),
+ Operation::Fetch => (Some("HEAD".into()), None),
+ },
+ (None, Some(dst)) => match operation {
+ Operation::Push => (None, Some(dst)),
+ Operation::Fetch => (Some("HEAD".into()), Some(dst)),
+ },
+ (Some(src), None) => match operation {
+ Operation::Push => return Err(Error::PushToEmpty),
+ Operation::Fetch => (Some(src), None),
+ },
+ (Some(src), Some(dst)) => (Some(src), Some(dst)),
+ }
+ }
+ None => {
+ let src = (!spec.is_empty()).then(|| spec);
+ if Operation::Fetch == operation && mode != Mode::Negative && src.is_none() {
+ return Ok(fetch_head_only(mode));
+ } else {
+ (src, None)
+ }
+ }
+ };
+
+ 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();
+ }
+ }
+ 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);
+ }
+ Ok(RefSpecRef {
+ op: operation,
+ mode,
+ src,
+ dst,
+ })
+ }
+
+ fn looks_like_object_hash(spec: &BStr) -> bool {
+ spec.len() >= git_hash::Kind::shortest().len_in_hex() && spec.iter().all(|b| b.is_ascii_hexdigit())
+ }
+
+ fn validated(spec: Option<&BStr>, allow_revspecs: bool) -> Result<(Option<&BStr>, bool), Error> {
+ match spec {
+ Some(spec) => {
+ let glob_count = spec.iter().filter(|b| **b == b'*').take(2).count();
+ if glob_count > 1 {
+ return Err(Error::PatternUnsupported { pattern: spec.into() });
+ }
+ let has_globs = glob_count == 1;
+ if has_globs {
+ let mut buf = smallvec::SmallVec::<[u8; 256]>::with_capacity(spec.len());
+ buf.extend_from_slice(spec);
+ let glob_pos = buf.find_byte(b'*').expect("glob present");
+ buf[glob_pos] = b'a';
+ git_validate::reference::name_partial(buf.as_bstr())?;
+ } else {
+ git_validate::reference::name_partial(spec)
+ .map_err(Error::from)
+ .or_else(|err| {
+ if allow_revspecs {
+ match git_revision::spec::parse(spec, &mut super::revparse::Noop) {
+ Ok(_) => {
+ if spec.iter().any(|b| b.is_ascii_whitespace()) {
+ Err(err)
+ } else {
+ Ok(spec)
+ }
+ }
+ Err(err) => Err(err.into()),
+ }
+ } else {
+ Err(err)
+ }
+ })?;
+ }
+ Ok((Some(spec), has_globs))
+ }
+ None => Ok((None, false)),
+ }
+ }
+}
+
+mod revparse {
+ use bstr::BStr;
+ use git_revision::spec::parse::delegate::{
+ Kind, Navigate, PeelTo, PrefixHint, ReflogLookup, Revision, SiblingBranch, Traversal,
+ };
+
+ pub(crate) struct Noop;
+
+ impl Revision for Noop {
+ fn find_ref(&mut self, _name: &BStr) -> Option<()> {
+ Some(())
+ }
+
+ fn disambiguate_prefix(&mut self, _prefix: git_hash::Prefix, _hint: Option>) -> Option<()> {
+ Some(())
+ }
+
+ fn reflog(&mut self, _query: ReflogLookup) -> Option<()> {
+ Some(())
+ }
+
+ fn nth_checked_out_branch(&mut self, _branch_no: usize) -> Option<()> {
+ Some(())
+ }
+
+ fn sibling_branch(&mut self, _kind: SiblingBranch) -> Option<()> {
+ Some(())
+ }
+ }
+
+ impl Navigate for Noop {
+ fn traverse(&mut self, _kind: Traversal) -> Option<()> {
+ Some(())
+ }
+
+ fn peel_until(&mut self, _kind: PeelTo<'_>) -> Option<()> {
+ Some(())
+ }
+
+ fn find(&mut self, _regex: &BStr, _negated: bool) -> Option<()> {
+ Some(())
+ }
+
+ fn index_lookup(&mut self, _path: &BStr, _stage: u8) -> Option<()> {
+ Some(())
+ }
+ }
+
+ impl Kind for Noop {
+ fn kind(&mut self, _kind: git_revision::spec::Kind) -> Option<()> {
+ Some(())
+ }
+ }
+
+ impl git_revision::spec::parse::Delegate for Noop {
+ fn done(&mut self) {}
+ }
+}
diff --git a/git-refspec/src/spec.rs b/git-refspec/src/spec.rs
new file mode 100644
index 00000000000..14fd2a090e2
--- /dev/null
+++ b/git-refspec/src/spec.rs
@@ -0,0 +1,86 @@
+use crate::instruction::{Fetch, Push};
+use crate::{parse::Operation, types::Mode, Instruction, RefSpec, RefSpecRef};
+
+/// Conversion. Use the [RefSpecRef][RefSpec::to_ref()] type for more usage options.
+impl RefSpec {
+ /// Return ourselves as reference type.
+ pub fn to_ref(&self) -> RefSpecRef<'_> {
+ RefSpecRef {
+ mode: self.mode,
+ op: self.op,
+ src: self.src.as_ref().map(|b| b.as_ref()),
+ dst: self.dst.as_ref().map(|b| b.as_ref()),
+ }
+ }
+}
+
+mod impls {
+ use crate::{RefSpec, RefSpecRef};
+
+ impl From> for RefSpec {
+ fn from(v: RefSpecRef<'_>) -> Self {
+ v.to_owned()
+ }
+ }
+}
+
+/// Access
+impl RefSpecRef<'_> {
+ /// Transform the state of the refspec into an instruction making clear what to do with it.
+ pub fn instruction(&self) -> Instruction<'_> {
+ match self.op {
+ Operation::Fetch => match (self.mode, self.src, self.dst) {
+ (Mode::Normal | Mode::Force, Some(src), None) => Instruction::Fetch(Fetch::Only { src }),
+ (Mode::Normal | Mode::Force, Some(src), Some(dst)) => Instruction::Fetch(Fetch::AndUpdate {
+ src,
+ dst,
+ allow_non_fast_forward: matches!(self.mode, Mode::Force),
+ }),
+ (Mode::Negative, Some(src), None) => Instruction::Fetch(Fetch::Exclude { src }),
+ (mode, src, dest) => {
+ unreachable!(
+ "BUG: fetch instructions with {:?} {:?} {:?} are not possible",
+ mode, src, dest
+ )
+ }
+ },
+ Operation::Push => match (self.mode, self.src, self.dst) {
+ (Mode::Normal | Mode::Force, Some(src), None) => Instruction::Push(Push::Matching {
+ src,
+ dst: src,
+ allow_non_fast_forward: matches!(self.mode, Mode::Force),
+ }),
+ (Mode::Normal | Mode::Force, None, Some(dst)) => {
+ Instruction::Push(Push::Delete { ref_or_pattern: dst })
+ }
+ (Mode::Normal | Mode::Force, None, None) => Instruction::Push(Push::AllMatchingBranches {
+ allow_non_fast_forward: matches!(self.mode, Mode::Force),
+ }),
+ (Mode::Normal | Mode::Force, Some(src), Some(dst)) => Instruction::Push(Push::Matching {
+ src,
+ dst,
+ allow_non_fast_forward: matches!(self.mode, Mode::Force),
+ }),
+ (mode, src, dest) => {
+ unreachable!(
+ "BUG: push instructions with {:?} {:?} {:?} are not possible",
+ mode, src, dest
+ )
+ }
+ },
+ }
+ }
+}
+
+/// Conversion
+impl RefSpecRef<'_> {
+ /// Convert this ref into a standalone, owned copy.
+ pub fn to_owned(&self) -> RefSpec {
+ RefSpec {
+ mode: self.mode,
+ op: self.op,
+ src: self.src.map(ToOwned::to_owned),
+ dst: self.dst.map(ToOwned::to_owned),
+ }
+ }
+}
diff --git a/git-refspec/src/types.rs b/git-refspec/src/types.rs
new file mode 100644
index 00000000000..f7c453a0a71
--- /dev/null
+++ b/git-refspec/src/types.rs
@@ -0,0 +1,21 @@
+use crate::instruction;
+
+/// The way to interpret a refspec.
+#[derive(PartialEq, Eq, Copy, Clone, Hash, Debug)]
+pub(crate) enum Mode {
+ /// Apply standard rules for refspecs which are including refs with specific rules related to allowing fast forwards of destinations.
+ Normal,
+ /// Even though according to normal rules a non-fastforward would be denied, override this and reset a ref forcefully in the destination.
+ Force,
+ /// Instead of considering matching refs included, we consider them excluded. This applies only to the source side of a refspec.
+ Negative,
+}
+
+/// Tells what to do and is derived from a [`RefSpec`][crate::RefSpecRef].
+#[derive(PartialEq, Eq, Copy, Clone, Hash, Debug)]
+pub enum Instruction<'a> {
+ /// An instruction for pushing.
+ Push(instruction::Push<'a>),
+ /// An instruction for fetching.
+ Fetch(instruction::Fetch<'a>),
+}
diff --git a/git-refspec/tests/fixtures/generated-archives/make_baseline.tar.xz b/git-refspec/tests/fixtures/generated-archives/make_baseline.tar.xz
new file mode 100644
index 00000000000..aa3836355df
--- /dev/null
+++ b/git-refspec/tests/fixtures/generated-archives/make_baseline.tar.xz
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:9d52f5fe25601c545187b0ac19b004f30488937b6907e8206333d2ba923c7b29
+size 9372
diff --git a/git-refspec/tests/fixtures/make_baseline.sh b/git-refspec/tests/fixtures/make_baseline.sh
new file mode 100644
index 00000000000..3e78ce9788a
--- /dev/null
+++ b/git-refspec/tests/fixtures/make_baseline.sh
@@ -0,0 +1,122 @@
+#!/bin/bash
+set -eu -o pipefail
+
+git init;
+
+function baseline() {
+ local kind=$1
+ local refspec=$2
+ local force_fail=${3:-}
+
+ cat <.git/config
+[remote "test"]
+ url = .
+ $kind = "$refspec"
+EOF
+
+ git ls-remote "test" && status=0 || status=$?
+ if [ -n "$force_fail" ]; then
+ status=128
+ fi
+
+ {
+ echo "$kind" "$refspec"
+ echo "$status"
+ } >> baseline.git
+}
+
+
+# invalid
+
+baseline push ''
+baseline push '::'
+baseline fetch '::'
+baseline fetch '^a:'
+baseline fetch '^a:b'
+baseline fetch '^:'
+baseline fetch '^:b'
+baseline fetch '^'
+baseline push '^'
+
+baseline fetch '^refs/heads/qa/*/*'
+baseline push '^refs/heads/qa/*/*'
+baseline push 'main~1'
+baseline fetch 'main~1'
+baseline push 'main~1:other~1'
+baseline push ':main~1'
+
+baseline push 'refs/heads/*:refs/remotes/frotz'
+baseline push 'refs/heads:refs/remotes/frotz/*'
+
+baseline fetch 'refs/heads/*:refs/remotes/frotz'
+baseline fetch 'refs/heads:refs/remotes/frotz/*'
+baseline fetch 'refs/heads/main::refs/remotes/frotz/xyzzy'
+baseline fetch 'refs/heads/maste :refs/remotes/frotz/xyzzy'
+baseline fetch 'main~1:refs/remotes/frotz/backup'
+baseline fetch 'HEAD~4:refs/remotes/frotz/new'
+baseline push 'refs/heads/ nitfol'
+baseline fetch 'refs/heads/ nitfol'
+baseline push 'HEAD:'
+baseline push 'refs/heads/ nitfol:'
+baseline fetch 'refs/heads/ nitfol:'
+baseline push ':refs/remotes/frotz/delete me'
+baseline fetch ':refs/remotes/frotz/HEAD to me'
+baseline fetch 'refs/heads/*/*/for-linus:refs/remotes/mine/*'
+baseline push 'refs/heads/*/*/for-linus:refs/remotes/mine/*'
+
+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}"
+
+# valid
+baseline push '+:'
+baseline push ':'
+
+baseline fetch ''
+baseline fetch ':'
+baseline fetch '+'
+baseline push 'refs/heads/main:refs/remotes/frotz/xyzzy'
+baseline push 'refs/heads/*:refs/remotes/frotz/*'
+
+
+baseline fetch 'refs/heads/*:refs/remotes/frotz/*'
+baseline fetch 'refs/heads/main:refs/remotes/frotz/xyzzy'
+
+baseline push 'main~1:refs/remotes/frotz/backup'
+baseline push 'HEAD~4:refs/remotes/frotz/new'
+
+baseline push 'HEAD'
+baseline fetch 'HEAD'
+baseline push '@'
+baseline fetch '@'
+
+baseline push '^@' fail
+baseline fetch '^@'
+
+baseline push '+@'
+baseline fetch '+@'
+
+baseline fetch 'HEAD:'
+
+baseline push ':refs/remotes/frotz/deleteme'
+baseline fetch ':refs/remotes/frotz/HEAD-to-me'
+
+baseline push ':a'
+baseline push '+:a'
+
+baseline fetch ':a'
+baseline fetch '+:a'
+
+baseline fetch 'refs/heads/*/for-linus:refs/remotes/mine/*-blah'
+baseline push 'refs/heads/*/for-linus:refs/remotes/mine/*-blah'
+
+baseline fetch 'refs/heads*/for-linus:refs/remotes/mine/*'
+baseline push 'refs/heads*/for-linus:refs/remotes/mine/*'
+
+
+baseline fetch 'refs/heads/*/for-linus:refs/remotes/mine/*'
+baseline push 'refs/heads/*/for-linus:refs/remotes/mine/*'
+
+good=$(printf '\303\204')
+baseline fetch "refs/heads/${good}"
diff --git a/git-refspec/tests/parse/fetch.rs b/git-refspec/tests/parse/fetch.rs
new file mode 100644
index 00000000000..b844fc2d403
--- /dev/null
+++ b/git-refspec/tests/parse/fetch.rs
@@ -0,0 +1,144 @@
+use crate::parse::{assert_parse, b, try_parse};
+use git_refspec::{instruction::Fetch, parse::Error, parse::Operation, Instruction};
+
+#[test]
+fn revspecs_are_disallowed() {
+ for spec in ["main~1", "^@^{}", "HEAD:main~1"] {
+ assert!(matches!(
+ try_parse(spec, Operation::Fetch).unwrap_err(),
+ Error::ReferenceName(_)
+ ));
+ }
+}
+
+#[test]
+fn object_hash_as_source() {
+ assert_parse(
+ "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391:",
+ Instruction::Fetch(Fetch::Only {
+ src: b("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"),
+ }),
+ );
+}
+
+#[test]
+fn object_hash_destination_is_invalid() {
+ assert!(matches!(
+ try_parse("a:e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", Operation::Fetch).unwrap_err(),
+ Error::InvalidFetchDestination
+ ));
+}
+
+#[test]
+fn negative_must_not_be_empty() {
+ assert!(matches!(
+ try_parse("^", Operation::Fetch).unwrap_err(),
+ Error::NegativeEmpty
+ ));
+}
+
+#[test]
+fn negative_must_not_be_object_hash() {
+ assert!(matches!(
+ try_parse("^e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", Operation::Fetch).unwrap_err(),
+ Error::NegativeObjectHash
+ ));
+}
+
+#[test]
+fn negative_with_destination() {
+ for spec in ["^a:b", "^a:", "^:", "^:b"] {
+ assert!(matches!(
+ try_parse(spec, Operation::Fetch).unwrap_err(),
+ Error::NegativeWithDestination
+ ));
+ }
+}
+
+#[test]
+fn exclude() {
+ assert_parse("^a", Instruction::Fetch(Fetch::Exclude { src: b("a") }));
+ assert_parse("^a*", Instruction::Fetch(Fetch::Exclude { src: b("a*") }));
+}
+
+#[test]
+fn ampersand_is_resolved_to_head() {
+ assert_parse("@", Instruction::Fetch(Fetch::Only { src: b("HEAD") }));
+ assert_parse("+@", Instruction::Fetch(Fetch::Only { src: b("HEAD") }));
+ assert_parse("^@", Instruction::Fetch(Fetch::Exclude { src: b("HEAD") }));
+}
+
+#[test]
+fn lhs_colon_empty_fetches_only() {
+ assert_parse("src:", Instruction::Fetch(Fetch::Only { src: b("src") }));
+ assert_parse("+src:", Instruction::Fetch(Fetch::Only { src: b("src") }));
+}
+
+#[test]
+fn lhs_colon_rhs_updates_single_ref() {
+ assert_parse(
+ "a:b",
+ Instruction::Fetch(Fetch::AndUpdate {
+ src: b("a"),
+ dst: b("b"),
+ allow_non_fast_forward: false,
+ }),
+ );
+ assert_parse(
+ "+a:b",
+ Instruction::Fetch(Fetch::AndUpdate {
+ src: b("a"),
+ dst: b("b"),
+ allow_non_fast_forward: true,
+ }),
+ );
+
+ assert_parse(
+ "a/*:b/*",
+ Instruction::Fetch(Fetch::AndUpdate {
+ src: b("a/*"),
+ dst: b("b/*"),
+ allow_non_fast_forward: false,
+ }),
+ );
+ assert_parse(
+ "+a/*:b/*",
+ Instruction::Fetch(Fetch::AndUpdate {
+ src: b("a/*"),
+ dst: b("b/*"),
+ allow_non_fast_forward: true,
+ }),
+ );
+}
+
+#[test]
+fn empty_lhs_colon_rhs_fetches_head_to_destination() {
+ assert_parse(
+ ":a",
+ Instruction::Fetch(Fetch::AndUpdate {
+ src: b("HEAD"),
+ dst: b("a"),
+ allow_non_fast_forward: false,
+ }),
+ );
+
+ assert_parse(
+ "+:a",
+ Instruction::Fetch(Fetch::AndUpdate {
+ src: b("HEAD"),
+ dst: b("a"),
+ allow_non_fast_forward: true,
+ }),
+ );
+}
+
+#[test]
+fn colon_alone_is_for_fetching_head_into_fetchhead() {
+ assert_parse(":", Instruction::Fetch(Fetch::Only { src: b("HEAD") }));
+ assert_parse("+:", Instruction::Fetch(Fetch::Only { src: b("HEAD") }));
+}
+
+#[test]
+fn empty_refspec_is_enough_for_fetching_head_into_fetchhead() {
+ assert_parse("", Instruction::Fetch(Fetch::Only { src: b("HEAD") }));
+}
diff --git a/git-refspec/tests/parse/invalid.rs b/git-refspec/tests/parse/invalid.rs
new file mode 100644
index 00000000000..b56aec4df6f
--- /dev/null
+++ b/git-refspec/tests/parse/invalid.rs
@@ -0,0 +1,44 @@
+use crate::parse::try_parse;
+use git_refspec::{parse::Error, parse::Operation};
+
+#[test]
+fn empty() {
+ assert!(matches!(try_parse("", Operation::Push).unwrap_err(), Error::Empty));
+}
+
+#[test]
+fn complex_patterns_with_more_than_one_asterisk() {
+ for op in [Operation::Fetch, Operation::Push] {
+ for spec in ["a/*/c/*", "a**:**b", "+:**/"] {
+ assert!(matches!(
+ try_parse(spec, op).unwrap_err(),
+ Error::PatternUnsupported { .. }
+ ));
+ }
+ }
+ assert!(matches!(
+ try_parse("^*/*", Operation::Fetch).unwrap_err(),
+ Error::PatternUnsupported { .. }
+ ));
+}
+
+#[test]
+fn both_sides_need_pattern_if_one_uses_it() {
+ for op in [Operation::Fetch, Operation::Push] {
+ for spec in ["refs/*/a", ":a/*", "+:a/*", "a*:b/c", "a:b/*"] {
+ assert!(
+ matches!(try_parse(spec, op).unwrap_err(), Error::PatternUnbalanced),
+ "{}",
+ spec
+ );
+ }
+ }
+}
+
+#[test]
+fn push_to_empty() {
+ assert!(matches!(
+ try_parse("HEAD:", Operation::Push).unwrap_err(),
+ Error::PushToEmpty
+ ));
+}
diff --git a/git-refspec/tests/parse/mod.rs b/git-refspec/tests/parse/mod.rs
new file mode 100644
index 00000000000..597359f9b9e
--- /dev/null
+++ b/git-refspec/tests/parse/mod.rs
@@ -0,0 +1,80 @@
+use bstr::ByteSlice;
+use git_refspec::parse::Operation;
+use git_testtools::scripted_fixture_repo_read_only;
+use std::panic::catch_unwind;
+
+#[test]
+fn baseline() {
+ let dir = scripted_fixture_repo_read_only("make_baseline.sh").unwrap();
+ let baseline = std::fs::read(dir.join("baseline.git")).unwrap();
+ let mut lines = baseline.lines();
+ let mut panics = 0;
+ let mut mismatch = 0;
+ let mut count = 0;
+ while let Some(kind_spec) = lines.next() {
+ count += 1;
+ let (kind, spec) = kind_spec.split_at(kind_spec.find_byte(b' ').expect("space between kind and spec"));
+ let spec = &spec[1..];
+ let err_code: usize = lines
+ .next()
+ .expect("err code")
+ .to_str()
+ .unwrap()
+ .parse()
+ .expect("number");
+ let op = match kind {
+ b"fetch" => Operation::Fetch,
+ b"push" => Operation::Push,
+ _ => unreachable!("{} unexpected", kind.as_bstr()),
+ };
+ let res = catch_unwind(|| try_parse(spec.to_str().unwrap(), op));
+ match res {
+ Ok(res) => match (res.is_ok(), err_code == 0) {
+ (true, true) | (false, false) => {
+ if let Ok(spec) = res {
+ spec.instruction(); // should not panic
+ }
+ }
+ _ => {
+ eprintln!("{err_code} {res:?} {} {:?}", kind.as_bstr(), spec.as_bstr());
+ mismatch += 1;
+ }
+ },
+ Err(_) => {
+ panics += 1;
+ }
+ }
+ }
+ if panics != 0 || mismatch != 0 {
+ panic!(
+ "Out of {} baseline entries, got {} right, ({} mismatches and {} panics)",
+ count,
+ count - (mismatch + panics),
+ mismatch,
+ panics
+ );
+ }
+}
+
+mod fetch;
+mod invalid;
+mod push;
+
+mod util {
+ use git_refspec::{parse::Operation, Instruction, RefSpecRef};
+
+ pub fn b(input: &str) -> &bstr::BStr {
+ input.into()
+ }
+
+ pub fn try_parse(spec: &str, op: Operation) -> Result, git_refspec::parse::Error> {
+ git_refspec::parse(spec.into(), op)
+ }
+
+ pub fn assert_parse<'a>(spec: &'a str, expected: Instruction<'_>) -> RefSpecRef<'a> {
+ let spec = try_parse(spec, expected.operation()).expect("no error");
+ assert_eq!(spec.instruction(), expected);
+ spec
+ }
+}
+pub use util::*;
diff --git a/git-refspec/tests/parse/push.rs b/git-refspec/tests/parse/push.rs
new file mode 100644
index 00000000000..93b414223ef
--- /dev/null
+++ b/git-refspec/tests/parse/push.rs
@@ -0,0 +1,127 @@
+use crate::parse::{assert_parse, b, try_parse};
+use git_refspec::{instruction::Push, parse::Error, parse::Operation, Instruction};
+
+#[test]
+fn negative_unsupported() {
+ for spec in ["^a:b", "^a:", "^:", "^:b", "^"] {
+ assert!(matches!(
+ try_parse(spec, Operation::Push).unwrap_err(),
+ Error::NegativeUnsupported
+ ));
+ }
+}
+
+#[test]
+fn revspecs_with_ref_name_destination() {
+ assert_parse(
+ "main~1:b",
+ Instruction::Push(Push::Matching {
+ src: b("main~1"),
+ dst: b("b"),
+ allow_non_fast_forward: false,
+ }),
+ );
+ assert_parse(
+ "+main~1:b",
+ Instruction::Push(Push::Matching {
+ src: b("main~1"),
+ dst: b("b"),
+ allow_non_fast_forward: true,
+ }),
+ );
+}
+
+#[test]
+fn destinations_must_be_ref_names() {
+ assert!(matches!(
+ try_parse("a~1:b~1", Operation::Push).unwrap_err(),
+ Error::ReferenceName(_)
+ ));
+}
+
+#[test]
+fn single_refs_must_be_refnames() {
+ assert!(matches!(
+ try_parse("a~1", Operation::Push).unwrap_err(),
+ Error::ReferenceName(_)
+ ));
+}
+
+#[test]
+fn ampersand_is_resolved_to_head() {
+ assert_parse(
+ "@",
+ Instruction::Push(Push::Matching {
+ src: b("HEAD"),
+ dst: b("HEAD"),
+ allow_non_fast_forward: false,
+ }),
+ );
+
+ assert_parse(
+ "+@",
+ Instruction::Push(Push::Matching {
+ src: b("HEAD"),
+ dst: b("HEAD"),
+ allow_non_fast_forward: true,
+ }),
+ );
+}
+
+#[test]
+fn lhs_colon_rhs_pushes_single_ref() {
+ assert_parse(
+ "a:b",
+ Instruction::Push(Push::Matching {
+ src: b("a"),
+ dst: b("b"),
+ allow_non_fast_forward: false,
+ }),
+ );
+ assert_parse(
+ "+a:b",
+ Instruction::Push(Push::Matching {
+ src: b("a"),
+ dst: b("b"),
+ allow_non_fast_forward: true,
+ }),
+ );
+ assert_parse(
+ "a/*:b/*",
+ Instruction::Push(Push::Matching {
+ src: b("a/*"),
+ dst: b("b/*"),
+ allow_non_fast_forward: false,
+ }),
+ );
+ assert_parse(
+ "+a/*:b/*",
+ Instruction::Push(Push::Matching {
+ src: b("a/*"),
+ dst: b("b/*"),
+ allow_non_fast_forward: true,
+ }),
+ );
+}
+
+#[test]
+fn colon_alone_is_for_pushing_matching_refs() {
+ assert_parse(
+ ":",
+ Instruction::Push(Push::AllMatchingBranches {
+ allow_non_fast_forward: false,
+ }),
+ );
+ assert_parse(
+ "+:",
+ Instruction::Push(Push::AllMatchingBranches {
+ allow_non_fast_forward: true,
+ }),
+ );
+}
+
+#[test]
+fn delete() {
+ assert_parse(":a", Instruction::Push(Push::Delete { ref_or_pattern: b("a") }));
+ assert_parse("+:a", Instruction::Push(Push::Delete { ref_or_pattern: b("a") }));
+}
diff --git a/git-refspec/tests/refspec.rs b/git-refspec/tests/refspec.rs
new file mode 100644
index 00000000000..06f1a3c69d4
--- /dev/null
+++ b/git-refspec/tests/refspec.rs
@@ -0,0 +1 @@
+mod parse;
diff --git a/git-revision/Cargo.toml b/git-revision/Cargo.toml
index f1490626e65..ada748fd7f3 100644
--- a/git-revision/Cargo.toml
+++ b/git-revision/Cargo.toml
@@ -6,7 +6,7 @@ license = "MIT/Apache-2.0"
description = "A WIP crate of the gitoxide project dealing with finding names for revisions and parsing specifications"
authors = ["Sebastian Thiel "]
edition = "2018"
-include = ["src/**/*", "CHANGELOG.md"]
+include = ["src/**/*", "CHANGELOG.md", "README.md"]
[lib]
doctest = false
diff --git a/git-revision/README.md b/git-revision/README.md
new file mode 100644
index 00000000000..8d00185fac2
--- /dev/null
+++ b/git-revision/README.md
@@ -0,0 +1,11 @@
+# `git-revision`
+
+### Testing
+
+#### Fuzzing
+
+`cargo fuzz` is used for fuzzing, installable with `cargo install cargo-fuzz`.
+
+Targets can be listed with `cargo fuzz list` and executed via `cargo +nightly fuzz run `,
+where `` can be `parse` for example.
+
diff --git a/gitoxide-core/src/repository/revision/resolve.rs b/gitoxide-core/src/repository/revision/resolve.rs
index d58851715c9..e20097f7e79 100644
--- a/gitoxide-core/src/repository/revision/resolve.rs
+++ b/gitoxide-core/src/repository/revision/resolve.rs
@@ -7,7 +7,7 @@ pub struct Options {
}
pub(crate) mod function {
- use anyhow::{bail, Context};
+ use anyhow::Context;
use std::ffi::OsString;
use git_repository as git;
@@ -45,7 +45,7 @@ pub(crate) mod function {
#[cfg(feature = "serde1")]
OutputFormat::Json => {
if explain {
- bail!("Explanations are only for human consumption")
+ anyhow::bail!("Explanations are only for human consumption")
}
serde_json::to_writer_pretty(
&mut out,