gix-refspec-0.27.0/.cargo_vcs_info.json0000644000000001510000000000100133330ustar { "git": { "sha1": "4000197ecc8cf1a5d79361620e4c114f86476703" }, "path_in_vcs": "gix-refspec" }gix-refspec-0.27.0/Cargo.toml0000644000000065750000000000100113510ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.65" name = "gix-refspec" version = "0.27.0" authors = ["Sebastian Thiel "] build = false include = [ "src/**/*", "LICENSE-*", "README.md", ] autobins = false autoexamples = false autotests = false autobenches = false description = "A crate of the gitoxide project for parsing and representing refspecs" readme = "README.md" license = "MIT OR Apache-2.0" repository = "https://github.com/GitoxideLabs/gitoxide" [lib] name = "gix_refspec" path = "src/lib.rs" doctest = false [dependencies.bstr] version = "1.3.0" features = ["std"] default-features = false [dependencies.gix-hash] version = "^0.15.1" [dependencies.gix-revision] version = "^0.31.0" default-features = false [dependencies.gix-validate] version = "^0.9.2" [dependencies.smallvec] version = "1.9.0" [dependencies.thiserror] version = "2.0.0" [dev-dependencies] [lints.clippy] bool_to_int_with_if = "allow" borrow_as_ptr = "allow" cast_lossless = "allow" cast_possible_truncation = "allow" cast_possible_wrap = "allow" cast_precision_loss = "allow" cast_sign_loss = "allow" checked_conversions = "allow" copy_iterator = "allow" default_trait_access = "allow" doc_markdown = "allow" empty_docs = "allow" enum_glob_use = "allow" explicit_deref_methods = "allow" explicit_into_iter_loop = "allow" explicit_iter_loop = "allow" filter_map_next = "allow" fn_params_excessive_bools = "allow" from_iter_instead_of_collect = "allow" if_not_else = "allow" ignored_unit_patterns = "allow" implicit_clone = "allow" inconsistent_struct_constructor = "allow" inefficient_to_string = "allow" inline_always = "allow" items_after_statements = "allow" iter_not_returning_iterator = "allow" iter_without_into_iter = "allow" manual_assert = "allow" manual_is_variant_and = "allow" manual_let_else = "allow" manual_string_new = "allow" many_single_char_names = "allow" match_bool = "allow" match_same_arms = "allow" match_wild_err_arm = "allow" match_wildcard_for_single_variants = "allow" missing_errors_doc = "allow" missing_panics_doc = "allow" module_name_repetitions = "allow" must_use_candidate = "allow" mut_mut = "allow" naive_bytecount = "allow" needless_for_each = "allow" needless_pass_by_value = "allow" needless_raw_string_hashes = "allow" no_effect_underscore_binding = "allow" option_option = "allow" range_plus_one = "allow" redundant_else = "allow" return_self_not_must_use = "allow" should_panic_without_expect = "allow" similar_names = "allow" single_match_else = "allow" stable_sort_primitive = "allow" struct_excessive_bools = "allow" struct_field_names = "allow" too_long_first_doc_paragraph = "allow" too_many_lines = "allow" transmute_ptr_to_ptr = "allow" trivially_copy_pass_by_ref = "allow" unnecessary_join = "allow" unnecessary_wraps = "allow" unreadable_literal = "allow" unused_self = "allow" used_underscore_binding = "allow" wildcard_imports = "allow" [lints.clippy.pedantic] level = "warn" priority = -1 [lints.rust] gix-refspec-0.27.0/Cargo.toml.orig000064400000000000000000000014671046102023000150250ustar 00000000000000lints.workspace = true [package] name = "gix-refspec" version = "0.27.0" repository = "https://github.com/GitoxideLabs/gitoxide" license = "MIT OR Apache-2.0" description = "A crate of the gitoxide project for parsing and representing refspecs" authors = ["Sebastian Thiel "] edition = "2021" include = ["src/**/*", "LICENSE-*", "README.md"] rust-version = "1.65" [lib] doctest = false [dependencies] gix-revision = { version = "^0.31.0", path = "../gix-revision", default-features = false } gix-validate = { version = "^0.9.2", path = "../gix-validate" } gix-hash = { version = "^0.15.1", path = "../gix-hash" } bstr = { version = "1.3.0", default-features = false, features = ["std"] } thiserror = "2.0.0" smallvec = "1.9.0" [dev-dependencies] gix-testtools = { path = "../tests/tools" } gix-refspec-0.27.0/LICENSE-APACHE000064400000000000000000000247461046102023000140670ustar 00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. gix-refspec-0.27.0/LICENSE-MIT000064400000000000000000000017771046102023000135760ustar 00000000000000Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. gix-refspec-0.27.0/README.md000064400000000000000000000004141046102023000134040ustar 00000000000000# `gix-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. gix-refspec-0.27.0/src/instruction.rs000064400000000000000000000061461046102023000156530ustar 00000000000000use bstr::BStr; use crate::{parse::Operation, Instruction}; 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(PartialOrd, Ord, 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 `*`. /// Examples are refnames like `HEAD` or `refs/heads/main`, or patterns like `refs/heads/*`. src: &'a BStr, /// The ref to update with the object from `src`. If `src` is a pattern, this is a pattern too. /// Examples are refnames like `HEAD` or `refs/heads/main`, or patterns like `refs/heads/*`. 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(PartialOrd, Ord, PartialEq, Eq, Copy, Clone, Hash, Debug)] pub enum Fetch<'a> { /// Fetch a ref or refs, without updating local branches. Only { /// 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. 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, or the full object hex-name. 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, }, } gix-refspec-0.27.0/src/lib.rs000064400000000000000000000013531046102023000140330ustar 00000000000000//! Parse git ref-specs and represent them. #![deny(missing_docs, rust_2018_idioms)] #![forbid(unsafe_code)] /// pub mod parse; pub use parse::function::parse; /// pub mod instruction; /// A refspec with references to the memory it was parsed from. #[derive(Eq, Copy, Clone, 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(Eq, Clone, Debug)] pub struct RefSpec { mode: types::Mode, op: parse::Operation, src: Option, dst: Option, } mod spec; mod write; /// pub mod match_group; pub use match_group::types::MatchGroup; mod types; pub use types::Instruction; gix-refspec-0.27.0/src/match_group/mod.rs000064400000000000000000000103531046102023000163540ustar 00000000000000use std::collections::BTreeSet; use crate::{parse::Operation, types::Mode, MatchGroup, RefSpecRef}; pub(crate) mod types; pub use types::{Item, Mapping, Outcome, Source, SourceRef}; /// pub mod validate; /// Initialization impl<'a> MatchGroup<'a> { /// Take all the fetch ref specs from `specs` get a match group ready. pub fn from_fetch_specs(specs: impl IntoIterator>) -> Self { MatchGroup { specs: specs.into_iter().filter(|s| s.op == Operation::Fetch).collect(), } } /// Take all the push ref specs from `specs` get a match group ready. pub fn from_push_specs(specs: impl IntoIterator>) -> Self { MatchGroup { specs: specs.into_iter().filter(|s| s.op == Operation::Push).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 is correct only for specs*, even though it also *works for push-specs*. /// /// Note that negative matches are not part of the return value, so they are not observable but will be used to remove mappings. // TODO: figure out how to deal with push-specs, probably when push is being implemented. pub fn match_remotes<'item>(self, mut items: impl Iterator> + Clone) -> Outcome<'a, 'item> { let mut out = Vec::new(); let mut seen = BTreeSet::default(); let mut push_unique = |mapping| { if seen.insert(calculate_hash(&mapping)) { out.push(mapping); } }; let mut matchers: Vec>> = self .specs .iter() .copied() .map(Matcher::from) .enumerate() .map(|(idx, m)| match m.lhs { Some(Needle::Object(id)) => { push_unique(Mapping { item_index: None, lhs: SourceRef::ObjectId(id), rhs: m.rhs.map(Needle::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() { if spec.mode == Mode::Negative { has_negation = true; continue; } for (item_index, item) in items.clone().enumerate() { if let Some(matcher) = matcher { let (matched, rhs) = matcher.matches_lhs(item); if matched { push_unique(Mapping { item_index: Some(item_index), lhs: SourceRef::FullName(item.full_ref_name), rhs, spec_index, }); } } } } if let Some(hash_kind) = has_negation.then(|| items.next().map(|i| i.target.kind())).flatten() { let null_id = hash_kind.null(); for matcher in matchers .into_iter() .zip(self.specs.iter()) .filter_map(|(m, spec)| m.and_then(|m| (spec.mode == Mode::Negative).then_some(m))) { out.retain(|m| match m.lhs { SourceRef::ObjectId(_) => true, SourceRef::FullName(name) => { !matcher .matches_lhs(Item { full_ref_name: name, target: &null_id, object: None, }) .0 } }); } } Outcome { group: self, mappings: out, } } } fn calculate_hash(t: &T) -> u64 { use std::hash::Hasher; let mut s = std::collections::hash_map::DefaultHasher::new(); t.hash(&mut s); s.finish() } mod util; use util::{Matcher, Needle}; gix-refspec-0.27.0/src/match_group/types.rs000064400000000000000000000076051046102023000167470ustar 00000000000000use std::borrow::Cow; use bstr::{BStr, BString}; use gix_hash::oid; use crate::RefSpecRef; /// A match group is able to match a list of ref specs in order while handling negation, conflicts and one to many mappings. #[derive(Default, Debug, Clone)] pub struct MatchGroup<'a> { /// The specs that take part in item matching. pub 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 id that `full_ref_name` points to, which typically is a commit, but can also be a tag object (or anything else). pub target: &'a oid, /// The object an annotated tag is pointing to, if `target` is an annotated tag. pub object: Option<&'a oid>, } #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] /// The source (or left-hand) side of a mapping, which references its name. pub enum SourceRef<'a> { /// A full reference name, which is expected to be valid. /// /// Validity, however, is not enforced here. FullName(&'a BStr), /// The name of an object that is expected to exist on the remote side. /// Note that it might not be advertised by the remote but part of the object graph, /// and thus gets sent in the pack. The server is expected to fail unless the desired /// object is present but at some time it is merely a request by the user. ObjectId(gix_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(gix_hash::ObjectId), } impl std::fmt::Display for Source { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Source::FullName(name) => name.fmt(f), Source::ObjectId(id) => id.fmt(f), } } } /// A mapping from a remote to a local refs for fetches or local to remote refs for pushes. /// /// Mappings are like edges in a graph, initially without any constraints. #[derive(Debug, Clone)] pub struct Mapping<'a, 'b> { /// The index into the initial `items` list that matched against a spec. pub item_index: Option, /// The name of the remote side for fetches or the local one for pushes that matched. pub lhs: SourceRef<'a>, /// The name of the local side for fetches or the remote one for pushes that corresponds to `lhs`, if available. pub rhs: Option>, /// The index of the matched ref-spec as seen from the match group. pub spec_index: usize, } impl std::hash::Hash for Mapping<'_, '_> { fn hash(&self, state: &mut H) { self.lhs.hash(state); self.rhs.hash(state); } } gix-refspec-0.27.0/src/match_group/util.rs000064400000000000000000000130301046102023000165450ustar 00000000000000use std::{borrow::Cow, ops::Range}; use bstr::{BStr, BString, ByteSlice, ByteVec}; use gix_hash::ObjectId; use crate::{match_group::Item, RefSpecRef}; /// A type keeping enough information about a ref-spec to be able to efficiently match it against multiple matcher items. pub struct Matcher<'a> { pub(crate) lhs: Option>, pub(crate) rhs: Option>, } impl<'a> Matcher<'a> { /// Match `item` against this spec and return `(true, Some)` to gain the other side of the match as configured, or `(true, None)` /// if there was no `rhs` but the `item` matched. Lastly, return `(false, None)` if `item` didn't match at all. /// /// This may involve resolving a glob with an allocation, as the destination is built using the matching portion of a glob. pub fn matches_lhs(&self, item: Item<'_>) -> (bool, Option>) { match (self.lhs, self.rhs) { (Some(lhs), None) => (lhs.matches(item).is_match(), None), (Some(lhs), Some(rhs)) => lhs.matches(item).into_match_outcome(rhs, item), (None, _) => (false, None), } } } #[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) => crate::spec::expand_partial_name(name, |expanded| { (expanded == item.full_ref_name).then_some(Match::Normal) }) .unwrap_or(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(); Match::GlobRange(*asterisk_pos..end) } Needle::Object(id) => { if *id == item.target { return Match::Normal; } match item.object { Some(object) if object == *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) = gix_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), } } } gix-refspec-0.27.0/src/match_group/validate.rs000064400000000000000000000116041046102023000173660ustar 00000000000000use std::collections::BTreeMap; use bstr::BString; use crate::{ match_group::{Outcome, Source}, RefSpec, }; /// 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, Clone)] pub enum Fix { /// Removed a mapping that contained a partial destination entirely. MappingWithPartialDestinationRemoved { /// The destination ref name that was ignored. name: BString, /// The spec that defined the mapping spec: RefSpec, }, } /// 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(), if self.issues.len() == 1 { "issue that prevents" } else { "issues that prevent" }, self.issues .iter() .map(ToString::to_string) .collect::>() .join("\n\t") ) } } impl std::error::Error for Error {} impl Outcome<'_, '_> { /// Validate all mappings or dissolve them into an error stating the discovered issues. /// Return `(modified self, issues)` providing a fixed-up set of mappings in `self` with the fixed `issues` /// provided as part of it. /// Terminal issues are communicated using the [`Error`] type accordingly. pub fn validated(mut self) -> Result<(Self, Vec), Error> { let mut sources_by_destinations = BTreeMap::new(); for (dst, (spec_index, src)) in self .mappings .iter() .filter_map(|m| m.rhs.as_ref().map(|dst| (dst.as_ref(), (m.spec_index, &m.lhs)))) { let sources = sources_by_destinations.entry(dst).or_insert_with(Vec::new); if !sources.iter().any(|(_, lhs)| lhs == &src) { sources.push((spec_index, src)); } } let mut issues = Vec::new(); for (dst, conflicting_sources) in sources_by_destinations.into_iter().filter(|(_, v)| v.len() > 1) { issues.push(Issue::Conflict { destination_full_ref_name: dst.to_owned(), specs: conflicting_sources .iter() .map(|(spec_idx, _)| self.group.specs[*spec_idx].to_bstring()) .collect(), sources: conflicting_sources.into_iter().map(|(_, src)| src.to_owned()).collect(), }); } if !issues.is_empty() { Err(Error { issues }) } else { let mut fixed = Vec::new(); let group = &self.group; self.mappings.retain(|m| match m.rhs.as_ref() { Some(dst) => { if dst.starts_with(b"refs/") || dst.as_ref() == "HEAD" { true } else { fixed.push(Fix::MappingWithPartialDestinationRemoved { name: dst.as_ref().to_owned(), spec: group.specs[m.spec_index].to_owned(), }); false } } None => true, }); Ok((self, fixed)) } } } gix-refspec-0.27.0/src/parse.rs000064400000000000000000000206511046102023000144010ustar 00000000000000/// 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("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")] 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] gix_validate::reference::name::Error), #[error(transparent)] RevSpec(#[from] gix_revision::spec::parse::Error), } /// Define how the parsed refspec should be used. #[derive(PartialOrd, Ord, 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 bstr::{BStr, ByteSlice}; use crate::{ parse::{Error, Operation}, types::Mode, RefSpecRef, }; /// 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.first() { 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_some(spec); if Operation::Fetch == operation && mode != Mode::Negative && src.is_none() { return Ok(fetch_head_only(mode)); } else { (src, None) } } }; 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 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, src, dst, }) } fn looks_like_object_hash(spec: &BStr) -> bool { spec.len() >= gix_hash::Kind::shortest().len_in_hex() && spec.iter().all(u8::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'; gix_validate::reference::name_partial(buf.as_bstr())?; } else { gix_validate::reference::name_partial(spec) .map_err(Error::from) .or_else(|err| { if allow_revspecs { gix_revision::spec::parse(spec, &mut super::revparse::Noop)?; Ok(spec) } else { Err(err) } })?; } Ok((Some(spec), has_globs)) } None => Ok((None, false)), } } } mod revparse { use bstr::BStr; use gix_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: gix_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: gix_revision::spec::Kind) -> Option<()> { Some(()) } } impl gix_revision::spec::parse::Delegate for Noop { fn done(&mut self) {} } } gix-refspec-0.27.0/src/spec.rs000064400000000000000000000206261046102023000142230ustar 00000000000000use bstr::{BStr, BString, ByteSlice}; use crate::{ instruction::{Fetch, Push}, 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(AsRef::as_ref), dst: self.dst.as_ref().map(AsRef::as_ref), } } /// Return true if the spec stats with a `+` and thus forces setting the reference. pub fn allow_non_fast_forward(&self) -> bool { matches!(self.mode, Mode::Force) } } mod impls { use std::{ cmp::Ordering, hash::{Hash, Hasher}, }; use crate::{RefSpec, RefSpecRef}; impl From> for RefSpec { fn from(v: RefSpecRef<'_>) -> Self { v.to_owned() } } impl Hash for RefSpec { fn hash(&self, state: &mut H) { self.to_ref().hash(state); } } impl Hash for RefSpecRef<'_> { fn hash(&self, state: &mut H) { self.instruction().hash(state); } } impl PartialEq for RefSpec { fn eq(&self, other: &Self) -> bool { self.to_ref().eq(&other.to_ref()) } } impl PartialEq for RefSpecRef<'_> { fn eq(&self, other: &Self) -> bool { self.instruction().eq(&other.instruction()) } } impl PartialOrd for RefSpecRef<'_> { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl PartialOrd for RefSpec { fn partial_cmp(&self, other: &Self) -> Option { Some(self.to_ref().cmp(&other.to_ref())) } } impl Ord for RefSpecRef<'_> { fn cmp(&self, other: &Self) -> Ordering { self.instruction().cmp(&other.instruction()) } } impl Ord for RefSpec { fn cmp(&self, other: &Self) -> Ordering { self.to_ref().cmp(&other.to_ref()) } } } /// Access impl<'a> RefSpecRef<'a> { /// Return the left-hand side of the spec, typically the source. /// It takes many different forms so don't rely on this being a ref name. /// /// It's not present in case of deletions. pub fn source(&self) -> Option<&BStr> { self.src } /// Return the right-hand side of the spec, typically the destination. /// It takes many different forms so don't rely on this being a ref name. /// /// It's not present in case of source-only specs. pub fn destination(&self) -> Option<&BStr> { self.dst } /// Always returns the remote side, whose actual side in the refspec depends on how it was parsed. pub fn remote(&self) -> Option<&BStr> { match self.op { Operation::Push => self.dst, Operation::Fetch => self.src, } } /// Always returns the local side, whose actual side in the refspec depends on how it was parsed. pub fn local(&self) -> Option<&BStr> { match self.op { Operation::Push => self.src, Operation::Fetch => self.dst, } } /// Derive the prefix from the [`source`][Self::source()] side of this spec if this is a fetch spec, /// or the [`destination`][Self::destination()] side if it is a push spec, if it is possible to do so without ambiguity. /// /// This means it starts with `refs/`. Note that it won't contain more than two components, like `refs/heads/` pub fn prefix(&self) -> Option<&BStr> { if self.mode == Mode::Negative { return None; } let source = match self.op { Operation::Fetch => self.source(), Operation::Push => self.destination(), }?; if source == "HEAD" { return source.into(); } let suffix = source.strip_prefix(b"refs/")?; let slash_pos = suffix.find_byte(b'/')?; let prefix = source[..="refs/".len() + slash_pos].as_bstr(); (!prefix.contains(&b'*')).then_some(prefix) } /// As opposed to [`prefix()`][Self::prefix], if the latter is `None` it will expand to all possible prefixes and place them in `out`. /// /// Note that only the `source` side is considered. pub fn expand_prefixes(&self, out: &mut Vec) { match self.prefix() { Some(prefix) => out.push(prefix.into()), None => { let source = match match self.op { Operation::Fetch => self.source(), Operation::Push => self.destination(), } { Some(source) => source, None => return, }; if let Some(rest) = source.strip_prefix(b"refs/") { if !rest.contains(&b'/') { out.push(source.into()); } return; } else if gix_hash::ObjectId::from_hex(source).is_ok() { return; } expand_partial_name(source, |expanded| { out.push(expanded.into()); None::<()> }); } } } /// Transform the state of the refspec into an instruction making clear what to do with it. pub fn instruction(&self) -> Instruction<'a> { 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), } } } pub(crate) fn expand_partial_name(name: &BStr, mut cb: impl FnMut(&BStr) -> Option) -> Option { use bstr::ByteVec; let mut buf = BString::from(Vec::with_capacity(128)); for (base, append_head) in [ ("", false), ("refs/", false), ("refs/tags/", false), ("refs/heads/", false), ("refs/remotes/", false), ("refs/remotes/", true), ] { buf.clear(); buf.push_str(base); buf.push_str(name); if append_head { buf.push_str("/HEAD"); } if let Some(res) = cb(buf.as_ref()) { return Some(res); } } None } gix-refspec-0.27.0/src/types.rs000064400000000000000000000016261046102023000144340ustar 00000000000000use crate::instruction; /// The way to interpret a refspec. #[derive(PartialOrd, Ord, 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(PartialOrd, Ord, 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>), } gix-refspec-0.27.0/src/write.rs000064400000000000000000000044451046102023000144240ustar 00000000000000use bstr::BString; use crate::{ instruction::{Fetch, Push}, Instruction, RefSpecRef, }; 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: &mut dyn 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, out: &mut dyn 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) } } } }