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,