diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-04 12:41:41 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-04 12:41:41 +0000 |
commit | 10ee2acdd26a7f1298c6f6d6b7af9b469fe29b87 (patch) | |
tree | bdffd5d80c26cf4a7a518281a204be1ace85b4c1 /vendor/gix-refspec/src | |
parent | Releasing progress-linux version 1.70.0+dfsg1-9~progress7.99u1. (diff) | |
download | rustc-10ee2acdd26a7f1298c6f6d6b7af9b469fe29b87.tar.xz rustc-10ee2acdd26a7f1298c6f6d6b7af9b469fe29b87.zip |
Merging upstream version 1.70.0+dfsg2.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'vendor/gix-refspec/src')
-rw-r--r-- | vendor/gix-refspec/src/instruction.rs | 68 | ||||
-rw-r--r-- | vendor/gix-refspec/src/lib.rs | 39 | ||||
-rw-r--r-- | vendor/gix-refspec/src/match_group/mod.rs | 112 | ||||
-rw-r--r-- | vendor/gix-refspec/src/match_group/types.rs | 104 | ||||
-rw-r--r-- | vendor/gix-refspec/src/match_group/util.rs | 162 | ||||
-rw-r--r-- | vendor/gix-refspec/src/match_group/validate.rs | 141 | ||||
-rw-r--r-- | vendor/gix-refspec/src/parse.rs | 255 | ||||
-rw-r--r-- | vendor/gix-refspec/src/spec.rs | 257 | ||||
-rw-r--r-- | vendor/gix-refspec/src/types.rs | 21 | ||||
-rw-r--r-- | vendor/gix-refspec/src/write.rs | 74 |
10 files changed, 1233 insertions, 0 deletions
diff --git a/vendor/gix-refspec/src/instruction.rs b/vendor/gix-refspec/src/instruction.rs new file mode 100644 index 000000000..990d0debc --- /dev/null +++ b/vendor/gix-refspec/src/instruction.rs @@ -0,0 +1,68 @@ +use 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. + 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/vendor/gix-refspec/src/lib.rs b/vendor/gix-refspec/src/lib.rs new file mode 100644 index 000000000..54d5f3057 --- /dev/null +++ b/vendor/gix-refspec/src/lib.rs @@ -0,0 +1,39 @@ +//! 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<bstr::BString>, + dst: Option<bstr::BString>, +} + +mod spec; + +mod write; + +/// +pub mod match_group; +pub use match_group::types::MatchGroup; + +mod types; +pub use types::Instruction; diff --git a/vendor/gix-refspec/src/match_group/mod.rs b/vendor/gix-refspec/src/match_group/mod.rs new file mode 100644 index 000000000..c53b5b531 --- /dev/null +++ b/vendor/gix-refspec/src/match_group/mod.rs @@ -0,0 +1,112 @@ +use 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<Item = RefSpecRef<'a>>) -> Self { + MatchGroup { + specs: specs.into_iter().filter(|s| s.op == Operation::Fetch).collect(), + } + } +} + +/// Matching +impl<'a> MatchGroup<'a> { + /// Match all `items` against all fetch specs present in this group, returning deduplicated mappings from source to destination. + /// Note that this method only makes sense if the specs are indeed fetch specs and may panic otherwise. + /// + /// Note that negative matches are not part of the return value, so they are not observable but will be used to remove mappings. + pub fn match_remotes<'item>(self, mut items: impl Iterator<Item = Item<'item>> + 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<Option<Matcher<'_>>> = self + .specs + .iter() + .copied() + .map(Matcher::from) + .enumerate() + .map(|(idx, m)| match m.lhs { + Some(Needle::Object(id)) => { + push_unique(Mapping { + item_index: None, + lhs: SourceRef::ObjectId(id), + rhs: m.rhs.map(|n| n.to_bstr()), + spec_index: idx, + }); + None + } + _ => Some(m), + }) + .collect(); + + let mut has_negation = false; + for (spec_index, (spec, matcher)) in self.specs.iter().zip(matchers.iter_mut()).enumerate() { + for (item_index, item) in items.clone().enumerate() { + if spec.mode == Mode::Negative { + has_negation = true; + continue; + } + if let Some(matcher) = matcher { + let (matched, rhs) = matcher.matches_lhs(item); + if matched { + push_unique(Mapping { + item_index: Some(item_index), + lhs: SourceRef::FullName(item.full_ref_name), + rhs, + spec_index, + }) + } + } + } + } + + if let Some(id) = has_negation.then(|| items.next().map(|i| i.target)).flatten() { + let null_id = gix_hash::ObjectId::null(id.kind()); + for matcher in matchers + .into_iter() + .zip(self.specs.iter()) + .filter_map(|(m, spec)| m.and_then(|m| (spec.mode == Mode::Negative).then_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: std::hash::Hash>(t: &T) -> u64 { + use std::hash::Hasher; + let mut s = std::collections::hash_map::DefaultHasher::new(); + t.hash(&mut s); + s.finish() +} + +mod util; +use util::{Matcher, Needle}; diff --git a/vendor/gix-refspec/src/match_group/types.rs b/vendor/gix-refspec/src/match_group/types.rs new file mode 100644 index 000000000..6be601dd5 --- /dev/null +++ b/vendor/gix-refspec/src/match_group/types.rs @@ -0,0 +1,104 @@ +use 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<RefSpecRef<'a>>, +} + +/// 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<Mapping<'item, 'spec>>, +} + +/// 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<usize>, + /// 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<Cow<'b, BStr>>, + /// 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<H: std::hash::Hasher>(&self, state: &mut H) { + self.lhs.hash(state); + self.rhs.hash(state); + } +} diff --git a/vendor/gix-refspec/src/match_group/util.rs b/vendor/gix-refspec/src/match_group/util.rs new file mode 100644 index 000000000..5339aef32 --- /dev/null +++ b/vendor/gix-refspec/src/match_group/util.rs @@ -0,0 +1,162 @@ +use 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<Needle<'a>>, + pub(crate) rhs: Option<Needle<'a>>, +} + +impl<'a> Matcher<'a> { + /// Match `item` against this spec and return `(true, Some<rhs>)` to gain the other side of the match as configured, or `(true, None)` + /// if there was no `rhs`. + /// + /// This may involve resolving a glob with an allocation, as the destination is built using the matching portion of a glob. + pub fn matches_lhs(&self, item: Item<'_>) -> (bool, Option<Cow<'a, BStr>>) { + match (self.lhs, self.rhs) { + (Some(lhs), None) => (lhs.matches(item).is_match(), None), + (Some(lhs), Some(rhs)) => lhs.matches(item).into_match_outcome(rhs, item), + (None, None) | (None, Some(_)) => { + unreachable!("For all we know, the lefthand side is never empty. Push specs might change that.") + } + } + } +} + +#[derive(Debug, Copy, Clone)] +pub(crate) enum Needle<'a> { + FullName(&'a BStr), + PartialName(&'a BStr), + Glob { name: &'a BStr, asterisk_pos: usize }, + Object(ObjectId), +} + +enum Match { + /// There was no match. + None, + /// No additional data is provided as part of the match. + Normal, + /// The range of text to copy from the originating item name + GlobRange(Range<usize>), +} + +impl Match { + fn is_match(&self) -> bool { + !matches!(self, Match::None) + } + fn into_match_outcome<'a>(self, destination: Needle<'a>, item: Item<'_>) -> (bool, Option<Cow<'a, BStr>>) { + 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<usize>, 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<RefSpecRef<'a>> for Matcher<'a> { + fn from(v: RefSpecRef<'a>) -> Self { + Matcher { + lhs: v.src.map(Into::into), + rhs: v.dst.map(Into::into), + } + } +} diff --git a/vendor/gix-refspec/src/match_group/validate.rs b/vendor/gix-refspec/src/match_group/validate.rs new file mode 100644 index 000000000..097a64587 --- /dev/null +++ b/vendor/gix-refspec/src/match_group/validate.rs @@ -0,0 +1,141 @@ +use 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<Source>, + /// 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<BString>, + }, +} + +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::<Vec<_>>() + .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<Issue>, +} + +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(|issue| issue.to_string()) + .collect::<Vec<_>>() + .join("\n\t") + ) + } +} + +impl std::error::Error for Error {} + +impl<'spec, 'item> Outcome<'spec, 'item> { + /// Validate all mappings or dissolve them into an error stating the discovered issues. + /// Return `(modified self, issues)` providing a fixed-up set of mappings in `self` with the fixed `issues` + /// provided as part of it. + /// Terminal issues are communicated using the [`Error`] type accordingly. + pub fn validated(mut self) -> Result<(Self, Vec<Fix>), 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)) + } + } +} diff --git a/vendor/gix-refspec/src/parse.rs b/vendor/gix-refspec/src/parse.rs new file mode 100644 index 000000000..390db13fa --- /dev/null +++ b/vendor/gix-refspec/src/parse.rs @@ -0,0 +1,255 @@ +/// 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::refname::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<RefSpecRef<'_>, 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(|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'; + 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 { + match gix_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 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<PrefixHint<'_>>) -> 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) {} + } +} diff --git a/vendor/gix-refspec/src/spec.rs b/vendor/gix-refspec/src/spec.rs new file mode 100644 index 000000000..9361ae682 --- /dev/null +++ b/vendor/gix-refspec/src/spec.rs @@ -0,0 +1,257 @@ +use 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(|b| b.as_ref()), + dst: self.dst.as_ref().map(|b| b.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<RefSpecRef<'_>> for RefSpec { + fn from(v: RefSpecRef<'_>) -> Self { + v.to_owned() + } + } + + impl Hash for RefSpec { + fn hash<H: Hasher>(&self, state: &mut H) { + self.to_ref().hash(state) + } + } + + impl Hash for RefSpecRef<'_> { + fn hash<H: Hasher>(&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<Ordering> { + self.instruction().partial_cmp(&other.instruction()) + } + } + + impl PartialOrd for RefSpec { + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { + self.to_ref().partial_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<BString>) { + 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<T>(name: &BStr, mut cb: impl FnMut(&BStr) -> Option<T>) -> Option<T> { + 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 +} diff --git a/vendor/gix-refspec/src/types.rs b/vendor/gix-refspec/src/types.rs new file mode 100644 index 000000000..0a0e24e36 --- /dev/null +++ b/vendor/gix-refspec/src/types.rs @@ -0,0 +1,21 @@ +use 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>), +} diff --git a/vendor/gix-refspec/src/write.rs b/vendor/gix-refspec/src/write.rs new file mode 100644 index 000000000..74c71c6e1 --- /dev/null +++ b/vendor/gix-refspec/src/write.rs @@ -0,0 +1,74 @@ +use 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: impl std::io::Write) -> std::io::Result<()> { + self.instruction().write_to(out) + } +} + +impl Instruction<'_> { + /// Reproduce ourselves in parseable form. + pub fn to_bstring(&self) -> BString { + let mut buf = Vec::with_capacity(128); + self.write_to(&mut buf).expect("no io error"); + buf.into() + } + + /// Serialize ourselves in a parseable format to `out`. + pub fn write_to(&self, mut out: impl std::io::Write) -> std::io::Result<()> { + match self { + Instruction::Push(Push::Matching { + src, + dst, + allow_non_fast_forward, + }) => { + if *allow_non_fast_forward { + out.write_all(&[b'+'])?; + } + out.write_all(src)?; + out.write_all(&[b':'])?; + out.write_all(dst) + } + Instruction::Push(Push::AllMatchingBranches { allow_non_fast_forward }) => { + if *allow_non_fast_forward { + out.write_all(&[b'+'])?; + } + out.write_all(&[b':']) + } + Instruction::Push(Push::Delete { ref_or_pattern }) => { + out.write_all(&[b':'])?; + out.write_all(ref_or_pattern) + } + Instruction::Fetch(Fetch::Only { src }) => out.write_all(src), + Instruction::Fetch(Fetch::Exclude { src }) => { + out.write_all(&[b'^'])?; + out.write_all(src) + } + Instruction::Fetch(Fetch::AndUpdate { + src, + dst, + allow_non_fast_forward, + }) => { + if *allow_non_fast_forward { + out.write_all(&[b'+'])?; + } + out.write_all(src)?; + out.write_all(&[b':'])?; + out.write_all(dst) + } + } + } +} |