mirror of
https://github.com/awfixers-stuff/src.git
synced 2026-03-23 11:05:59 +00:00
create src
This commit is contained in:
2089
src-revision/CHANGELOG.md
Normal file
2089
src-revision/CHANGELOG.md
Normal file
File diff suppressed because it is too large
Load Diff
55
src-revision/Cargo.toml
Normal file
55
src-revision/Cargo.toml
Normal file
@@ -0,0 +1,55 @@
|
||||
lints.workspace = true
|
||||
|
||||
[package]
|
||||
name = "src-revision"
|
||||
version = "0.42.0"
|
||||
repository = "https://github.com/GitoxideLabs/gitoxide"
|
||||
license = "MIT OR Apache-2.0"
|
||||
description = "A crate of the gitoxide project dealing with finding names for revisions and parsing specifications"
|
||||
authors = ["Sebastian Thiel <sebastian.thiel@icloud.com>"]
|
||||
edition = "2021"
|
||||
include = ["src/**/*", "LICENSE-*", "README.md"]
|
||||
rust-version = "1.82"
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
default = ["describe", "merge_base"]
|
||||
## Enable support for the SHA-1 hash by enabling the respective feature in the `src-hash` crate.
|
||||
sha1 = ["src-hash/sha1"]
|
||||
|
||||
## `git describe` functionality
|
||||
describe = ["dep:src-trace", "dep:src-hashtable"]
|
||||
|
||||
## `git merge-base` functionality
|
||||
merge_base = ["dep:src-trace", "dep:bitflags"]
|
||||
|
||||
## Data structures implement `serde::Serialize` and `serde::Deserialize`.
|
||||
serde = ["dep:serde", "src-hash/serde", "src-object/serde"]
|
||||
|
||||
[dependencies]
|
||||
src-error = { version = "^0.2.0", path = "../src-error" }
|
||||
src-hash = { version = "^0.22.1", path = "../src-hash" }
|
||||
src-object = { version = "^0.57.0", path = "../src-object" }
|
||||
src-date = { version = "^0.15.0", path = "../src-date" }
|
||||
src-hashtable = { version = "^0.12.0", path = "../src-hashtable", optional = true }
|
||||
src-revwalk = { version = "^0.28.0", path = "../src-revwalk" }
|
||||
src-commitgraph = { version = "^0.34.0", path = "../src-commitgraph" }
|
||||
src-trace = { version = "^0.1.18", path = "../src-trace", optional = true }
|
||||
|
||||
bstr = { version = "1.12.0", default-features = false, features = ["std"] }
|
||||
bitflags = { version = "2", optional = true }
|
||||
nonempty = "0.12.0"
|
||||
serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"] }
|
||||
document-features = { version = "0.2.1", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
insta = "1.46.3"
|
||||
src-odb = { path = "../src-odb" }
|
||||
src-testtools = { path = "../tests/tools" }
|
||||
permutohedron = "0.2.4"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
features = ["sha1", "document-features"]
|
||||
1
src-revision/LICENSE-APACHE
Symbolic link
1
src-revision/LICENSE-APACHE
Symbolic link
@@ -0,0 +1 @@
|
||||
../LICENSE-APACHE
|
||||
1
src-revision/LICENSE-MIT
Symbolic link
1
src-revision/LICENSE-MIT
Symbolic link
@@ -0,0 +1 @@
|
||||
../LICENSE-MIT
|
||||
11
src-revision/README.md
Normal file
11
src-revision/README.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# `src-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 <target>`,
|
||||
where `<target>` can be `parse` for example.
|
||||
|
||||
7
src-revision/fuzz/.gitignore
vendored
Normal file
7
src-revision/fuzz/.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
target
|
||||
corpus
|
||||
artifacts
|
||||
|
||||
# These usually involve a lot of local CPU time, keep them.
|
||||
$artifacts
|
||||
$corpus
|
||||
30
src-revision/fuzz/Cargo.toml
Normal file
30
src-revision/fuzz/Cargo.toml
Normal file
@@ -0,0 +1,30 @@
|
||||
[package]
|
||||
name = "src-revision-fuzz"
|
||||
version = "0.0.0"
|
||||
authors = ["Automatically generated"]
|
||||
publish = false
|
||||
edition = "2021"
|
||||
|
||||
[package.metadata]
|
||||
cargo-fuzz = true
|
||||
|
||||
[dependencies]
|
||||
libfuzzer-sys = "0.4"
|
||||
src-hash = { path = "../../src-hash" }
|
||||
src-error = { path = "../../src-error" }
|
||||
bstr = { version = "1.5.0", default-features = false }
|
||||
|
||||
[dependencies.src-revision]
|
||||
path = ".."
|
||||
features = ["sha1"]
|
||||
|
||||
# Prevent this from interfering with workspaces
|
||||
[workspace]
|
||||
members = ["."]
|
||||
|
||||
|
||||
[[bin]]
|
||||
name = "parse"
|
||||
path = "fuzz_targets/parse.rs"
|
||||
test = false
|
||||
doc = false
|
||||
68
src-revision/fuzz/fuzz_targets/parse.rs
Normal file
68
src-revision/fuzz/fuzz_targets/parse.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
#![no_main]
|
||||
use gix_revision::spec::parse::{delegate, Delegate};
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
|
||||
use bstr::BStr;
|
||||
use gix_error::Exn;
|
||||
|
||||
fuzz_target!(|data: &[u8]| {
|
||||
drop(gix_revision::spec::parse(data.into(), &mut Noop));
|
||||
});
|
||||
|
||||
struct Noop;
|
||||
|
||||
impl Delegate for Noop {
|
||||
fn done(&mut self) -> Result<(), Exn> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl delegate::Kind for Noop {
|
||||
fn kind(&mut self, _kind: gix_revision::spec::Kind) -> Result<(), Exn> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl delegate::Navigate for Noop {
|
||||
fn traverse(&mut self, _kind: delegate::Traversal) -> Result<(), Exn> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn peel_until(&mut self, _kind: delegate::PeelTo<'_>) -> Result<(), Exn> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn find(&mut self, _regex: &BStr, _negated: bool) -> Result<(), Exn> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn index_lookup(&mut self, _path: &BStr, _stage: u8) -> Result<(), Exn> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl delegate::Revision for Noop {
|
||||
fn find_ref(&mut self, _name: &BStr) -> Result<(), Exn> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn disambiguate_prefix(
|
||||
&mut self,
|
||||
_prefix: gix_hash::Prefix,
|
||||
_hint: Option<delegate::PrefixHint<'_>>,
|
||||
) -> Result<(), Exn> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn reflog(&mut self, _query: delegate::ReflogLookup) -> Result<(), Exn> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn nth_checked_out_branch(&mut self, _branch_no: usize) -> Result<(), Exn> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sibling_branch(&mut self, _kind: delegate::SiblingBranch) -> Result<(), Exn> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
355
src-revision/src/describe.rs
Normal file
355
src-revision/src/describe.rs
Normal file
@@ -0,0 +1,355 @@
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
fmt::{Display, Formatter},
|
||||
};
|
||||
|
||||
use bstr::BStr;
|
||||
use gix_hashtable::HashMap;
|
||||
|
||||
/// The positive result produced by [describe()][function::describe()].
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Outcome<'name> {
|
||||
/// The name of the tag or branch that is closest to the commit `id`.
|
||||
///
|
||||
/// If `None`, no name was found but it was requested to provide the `id` itself as fallback.
|
||||
pub name: Option<Cow<'name, BStr>>,
|
||||
/// The input commit object id that we describe.
|
||||
pub id: gix_hash::ObjectId,
|
||||
/// The number of commits that are between the tag or branch with `name` and `id`.
|
||||
/// These commits are all in the future of the named tag or branch.
|
||||
pub depth: u32,
|
||||
/// The mapping between object ids and their names initially provided by the describe call.
|
||||
pub name_by_oid: HashMap<gix_hash::ObjectId, Cow<'name, BStr>>,
|
||||
/// The amount of commits we traversed.
|
||||
pub commits_seen: u32,
|
||||
}
|
||||
|
||||
impl<'a> Outcome<'a> {
|
||||
/// Turn this outcome into a structure that can display itself in the typical `git describe` format.
|
||||
pub fn into_format(self, hex_len: usize) -> Format<'a> {
|
||||
Format {
|
||||
name: self.name,
|
||||
id: self.id,
|
||||
hex_len,
|
||||
depth: self.depth,
|
||||
long: false,
|
||||
dirty_suffix: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A structure implementing `Display`, producing a `git describe` like string.
|
||||
#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]
|
||||
pub struct Format<'a> {
|
||||
/// The name of the branch or tag to display, as is.
|
||||
///
|
||||
/// If `None`, the `id` will be displayed as a fallback.
|
||||
pub name: Option<Cow<'a, BStr>>,
|
||||
/// The `id` of the commit to describe.
|
||||
pub id: gix_hash::ObjectId,
|
||||
/// The amount of hex characters to use to display `id`.
|
||||
pub hex_len: usize,
|
||||
/// The amount of commits between `name` and `id`, where `id` is in the future of `name`.
|
||||
pub depth: u32,
|
||||
/// If true, the long form of the describe string will be produced even if `id` lies directly on `name`,
|
||||
/// hence has a depth of 0.
|
||||
pub long: bool,
|
||||
/// If `Some(suffix)`, it will be appended to the describe string.
|
||||
/// This should be set if the working tree was determined to be dirty.
|
||||
pub dirty_suffix: Option<String>,
|
||||
}
|
||||
|
||||
impl Format<'_> {
|
||||
/// Return true if the `name` is directly associated with `id`, i.e. there are no commits between them.
|
||||
pub fn is_exact_match(&self) -> bool {
|
||||
self.depth == 0
|
||||
}
|
||||
|
||||
/// Set this instance to print in long mode, that is if `depth` is 0, it will still print the whole
|
||||
/// long form even though it's not quite necessary.
|
||||
///
|
||||
/// Otherwise, it is allowed to shorten itself.
|
||||
pub fn long(&mut self, long: bool) -> &mut Self {
|
||||
self.long = long;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Format<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
if let Some(name) = self.name.as_deref() {
|
||||
if !self.long && self.is_exact_match() {
|
||||
name.fmt(f)?;
|
||||
} else {
|
||||
write!(f, "{}-{}-g{}", name, self.depth, self.id.to_hex_with_len(self.hex_len))?;
|
||||
}
|
||||
} else {
|
||||
self.id.to_hex_with_len(self.hex_len).fmt(f)?;
|
||||
}
|
||||
|
||||
if let Some(suffix) = &self.dirty_suffix {
|
||||
write!(f, "-{suffix}")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// A bit-field which keeps track of which commit is reachable by one of 32 candidates names.
|
||||
pub type Flags = u32;
|
||||
const MAX_CANDIDATES: usize = std::mem::size_of::<Flags>() * 8;
|
||||
|
||||
/// The options required to call [`describe()`][function::describe()].
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Options<'name> {
|
||||
/// The candidate names from which to determine the `name` to use for the describe string,
|
||||
/// as a mapping from a commit id and the name associated with it.
|
||||
pub name_by_oid: HashMap<gix_hash::ObjectId, Cow<'name, BStr>>,
|
||||
/// The amount of names we will keep track of. Defaults to the maximum of 32.
|
||||
///
|
||||
/// If the number is exceeded, it will be capped at 32 and defaults to 10.
|
||||
pub max_candidates: usize,
|
||||
/// If no candidate for naming, always show the abbreviated hash. Default: false.
|
||||
pub fallback_to_oid: bool,
|
||||
/// Only follow the first parent during graph traversal. Default: false.
|
||||
///
|
||||
/// This may speed up the traversal at the cost of accuracy.
|
||||
pub first_parent: bool,
|
||||
}
|
||||
|
||||
impl Default for Options<'_> {
|
||||
fn default() -> Self {
|
||||
Options {
|
||||
max_candidates: 10, // the same number as git uses, otherwise we perform worse by default on big repos
|
||||
name_by_oid: Default::default(),
|
||||
fallback_to_oid: false,
|
||||
first_parent: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The error returned by the [`describe()`](function::describe()) function.
|
||||
pub type Error = gix_error::Message;
|
||||
|
||||
pub(crate) mod function {
|
||||
use std::{borrow::Cow, cmp::Ordering};
|
||||
|
||||
use bstr::BStr;
|
||||
use gix_error::{message, Exn, ResultExt};
|
||||
use gix_hash::oid;
|
||||
|
||||
use super::{Error, Outcome};
|
||||
use crate::{
|
||||
describe::{CommitTime, Flags, Options, MAX_CANDIDATES},
|
||||
Graph, PriorityQueue,
|
||||
};
|
||||
|
||||
/// Given a `commit` id, traverse the commit `graph` and collect candidate names from the `name_by_oid` mapping to produce
|
||||
/// an `Outcome`, which converted [`into_format()`](Outcome::into_format()) will produce a typical `git describe` string.
|
||||
///
|
||||
/// Note that the `name_by_oid` map is returned in the [`Outcome`], which can be forcefully returned even if there was no matching
|
||||
/// candidate by setting `fallback_to_oid` to true.
|
||||
pub fn describe<'name>(
|
||||
commit: &oid,
|
||||
graph: &mut Graph<'_, '_, Flags>,
|
||||
Options {
|
||||
name_by_oid,
|
||||
mut max_candidates,
|
||||
fallback_to_oid,
|
||||
first_parent,
|
||||
}: Options<'name>,
|
||||
) -> Result<Option<Outcome<'name>>, Exn<Error>> {
|
||||
let _span = gix_trace::coarse!(
|
||||
"gix_revision::describe()",
|
||||
commit = %commit,
|
||||
name_count = name_by_oid.len(),
|
||||
max_candidates,
|
||||
first_parent
|
||||
);
|
||||
max_candidates = max_candidates.min(MAX_CANDIDATES);
|
||||
if let Some(name) = name_by_oid.get(commit) {
|
||||
return Ok(Some(Outcome {
|
||||
name: name.clone().into(),
|
||||
id: commit.to_owned(),
|
||||
depth: 0,
|
||||
name_by_oid,
|
||||
commits_seen: 0,
|
||||
}));
|
||||
}
|
||||
|
||||
if max_candidates == 0 || name_by_oid.is_empty() {
|
||||
return if fallback_to_oid {
|
||||
Ok(Some(Outcome {
|
||||
id: commit.to_owned(),
|
||||
name: None,
|
||||
name_by_oid,
|
||||
depth: 0,
|
||||
commits_seen: 0,
|
||||
}))
|
||||
} else {
|
||||
Ok(None)
|
||||
};
|
||||
}
|
||||
|
||||
let mut queue = PriorityQueue::from_iter(Some((u32::MAX, commit.to_owned())));
|
||||
let mut candidates = Vec::new();
|
||||
let mut commits_seen = 0;
|
||||
let mut gave_up_on_commit = None;
|
||||
graph.clear();
|
||||
graph.insert(commit.to_owned(), 0u32);
|
||||
|
||||
while let Some(commit) = queue.pop_value() {
|
||||
commits_seen += 1;
|
||||
let flags = if let Some(name) = name_by_oid.get(&commit) {
|
||||
if candidates.len() < max_candidates {
|
||||
let identity_bit = 1 << candidates.len();
|
||||
candidates.push(Candidate {
|
||||
name: name.clone(),
|
||||
commits_in_its_future: commits_seen - 1,
|
||||
identity_bit,
|
||||
order: candidates.len(),
|
||||
});
|
||||
let flags = graph.get_mut(&commit).expect("inserted");
|
||||
*flags |= identity_bit;
|
||||
*flags
|
||||
} else {
|
||||
gave_up_on_commit = Some(commit);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
graph[&commit]
|
||||
};
|
||||
|
||||
for candidate in candidates
|
||||
.iter_mut()
|
||||
.filter(|c| (flags & c.identity_bit) != c.identity_bit)
|
||||
{
|
||||
candidate.commits_in_its_future += 1;
|
||||
}
|
||||
|
||||
if queue.is_empty() && !candidates.is_empty() {
|
||||
// single-trunk history that waits to be replenished.
|
||||
// Abort early if the best-candidate is in the current commits past.
|
||||
let mut shortest_depth = Flags::MAX;
|
||||
let mut best_candidates_at_same_depth = 0_u32;
|
||||
for candidate in &candidates {
|
||||
match candidate.commits_in_its_future.cmp(&shortest_depth) {
|
||||
Ordering::Less => {
|
||||
shortest_depth = candidate.commits_in_its_future;
|
||||
best_candidates_at_same_depth = candidate.identity_bit;
|
||||
}
|
||||
Ordering::Equal => {
|
||||
best_candidates_at_same_depth |= candidate.identity_bit;
|
||||
}
|
||||
Ordering::Greater => {}
|
||||
}
|
||||
}
|
||||
|
||||
if (flags & best_candidates_at_same_depth) == best_candidates_at_same_depth {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
parents_by_date_onto_queue_and_track_names(graph, &mut queue, commit, flags, first_parent)?;
|
||||
}
|
||||
|
||||
if candidates.is_empty() {
|
||||
return if fallback_to_oid {
|
||||
Ok(Some(Outcome {
|
||||
id: commit.to_owned(),
|
||||
name: None,
|
||||
name_by_oid,
|
||||
depth: 0,
|
||||
commits_seen,
|
||||
}))
|
||||
} else {
|
||||
Ok(None)
|
||||
};
|
||||
}
|
||||
|
||||
candidates.sort_by(|a, b| {
|
||||
a.commits_in_its_future
|
||||
.cmp(&b.commits_in_its_future)
|
||||
.then_with(|| a.order.cmp(&b.order))
|
||||
});
|
||||
|
||||
if let Some(commit_id) = gave_up_on_commit {
|
||||
queue.insert(u32::MAX, commit_id);
|
||||
commits_seen -= 1;
|
||||
}
|
||||
|
||||
commits_seen += finish_depth_computation(
|
||||
queue,
|
||||
graph,
|
||||
candidates.first_mut().expect("at least one candidate"),
|
||||
first_parent,
|
||||
)?;
|
||||
|
||||
Ok(candidates.into_iter().next().map(|c| Outcome {
|
||||
name: c.name.into(),
|
||||
id: commit.to_owned(),
|
||||
depth: c.commits_in_its_future,
|
||||
name_by_oid,
|
||||
commits_seen,
|
||||
}))
|
||||
}
|
||||
|
||||
fn parents_by_date_onto_queue_and_track_names(
|
||||
graph: &mut Graph<'_, '_, Flags>,
|
||||
queue: &mut PriorityQueue<CommitTime, gix_hash::ObjectId>,
|
||||
commit: gix_hash::ObjectId,
|
||||
commit_flags: Flags,
|
||||
first_parent: bool,
|
||||
) -> Result<(), Exn<Error>> {
|
||||
graph
|
||||
.insert_parents(
|
||||
&commit,
|
||||
&mut |parent_id, parent_commit_date| {
|
||||
queue.insert(parent_commit_date as u32, parent_id);
|
||||
commit_flags
|
||||
},
|
||||
&mut |_parent_id, flags| *flags |= commit_flags,
|
||||
first_parent,
|
||||
)
|
||||
.or_raise(|| message!("could not insert parents of commit {} into graph", commit.to_hex()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn finish_depth_computation(
|
||||
mut queue: PriorityQueue<CommitTime, gix_hash::ObjectId>,
|
||||
graph: &mut Graph<'_, '_, Flags>,
|
||||
best_candidate: &mut Candidate<'_>,
|
||||
first_parent: bool,
|
||||
) -> Result<u32, Exn<Error>> {
|
||||
let mut commits_seen = 0;
|
||||
while let Some(commit) = queue.pop_value() {
|
||||
commits_seen += 1;
|
||||
let flags = graph[&commit];
|
||||
if (flags & best_candidate.identity_bit) == best_candidate.identity_bit {
|
||||
if queue
|
||||
.iter_unordered()
|
||||
.all(|id| (graph[id] & best_candidate.identity_bit) == best_candidate.identity_bit)
|
||||
{
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
best_candidate.commits_in_its_future += 1;
|
||||
}
|
||||
|
||||
parents_by_date_onto_queue_and_track_names(graph, &mut queue, commit, flags, first_parent)?;
|
||||
}
|
||||
Ok(commits_seen)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Candidate<'a> {
|
||||
name: Cow<'a, BStr>,
|
||||
commits_in_its_future: Flags,
|
||||
/// A single bit identifying this candidate uniquely in a bitset
|
||||
identity_bit: Flags,
|
||||
/// The order at which we found the candidate, first one has order = 0
|
||||
order: usize,
|
||||
}
|
||||
}
|
||||
|
||||
/// The timestamp for the creation date of a commit in seconds since unix epoch.
|
||||
type CommitTime = u32;
|
||||
26
src-revision/src/lib.rs
Normal file
26
src-revision/src/lib.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
//! Interact with git revisions by parsing them from rev-specs and describing them in terms of reference names.
|
||||
//!
|
||||
//! ## Feature Flags
|
||||
#![cfg_attr(
|
||||
all(doc, feature = "document-features"),
|
||||
doc = ::document_features::document_features!()
|
||||
)]
|
||||
#![cfg_attr(all(doc, feature = "document-features"), feature(doc_cfg))]
|
||||
#![deny(missing_docs, rust_2018_idioms, unsafe_code)]
|
||||
|
||||
///
|
||||
#[cfg(feature = "describe")]
|
||||
pub mod describe;
|
||||
#[cfg(feature = "describe")]
|
||||
pub use describe::function::describe;
|
||||
///
|
||||
#[allow(clippy::empty_docs)]
|
||||
#[cfg(feature = "merge_base")]
|
||||
pub mod merge_base;
|
||||
#[cfg(feature = "merge_base")]
|
||||
pub use merge_base::function::merge_base;
|
||||
|
||||
///
|
||||
pub mod spec;
|
||||
pub use gix_revwalk::{graph, Graph, PriorityQueue};
|
||||
pub use spec::types::Spec;
|
||||
240
src-revision/src/merge_base/function.rs
Normal file
240
src-revision/src/merge_base/function.rs
Normal file
@@ -0,0 +1,240 @@
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use gix_hash::ObjectId;
|
||||
use gix_revwalk::graph;
|
||||
|
||||
use super::{Error, Simple};
|
||||
use crate::{merge_base::Flags, Graph, PriorityQueue};
|
||||
|
||||
/// Given a commit at `first` id, traverse the commit `graph` and return all possible merge-base between it and `others`,
|
||||
/// sorted from best to worst. Returns `None` if there is no merge-base as `first` and `others` don't share history.
|
||||
/// If `others` is empty, `Some(first)` is returned.
|
||||
///
|
||||
/// Note that this function doesn't do any work if `first` is contained in `others`, which is when `first` will be returned
|
||||
/// as only merge-base right away. This is even the case if some commits of `others` are disjoint.
|
||||
///
|
||||
/// Additionally, this function isn't stable and results may differ dependeing on the order in which `first` and `others` are
|
||||
/// provided due to its special rules.
|
||||
///
|
||||
/// If a stable result is needed, use [`merge_base::octopus()`](crate::merge_base::octopus()).
|
||||
///
|
||||
/// # Performance
|
||||
///
|
||||
/// For repeated calls, be sure to re-use `graph` as its content will be kept and reused for a great speed-up. The contained flags
|
||||
/// will automatically be cleared.
|
||||
pub fn merge_base(
|
||||
first: ObjectId,
|
||||
others: &[ObjectId],
|
||||
graph: &mut Graph<'_, '_, graph::Commit<Flags>>,
|
||||
) -> Result<Option<nonempty::NonEmpty<ObjectId>>, Error> {
|
||||
let _span = gix_trace::coarse!("gix_revision::merge_base()", ?first, ?others);
|
||||
if others.is_empty() || others.contains(&first) {
|
||||
return Ok(Some(nonempty::NonEmpty::new(first)));
|
||||
}
|
||||
|
||||
graph.clear_commit_data(|f| *f = Flags::empty());
|
||||
let bases = paint_down_to_common(first, others, graph)?;
|
||||
|
||||
let bases = remove_redundant(&bases, graph)?;
|
||||
Ok(nonempty::NonEmpty::from_vec(bases))
|
||||
}
|
||||
|
||||
/// Remove all those commits from `commits` if they are in the history of another commit in `commits`.
|
||||
/// That way, we return only the topologically most recent commits in `commits`.
|
||||
fn remove_redundant(
|
||||
commits: &[(ObjectId, GenThenTime)],
|
||||
graph: &mut Graph<'_, '_, graph::Commit<Flags>>,
|
||||
) -> Result<Vec<ObjectId>, Error> {
|
||||
if commits.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
graph.clear_commit_data(|f| *f = Flags::empty());
|
||||
let _span = gix_trace::detail!("gix_revision::remove_redundant()", num_commits = %commits.len());
|
||||
let sorted_commits = {
|
||||
let mut v = commits.to_vec();
|
||||
v.sort_by(|a, b| a.1.cmp(&b.1));
|
||||
v
|
||||
};
|
||||
let mut min_gen_pos = 0;
|
||||
let mut min_gen = sorted_commits[min_gen_pos].1.generation;
|
||||
|
||||
let mut walk_start = Vec::with_capacity(commits.len());
|
||||
for (id, _) in commits {
|
||||
let commit = graph.get_mut(id).expect("previously added");
|
||||
commit.data |= Flags::RESULT;
|
||||
for parent_id in commit.parents.clone() {
|
||||
graph
|
||||
.get_or_insert_full_commit(parent_id, |parent| {
|
||||
// prevent double-addition
|
||||
if !parent.data.contains(Flags::STALE) {
|
||||
parent.data |= Flags::STALE;
|
||||
walk_start.push((parent_id, GenThenTime::from(&*parent)));
|
||||
}
|
||||
})
|
||||
.map_err(|_| Simple("could not insert parent commit into graph"))?;
|
||||
}
|
||||
}
|
||||
walk_start.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
// allow walking everything at first.
|
||||
walk_start
|
||||
.iter_mut()
|
||||
.for_each(|(id, _)| graph.get_mut(id).expect("added previously").data.remove(Flags::STALE));
|
||||
let mut count_still_independent = commits.len();
|
||||
|
||||
let mut stack = Vec::new();
|
||||
while let Some((commit_id, commit_info)) = walk_start.pop().filter(|_| count_still_independent > 1) {
|
||||
stack.clear();
|
||||
graph.get_mut(&commit_id).expect("added").data |= Flags::STALE;
|
||||
stack.push((commit_id, commit_info));
|
||||
|
||||
while let Some((commit_id, commit_info)) = stack.last().copied() {
|
||||
let commit = graph.get_mut(&commit_id).expect("all commits have been added");
|
||||
let commit_parents = commit.parents.clone();
|
||||
if commit.data.contains(Flags::RESULT) {
|
||||
commit.data.remove(Flags::RESULT);
|
||||
count_still_independent -= 1;
|
||||
if count_still_independent <= 1 {
|
||||
break;
|
||||
}
|
||||
if *commit_id == *sorted_commits[min_gen_pos].0 {
|
||||
while min_gen_pos < commits.len() - 1
|
||||
&& graph
|
||||
.get(&sorted_commits[min_gen_pos].0)
|
||||
.expect("already added")
|
||||
.data
|
||||
.contains(Flags::STALE)
|
||||
{
|
||||
min_gen_pos += 1;
|
||||
}
|
||||
min_gen = sorted_commits[min_gen_pos].1.generation;
|
||||
}
|
||||
}
|
||||
|
||||
if commit_info.generation < min_gen {
|
||||
stack.pop();
|
||||
continue;
|
||||
}
|
||||
|
||||
let previous_len = stack.len();
|
||||
for parent_id in &commit_parents {
|
||||
if graph
|
||||
.get_or_insert_full_commit(*parent_id, |parent| {
|
||||
if !parent.data.contains(Flags::STALE) {
|
||||
parent.data |= Flags::STALE;
|
||||
stack.push((*parent_id, GenThenTime::from(&*parent)));
|
||||
}
|
||||
})
|
||||
.map_err(|_| Simple("could not insert parent commit into graph"))?
|
||||
.is_some()
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if previous_len == stack.len() {
|
||||
stack.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(commits
|
||||
.iter()
|
||||
.filter_map(|(id, _info)| {
|
||||
graph
|
||||
.get(id)
|
||||
.filter(|commit| !commit.data.contains(Flags::STALE))
|
||||
.map(|_| *id)
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn paint_down_to_common(
|
||||
first: ObjectId,
|
||||
others: &[ObjectId],
|
||||
graph: &mut Graph<'_, '_, graph::Commit<Flags>>,
|
||||
) -> Result<Vec<(ObjectId, GenThenTime)>, Error> {
|
||||
let mut queue = PriorityQueue::<GenThenTime, ObjectId>::new();
|
||||
graph
|
||||
.get_or_insert_full_commit(first, |commit| {
|
||||
commit.data |= Flags::COMMIT1;
|
||||
queue.insert(GenThenTime::from(&*commit), first);
|
||||
})
|
||||
.map_err(|_| Simple("could not insert commit into graph"))?;
|
||||
|
||||
for other in others {
|
||||
graph
|
||||
.get_or_insert_full_commit(*other, |commit| {
|
||||
commit.data |= Flags::COMMIT2;
|
||||
queue.insert(GenThenTime::from(&*commit), *other);
|
||||
})
|
||||
.map_err(|_| Simple("could not insert commit into graph"))?;
|
||||
}
|
||||
|
||||
let mut out = Vec::new();
|
||||
while queue
|
||||
.iter_unordered()
|
||||
.any(|id| graph.get(id).is_some_and(|commit| !commit.data.contains(Flags::STALE)))
|
||||
{
|
||||
let (info, commit_id) = queue.pop().expect("we have non-stale");
|
||||
let commit = graph.get_mut(&commit_id).expect("everything queued is in graph");
|
||||
let mut flags_without_result = commit.data & (Flags::COMMIT1 | Flags::COMMIT2 | Flags::STALE);
|
||||
if flags_without_result == (Flags::COMMIT1 | Flags::COMMIT2) {
|
||||
if !commit.data.contains(Flags::RESULT) {
|
||||
commit.data |= Flags::RESULT;
|
||||
out.push((commit_id, info));
|
||||
}
|
||||
flags_without_result |= Flags::STALE;
|
||||
}
|
||||
|
||||
for parent_id in commit.parents.clone() {
|
||||
graph
|
||||
.get_or_insert_full_commit(parent_id, |parent| {
|
||||
if (parent.data & flags_without_result) != flags_without_result {
|
||||
parent.data |= flags_without_result;
|
||||
queue.insert(GenThenTime::from(&*parent), parent_id);
|
||||
}
|
||||
})
|
||||
.map_err(|_| Simple("could not insert parent commit into graph"))?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
// TODO(ST): Should this type be used for `describe` as well?
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct GenThenTime {
|
||||
/// Note that the special [`GENERATION_NUMBER_INFINITY`](gix_commitgraph::GENERATION_NUMBER_INFINITY) is used to indicate
|
||||
/// that no commitgraph is available.
|
||||
generation: gix_revwalk::graph::Generation,
|
||||
time: gix_date::SecondsSinceUnixEpoch,
|
||||
}
|
||||
|
||||
impl From<&graph::Commit<Flags>> for GenThenTime {
|
||||
fn from(commit: &graph::Commit<Flags>) -> Self {
|
||||
GenThenTime {
|
||||
generation: commit.generation.unwrap_or(gix_commitgraph::GENERATION_NUMBER_INFINITY),
|
||||
time: commit.commit_time,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for GenThenTime {}
|
||||
|
||||
impl PartialEq<Self> for GenThenTime {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.cmp(other).is_eq()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd<Self> for GenThenTime {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for GenThenTime {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.generation.cmp(&other.generation).then(self.time.cmp(&other.time))
|
||||
}
|
||||
}
|
||||
65
src-revision/src/merge_base/mod.rs
Normal file
65
src-revision/src/merge_base/mod.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
bitflags::bitflags! {
|
||||
/// The flags used in the graph for finding [merge bases](crate::merge_base()).
|
||||
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)]
|
||||
pub struct Flags: u8 {
|
||||
/// The commit belongs to the graph reachable by the first commit
|
||||
const COMMIT1 = 1 << 0;
|
||||
/// The commit belongs to the graph reachable by all other commits.
|
||||
const COMMIT2 = 1 << 1;
|
||||
|
||||
/// Marks the commit as done, it's reachable by both COMMIT1 and COMMIT2.
|
||||
const STALE = 1 << 2;
|
||||
/// The commit was already put ontto the results list.
|
||||
const RESULT = 1 << 3;
|
||||
}
|
||||
}
|
||||
|
||||
/// The error returned by the [`merge_base()`][function::merge_base()] function.
|
||||
pub type Error = Simple;
|
||||
|
||||
/// A simple error type for merge base operations.
|
||||
#[derive(Debug)]
|
||||
pub struct Simple(pub &'static str);
|
||||
|
||||
impl std::fmt::Display for Simple {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Simple {}
|
||||
|
||||
pub(crate) mod function;
|
||||
|
||||
mod octopus {
|
||||
use gix_hash::ObjectId;
|
||||
use gix_revwalk::{graph, Graph};
|
||||
|
||||
use crate::merge_base::{Error, Flags};
|
||||
|
||||
/// Given a commit at `first` id, traverse the commit `graph` and return *the best common ancestor* between it and `others`,
|
||||
/// sorted from best to worst. Returns `None` if there is no common merge-base as `first` and `others` don't *all* share history.
|
||||
/// If `others` is empty, `Some(first)` is returned.
|
||||
///
|
||||
/// # Performance
|
||||
///
|
||||
/// For repeated calls, be sure to re-use `graph` as its content will be kept and reused for a great speed-up. The contained flags
|
||||
/// will automatically be cleared.
|
||||
pub fn octopus(
|
||||
mut first: ObjectId,
|
||||
others: &[ObjectId],
|
||||
graph: &mut Graph<'_, '_, graph::Commit<Flags>>,
|
||||
) -> Result<Option<ObjectId>, Error> {
|
||||
for other in others {
|
||||
if let Some(next) =
|
||||
crate::merge_base(first, std::slice::from_ref(other), graph)?.map(|bases| *bases.first())
|
||||
{
|
||||
first = next;
|
||||
} else {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
Ok(Some(first))
|
||||
}
|
||||
}
|
||||
pub use octopus::octopus;
|
||||
109
src-revision/src/spec/mod.rs
Normal file
109
src-revision/src/spec/mod.rs
Normal file
@@ -0,0 +1,109 @@
|
||||
use crate::Spec;
|
||||
|
||||
/// How to interpret a revision specification, or `revspec`.
|
||||
#[derive(Default, Debug, Copy, Clone, PartialOrd, PartialEq, Ord, Eq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum Kind {
|
||||
/// Include commits reachable from this revision, the default when parsing revision `a` for example, i.e. `a` and its ancestors.
|
||||
/// Example: `a`.
|
||||
#[default]
|
||||
IncludeReachable,
|
||||
/// Exclude commits reachable from this revision, i.e. `a` and its ancestors. Example: `^a`.
|
||||
ExcludeReachable,
|
||||
/// Every commit that is reachable from `b` but not from `a`. Example: `a..b`.
|
||||
RangeBetween,
|
||||
/// Every commit reachable through either `a` or `b` but no commit that is reachable by both. Example: `a...b`.
|
||||
ReachableToMergeBase,
|
||||
/// Include every commit of all parents of `a`, but not `a` itself. Example: `a^@`.
|
||||
IncludeReachableFromParents,
|
||||
/// Exclude every commit of all parents of `a`, but not `a` itself. Example: `a^!`.
|
||||
ExcludeReachableFromParents,
|
||||
}
|
||||
|
||||
impl Spec {
|
||||
/// Return the kind of this specification.
|
||||
pub fn kind(&self) -> Kind {
|
||||
match self {
|
||||
Spec::Include(_) => Kind::IncludeReachable,
|
||||
Spec::Exclude(_) => Kind::ExcludeReachable,
|
||||
Spec::Range { .. } => Kind::RangeBetween,
|
||||
Spec::Merge { .. } => Kind::ReachableToMergeBase,
|
||||
Spec::IncludeOnlyParents { .. } => Kind::IncludeReachableFromParents,
|
||||
Spec::ExcludeParents { .. } => Kind::ExcludeReachableFromParents,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod _impls {
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
use crate::Spec;
|
||||
|
||||
impl Display for Spec {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Spec::Include(oid) => Display::fmt(oid, f),
|
||||
Spec::Exclude(oid) => write!(f, "^{oid}"),
|
||||
Spec::Range { from, to } => write!(f, "{from}..{to}"),
|
||||
Spec::Merge { theirs, ours } => write!(f, "{theirs}...{ours}"),
|
||||
Spec::IncludeOnlyParents(from_exclusive) => write!(f, "{from_exclusive}^@"),
|
||||
Spec::ExcludeParents(oid) => write!(f, "{oid}^!"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) mod types {
|
||||
/// A revision specification without any bindings to a repository, useful for serialization or movement over thread boundaries.
|
||||
///
|
||||
/// Note that all [object ids][gix_hash::ObjectId] should be a committish, but don't have to be.
|
||||
/// Unless the field name contains `_exclusive`, the respective objects are included in the set.
|
||||
#[derive(Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum Spec {
|
||||
/// Include commits reachable from this revision, i.e. `a` and its ancestors.
|
||||
///
|
||||
/// The equivalent to [crate::spec::Kind::IncludeReachable], but with data.
|
||||
Include(gix_hash::ObjectId),
|
||||
/// Exclude commits reachable from this revision, i.e. `a` and its ancestors. Example: `^a`.
|
||||
///
|
||||
/// The equivalent to [crate::spec::Kind::ExcludeReachable], but with data.
|
||||
Exclude(gix_hash::ObjectId),
|
||||
/// Every commit that is reachable from `from` to `to`, but not any ancestors of `from`. Example: `from..to`.
|
||||
///
|
||||
/// The equivalent to [crate::spec::Kind::RangeBetween], but with data.
|
||||
Range {
|
||||
/// The starting point of the range, which is included in the set.
|
||||
from: gix_hash::ObjectId,
|
||||
/// The end point of the range, which is included in the set.
|
||||
to: gix_hash::ObjectId,
|
||||
},
|
||||
/// Every commit reachable through either `theirs` or `ours`, but no commit that is reachable by both. Example: `theirs...ours`.
|
||||
///
|
||||
/// The equivalent to [crate::spec::Kind::ReachableToMergeBase], but with data.
|
||||
Merge {
|
||||
/// Their side of the merge, which is included in the set.
|
||||
theirs: gix_hash::ObjectId,
|
||||
/// Our side of the merge, which is included in the set.
|
||||
ours: gix_hash::ObjectId,
|
||||
},
|
||||
/// Include every commit of all parents of `a`, but not `a` itself. Example: `a^@`.
|
||||
///
|
||||
/// The equivalent to [crate::spec::Kind::IncludeReachableFromParents], but with data.
|
||||
IncludeOnlyParents(
|
||||
/// Include only the parents of this object, but not the object itself.
|
||||
gix_hash::ObjectId,
|
||||
),
|
||||
/// Exclude every commit of all parents of `a`, but not `a` itself. Example: `a^!`.
|
||||
///
|
||||
/// The equivalent to [crate::spec::Kind::ExcludeReachableFromParents], but with data.
|
||||
ExcludeParents(
|
||||
/// Exclude the parents of this object, but not the object itself.
|
||||
gix_hash::ObjectId,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
pub mod parse;
|
||||
pub use parse::function::parse;
|
||||
157
src-revision/src/spec/parse/delegate.rs
Normal file
157
src-revision/src/spec/parse/delegate.rs
Normal file
@@ -0,0 +1,157 @@
|
||||
use bstr::BStr;
|
||||
use gix_error::Exn;
|
||||
|
||||
/// Usually the first methods to call when parsing a rev-spec to set an anchoring revision (which is typically a `Commit` object).
|
||||
/// Methods can be called multiple time to either try input or to parse another rev-spec that is part of a range.
|
||||
///
|
||||
/// In one case they will not be called at all, e.g. `@{[-]n}` indicates the current branch (what `HEAD` dereferences to),
|
||||
/// without ever naming it, and so does `@{upstream}` or `@{<date>}`.
|
||||
///
|
||||
/// Note that when dereferencing `HEAD` implicitly, a revision must be set for later navigation.
|
||||
pub trait Revision {
|
||||
/// Resolve `name` as reference which might not be a valid reference name. The name may be partial like `main` or full like
|
||||
/// `refs/heads/main` solely depending on the users input.
|
||||
/// Symbolic referenced should be followed till their object, but objects **must not yet** be peeled.
|
||||
fn find_ref(&mut self, name: &BStr) -> Result<(), Exn>;
|
||||
|
||||
/// An object prefix to disambiguate, returning `None` if it is ambiguous or wasn't found at all.
|
||||
///
|
||||
/// If `hint` is set, it should be used to disambiguate multiple objects with the same prefix.
|
||||
fn disambiguate_prefix(&mut self, prefix: gix_hash::Prefix, hint: Option<PrefixHint<'_>>) -> Result<(), Exn>;
|
||||
|
||||
/// Lookup the reflog of the previously set reference, or dereference `HEAD` to its reference
|
||||
/// to obtain the ref name (as opposed to `HEAD` itself).
|
||||
/// If there is no such reflog entry, return `None`.
|
||||
fn reflog(&mut self, query: ReflogLookup) -> Result<(), Exn>;
|
||||
|
||||
/// When looking at `HEAD`, `branch_no` is the non-null checkout in the path, e.g. `1` means the last branch checked out,
|
||||
/// `2` is the one before that.
|
||||
/// Return `None` if there is no branch as the checkout history (via the reflog) isn't long enough.
|
||||
fn nth_checked_out_branch(&mut self, branch_no: usize) -> Result<(), Exn>;
|
||||
|
||||
/// Lookup the previously set branch or dereference `HEAD` to its reference to use its name to lookup the sibling branch of `kind`
|
||||
/// in the configuration (typically in `refs/remotes/…`). The sibling branches are always local tracking branches.
|
||||
/// Return `None` of no such configuration exists and no sibling could be found, which is also the case for all reference outside
|
||||
/// of `refs/heads/`.
|
||||
/// Note that the caller isn't aware if the previously set reference is a branch or not and might call this method even though no reference
|
||||
/// is known.
|
||||
fn sibling_branch(&mut self, kind: SiblingBranch) -> Result<(), Exn>;
|
||||
}
|
||||
|
||||
/// Combine one or more specs into a range of multiple.
|
||||
pub trait Kind {
|
||||
/// Set the kind of the spec, which happens only once if it happens at all.
|
||||
/// In case this method isn't called, assume `Single`.
|
||||
/// Reject a kind by returning `None` to stop the parsing.
|
||||
///
|
||||
/// Note that ranges don't necessarily assure that a second specification will be parsed.
|
||||
/// If `^rev` is given, this method is called with [`spec::Kind::RangeBetween`][crate::spec::Kind::RangeBetween]
|
||||
/// and no second specification is provided.
|
||||
///
|
||||
/// Note that the method can be called even if other invariants are not fulfilled, treat these as errors.
|
||||
fn kind(&mut self, kind: crate::spec::Kind) -> Result<(), Exn>;
|
||||
}
|
||||
|
||||
/// Once an anchor is set one can adjust it using traversal methods.
|
||||
pub trait Navigate {
|
||||
/// Adjust the current revision to traverse the graph according to `kind`.
|
||||
fn traverse(&mut self, kind: Traversal) -> Result<(), Exn>;
|
||||
|
||||
/// Peel the current object until it reached `kind` or `None` if the chain does not contain such object.
|
||||
fn peel_until(&mut self, kind: PeelTo<'_>) -> Result<(), Exn>;
|
||||
|
||||
/// Find the first revision/commit whose message matches the given `regex` (which is never empty).
|
||||
/// to see how it should be matched.
|
||||
/// If `negated` is `true`, the first non-match will be a match.
|
||||
///
|
||||
/// If no revision is known yet, find the _youngest_ matching commit from _any_ reference, including `HEAD`.
|
||||
/// Otherwise, only find commits reachable from the currently set revision.
|
||||
fn find(&mut self, regex: &BStr, negated: bool) -> Result<(), Exn>;
|
||||
|
||||
/// Look up the given `path` at the given `stage` in the index returning its blob id,
|
||||
/// or return `None` if it doesn't exist at this `stage`.
|
||||
/// Note that this implies no revision is needed and no anchor is set yet.
|
||||
///
|
||||
/// * `stage` ranges from 0 to 2, with 0 being the base, 1 being ours, 2 being theirs.
|
||||
/// * `path` without prefix is relative to the root of the repository, while prefixes like `./` and `../` make it
|
||||
/// relative to the current working directory.
|
||||
fn index_lookup(&mut self, path: &BStr, stage: u8) -> Result<(), Exn>;
|
||||
}
|
||||
|
||||
/// A hint to make disambiguation when looking up prefixes possible.
|
||||
#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]
|
||||
pub enum PrefixHint<'a> {
|
||||
/// The prefix must be a commit.
|
||||
MustBeCommit,
|
||||
/// The prefix refers to a commit, anchored to a ref and a revision generation in its future.
|
||||
DescribeAnchor {
|
||||
/// The name of the reference, like `v1.2.3` or `main`.
|
||||
ref_name: &'a BStr,
|
||||
/// The future generation of the commit we look for, with 0 meaning the commit is referenced by
|
||||
/// `ref_name` directly.
|
||||
generation: usize,
|
||||
},
|
||||
}
|
||||
|
||||
/// A lookup into the reflog of a reference.
|
||||
#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]
|
||||
pub enum ReflogLookup {
|
||||
/// Lookup by entry, where `0` is the most recent entry, and `1` is the older one behind `0`.
|
||||
Entry(usize),
|
||||
/// Lookup the reflog at the given time and find the closest matching entry.
|
||||
Date(gix_date::Time),
|
||||
}
|
||||
|
||||
/// Define how to traverse the commit graph.
|
||||
#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]
|
||||
pub enum Traversal {
|
||||
/// Select the given parent commit of the currently selected commit, start at `1` for the first parent.
|
||||
/// The value will never be `0`.
|
||||
NthParent(usize),
|
||||
/// Select the given ancestor of the currently selected commit, start at `1` for the first ancestor.
|
||||
/// The value will never be `0`.
|
||||
NthAncestor(usize),
|
||||
}
|
||||
|
||||
/// Define where a tag object should be peeled to.
|
||||
#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]
|
||||
pub enum PeelTo<'a> {
|
||||
/// An object of the given kind.
|
||||
ObjectKind(gix_object::Kind),
|
||||
/// Ensure the object at hand exists and is valid (actually without peeling it),
|
||||
/// without imposing any restrictions to its type.
|
||||
/// The object needs to be looked up to assure that it is valid, but it doesn't need to be decoded.
|
||||
ValidObject,
|
||||
/// Follow an annotated tag object recursively until an object is found.
|
||||
RecursiveTagObject,
|
||||
/// The path to drill into as seen relative to the current tree-ish.
|
||||
///
|
||||
/// Note that the path can be relative, and `./` and `../` prefixes are seen as relative to the current
|
||||
/// working directory.
|
||||
///
|
||||
/// The path may be empty, which makes it refer to the tree at the current revision, similar to `^{tree}`.
|
||||
/// Note that paths like `../` are valid and refer to a tree as seen relative to the current working directory.
|
||||
Path(&'a BStr),
|
||||
}
|
||||
|
||||
/// The kind of sibling branch to obtain.
|
||||
#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]
|
||||
pub enum SiblingBranch {
|
||||
/// The upstream branch as configured in `branch.<name>.remote` or `branch.<name>.merge`.
|
||||
Upstream,
|
||||
/// The upstream branch to which we would push.
|
||||
Push,
|
||||
}
|
||||
|
||||
impl SiblingBranch {
|
||||
/// Parse `input` as branch representation, if possible.
|
||||
pub fn parse(input: &BStr) -> Option<Self> {
|
||||
if input.eq_ignore_ascii_case(b"u") || input.eq_ignore_ascii_case(b"upstream") {
|
||||
SiblingBranch::Upstream.into()
|
||||
} else if input.eq_ignore_ascii_case(b"push") {
|
||||
SiblingBranch::Push.into()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
780
src-revision/src/spec/parse/function.rs
Normal file
780
src-revision/src/spec/parse/function.rs
Normal file
@@ -0,0 +1,780 @@
|
||||
use std::{str::FromStr, time::SystemTime};
|
||||
|
||||
use crate::{
|
||||
spec,
|
||||
spec::parse::{delegate, delegate::SiblingBranch, Delegate, Error},
|
||||
};
|
||||
use bstr::{BStr, BString, ByteSlice, ByteVec};
|
||||
use gix_error::{ErrorExt, Exn, ResultExt};
|
||||
|
||||
/// Parse a git [`revspec`](https://git-scm.com/docs/git-rev-parse#_specifying_revisions) and call `delegate` for each token
|
||||
/// successfully parsed.
|
||||
///
|
||||
/// Note that the `delegate` is expected to maintain enough state to lookup revisions properly.
|
||||
/// Returns `Ok(())` if all of `input` was consumed, or the error if either the `revspec` syntax was incorrect or
|
||||
/// the `delegate` failed to perform the request.
|
||||
pub fn parse(mut input: &BStr, delegate: &mut impl Delegate) -> Result<(), Exn<Error>> {
|
||||
use delegate::{Kind, Revision};
|
||||
let mut delegate = InterceptRev::new(delegate);
|
||||
let mut prev_kind = None;
|
||||
if let Some(b'^') = input.first() {
|
||||
input = next(input).1;
|
||||
let kind = spec::Kind::ExcludeReachable;
|
||||
delegate
|
||||
.kind(kind)
|
||||
.or_raise(|| Error::new(format!("delegate.kind({kind:?}) failed")))?;
|
||||
prev_kind = kind.into();
|
||||
}
|
||||
|
||||
let mut found_revision;
|
||||
(input, found_revision) = {
|
||||
let rest = revision(input, &mut delegate)?;
|
||||
(rest, rest != input)
|
||||
};
|
||||
if delegate.done {
|
||||
return if input.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::new_with_input("unconsumed input", input).raise())
|
||||
};
|
||||
}
|
||||
if let Some((rest, kind)) = try_range(input) {
|
||||
if let Some(prev_kind) = prev_kind {
|
||||
return Err(Error::new(format!(
|
||||
"cannot set spec kind more than once (was {prev_kind:?}, now {kind:?})"
|
||||
))
|
||||
.raise());
|
||||
}
|
||||
if !found_revision {
|
||||
delegate
|
||||
.find_ref("HEAD".into())
|
||||
.or_raise(|| Error::new("delegate did not find the HEAD reference"))?;
|
||||
}
|
||||
delegate
|
||||
.kind(kind)
|
||||
.or_raise(|| Error::new(format!("delegate.kind({kind:?}) failed")))?;
|
||||
(input, found_revision) = {
|
||||
let remainder = revision(rest.as_bstr(), &mut delegate)?;
|
||||
(remainder, remainder != rest)
|
||||
};
|
||||
if !found_revision {
|
||||
delegate
|
||||
.find_ref("HEAD".into())
|
||||
.or_raise(|| Error::new("delegate did not find the HEAD reference"))?;
|
||||
}
|
||||
}
|
||||
|
||||
if input.is_empty() {
|
||||
delegate
|
||||
.done()
|
||||
.or_raise(|| Error::new("No revision was produced after all input was consumed"))
|
||||
} else {
|
||||
Err(Error::new_with_input("unconsumed input", input).raise())
|
||||
}
|
||||
}
|
||||
|
||||
mod intercept {
|
||||
use crate::spec::parse::{delegate, Delegate};
|
||||
use bstr::{BStr, BString};
|
||||
use gix_error::Exn;
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]
|
||||
pub(crate) enum PrefixHintOwned {
|
||||
MustBeCommit,
|
||||
DescribeAnchor { ref_name: BString, generation: usize },
|
||||
}
|
||||
|
||||
impl PrefixHintOwned {
|
||||
pub fn to_ref(&self) -> delegate::PrefixHint<'_> {
|
||||
match self {
|
||||
PrefixHintOwned::MustBeCommit => delegate::PrefixHint::MustBeCommit,
|
||||
PrefixHintOwned::DescribeAnchor { ref_name, generation } => delegate::PrefixHint::DescribeAnchor {
|
||||
ref_name: ref_name.as_ref(),
|
||||
generation: *generation,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<delegate::PrefixHint<'a>> for PrefixHintOwned {
|
||||
fn from(v: delegate::PrefixHint<'a>) -> Self {
|
||||
match v {
|
||||
delegate::PrefixHint::MustBeCommit => PrefixHintOwned::MustBeCommit,
|
||||
delegate::PrefixHint::DescribeAnchor { generation, ref_name } => PrefixHintOwned::DescribeAnchor {
|
||||
ref_name: ref_name.to_owned(),
|
||||
generation,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct InterceptRev<'a, T> {
|
||||
pub inner: &'a mut T,
|
||||
pub last_ref: Option<BString>, // TODO: smallvec to save the unnecessary allocation? Can't keep ref due to lifetime constraints in traits
|
||||
pub last_prefix: Option<(gix_hash::Prefix, Option<PrefixHintOwned>)>,
|
||||
pub done: bool,
|
||||
}
|
||||
|
||||
impl<'a, T> InterceptRev<'a, T>
|
||||
where
|
||||
T: Delegate,
|
||||
{
|
||||
pub fn new(delegate: &'a mut T) -> Self {
|
||||
InterceptRev {
|
||||
inner: delegate,
|
||||
last_ref: None,
|
||||
last_prefix: None,
|
||||
done: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Delegate for InterceptRev<'_, T>
|
||||
where
|
||||
T: Delegate,
|
||||
{
|
||||
fn done(&mut self) -> Result<(), Exn> {
|
||||
self.done = true;
|
||||
self.inner.done()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> delegate::Revision for InterceptRev<'_, T>
|
||||
where
|
||||
T: Delegate,
|
||||
{
|
||||
fn find_ref(&mut self, name: &BStr) -> Result<(), Exn> {
|
||||
self.last_ref = name.to_owned().into();
|
||||
self.inner.find_ref(name)
|
||||
}
|
||||
|
||||
fn disambiguate_prefix(
|
||||
&mut self,
|
||||
prefix: gix_hash::Prefix,
|
||||
hint: Option<delegate::PrefixHint<'_>>,
|
||||
) -> Result<(), Exn> {
|
||||
self.last_prefix = Some((prefix, hint.map(Into::into)));
|
||||
self.inner.disambiguate_prefix(prefix, hint)
|
||||
}
|
||||
|
||||
fn reflog(&mut self, query: delegate::ReflogLookup) -> Result<(), Exn> {
|
||||
self.inner.reflog(query)
|
||||
}
|
||||
|
||||
fn nth_checked_out_branch(&mut self, branch_no: usize) -> Result<(), Exn> {
|
||||
self.inner.nth_checked_out_branch(branch_no)
|
||||
}
|
||||
|
||||
fn sibling_branch(&mut self, kind: delegate::SiblingBranch) -> Result<(), Exn> {
|
||||
self.inner.sibling_branch(kind)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> delegate::Navigate for InterceptRev<'_, T>
|
||||
where
|
||||
T: Delegate,
|
||||
{
|
||||
fn traverse(&mut self, kind: delegate::Traversal) -> Result<(), Exn> {
|
||||
self.inner.traverse(kind)
|
||||
}
|
||||
|
||||
fn peel_until(&mut self, kind: delegate::PeelTo<'_>) -> Result<(), Exn> {
|
||||
self.inner.peel_until(kind)
|
||||
}
|
||||
|
||||
fn find(&mut self, regex: &BStr, negated: bool) -> Result<(), Exn> {
|
||||
self.inner.find(regex, negated)
|
||||
}
|
||||
|
||||
fn index_lookup(&mut self, path: &BStr, stage: u8) -> Result<(), Exn> {
|
||||
self.inner.index_lookup(path, stage)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> delegate::Kind for InterceptRev<'_, T>
|
||||
where
|
||||
T: Delegate,
|
||||
{
|
||||
fn kind(&mut self, kind: crate::spec::Kind) -> Result<(), Exn> {
|
||||
self.inner.kind(kind)
|
||||
}
|
||||
}
|
||||
}
|
||||
use intercept::InterceptRev;
|
||||
|
||||
trait ResultExt2 {
|
||||
fn or_else_none<F>(self, f: F) -> Option<()>
|
||||
where
|
||||
F: FnOnce(Exn);
|
||||
}
|
||||
|
||||
impl ResultExt2 for Result<(), Exn> {
|
||||
fn or_else_none<F>(self, f: F) -> Option<()>
|
||||
where
|
||||
F: FnOnce(Exn),
|
||||
{
|
||||
match self {
|
||||
Ok(()) => Some(()),
|
||||
Err(err) => {
|
||||
f(err);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn try_set_prefix(
|
||||
delegate: &mut impl Delegate,
|
||||
hex_name: &BStr,
|
||||
hint: Option<delegate::PrefixHint<'_>>,
|
||||
errors: &mut Vec<Exn>,
|
||||
) -> Option<()> {
|
||||
gix_hash::Prefix::from_hex(hex_name.to_str().expect("hexadecimal only"))
|
||||
.ok()
|
||||
.and_then(|prefix| {
|
||||
delegate
|
||||
.disambiguate_prefix(prefix, hint)
|
||||
.or_else_none(|err| errors.push(err))
|
||||
})
|
||||
}
|
||||
|
||||
fn long_describe_prefix(name: &BStr) -> Option<(&BStr, delegate::PrefixHint<'_>)> {
|
||||
let mut iter = name.rsplit(|b| *b == b'-');
|
||||
let candidate = iter.by_ref().find_map(|substr| {
|
||||
if substr.first()? != &b'g' {
|
||||
return None;
|
||||
}
|
||||
let rest = substr.get(1..)?;
|
||||
rest.iter().all(u8::is_ascii_hexdigit).then(|| rest.as_bstr())
|
||||
})?;
|
||||
|
||||
let candidate = iter.clone().any(|token| !token.is_empty()).then_some(candidate);
|
||||
let hint = iter
|
||||
.next()
|
||||
.and_then(|gen| gen.to_str().ok().and_then(|gen| usize::from_str(gen).ok()))
|
||||
.and_then(|generation| {
|
||||
iter.next().map(|token| {
|
||||
let last_token_len = token.len();
|
||||
let first_token_ptr = iter.next_back().map_or(token.as_ptr(), <[_]>::as_ptr);
|
||||
// SAFETY: both pointers are definitely part of the same object
|
||||
#[allow(unsafe_code)]
|
||||
let prior_tokens_len: usize = unsafe { token.as_ptr().offset_from(first_token_ptr) }
|
||||
.try_into()
|
||||
.expect("positive value");
|
||||
delegate::PrefixHint::DescribeAnchor {
|
||||
ref_name: name[..prior_tokens_len + last_token_len].as_bstr(),
|
||||
generation,
|
||||
}
|
||||
})
|
||||
})
|
||||
.unwrap_or(delegate::PrefixHint::MustBeCommit);
|
||||
|
||||
candidate.map(|c| (c, hint))
|
||||
}
|
||||
|
||||
fn short_describe_prefix(name: &BStr) -> Option<&BStr> {
|
||||
let mut iter = name.split(|b| *b == b'-');
|
||||
let candidate = iter
|
||||
.next()
|
||||
.and_then(|prefix| prefix.iter().all(u8::is_ascii_hexdigit).then(|| prefix.as_bstr()));
|
||||
(iter.count() == 1).then_some(candidate).flatten()
|
||||
}
|
||||
|
||||
type InsideParensRestConsumed<'a> = (std::borrow::Cow<'a, BStr>, &'a BStr, usize);
|
||||
fn parens(input: &[u8]) -> Result<Option<InsideParensRestConsumed<'_>>, Error> {
|
||||
if input.first() != Some(&b'{') {
|
||||
return Ok(None);
|
||||
}
|
||||
let mut open_braces = 0;
|
||||
let mut ignore_next = false;
|
||||
let mut skip_list = Vec::new();
|
||||
for (idx, b) in input.iter().enumerate() {
|
||||
match *b {
|
||||
b'{' => {
|
||||
if ignore_next {
|
||||
ignore_next = false;
|
||||
} else {
|
||||
open_braces += 1;
|
||||
}
|
||||
}
|
||||
b'}' => {
|
||||
if ignore_next {
|
||||
ignore_next = false;
|
||||
} else {
|
||||
open_braces -= 1;
|
||||
}
|
||||
}
|
||||
b'\\' => {
|
||||
skip_list.push(idx);
|
||||
if ignore_next {
|
||||
skip_list.pop();
|
||||
ignore_next = false;
|
||||
} else {
|
||||
ignore_next = true;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if ignore_next {
|
||||
skip_list.pop();
|
||||
}
|
||||
ignore_next = false;
|
||||
}
|
||||
}
|
||||
if open_braces == 0 {
|
||||
let inner: std::borrow::Cow<'_, _> = if skip_list.is_empty() {
|
||||
input[1..idx].as_bstr().into()
|
||||
} else {
|
||||
let mut from = 1;
|
||||
let mut buf = BString::default();
|
||||
for next in skip_list.into_iter() {
|
||||
buf.push_str(&input[from..next]);
|
||||
from = next + 1;
|
||||
}
|
||||
if let Some(rest) = input.get(from..idx) {
|
||||
buf.push_str(rest);
|
||||
}
|
||||
buf.into()
|
||||
};
|
||||
return Ok(Some((inner, input[idx + 1..].as_bstr(), idx + 1)));
|
||||
}
|
||||
}
|
||||
Err(Error::new_with_input("unclosed brace pair", input))
|
||||
}
|
||||
|
||||
fn try_parse<T: FromStr + PartialEq + Default>(input: &BStr) -> Result<Option<T>, Error> {
|
||||
input
|
||||
.to_str()
|
||||
.ok()
|
||||
.and_then(|n| {
|
||||
n.parse().ok().map(|n| {
|
||||
if n == T::default() && input[0] == b'-' {
|
||||
return Err(Error::new_with_input(
|
||||
"negative zero is invalid - remove the minus sign",
|
||||
input,
|
||||
));
|
||||
}
|
||||
Ok(n)
|
||||
})
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
|
||||
fn revision<'a, T>(mut input: &'a BStr, delegate: &mut InterceptRev<'_, T>) -> Result<&'a BStr, Exn<Error>>
|
||||
where
|
||||
T: Delegate,
|
||||
{
|
||||
use delegate::{Navigate, Revision};
|
||||
fn consume_all(res: Result<(), Exn>, err: impl FnOnce() -> String) -> Result<&'static BStr, Exn<Error>> {
|
||||
res.map(|_| "".into()).or_raise(|| Error::new(err()))
|
||||
}
|
||||
match input.as_bytes() {
|
||||
[b':'] => {
|
||||
return Err(
|
||||
Error::new("':' must be followed by either slash and regex or path to lookup in HEAD tree").raise(),
|
||||
)
|
||||
}
|
||||
[b':', b'/'] => return Err(Error::new("':/' must be followed by a regular expression").raise()),
|
||||
[b':', b'/', regex @ ..] => {
|
||||
let (regex, negated) = parse_regex_prefix(regex.as_bstr())?;
|
||||
if regex.is_empty() {
|
||||
return Err(Error::new_with_input("unconsumed input", input).raise());
|
||||
}
|
||||
return consume_all(delegate.find(regex, negated), || {
|
||||
format!("Delegate couldn't find '{regex}' (negated: {negated})")
|
||||
});
|
||||
}
|
||||
[b':', b'0', b':', path @ ..] => {
|
||||
return consume_all(delegate.index_lookup(path.as_bstr(), 0), || {
|
||||
format!("Couldn't find index '{path}' stage 0", path = path.as_bstr())
|
||||
})
|
||||
}
|
||||
[b':', b'1', b':', path @ ..] => {
|
||||
return consume_all(delegate.index_lookup(path.as_bstr(), 1), || {
|
||||
format!("Couldn't find index '{path}' stage 1", path = path.as_bstr())
|
||||
})
|
||||
}
|
||||
[b':', b'2', b':', path @ ..] => {
|
||||
return consume_all(delegate.index_lookup(path.as_bstr(), 2), || {
|
||||
format!("Couldn't find index '{path}' stage 2", path = path.as_bstr())
|
||||
})
|
||||
}
|
||||
[b':', path @ ..] => {
|
||||
return consume_all(delegate.index_lookup(path.as_bstr(), 0), || {
|
||||
format!("Couldn't find index '{path}' stage 0 (implicit)", path = path.as_bstr())
|
||||
})
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let mut sep_pos = None;
|
||||
let mut consecutive_hex_chars = Some(0);
|
||||
{
|
||||
let mut cursor = input;
|
||||
let mut ofs = 0;
|
||||
const SEPARATORS: &[u8] = b"~^:.";
|
||||
while let Some((pos, b)) = cursor.iter().enumerate().find(|(pos, b)| {
|
||||
if **b == b'@' {
|
||||
if cursor.len() == 1 {
|
||||
return true;
|
||||
}
|
||||
let next = cursor.get(pos + 1);
|
||||
let next_next = cursor.get(pos + 2);
|
||||
if *pos != 0 && (next, next_next) == (Some(&b'.'), Some(&b'.')) {
|
||||
return false;
|
||||
}
|
||||
next == Some(&b'{') || next.is_some_and(|b| SEPARATORS.contains(b))
|
||||
} else if SEPARATORS.contains(b) {
|
||||
true
|
||||
} else {
|
||||
if let Some(num) = consecutive_hex_chars.as_mut() {
|
||||
if b.is_ascii_hexdigit() {
|
||||
*num += 1;
|
||||
} else {
|
||||
consecutive_hex_chars = None;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}) {
|
||||
if *b != b'.' || cursor.get(pos + 1) == Some(&b'.') {
|
||||
sep_pos = Some(ofs + pos);
|
||||
break;
|
||||
}
|
||||
ofs += pos + 1;
|
||||
cursor = &cursor[pos + 1..];
|
||||
}
|
||||
}
|
||||
|
||||
let name = &input[..sep_pos.unwrap_or(input.len())].as_bstr();
|
||||
let mut sep = sep_pos.map(|pos| input[pos]);
|
||||
let mut has_ref_or_implied_name = name.is_empty();
|
||||
if name.is_empty() && sep == Some(b'@') && sep_pos.and_then(|pos| input.get(pos + 1)) != Some(&b'{') {
|
||||
delegate
|
||||
.find_ref("HEAD".into())
|
||||
.or_raise(|| Error::new("delegate did not find the HEAD reference"))?;
|
||||
sep_pos = sep_pos.map(|pos| pos + 1);
|
||||
sep = match sep_pos.and_then(|pos| input.get(pos).copied()) {
|
||||
None => return Ok("".into()),
|
||||
Some(pos) => Some(pos),
|
||||
};
|
||||
} else {
|
||||
let mut errors = Vec::new();
|
||||
(consecutive_hex_chars.unwrap_or(0) >= gix_hash::Prefix::MIN_HEX_LEN)
|
||||
.then(|| try_set_prefix(delegate, name, None, &mut errors))
|
||||
.flatten()
|
||||
.or_else(|| {
|
||||
let (prefix, hint) = long_describe_prefix(name)
|
||||
.map(|(c, h)| (c, Some(h)))
|
||||
.or_else(|| short_describe_prefix(name).map(|c| (c, None)))?;
|
||||
try_set_prefix(delegate, prefix, hint, &mut errors)
|
||||
})
|
||||
.or_else(|| {
|
||||
name.is_empty().then_some(()).or_else(|| {
|
||||
#[allow(clippy::let_unit_value)]
|
||||
{
|
||||
let res = delegate.find_ref(name).or_else_none(|err| {
|
||||
errors.push(err);
|
||||
})?;
|
||||
has_ref_or_implied_name = true;
|
||||
res.into()
|
||||
}
|
||||
})
|
||||
})
|
||||
.ok_or_else(|| Error::new_with_input("couldn't parse revision", input).raise_all(errors))?;
|
||||
}
|
||||
|
||||
input = {
|
||||
if let Some(b'@') = sep {
|
||||
let past_sep = input[sep_pos.map_or(input.len(), |pos| pos + 1)..].as_bstr();
|
||||
let (nav, rest, _consumed) = parens(past_sep)?.ok_or_else(|| {
|
||||
Error::new_with_input(
|
||||
"@ character must be standalone or followed by {<content>}",
|
||||
&input[sep_pos.unwrap_or(input.len())..],
|
||||
)
|
||||
})?;
|
||||
let nav = nav.as_ref();
|
||||
if let Some(n) = try_parse::<isize>(nav)? {
|
||||
if n < 0 {
|
||||
if name.is_empty() {
|
||||
delegate.nth_checked_out_branch(n.unsigned_abs()).or_raise(|| {
|
||||
Error::new_with_input(
|
||||
format!("delegate.nth_checked_out_branch({n:?}) didn't find a branch"),
|
||||
nav,
|
||||
)
|
||||
})?;
|
||||
} else {
|
||||
return Err(Error::new_with_input(
|
||||
"reference name must be followed by positive numbers in @{n}",
|
||||
nav,
|
||||
)
|
||||
.raise());
|
||||
}
|
||||
} else if has_ref_or_implied_name {
|
||||
let lookup = if n >= 100000000 {
|
||||
let time = nav
|
||||
.to_str()
|
||||
.or_raise(|| Error::new_with_input("could not parse time for reflog lookup", nav))
|
||||
.and_then(|date| {
|
||||
gix_date::parse(date, None)
|
||||
.or_raise(|| Error::new_with_input("could not parse time for reflog lookup", nav))
|
||||
})?;
|
||||
delegate::ReflogLookup::Date(time)
|
||||
} else {
|
||||
delegate::ReflogLookup::Entry(n.try_into().expect("non-negative isize fits usize"))
|
||||
};
|
||||
delegate
|
||||
.reflog(lookup)
|
||||
.or_raise(|| Error::new_with_input(format!("delegate.reflog({lookup:?}) failed"), nav))?;
|
||||
} else {
|
||||
return Err(Error::new_with_input("reflog entries require a ref name", *name).raise());
|
||||
}
|
||||
} else if let Some(kind) = SiblingBranch::parse(nav) {
|
||||
if has_ref_or_implied_name {
|
||||
delegate
|
||||
.sibling_branch(kind)
|
||||
.or_raise(|| Error::new_with_input(format!("delegate.sibling_branch({kind:?}) failed"), nav))
|
||||
} else {
|
||||
Err(Error::new_with_input(
|
||||
"sibling branches like 'upstream' or 'push' require a branch name with remote configuration",
|
||||
*name,
|
||||
)
|
||||
.raise())
|
||||
}?;
|
||||
} else if has_ref_or_implied_name {
|
||||
let time = nav
|
||||
.to_str()
|
||||
.map_err(|_| Error::new_with_input("could not parse time for reflog lookup", nav))
|
||||
.and_then(|date| {
|
||||
gix_date::parse(date, Some(SystemTime::now()))
|
||||
.map_err(|_| Error::new_with_input("could not parse time for reflog lookup", nav))
|
||||
})?;
|
||||
let lookup = delegate::ReflogLookup::Date(time);
|
||||
delegate
|
||||
.reflog(lookup)
|
||||
.or_raise(|| Error::new_with_input(format!("delegate.reflog({lookup:?}) failed"), nav))?;
|
||||
} else {
|
||||
return Err(Error::new_with_input("reflog entries require a ref name", *name).raise());
|
||||
}
|
||||
rest
|
||||
} else {
|
||||
if sep_pos == Some(0) && sep == Some(b'~') {
|
||||
return Err(Error::new("tilde needs to follow an anchor, like @~").raise());
|
||||
}
|
||||
input[sep_pos.unwrap_or(input.len())..].as_bstr()
|
||||
}
|
||||
};
|
||||
|
||||
navigate(input, delegate)
|
||||
}
|
||||
|
||||
fn navigate<'a, T>(input: &'a BStr, delegate: &mut InterceptRev<'_, T>) -> Result<&'a BStr, Exn<Error>>
|
||||
where
|
||||
T: Delegate,
|
||||
{
|
||||
use delegate::{Kind, Navigate, Revision};
|
||||
let mut cursor = 0;
|
||||
let done_msg = "navigation succeeded, but no revision was produced as intermediate step";
|
||||
while let Some(b) = input.get(cursor) {
|
||||
cursor += 1;
|
||||
match *b {
|
||||
b'~' => {
|
||||
let (number, consumed) = input
|
||||
.get(cursor..)
|
||||
.and_then(|past_sep| try_parse_usize(past_sep.as_bstr()).transpose())
|
||||
.transpose()?
|
||||
.unwrap_or((1, 0));
|
||||
if number != 0 {
|
||||
let traversal = delegate::Traversal::NthAncestor(number);
|
||||
delegate.traverse(traversal).or_raise(|| {
|
||||
Error::new_with_input(format!("delegate.traverse({traversal:?}) failed"), input)
|
||||
})?;
|
||||
}
|
||||
cursor += consumed;
|
||||
}
|
||||
b'^' => {
|
||||
let past_sep = input.get(cursor..);
|
||||
if let Some((number, negative, consumed)) = past_sep
|
||||
.and_then(|past_sep| try_parse_isize(past_sep.as_bstr()).transpose())
|
||||
.transpose()?
|
||||
{
|
||||
if negative {
|
||||
let traversal = delegate::Traversal::NthParent(
|
||||
number
|
||||
.checked_mul(-1)
|
||||
.ok_or_else(|| {
|
||||
Error::new_with_input("could not parse number", past_sep.expect("present"))
|
||||
})?
|
||||
.try_into()
|
||||
.expect("non-negative"),
|
||||
);
|
||||
delegate.traverse(traversal).or_raise(|| {
|
||||
Error::new_with_input(
|
||||
"delegate.traverse({traversal:?}) failed",
|
||||
past_sep.unwrap_or_default(),
|
||||
)
|
||||
})?;
|
||||
let kind = spec::Kind::RangeBetween;
|
||||
delegate.kind(kind).or_raise(|| {
|
||||
Error::new_with_input(
|
||||
format!("delegate.kind({kind:?}) failed"),
|
||||
past_sep.unwrap_or_default(),
|
||||
)
|
||||
})?;
|
||||
if let Some((prefix, hint)) = delegate.last_prefix.take() {
|
||||
match &hint {
|
||||
Some(hint) => delegate.disambiguate_prefix(prefix, hint.to_ref().into()),
|
||||
None => delegate.disambiguate_prefix(prefix, None),
|
||||
}
|
||||
.or_raise(|| {
|
||||
Error::new_with_input(
|
||||
format!("delegate.disambiguate_prefix({hint:?}) failed"),
|
||||
past_sep.unwrap_or_default(),
|
||||
)
|
||||
})?;
|
||||
} else if let Some(name) = delegate.last_ref.take() {
|
||||
delegate.find_ref(name.as_bstr()).or_raise(|| {
|
||||
Error::new_with_input(
|
||||
format!("delegate.find_ref({name}) failed"),
|
||||
past_sep.unwrap_or_default(),
|
||||
)
|
||||
})?;
|
||||
} else {
|
||||
return Err(Error::new_with_input("unconsumed input", &input[cursor..]).raise());
|
||||
}
|
||||
cursor += consumed;
|
||||
let rest = input[cursor..].as_bstr();
|
||||
delegate.done().or_raise(|| Error::new_with_input(done_msg, rest))?;
|
||||
return Ok(rest);
|
||||
} else if number == 0 {
|
||||
delegate.peel_until(delegate::PeelTo::ObjectKind(gix_object::Kind::Commit))
|
||||
} else {
|
||||
delegate.traverse(delegate::Traversal::NthParent(
|
||||
number.try_into().expect("positive number"),
|
||||
))
|
||||
}
|
||||
.or_raise(|| Error::new_with_input("unknown navigation", past_sep.unwrap_or_default()))?;
|
||||
cursor += consumed;
|
||||
} else if let Some((kind, _rest, consumed)) =
|
||||
past_sep.and_then(|past_sep| parens(past_sep).transpose()).transpose()?
|
||||
{
|
||||
cursor += consumed;
|
||||
let target = match kind.as_ref().as_bytes() {
|
||||
b"commit" => delegate::PeelTo::ObjectKind(gix_object::Kind::Commit),
|
||||
b"tag" => delegate::PeelTo::ObjectKind(gix_object::Kind::Tag),
|
||||
b"tree" => delegate::PeelTo::ObjectKind(gix_object::Kind::Tree),
|
||||
b"blob" => delegate::PeelTo::ObjectKind(gix_object::Kind::Blob),
|
||||
b"object" => delegate::PeelTo::ValidObject,
|
||||
b"" => delegate::PeelTo::RecursiveTagObject,
|
||||
regex if regex.starts_with(b"/") => {
|
||||
let (regex, negated) = parse_regex_prefix(regex[1..].as_bstr())?;
|
||||
if !regex.is_empty() {
|
||||
delegate.find(regex, negated).or_raise(|| {
|
||||
Error::new(format!("Delegate couldn't find '{regex}' (negated: {negated})"))
|
||||
})?;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
invalid => return Err(Error::new_with_input("cannot peel to unknown target", invalid).raise()),
|
||||
};
|
||||
delegate.peel_until(target).or_raise(|| {
|
||||
Error::new_with_input(
|
||||
format!("delegate.peel_until({target:?}) failed"),
|
||||
past_sep.unwrap_or_default(),
|
||||
)
|
||||
})?;
|
||||
} else if past_sep.and_then(<[_]>::first) == Some(&b'!') {
|
||||
let rest = input[cursor + 1..].as_bstr();
|
||||
let kind = spec::Kind::ExcludeReachableFromParents;
|
||||
delegate
|
||||
.kind(kind)
|
||||
.or_raise(|| Error::new_with_input(format!("delegate.kind({kind:?}) failed"), rest))?;
|
||||
delegate.done().or_raise(|| Error::new_with_input(done_msg, rest))?;
|
||||
return Ok(rest);
|
||||
} else if past_sep.and_then(<[_]>::first) == Some(&b'@') {
|
||||
let rest = input[cursor + 1..].as_bstr();
|
||||
let kind = spec::Kind::IncludeReachableFromParents;
|
||||
delegate
|
||||
.kind(kind)
|
||||
.or_raise(|| Error::new_with_input(format!("delegate.kind({kind:?}) failed"), rest))?;
|
||||
delegate.done().or_raise(|| Error::new_with_input(done_msg, rest))?;
|
||||
return Ok(rest);
|
||||
} else {
|
||||
let parent = delegate::Traversal::NthParent(1);
|
||||
delegate.traverse(parent).or_raise(|| {
|
||||
Error::new_with_input(
|
||||
format!("delegate.parent({parent:?}) failed",),
|
||||
past_sep.unwrap_or_default(),
|
||||
)
|
||||
})?;
|
||||
}
|
||||
}
|
||||
b':' => {
|
||||
let to = delegate::PeelTo::Path(input[cursor..].as_bstr());
|
||||
delegate
|
||||
.peel_until(to)
|
||||
.or_raise(|| Error::new(format!("delegate.peel_until({to:?}) failed")))?;
|
||||
return Ok("".into());
|
||||
}
|
||||
_ => return Ok(input[cursor - 1..].as_bstr()),
|
||||
}
|
||||
}
|
||||
Ok("".into())
|
||||
}
|
||||
|
||||
fn parse_regex_prefix(regex: &BStr) -> Result<(&BStr, bool), Error> {
|
||||
Ok(match regex.strip_prefix(b"!") {
|
||||
Some(regex) if regex.first() == Some(&b'!') => (regex.as_bstr(), false),
|
||||
Some(regex) if regex.first() == Some(&b'-') => (regex[1..].as_bstr(), true),
|
||||
Some(_regex) => return Err(Error::new_with_input("need one character after /!, typically -", regex)),
|
||||
None => (regex, false),
|
||||
})
|
||||
}
|
||||
|
||||
fn try_parse_usize(input: &BStr) -> Result<Option<(usize, usize)>, Error> {
|
||||
let mut bytes = input.iter().peekable();
|
||||
if bytes.peek().filter(|&&&b| b == b'-' || b == b'+').is_some() {
|
||||
return Err(Error::new_with_input(
|
||||
"negative or explicitly positive numbers are invalid here",
|
||||
input,
|
||||
));
|
||||
}
|
||||
let num_digits = bytes.take_while(|b| b.is_ascii_digit()).count();
|
||||
if num_digits == 0 {
|
||||
return Ok(None);
|
||||
}
|
||||
let input = &input[..num_digits];
|
||||
let number = try_parse(input)?.ok_or_else(|| Error::new_with_input("could not parse number", input))?;
|
||||
Ok(Some((number, num_digits)))
|
||||
}
|
||||
|
||||
fn try_parse_isize(input: &BStr) -> Result<Option<(isize, bool, usize)>, Error> {
|
||||
let mut bytes = input.iter().peekable();
|
||||
if bytes.peek().filter(|&&&b| b == b'+').is_some() {
|
||||
return Err(Error::new_with_input(
|
||||
"explicitly positive numbers are invalid here",
|
||||
input,
|
||||
));
|
||||
}
|
||||
let negative = bytes.peek() == Some(&&b'-');
|
||||
let num_digits = bytes.take_while(|b| b.is_ascii_digit() || *b == &b'-').count();
|
||||
if num_digits == 0 {
|
||||
return Ok(None);
|
||||
} else if num_digits == 1 && negative {
|
||||
return Ok(Some((-1, negative, num_digits)));
|
||||
}
|
||||
let input = &input[..num_digits];
|
||||
let number = try_parse(input)?.ok_or_else(|| Error::new_with_input("could not parse number", input))?;
|
||||
Ok(Some((number, negative, num_digits)))
|
||||
}
|
||||
|
||||
fn try_range(input: &BStr) -> Option<(&[u8], spec::Kind)> {
|
||||
input
|
||||
.strip_prefix(b"...")
|
||||
.map(|rest| (rest, spec::Kind::ReachableToMergeBase))
|
||||
.or_else(|| input.strip_prefix(b"..").map(|rest| (rest, spec::Kind::RangeBetween)))
|
||||
}
|
||||
|
||||
fn next(i: &BStr) -> (u8, &BStr) {
|
||||
let b = i[0];
|
||||
(b, i[1..].as_bstr())
|
||||
}
|
||||
21
src-revision/src/spec/parse/mod.rs
Normal file
21
src-revision/src/spec/parse/mod.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use gix_error::Exn;
|
||||
/// The error returned by [`spec::parse()`](crate::spec::parse()).
|
||||
pub use gix_error::ValidationError as Error;
|
||||
|
||||
///
|
||||
pub mod delegate;
|
||||
|
||||
/// A delegate to be informed about parse events, with methods split into categories.
|
||||
///
|
||||
/// - **Anchors** - which revision to use as starting point for…
|
||||
/// - **Navigation** - where to go once from the initial revision
|
||||
/// - **Range** - to learn if the specification is for a single or multiple references, and how to combine them.
|
||||
pub trait Delegate: delegate::Revision + delegate::Navigate + delegate::Kind {
|
||||
/// Called at the end of a successful parsing operation.
|
||||
/// It can be used as a marker to finalize internal data structures.
|
||||
///
|
||||
/// Note that it will not be called if there is unconsumed input.
|
||||
fn done(&mut self) -> Result<(), Exn>;
|
||||
}
|
||||
|
||||
pub(crate) mod function;
|
||||
BIN
src-revision/tests/fixtures/generated-archives/make_merge_base_repos.tar
vendored
Normal file
BIN
src-revision/tests/fixtures/generated-archives/make_merge_base_repos.tar
vendored
Normal file
Binary file not shown.
BIN
src-revision/tests/fixtures/generated-archives/make_repo_with_branches.tar
vendored
Normal file
BIN
src-revision/tests/fixtures/generated-archives/make_repo_with_branches.tar
vendored
Normal file
Binary file not shown.
BIN
src-revision/tests/fixtures/generated-archives/merge_base_octopus_repos.tar
vendored
Normal file
BIN
src-revision/tests/fixtures/generated-archives/merge_base_octopus_repos.tar
vendored
Normal file
Binary file not shown.
222
src-revision/tests/fixtures/make_merge_base_repos.sh
Executable file
222
src-revision/tests/fixtures/make_merge_base_repos.sh
Executable file
@@ -0,0 +1,222 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eu -o pipefail
|
||||
|
||||
git init
|
||||
|
||||
EMPTY_TREE=$(git mktree </dev/null)
|
||||
function ofs_commit () {
|
||||
local OFFSET_SECONDS=$1
|
||||
local COMMIT_NAME=$2
|
||||
shift 2
|
||||
|
||||
PARENTS=
|
||||
for P; do
|
||||
PARENTS="${PARENTS}-p $P "
|
||||
done
|
||||
|
||||
GIT_COMMITTER_DATE="$((400403349 + OFFSET_SECONDS)) +0000"
|
||||
GIT_AUTHOR_DATE=$GIT_COMMITTER_DATE
|
||||
export GIT_COMMITTER_DATE GIT_AUTHOR_DATE
|
||||
|
||||
commit=$(echo $COMMIT_NAME | git commit-tree $EMPTY_TREE ${PARENTS:-})
|
||||
|
||||
git update-ref "refs/tags/$COMMIT_NAME" "$commit"
|
||||
echo $commit
|
||||
}
|
||||
|
||||
function baseline() {
|
||||
echo "$@"
|
||||
echo $(git rev-parse "$@")
|
||||
git merge-base --all "$@" || :
|
||||
echo
|
||||
}
|
||||
|
||||
# Merge-bases adapted from Git test suite
|
||||
# No merge base
|
||||
ofs_commit 0 DA
|
||||
ofs_commit 100 DB
|
||||
{
|
||||
echo "just-one-returns-one-in-code"
|
||||
echo $(git rev-parse DA)
|
||||
echo $(git rev-parse DA)
|
||||
echo
|
||||
baseline DA DB
|
||||
baseline DA DA DB
|
||||
} > 1_disjoint.baseline
|
||||
|
||||
# A graph that is purposefully using times that can't be trusted, i.e. the root E
|
||||
# has a higher time than its future commits, so that it would be preferred
|
||||
# unless if there was an additional pruning step to deal with this case.
|
||||
# E---D---C---B---A
|
||||
# \"-_ \ \
|
||||
# \ `---------G \
|
||||
# \ \
|
||||
# F----------------H
|
||||
E=$(ofs_commit 5 E)
|
||||
D=$(ofs_commit 4 D $E)
|
||||
F=$(ofs_commit 6 F $E)
|
||||
C=$(ofs_commit 3 C $D)
|
||||
B=$(ofs_commit 2 B $C)
|
||||
A=$(ofs_commit 1 A $B)
|
||||
G=$(ofs_commit 7 G $B $E)
|
||||
H=$(ofs_commit 8 H $A $F)
|
||||
|
||||
{
|
||||
baseline G H
|
||||
} > 2_a.baseline
|
||||
|
||||
# Permutation testing - let's do it early to avoid too many permutations
|
||||
commits=$(git log --all --format=%s)
|
||||
commit_array=($commits)
|
||||
num_commits=${#commit_array[@]}
|
||||
|
||||
for ((i=0; i<num_commits; i++)); do
|
||||
for ((j=0; j<num_commits; j++)); do
|
||||
baseline ${commit_array[$i]} ${commit_array[$j]}
|
||||
done
|
||||
done > 3_permutations.baseline
|
||||
|
||||
# Timestamps cannot be trusted.
|
||||
#
|
||||
# Relative
|
||||
# Structure timestamps
|
||||
#
|
||||
# PL PR +4 +4
|
||||
# / \/ \ / \/ \
|
||||
# L2 C2 R2 +3 -1 +3
|
||||
# | | | | | |
|
||||
# L1 C1 R1 +2 -2 +2
|
||||
# | | | | | |
|
||||
# L0 C0 R0 +1 -3 +1
|
||||
# \ | / \ | /
|
||||
# S 0
|
||||
#
|
||||
# The left and right chains of commits can be of any length and complexity as
|
||||
# long as all of the timestamps are greater than that of S.
|
||||
S=$(ofs_commit 0 S)
|
||||
|
||||
C0=$(ofs_commit -3 C0 $S)
|
||||
C1=$(ofs_commit -2 C1 $C0)
|
||||
C2=$(ofs_commit -1 C2 $C1)
|
||||
|
||||
L0=$(ofs_commit 1 L0 $S)
|
||||
L1=$(ofs_commit 2 L1 $L0)
|
||||
L2=$(ofs_commit 3 L2 $L1)
|
||||
|
||||
R0=$(ofs_commit 1 R0 $S)
|
||||
R1=$(ofs_commit 2 R1 $R0)
|
||||
R2=$(ofs_commit 3 R2 $R1)
|
||||
|
||||
PL=$(ofs_commit 4 PL $L2 $C2)
|
||||
PR=$(ofs_commit 4 PR $C2 $R2)
|
||||
|
||||
{
|
||||
baseline PL PR
|
||||
} > 4_b.baseline
|
||||
|
||||
|
||||
function tick () {
|
||||
if test -z "${tick+set}"
|
||||
then
|
||||
tick=1112911993
|
||||
else
|
||||
tick=$(($tick + 60))
|
||||
fi
|
||||
GIT_COMMITTER_DATE="$tick -0700"
|
||||
GIT_AUTHOR_DATE="$tick -0700"
|
||||
export GIT_COMMITTER_DATE GIT_AUTHOR_DATE
|
||||
}
|
||||
|
||||
tick
|
||||
function commit() {
|
||||
local message=${1:?first argument is the commit message}
|
||||
local date=${2:-}
|
||||
if [ -n "$date" ]; then
|
||||
export GIT_COMMITTER_DATE="$date"
|
||||
else
|
||||
tick
|
||||
fi
|
||||
git commit --allow-empty -m "$message"
|
||||
git tag "$message"
|
||||
}
|
||||
|
||||
# * C (MMC) * B (MMB) * A (MMA)
|
||||
# * o * o * o
|
||||
# * o * o * o
|
||||
# * o * o * o
|
||||
# * o | _______/
|
||||
# | |/
|
||||
# | * 1 (MM1)
|
||||
# | _______/
|
||||
# |/
|
||||
# * root (MMR)
|
||||
|
||||
commit MMR
|
||||
commit MM1
|
||||
commit MM-o
|
||||
commit MM-p
|
||||
commit MM-q
|
||||
commit MMA
|
||||
git checkout MM1
|
||||
commit MM-r
|
||||
commit MM-s
|
||||
commit MM-t
|
||||
commit MMB
|
||||
git checkout MMR
|
||||
commit MM-u
|
||||
commit MM-v
|
||||
commit MM-w
|
||||
commit MM-x
|
||||
commit MMC
|
||||
|
||||
{
|
||||
baseline MMA MMB MMC
|
||||
} > 5_c.baseline
|
||||
|
||||
merge () {
|
||||
label="$1"
|
||||
shift
|
||||
tick
|
||||
git merge -m "$label" "$@"
|
||||
git tag "$label"
|
||||
}
|
||||
|
||||
# JE
|
||||
# / |
|
||||
# / |
|
||||
# / |
|
||||
# JAA / |
|
||||
# |\ / |
|
||||
# | \ | JDD |
|
||||
# | \ |/ | |
|
||||
# | JC JD |
|
||||
# | | /| |
|
||||
# | |/ | |
|
||||
# JA | | |
|
||||
# |\ /| | |
|
||||
# X JB | X X
|
||||
# \ \ | / /
|
||||
# \__\|/___/
|
||||
# J
|
||||
commit J
|
||||
commit JB
|
||||
git reset --hard J
|
||||
commit JC
|
||||
git reset --hard J
|
||||
commit JTEMP1
|
||||
merge JA JB
|
||||
merge JAA JC
|
||||
git reset --hard J
|
||||
commit JTEMP2
|
||||
merge JD JB
|
||||
merge JDD JC
|
||||
git reset --hard J
|
||||
commit JTEMP3
|
||||
merge JE JC
|
||||
|
||||
{
|
||||
baseline JAA JDD JE
|
||||
} > 5_c.baseline
|
||||
|
||||
git commit-graph write --no-progress --reachable
|
||||
git repack -adq
|
||||
28
src-revision/tests/fixtures/make_repo_with_branches.sh
Executable file
28
src-revision/tests/fixtures/make_repo_with_branches.sh
Executable file
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eu -o pipefail
|
||||
|
||||
git init -q
|
||||
git config merge.ff false
|
||||
|
||||
git checkout -q -b main
|
||||
git commit -q --allow-empty -m c1
|
||||
git tag at-c1
|
||||
git commit -q --allow-empty -m c2
|
||||
git commit -q --allow-empty -m c3
|
||||
git commit -q --allow-empty -m c4
|
||||
|
||||
git checkout -q -b branch1
|
||||
git commit -q --allow-empty -m b1c1
|
||||
git tag at-b1c1
|
||||
git commit -q --allow-empty -m b1c2
|
||||
|
||||
git checkout -q main
|
||||
git commit -q --allow-empty -m c5
|
||||
git tag at-c5
|
||||
git merge branch1 -m m1b1
|
||||
|
||||
git commit-graph write --no-progress --reachable
|
||||
git repack -adq
|
||||
|
||||
git clone --depth 1 file://$PWD shallow-1-clone
|
||||
git clone --depth 2 file://$PWD shallow-2-clone
|
||||
43
src-revision/tests/fixtures/merge_base_octopus_repos.sh
Executable file
43
src-revision/tests/fixtures/merge_base_octopus_repos.sh
Executable file
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eu -o pipefail
|
||||
|
||||
git init three-sequential-commits
|
||||
(cd three-sequential-commits
|
||||
git commit -m "A" --allow-empty
|
||||
git commit -m "B" --allow-empty
|
||||
git commit -m "C" --allow-empty
|
||||
)
|
||||
|
||||
git init three-parallel-commits
|
||||
(cd three-parallel-commits
|
||||
git commit -m "BASE" --allow-empty
|
||||
git branch A
|
||||
git branch B
|
||||
git branch C
|
||||
|
||||
git checkout A
|
||||
git commit -m "A" --allow-empty
|
||||
|
||||
git checkout B
|
||||
git commit -m "B" --allow-empty
|
||||
|
||||
git checkout C
|
||||
git commit -m "C" --allow-empty
|
||||
)
|
||||
|
||||
git init three-forked-commits
|
||||
(cd three-forked-commits
|
||||
git commit -m "BASE" --allow-empty
|
||||
git branch A
|
||||
|
||||
git checkout -b C
|
||||
git commit -m "C" --allow-empty
|
||||
|
||||
git checkout A
|
||||
git commit -m "A-1" --allow-empty
|
||||
git branch B
|
||||
git commit -m "A-2" --allow-empty
|
||||
|
||||
git checkout B
|
||||
git commit -m "B" --allow-empty
|
||||
)
|
||||
56
src-revision/tests/revision/describe/format.rs
Normal file
56
src-revision/tests/revision/describe/format.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use gix_object::bstr::ByteSlice;
|
||||
use gix_revision::describe;
|
||||
|
||||
use crate::hex_to_id;
|
||||
|
||||
#[test]
|
||||
fn exact_match_with_dirty_and_long() {
|
||||
let mut format = describe::Outcome {
|
||||
name: Some(Cow::Borrowed(b"main".as_bstr())),
|
||||
id: hex_to_id("b920bbb055e1efb9080592a409d3975738b6efb3"),
|
||||
depth: 0,
|
||||
name_by_oid: Default::default(),
|
||||
commits_seen: 0,
|
||||
}
|
||||
.into_format(7);
|
||||
assert!(format.is_exact_match());
|
||||
assert_eq!(format.to_string(), "main");
|
||||
assert_eq!(format.long(true).to_string(), "main-0-gb920bbb");
|
||||
|
||||
format.dirty_suffix = Some("dirty".into());
|
||||
assert_eq!(format.long(false).to_string(), "main-dirty");
|
||||
assert_eq!(format.long(true).to_string(), "main-0-gb920bbb-dirty");
|
||||
|
||||
format.dirty_suffix = None;
|
||||
format.depth = 42;
|
||||
assert!(!format.is_exact_match());
|
||||
assert_eq!(format.long(false).to_string(), "main-42-gb920bbb");
|
||||
|
||||
format.dirty_suffix = Some("dirty".into());
|
||||
assert_eq!(format.to_string(), "main-42-gb920bbb-dirty");
|
||||
assert_eq!(format.long(true).to_string(), "main-42-gb920bbb-dirty");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn show_abbrev_hash_if_no_name_is_known() {
|
||||
let mut format = describe::Outcome {
|
||||
name: None,
|
||||
id: hex_to_id("b920bbb055e1efb9080592a409d3975738b6efb3"),
|
||||
depth: 0,
|
||||
name_by_oid: Default::default(),
|
||||
commits_seen: 0,
|
||||
}
|
||||
.into_format(7);
|
||||
assert!(
|
||||
format.is_exact_match(),
|
||||
"it reports true as it is only dependent on the depth which plays no role here"
|
||||
);
|
||||
assert_eq!(format.long(false).to_string(), "b920bbb");
|
||||
assert_eq!(format.long(true).to_string(), "b920bbb");
|
||||
|
||||
format.dirty_suffix = Some("dirty".into());
|
||||
assert_eq!(format.long(false).to_string(), "b920bbb-dirty");
|
||||
assert_eq!(format.long(true).to_string(), "b920bbb-dirty");
|
||||
}
|
||||
258
src-revision/tests/revision/describe/mod.rs
Normal file
258
src-revision/tests/revision/describe/mod.rs
Normal file
@@ -0,0 +1,258 @@
|
||||
use gix_error::Exn;
|
||||
use gix_object::bstr::ByteSlice;
|
||||
use gix_revision::{
|
||||
describe,
|
||||
describe::{Error, Outcome},
|
||||
};
|
||||
use std::{borrow::Cow, path::PathBuf};
|
||||
|
||||
use crate::hex_to_id;
|
||||
|
||||
mod format;
|
||||
|
||||
fn run_test(
|
||||
transform_odb: impl FnOnce(gix_odb::Handle) -> gix_odb::Handle,
|
||||
options: impl Fn(gix_hash::ObjectId) -> gix_revision::describe::Options<'static>,
|
||||
run_assertions: impl Fn(
|
||||
Result<Option<Outcome<'static>>, Exn<Error>>,
|
||||
gix_hash::ObjectId,
|
||||
) -> Result<(), gix_error::Error>,
|
||||
) -> Result<(), gix_error::Error> {
|
||||
let store = odb_at(".");
|
||||
let store = transform_odb(store);
|
||||
let commit_id = hex_to_id("01ec18a3ebf2855708ad3c9d244306bc1fae3e9b");
|
||||
for use_commitgraph in [false, true] {
|
||||
let cache = use_commitgraph
|
||||
.then(|| gix_commitgraph::Graph::from_info_dir(&store.store_ref().path().join("info")).ok())
|
||||
.flatten();
|
||||
let mut graph = gix_revision::Graph::new(&store, cache.as_ref());
|
||||
run_assertions(
|
||||
gix_revision::describe(&commit_id, &mut graph, options(commit_id)),
|
||||
commit_id,
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn option_none_if_no_tag_found() -> Result<(), gix_error::Error> {
|
||||
run_test(
|
||||
std::convert::identity,
|
||||
|_| Default::default(),
|
||||
|res, _id| {
|
||||
assert!(res?.is_none(), "cannot find anything if there's no candidate");
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fallback_if_configured_in_options_but_no_candidate_or_names() -> Result<(), gix_error::Error> {
|
||||
run_test(
|
||||
std::convert::identity,
|
||||
|_| describe::Options {
|
||||
fallback_to_oid: true,
|
||||
..Default::default()
|
||||
},
|
||||
|res, _id| {
|
||||
let res = res?.expect("fallback active");
|
||||
assert!(res.name.is_none(), "no name can be found");
|
||||
assert_eq!(res.depth, 0, "just a default, not relevant as there is no name");
|
||||
assert_eq!(
|
||||
res.commits_seen, 0,
|
||||
"a traversal is isn't performed as name map is empty, and that's the whole point"
|
||||
);
|
||||
assert_eq!(res.into_format(7).to_string(), "01ec18a");
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fallback_if_configured_in_options_and_max_candidates_zero() -> Result<(), gix_error::Error> {
|
||||
run_test(
|
||||
std::convert::identity,
|
||||
|_| describe::Options {
|
||||
fallback_to_oid: true,
|
||||
max_candidates: 0,
|
||||
..Default::default()
|
||||
},
|
||||
|res, _id| {
|
||||
let res = res?.expect("fallback active");
|
||||
assert!(res.name.is_none(), "no name can be found");
|
||||
assert_eq!(res.depth, 0, "just a default, not relevant as there is no name");
|
||||
assert_eq!(res.commits_seen, 0, "we don't do any traversal");
|
||||
assert_eq!(res.into_format(7).to_string(), "01ec18a");
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn not_enough_candidates() -> Result<(), gix_error::Error> {
|
||||
let name = Cow::Borrowed(b"at-c5".as_bstr());
|
||||
run_test(
|
||||
std::convert::identity,
|
||||
|_| describe::Options {
|
||||
name_by_oid: vec![
|
||||
(hex_to_id("efd9a841189668f1bab5b8ebade9cd0a1b139a37"), name.clone()),
|
||||
(
|
||||
hex_to_id("9152eeee2328073cf23dcf8e90c949170b711659"),
|
||||
b"at-b1c1".as_bstr().into(),
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
max_candidates: 1,
|
||||
..Default::default()
|
||||
},
|
||||
|res, id| {
|
||||
let res = res?.expect("candidate found");
|
||||
assert_eq!(res.name, Some(name.clone()), "it finds the youngest/most-recent name");
|
||||
assert_eq!(res.id, id);
|
||||
assert_eq!(res.commits_seen, 6, "it has to traverse commits");
|
||||
assert_eq!(
|
||||
res.depth, 3,
|
||||
"it calculates the final number of commits even though it aborted early"
|
||||
);
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typical_usecases() -> Result<(), gix_error::Error> {
|
||||
let name = Cow::Borrowed(b"main".as_bstr());
|
||||
run_test(
|
||||
std::convert::identity,
|
||||
|id| describe::Options {
|
||||
name_by_oid: vec![(id, name.clone())].into_iter().collect(),
|
||||
max_candidates: 0,
|
||||
..Default::default()
|
||||
},
|
||||
|res, id| {
|
||||
let res = res?.expect("candidate found");
|
||||
assert_eq!(
|
||||
res.name,
|
||||
Some(name.clone()),
|
||||
"this is an exact match, and it's found despite max-candidates being 0 (one lookup is always performed)"
|
||||
);
|
||||
assert_eq!(res.id, id);
|
||||
assert_eq!(res.depth, 0);
|
||||
assert_eq!(res.commits_seen, 0);
|
||||
Ok(())
|
||||
},
|
||||
)?;
|
||||
|
||||
let name = Cow::Borrowed(b"at-c5".as_bstr());
|
||||
run_test(
|
||||
std::convert::identity,
|
||||
|_| describe::Options {
|
||||
name_by_oid: vec![
|
||||
(hex_to_id("efd9a841189668f1bab5b8ebade9cd0a1b139a37"), name.clone()),
|
||||
(
|
||||
hex_to_id("9152eeee2328073cf23dcf8e90c949170b711659"),
|
||||
b"at-b1c1".as_bstr().into(),
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
..Default::default()
|
||||
},
|
||||
|res, id| {
|
||||
let res = res?.expect("candidate found");
|
||||
assert_eq!(
|
||||
res.name,
|
||||
Some(name.clone()),
|
||||
"a match to a tag 1 commit away with 2 commits on the other side of the merge/head"
|
||||
);
|
||||
assert_eq!(res.id, id);
|
||||
assert_eq!(res.depth, 3);
|
||||
assert_eq!(res.commits_seen, 6);
|
||||
Ok(())
|
||||
},
|
||||
)?;
|
||||
|
||||
run_test(
|
||||
std::convert::identity,
|
||||
|_| describe::Options {
|
||||
name_by_oid: vec![
|
||||
(hex_to_id("efd9a841189668f1bab5b8ebade9cd0a1b139a37"), name.clone()),
|
||||
(
|
||||
hex_to_id("9152eeee2328073cf23dcf8e90c949170b711659"),
|
||||
b"at-b1c1".as_bstr().into(),
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
first_parent: true,
|
||||
..Default::default()
|
||||
},
|
||||
|res, id| {
|
||||
let res = res?.expect("candidate found");
|
||||
assert_eq!(res.name, Some(name.clone()));
|
||||
assert_eq!(res.id, id);
|
||||
assert_eq!(res.depth, 1);
|
||||
assert_eq!(res.commits_seen, 2);
|
||||
assert_eq!(res.into_format(7).to_string(), "at-c5-1-g01ec18a");
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shallow_yields_no_result_if_provided_refs_are_in_truncated_part_of_history() -> Result<(), gix_error::Error> {
|
||||
run_test(
|
||||
|_| odb_at("shallow-1-clone"),
|
||||
|_| describe::Options {
|
||||
name_by_oid: vec![(
|
||||
hex_to_id("efd9a841189668f1bab5b8ebade9cd0a1b139a37"),
|
||||
Cow::Borrowed(b"at-c5".as_bstr()),
|
||||
)]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
first_parent: true,
|
||||
..Default::default()
|
||||
},
|
||||
|res, _id| {
|
||||
let res = res?;
|
||||
assert!(
|
||||
res.is_none(),
|
||||
"no candidate found on truncated history, and it doesn't crash"
|
||||
);
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shallow_yields_result_if_refs_are_available() -> Result<(), gix_error::Error> {
|
||||
let name = Cow::Borrowed(b"at-c5".as_bstr());
|
||||
run_test(
|
||||
|_| odb_at("shallow-2-clone"),
|
||||
|_| describe::Options {
|
||||
name_by_oid: vec![(hex_to_id("efd9a841189668f1bab5b8ebade9cd0a1b139a37"), name.clone())]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
first_parent: true,
|
||||
..Default::default()
|
||||
},
|
||||
|res, id| {
|
||||
let res = res?.expect("found candidate");
|
||||
assert_eq!(res.name, Some(name.clone()));
|
||||
assert_eq!(res.id, id);
|
||||
assert_eq!(res.depth, 1);
|
||||
assert_eq!(res.commits_seen, 2);
|
||||
assert_eq!(res.into_format(7).to_string(), "at-c5-1-g01ec18a");
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn odb_at(name: &str) -> gix_odb::Handle {
|
||||
gix_odb::at(fixture_path().join(name).join(".git/objects")).unwrap()
|
||||
}
|
||||
|
||||
fn fixture_path() -> PathBuf {
|
||||
gix_testtools::scripted_fixture_read_only("make_repo_with_branches.sh").unwrap()
|
||||
}
|
||||
11
src-revision/tests/revision/main.rs
Normal file
11
src-revision/tests/revision/main.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
#[cfg(feature = "describe")]
|
||||
mod describe;
|
||||
#[cfg(feature = "merge_base")]
|
||||
mod merge_base;
|
||||
mod spec;
|
||||
|
||||
pub use gix_testtools::Result;
|
||||
|
||||
fn hex_to_id(hex: &str) -> gix_hash::ObjectId {
|
||||
gix_hash::ObjectId::from_hex(hex.as_bytes()).expect("40 bytes hex")
|
||||
}
|
||||
161
src-revision/tests/revision/merge_base/mod.rs
Normal file
161
src-revision/tests/revision/merge_base/mod.rs
Normal file
@@ -0,0 +1,161 @@
|
||||
use gix_revision::merge_base;
|
||||
|
||||
#[test]
|
||||
fn validate() -> crate::Result {
|
||||
let root = gix_testtools::scripted_fixture_read_only("make_merge_base_repos.sh")?;
|
||||
let mut count = 0;
|
||||
let odb = gix_odb::at(root.join(".git/objects"))?;
|
||||
for baseline_path in baseline::expectation_paths(&root)? {
|
||||
count += 1;
|
||||
for use_commitgraph in [false, true] {
|
||||
let cache = use_commitgraph
|
||||
.then(|| gix_commitgraph::Graph::from_info_dir(&odb.store_ref().path().join("info")).unwrap());
|
||||
for expected in baseline::parse_expectations(&baseline_path)? {
|
||||
let mut graph = gix_revision::Graph::new(&odb, cache.as_ref());
|
||||
let actual = merge_base(expected.first, &expected.others, &mut graph)?;
|
||||
assert_eq!(
|
||||
actual,
|
||||
expected.bases,
|
||||
"sample {file:?}:{input}",
|
||||
file = baseline_path.with_extension("").file_name(),
|
||||
input = expected.plain_input
|
||||
);
|
||||
}
|
||||
let mut graph = gix_revision::Graph::new(&odb, cache.as_ref());
|
||||
for expected in baseline::parse_expectations(&baseline_path)? {
|
||||
let actual = merge_base(expected.first, &expected.others, &mut graph)?;
|
||||
assert_eq!(
|
||||
actual,
|
||||
expected.bases,
|
||||
"sample (reused graph) {file:?}:{input}",
|
||||
file = baseline_path.with_extension("").file_name(),
|
||||
input = expected.plain_input
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_ne!(count, 0, "there must be at least one baseline");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
mod octopus {
|
||||
use crate::hex_to_id;
|
||||
|
||||
#[test]
|
||||
fn three_sequential_commits() -> crate::Result {
|
||||
let odb = odb_at("three-sequential-commits")?;
|
||||
let mut graph = gix_revision::Graph::new(&odb, None);
|
||||
let first_commit = hex_to_id("e5d0542bd38431f105a8de8e982b3579647feb9f");
|
||||
let mut heads = vec![
|
||||
hex_to_id("4fbed377d3eab982d4a465cafaf34b64207da847"),
|
||||
hex_to_id("8bc2f99c9aacf07568a2bbfe1269f6e543f22d6b"),
|
||||
first_commit,
|
||||
];
|
||||
let mut heap = permutohedron::Heap::new(&mut heads);
|
||||
while let Some(heads) = heap.next_permutation() {
|
||||
let actual = gix_revision::merge_base::octopus(*heads.first().unwrap(), &heads[1..], &mut graph)?
|
||||
.expect("a merge base");
|
||||
assert_eq!(actual, first_commit);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn three_parallel_commits() -> crate::Result {
|
||||
let odb = odb_at("three-parallel-commits")?;
|
||||
let mut graph = gix_revision::Graph::new(&odb, None);
|
||||
let base = hex_to_id("3ca3e3dd12585fabbef311d524a5e54678090528");
|
||||
let mut heads = vec![
|
||||
hex_to_id("4ce66b336dff547fdeb6cd86e04c617c8d998ff5"),
|
||||
hex_to_id("6291f6d7da04208dc4ccbbdf9fda98ac9ae67bc0"),
|
||||
hex_to_id("c507d5413da00c32e5de1ea433030e8e4716bc60"),
|
||||
];
|
||||
let mut heap = permutohedron::Heap::new(&mut heads);
|
||||
while let Some(heads) = heap.next_permutation() {
|
||||
let actual = gix_revision::merge_base::octopus(*heads.first().unwrap(), &heads[1..], &mut graph)?
|
||||
.expect("a merge base");
|
||||
assert_eq!(actual, base);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn three_forked_commits() -> crate::Result {
|
||||
let odb = odb_at("three-forked-commits")?;
|
||||
let mut graph = gix_revision::Graph::new(&odb, None);
|
||||
let base = hex_to_id("3ca3e3dd12585fabbef311d524a5e54678090528");
|
||||
let mut heads = vec![
|
||||
hex_to_id("413d38a3fe7453c68cb7314739d7775f68ab89f5"),
|
||||
hex_to_id("d4d01a9b6f6fcb23d57cd560229cd9680ec9bd6e"),
|
||||
hex_to_id("c507d5413da00c32e5de1ea433030e8e4716bc60"),
|
||||
];
|
||||
let mut heap = permutohedron::Heap::new(&mut heads);
|
||||
while let Some(heads) = heap.next_permutation() {
|
||||
let actual = gix_revision::merge_base::octopus(*heads.first().unwrap(), &heads[1..], &mut graph)?
|
||||
.expect("a merge base");
|
||||
assert_eq!(actual, base);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn odb_at(name: &str) -> crate::Result<gix_odb::Handle> {
|
||||
let root = gix_testtools::scripted_fixture_read_only("merge_base_octopus_repos.sh")?;
|
||||
Ok(gix_odb::at(root.join(name).join(".git/objects"))?)
|
||||
}
|
||||
}
|
||||
|
||||
mod baseline {
|
||||
use std::{
|
||||
ffi::OsStr,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use bstr::ByteSlice;
|
||||
use gix_hash::ObjectId;
|
||||
use nonempty::NonEmpty;
|
||||
|
||||
/// The expectation as produced by Git itself
|
||||
#[derive(Debug)]
|
||||
pub struct Expectation {
|
||||
pub plain_input: String,
|
||||
pub first: ObjectId,
|
||||
pub others: Vec<ObjectId>,
|
||||
pub bases: Option<NonEmpty<ObjectId>>,
|
||||
}
|
||||
|
||||
pub fn parse_expectations(baseline: &Path) -> std::io::Result<Vec<Expectation>> {
|
||||
let lines = std::fs::read(baseline)?;
|
||||
let mut lines = lines.lines();
|
||||
let mut out = Vec::new();
|
||||
while let Some(plain_input) = lines.next() {
|
||||
let plain_input = plain_input.to_str_lossy().into_owned();
|
||||
let mut input = lines
|
||||
.next()
|
||||
.expect("second line is resolved input objects")
|
||||
.split(|b| *b == b' ');
|
||||
let first = ObjectId::from_hex(input.next().expect("at least one object")).unwrap();
|
||||
let others = input.map(|hex_id| ObjectId::from_hex(hex_id).unwrap()).collect();
|
||||
let bases: Vec<_> = lines
|
||||
.by_ref()
|
||||
.take_while(|l| !l.is_empty())
|
||||
.map(|hex_id| ObjectId::from_hex(hex_id).unwrap())
|
||||
.collect();
|
||||
out.push(Expectation {
|
||||
plain_input,
|
||||
first,
|
||||
others,
|
||||
bases: NonEmpty::from_vec(bases),
|
||||
});
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub fn expectation_paths(root: &Path) -> std::io::Result<Vec<PathBuf>> {
|
||||
let mut out: Vec<_> = std::fs::read_dir(root)?
|
||||
.map(Result::unwrap)
|
||||
.filter_map(|e| (e.path().extension() == Some(OsStr::new("baseline"))).then(|| e.path()))
|
||||
.collect();
|
||||
out.sort();
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
65
src-revision/tests/revision/spec/display.rs
Normal file
65
src-revision/tests/revision/spec/display.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
use crate::hex_to_id;
|
||||
|
||||
fn oid() -> gix_hash::ObjectId {
|
||||
hex_to_id("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
|
||||
}
|
||||
|
||||
fn oid2() -> gix_hash::ObjectId {
|
||||
hex_to_id("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn include() {
|
||||
assert_eq!(
|
||||
gix_revision::Spec::Include(oid()).to_string(),
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exclude() {
|
||||
assert_eq!(
|
||||
gix_revision::Spec::Exclude(oid()).to_string(),
|
||||
"^aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn range() {
|
||||
assert_eq!(
|
||||
gix_revision::Spec::Range {
|
||||
from: oid(),
|
||||
to: oid2()
|
||||
}
|
||||
.to_string(),
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa..bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge() {
|
||||
assert_eq!(
|
||||
gix_revision::Spec::Merge {
|
||||
theirs: oid(),
|
||||
ours: oid2()
|
||||
}
|
||||
.to_string(),
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn include_parents() {
|
||||
assert_eq!(
|
||||
gix_revision::Spec::IncludeOnlyParents(oid()).to_string(),
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa^@"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exclude_parents() {
|
||||
assert_eq!(
|
||||
gix_revision::Spec::ExcludeParents(oid()).to_string(),
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa^!"
|
||||
);
|
||||
}
|
||||
2
src-revision/tests/revision/spec/mod.rs
Normal file
2
src-revision/tests/revision/spec/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
mod display;
|
||||
mod parse;
|
||||
226
src-revision/tests/revision/spec/parse/anchor/at_symbol.rs
Normal file
226
src-revision/tests/revision/spec/parse/anchor/at_symbol.rs
Normal file
@@ -0,0 +1,226 @@
|
||||
use crate::spec::parse::{parse, try_parse};
|
||||
|
||||
#[test]
|
||||
fn braces_must_be_closed() {
|
||||
for unclosed_spec in ["@{something", "@{", "@{..@"] {
|
||||
let err = try_parse(unclosed_spec).unwrap_err();
|
||||
assert_eq!(
|
||||
err.input.as_ref().map(std::convert::AsRef::as_ref),
|
||||
Some(&unclosed_spec.as_bytes()[1..])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(target_pointer_width = "64")] // Only works this way on 64-bit systems.
|
||||
fn fuzzed() {
|
||||
let rec = parse("@{-9223372036854775808}");
|
||||
assert_eq!(rec.nth_checked_out_branch, [Some(9223372036854775808), None]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reflog_by_entry_for_current_branch() {
|
||||
for (spec, expected_entry) in [("@{0}", 0), ("@{42}", 42), ("@{00100}", 100)] {
|
||||
let rec = parse(spec);
|
||||
|
||||
assert!(rec.kind.is_none());
|
||||
assert_eq!(rec.find_ref[0], None);
|
||||
assert_eq!(
|
||||
rec.prefix[0], None,
|
||||
"neither ref nor prefixes are set, straight to navigation"
|
||||
);
|
||||
assert_eq!(rec.current_branch_reflog_entry[0], Some(expected_entry.to_string()));
|
||||
assert_eq!(rec.calls, 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reflog_by_date_for_current_branch() {
|
||||
let rec = parse("@{42 +0030}");
|
||||
|
||||
assert!(rec.kind.is_none());
|
||||
assert_eq!(rec.find_ref[0], None);
|
||||
assert_eq!(
|
||||
rec.prefix[0], None,
|
||||
"neither ref nor prefixes are set, straight to navigation"
|
||||
);
|
||||
assert_eq!(rec.current_branch_reflog_entry[0], Some("42 +0030".to_string()));
|
||||
assert_eq!(rec.calls, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reflog_by_unix_timestamp_for_current_branch() {
|
||||
let rec = parse("@{100000000}");
|
||||
|
||||
assert!(rec.kind.is_none());
|
||||
assert_eq!(rec.find_ref[0], None);
|
||||
assert_eq!(
|
||||
rec.prefix[0], None,
|
||||
"neither ref nor prefixes are set, straight to navigation"
|
||||
);
|
||||
assert_eq!(
|
||||
rec.current_branch_reflog_entry[0],
|
||||
Some("100000000 +0000".to_string()),
|
||||
"This number is the first to count as date"
|
||||
);
|
||||
assert_eq!(rec.calls, 1);
|
||||
|
||||
let rec = parse("@{99999999}");
|
||||
assert_eq!(
|
||||
rec.current_branch_reflog_entry[0],
|
||||
Some("99999999".to_string()),
|
||||
"one less is an offset though"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reflog_by_date_with_date_parse_failure() {
|
||||
let err = try_parse("@{foo}").unwrap_err();
|
||||
insta::assert_snapshot!(err, @"could not parse time for reflog lookup: foo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reflog_by_date_for_hash_is_invalid() {
|
||||
for (spec, full_name) in [
|
||||
("1234@{42 +0030}", "1234"),
|
||||
("abcd-dirty@{42 +0030}", "abcd-dirty"),
|
||||
("v1.2.3-0-g1234@{42 +0030}", "v1.2.3-0-g1234"),
|
||||
] {
|
||||
let err = try_parse(spec).unwrap_err();
|
||||
assert_eq!(err.input.as_ref().map(AsRef::as_ref), Some(full_name.as_bytes()));
|
||||
assert!(err.message.contains("reflog entries require a ref name"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reflog_by_date_for_given_ref_name() {
|
||||
for (spec, expected_ref) in [
|
||||
("main@{42 +0030}", "main"),
|
||||
("refs/heads/other@{42 +0030}", "refs/heads/other"),
|
||||
("refs/worktree/feature/a@{42 +0030}", "refs/worktree/feature/a"),
|
||||
] {
|
||||
let rec = parse(spec);
|
||||
|
||||
assert!(rec.kind.is_none());
|
||||
assert_eq!(rec.get_ref(0), expected_ref);
|
||||
assert_eq!(rec.prefix[0], None);
|
||||
assert_eq!(rec.current_branch_reflog_entry[0], Some("42 +0030".to_string()));
|
||||
assert_eq!(rec.calls, 2, "first the ref, then the reflog entry");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reflog_by_entry_for_given_ref_name() {
|
||||
for (spec, expected_ref, expected_entry) in [
|
||||
("main@{0}", "main", 0),
|
||||
("refs/heads/other@{42}", "refs/heads/other", 42),
|
||||
("refs/worktree/feature/a@{00100}", "refs/worktree/feature/a", 100),
|
||||
] {
|
||||
let rec = parse(spec);
|
||||
|
||||
assert!(rec.kind.is_none());
|
||||
assert_eq!(rec.get_ref(0), expected_ref);
|
||||
assert_eq!(rec.prefix[0], None);
|
||||
assert_eq!(rec.current_branch_reflog_entry[0], Some(expected_entry.to_string()));
|
||||
assert_eq!(rec.calls, 2, "first the ref, then the reflog entry");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reflog_by_entry_for_hash_is_invalid() {
|
||||
for (spec, full_name) in [
|
||||
("1234@{0}", "1234"),
|
||||
("abcd-dirty@{1}", "abcd-dirty"),
|
||||
("v1.2.3-0-g1234@{2}", "v1.2.3-0-g1234"),
|
||||
] {
|
||||
let err = try_parse(spec).unwrap_err();
|
||||
assert_eq!(err.input.as_ref().map(AsRef::as_ref), Some(full_name.as_bytes()));
|
||||
assert!(err.message.contains("reflog entries require a ref name"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sibling_branch_current_branch() {
|
||||
for (spec, kind_name) in [("@{u}", "Upstream"), ("@{push}", "Push"), ("@{UPSTREAM}", "Upstream")] {
|
||||
let rec = parse(spec);
|
||||
|
||||
assert!(rec.kind.is_none());
|
||||
assert_eq!(rec.find_ref[0], None);
|
||||
assert_eq!(rec.prefix[0], None, "neither ref nor prefix are explicitly set");
|
||||
assert_eq!(rec.sibling_branch[0].as_deref(), Some(kind_name));
|
||||
assert_eq!(rec.calls, 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sibling_branch_for_branch_name() {
|
||||
for (spec, ref_name, kind_name) in [
|
||||
("r1@{U}", "r1", "Upstream"),
|
||||
("refs/heads/main@{Push}", "refs/heads/main", "Push"),
|
||||
("refs/worktree/private@{UpStreaM}", "refs/worktree/private", "Upstream"),
|
||||
] {
|
||||
let rec = parse(spec);
|
||||
|
||||
assert!(rec.kind.is_none());
|
||||
assert_eq!(rec.get_ref(0), ref_name);
|
||||
assert_eq!(rec.prefix[0], None, "neither ref nor prefix are explicitly set");
|
||||
assert_eq!(
|
||||
rec.sibling_branch[0].as_deref(),
|
||||
Some(kind_name),
|
||||
"note that we do not know if something is a branch or not and make the call even if it would not be allowed. Configuration decides"
|
||||
);
|
||||
assert_eq!(rec.calls, 2);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sibling_branch_for_hash_is_invalid() {
|
||||
for (spec, full_name) in [
|
||||
("1234@{u}", "1234"),
|
||||
("abcd-dirty@{push}", "abcd-dirty"),
|
||||
("v1.2.3-0-g1234@{upstream}", "v1.2.3-0-g1234"),
|
||||
] {
|
||||
let err = try_parse(spec).unwrap_err();
|
||||
assert_eq!(err.input.as_ref().map(AsRef::as_ref), Some(full_name.as_bytes()));
|
||||
assert!(err.message.contains("sibling branches"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nth_checked_out_branch_for_refname_is_invalid() {
|
||||
let err = try_parse("r1@{-1}").unwrap_err();
|
||||
// its undefined how to handle negative numbers and specified ref names
|
||||
insta::assert_snapshot!(err, @"reference name must be followed by positive numbers in @{n}: -1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nth_checked_out_branch() {
|
||||
for (spec, expected_branch) in [("@{-1}", 1), ("@{-42}", 42), ("@{-00100}", 100)] {
|
||||
let rec = parse(spec);
|
||||
|
||||
assert!(rec.kind.is_none());
|
||||
assert_eq!(rec.find_ref[0], None);
|
||||
assert_eq!(
|
||||
rec.prefix[0], None,
|
||||
"neither ref nor prefixes are set, straight to navigation"
|
||||
);
|
||||
assert_eq!(rec.nth_checked_out_branch[0], Some(expected_branch));
|
||||
assert_eq!(rec.calls, 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn numbers_within_braces_cannot_be_negative_zero() {
|
||||
let err = try_parse("@{-0}").unwrap_err();
|
||||
// negative zero is not accepted, even though it could easily be defaulted to 0 which is a valid value
|
||||
insta::assert_snapshot!(err, @"negative zero is invalid - remove the minus sign: -0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn numbers_within_braces_can_be_positive_zero() {
|
||||
assert_eq!(
|
||||
parse("@{+0}"),
|
||||
parse("@{0}"),
|
||||
"+ prefixes are allowed though and the same as without it"
|
||||
);
|
||||
}
|
||||
140
src-revision/tests/revision/spec/parse/anchor/colon_symbol.rs
Normal file
140
src-revision/tests/revision/spec/parse/anchor/colon_symbol.rs
Normal file
@@ -0,0 +1,140 @@
|
||||
use crate::spec::parse::{parse, try_parse};
|
||||
|
||||
#[test]
|
||||
fn regex_parsing_ignores_ranges_as_opposed_to_git() {
|
||||
for spec in [":/a..b", ":/a...b"] {
|
||||
let rec = parse(spec);
|
||||
|
||||
assert!(rec.kind.is_none());
|
||||
assert_eq!(
|
||||
rec.patterns,
|
||||
vec![(spec[2..].into(), false)],
|
||||
"git parses ranges but I think it's merely coincidental rather than intended, not doing so allows to use '.' more liberally"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn index_lookups_ignores_ranges_as_opposed_to_git() {
|
||||
for spec in [":a..b", ":a...b"] {
|
||||
let rec = parse(spec);
|
||||
|
||||
assert!(rec.kind.is_none());
|
||||
assert_eq!(
|
||||
rec.index_lookups,
|
||||
vec![(spec[1..].into(), 0)],
|
||||
"git parses ranges but it's never useful as these specs only ever produce blob ids"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn various_forms_of_regex() {
|
||||
for (spec, (regex, negated)) in [
|
||||
(":/simple", ("simple", false)),
|
||||
(":/!-negated", ("negated", true)),
|
||||
(":/^from start", ("^from start", false)),
|
||||
(":/!!leading exclamation mark", ("!leading exclamation mark", false)),
|
||||
(":/with count{1}", ("with count{1}", false)),
|
||||
(
|
||||
":/all-consuming makes navigation impossible^5~10",
|
||||
("all-consuming makes navigation impossible^5~10", false),
|
||||
),
|
||||
] {
|
||||
let rec = parse(spec);
|
||||
|
||||
assert!(rec.kind.is_none());
|
||||
assert_eq!(rec.find_ref[0], None);
|
||||
assert_eq!(rec.prefix[0], None);
|
||||
assert_eq!(rec.patterns, vec![(regex.into(), negated)]);
|
||||
assert_eq!(rec.calls, 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn regex_do_not_get_any_backslash_processing() {
|
||||
for (spec, regex) in [(r#":/{"#, "{"), (r":/\{\}", r"\{\}"), (r":/\\\\\}", r"\\\\\}")] {
|
||||
let rec = parse(spec);
|
||||
|
||||
assert_eq!(rec.patterns, vec![(regex.into(), false)]);
|
||||
assert_eq!(rec.calls, 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn various_valid_index_lookups_by_path() {
|
||||
for spec in [
|
||||
":path",
|
||||
":dir/path",
|
||||
":./relative-to.cwd",
|
||||
":../relative-to-cwd-too",
|
||||
":navigation/is/ignored~10^^^",
|
||||
] {
|
||||
let rec = parse(spec);
|
||||
|
||||
assert!(rec.kind.is_none());
|
||||
assert_eq!(rec.find_ref[0], None);
|
||||
assert_eq!(rec.prefix[0], None);
|
||||
assert_eq!(rec.index_lookups, vec![(spec[1..].into(), 0)]);
|
||||
assert_eq!(rec.peel_to, vec![], "peeling only works for anchors");
|
||||
assert_eq!(rec.calls, 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn various_valid_index_lookups_by_path_and_stage() {
|
||||
for (spec, path, stage) in [
|
||||
(":0:path", "path", 0),
|
||||
(":1:dir/path", "dir/path", 1),
|
||||
(":2:dir/path@{part-of-path}", "dir/path@{part-of-path}", 2),
|
||||
] {
|
||||
let rec = parse(spec);
|
||||
|
||||
assert!(rec.kind.is_none());
|
||||
assert_eq!(rec.find_ref[0], None);
|
||||
assert_eq!(rec.prefix[0], None);
|
||||
assert_eq!(rec.index_lookups, vec![(path.into(), stage)]);
|
||||
assert_eq!(rec.peel_to, vec![], "peeling only works for anchors");
|
||||
assert_eq!(rec.calls, 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_top_level_regex_are_invalid() {
|
||||
let err = try_parse(":/").unwrap_err();
|
||||
// git also can't do it, finds nothing instead. It could be the youngest commit in theory, but isn't
|
||||
insta::assert_snapshot!(err, @"':/' must be followed by a regular expression");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn regex_with_empty_exclamation_mark_prefix_is_invalid() {
|
||||
let err = try_parse(r#":/!hello"#).unwrap_err();
|
||||
assert_eq!(err.input.as_ref().map(AsRef::as_ref), Some(b"!hello".as_ref()));
|
||||
insta::assert_snapshot!(err, @"need one character after /!, typically -: !hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn needs_suffix() {
|
||||
let err = try_parse(":").unwrap_err();
|
||||
// git also can't do it, finds nothing instead. It could be the youngest commit in theory, but isn't
|
||||
insta::assert_snapshot!(err, @"':' must be followed by either slash and regex or path to lookup in HEAD tree");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_index_stage_is_part_of_path() {
|
||||
for spec in [":4:file", ":5:file", ":01:file", ":10:file"] {
|
||||
let rec = parse(spec);
|
||||
|
||||
assert!(rec.kind.is_none());
|
||||
assert_eq!(rec.find_ref[0], None);
|
||||
assert_eq!(rec.prefix[0], None);
|
||||
assert_eq!(rec.patterns, vec![]);
|
||||
assert_eq!(
|
||||
rec.index_lookups,
|
||||
vec![(spec[1..].into(), 0)],
|
||||
"these count as stage 0 lookups"
|
||||
);
|
||||
assert_eq!(rec.peel_to, vec![]);
|
||||
assert_eq!(rec.calls, 1);
|
||||
}
|
||||
}
|
||||
132
src-revision/tests/revision/spec/parse/anchor/describe.rs
Normal file
132
src-revision/tests/revision/spec/parse/anchor/describe.rs
Normal file
@@ -0,0 +1,132 @@
|
||||
use crate::spec::parse::{parse, try_parse_opts, Options, PrefixHintOwned};
|
||||
|
||||
fn anchor_hint() -> Option<PrefixHintOwned> {
|
||||
Some(PrefixHintOwned::DescribeAnchor {
|
||||
ref_name: "cargo-smart-release".into(),
|
||||
generation: 679,
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_format_parses_hash_portion_as_prefix() {
|
||||
let rec = parse("cargo-smart-release-679-g3bee7fb");
|
||||
assert!(rec.kind.is_none());
|
||||
assert_eq!(rec.find_ref[0], None, "references are not resolved in describe output");
|
||||
assert_eq!(rec.prefix[0], Some(gix_hash::Prefix::from_hex("3bee7fb").unwrap()));
|
||||
assert_eq!(rec.prefix_hint[0], anchor_hint());
|
||||
assert_eq!(rec.calls, 1);
|
||||
|
||||
let rec = parse("v1.0-0-g3bee7fb");
|
||||
assert!(rec.kind.is_none());
|
||||
assert_eq!(rec.find_ref[0], None, "references are not resolved in describe output");
|
||||
assert_eq!(rec.prefix[0], Some(gix_hash::Prefix::from_hex("3bee7fb").unwrap()));
|
||||
assert_eq!(
|
||||
rec.prefix_hint[0],
|
||||
Some(PrefixHintOwned::DescribeAnchor {
|
||||
ref_name: "v1.0".into(),
|
||||
generation: 0,
|
||||
})
|
||||
);
|
||||
assert_eq!(rec.calls, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_format_lookalikes_fallback_to_ref() {
|
||||
let spec = "cargo-smart-release-679-g3bee7fb";
|
||||
let rec = try_parse_opts(
|
||||
spec,
|
||||
Options {
|
||||
reject_prefix: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert!(rec.kind.is_none());
|
||||
assert_eq!(rec.get_ref(0), spec);
|
||||
assert_eq!(rec.prefix[0], None);
|
||||
assert_eq!(rec.prefix_hint[0], None);
|
||||
assert_eq!(rec.calls, 2, "call prefix, then call ref");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn any_hash_without_suffix_and_prefix_g_is_assumed_to_be_describe_output() {
|
||||
let spec = "foo--bar-gabcdef1";
|
||||
let rec = parse(spec);
|
||||
assert!(rec.kind.is_none());
|
||||
assert_eq!(rec.find_ref[0], None);
|
||||
assert_eq!(
|
||||
rec.prefix[0],
|
||||
Some(gix_hash::Prefix::from_hex("abcdef1").unwrap()),
|
||||
"git does not parse very precisely here"
|
||||
);
|
||||
assert_eq!(rec.prefix_hint[0], Some(PrefixHintOwned::MustBeCommit));
|
||||
assert_eq!(rec.calls, 1);
|
||||
|
||||
for invalid_describe in ["-gabcdef1", "gabcdef1"] {
|
||||
let rec = parse(invalid_describe);
|
||||
assert!(rec.kind.is_none());
|
||||
assert_eq!(
|
||||
rec.get_ref(0),
|
||||
invalid_describe,
|
||||
"we don't consider this a prefix from a describe block"
|
||||
);
|
||||
assert_eq!(rec.prefix[0], None);
|
||||
assert_eq!(rec.prefix_hint[0], None);
|
||||
assert_eq!(rec.calls, 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_format_with_dirty_suffix_is_recognized() {
|
||||
let rec = parse("cargo-smart-release-679-g3bee7fb-dirty");
|
||||
assert!(rec.kind.is_none());
|
||||
assert_eq!(rec.find_ref[0], None, "git does not see this as prefix, we do");
|
||||
assert_eq!(rec.prefix[0], Some(gix_hash::Prefix::from_hex("3bee7fb").unwrap()));
|
||||
assert_eq!(rec.prefix_hint[0], anchor_hint());
|
||||
assert_eq!(rec.calls, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partial_format_with_dirty_suffix_is_recognized() {
|
||||
let spec = "abcdef1-dirty";
|
||||
let rec = parse(spec);
|
||||
assert!(rec.kind.is_none());
|
||||
assert_eq!(rec.find_ref[0], None);
|
||||
assert_eq!(
|
||||
rec.prefix[0],
|
||||
Some(gix_hash::Prefix::from_hex("abcdef1").unwrap()),
|
||||
"git does not see this as prefix anymore, we do"
|
||||
);
|
||||
assert_eq!(
|
||||
rec.prefix_hint[0], None,
|
||||
"This leaves room for improvement as we could assume that -dirty belongs to a revision, so this could be PrefixHint::MustBeCommit"
|
||||
);
|
||||
assert_eq!(rec.calls, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partial_format_lookalikes_are_never_considered() {
|
||||
let spec = "abcdef1-dirty-laundry";
|
||||
let rec = parse(spec);
|
||||
assert!(rec.kind.is_none());
|
||||
assert_eq!(rec.get_ref(0), spec);
|
||||
assert_eq!(rec.prefix[0], None);
|
||||
assert_eq!(rec.calls, 1, "we don't even try the prefix");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partial_format_with_dirty_suffix_lookalikes_are_treated_as_refs() {
|
||||
let spec = "abcdef1-dirty";
|
||||
let rec = try_parse_opts(
|
||||
spec,
|
||||
Options {
|
||||
reject_prefix: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert!(rec.kind.is_none());
|
||||
assert_eq!(rec.get_ref(0), spec);
|
||||
assert_eq!(rec.prefix[0], None);
|
||||
assert_eq!(rec.calls, 2);
|
||||
}
|
||||
66
src-revision/tests/revision/spec/parse/anchor/hash.rs
Normal file
66
src-revision/tests/revision/spec/parse/anchor/hash.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
use crate::spec::parse::{parse, try_parse_opts, Options};
|
||||
|
||||
#[test]
|
||||
fn short_hex_literals_are_considered_prefixes() {
|
||||
let rec = parse("abCD");
|
||||
assert!(rec.kind.is_none());
|
||||
assert_eq!(
|
||||
rec.find_ref[0], None,
|
||||
"references are not resolved if prefix lookups succeed"
|
||||
);
|
||||
assert_eq!(rec.prefix[0], Some(gix_hash::Prefix::from_hex("abcd").unwrap()));
|
||||
assert_eq!(rec.prefix_hint[0], None);
|
||||
assert_eq!(rec.calls, 1);
|
||||
|
||||
let rec = parse("gabcd123");
|
||||
assert!(rec.kind.is_none());
|
||||
assert_eq!(
|
||||
rec.get_ref(0),
|
||||
"gabcd123",
|
||||
"ref lookups are performed if it doesn't look like a hex sha"
|
||||
);
|
||||
assert_eq!(
|
||||
rec.prefix[0], None,
|
||||
"prefix lookups are not attempted at all (and they are impossible even)"
|
||||
);
|
||||
assert_eq!(rec.prefix_hint[0], None);
|
||||
assert_eq!(rec.calls, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unresolvable_hex_literals_are_resolved_as_refs() {
|
||||
let rec = try_parse_opts(
|
||||
"abCD",
|
||||
Options {
|
||||
reject_prefix: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert!(rec.kind.is_none());
|
||||
assert_eq!(rec.get_ref(0), "abCD");
|
||||
assert_eq!(rec.prefix[0], None);
|
||||
assert_eq!(rec.prefix_hint[0], None);
|
||||
assert_eq!(rec.calls, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hex_literals_that_are_too_long_are_resolved_as_refs() {
|
||||
let spec = "abcd123456789abcd123456789abcd123456789abcd123456789abcd123456789abcd123456789abcd123456789";
|
||||
let rec = try_parse_opts(
|
||||
spec,
|
||||
Options {
|
||||
reject_prefix: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert!(rec.kind.is_none());
|
||||
assert_eq!(rec.get_ref(0), spec);
|
||||
assert_eq!(rec.prefix[0], None);
|
||||
assert_eq!(rec.prefix_hint[0], None);
|
||||
assert_eq!(
|
||||
rec.calls, 1,
|
||||
"we can't create a prefix from it, hence only ref resolution is attempted"
|
||||
);
|
||||
}
|
||||
5
src-revision/tests/revision/spec/parse/anchor/mod.rs
Normal file
5
src-revision/tests/revision/spec/parse/anchor/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod at_symbol;
|
||||
mod colon_symbol;
|
||||
mod describe;
|
||||
mod hash;
|
||||
mod refnames;
|
||||
68
src-revision/tests/revision/spec/parse/anchor/refnames.rs
Normal file
68
src-revision/tests/revision/spec/parse/anchor/refnames.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
use crate::spec::parse::{parse, try_parse};
|
||||
|
||||
#[test]
|
||||
fn at_by_itself_is_shortcut_for_head() {
|
||||
let rec = parse("@");
|
||||
assert!(rec.kind.is_none());
|
||||
assert_eq!(rec.get_ref(0), "HEAD");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn at_is_allowed() {
|
||||
for name in ["a@b", "@branch", "branch@", "@@", "@inner@"] {
|
||||
let rec = parse(name);
|
||||
assert!(rec.kind.is_none());
|
||||
assert_eq!(rec.get_ref(0), name);
|
||||
assert_eq!(rec.find_ref[1], None);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn at_in_ranges_is_allowed() {
|
||||
let input = "@@@..";
|
||||
let rec = parse(input);
|
||||
assert_eq!(rec.kind, Some(gix_revision::spec::Kind::RangeBetween));
|
||||
assert_eq!(rec.get_ref(0), "@@@");
|
||||
assert_eq!(rec.get_ref(1), "HEAD");
|
||||
|
||||
let input = "@@...@@";
|
||||
let rec = parse(input);
|
||||
assert_eq!(rec.kind, Some(gix_revision::spec::Kind::ReachableToMergeBase));
|
||||
assert_eq!(rec.get_ref(0), "@@");
|
||||
assert_eq!(rec.get_ref(1), "@@");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strange_revspecs_do_not_panic() {
|
||||
let err = try_parse(".@.").unwrap_err();
|
||||
insta::assert_snapshot!(err, @"@ character must be standalone or followed by {<content>}: @.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refname_head() {
|
||||
let rec = parse("HEAD");
|
||||
assert!(rec.kind.is_none());
|
||||
assert_eq!(rec.get_ref(0), "HEAD");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refname_tag() {
|
||||
let spec = "v1.2.3.4-beta.1";
|
||||
let rec = parse(spec);
|
||||
assert!(rec.kind.is_none());
|
||||
assert_eq!(rec.get_ref(0), spec);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refname_with_head_prefix() {
|
||||
let rec = parse("HEADfake");
|
||||
assert!(rec.kind.is_none());
|
||||
assert_eq!(rec.get_ref(0), "HEADfake");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_head_ref_name() {
|
||||
let rec = parse("refs/heads/main");
|
||||
assert!(rec.kind.is_none());
|
||||
assert_eq!(rec.get_ref(0), "refs/heads/main");
|
||||
}
|
||||
425
src-revision/tests/revision/spec/parse/kind.rs
Normal file
425
src-revision/tests/revision/spec/parse/kind.rs
Normal file
@@ -0,0 +1,425 @@
|
||||
use crate::spec::parse::{try_parse, try_parse_opts, Options};
|
||||
|
||||
#[test]
|
||||
fn cannot_declare_ranges_multiple_times() {
|
||||
for invalid_spec in ["^HEAD..", "^HEAD..."] {
|
||||
let err = try_parse(invalid_spec).unwrap_err().into_inner();
|
||||
assert!(err.message.contains("cannot set spec kind more than once"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delegate_can_refuse_spec_kinds() {
|
||||
let err = try_parse_opts(
|
||||
"^HEAD",
|
||||
Options {
|
||||
reject_kind: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.unwrap_err()
|
||||
.into_inner();
|
||||
// Delegates can refuse spec kind changes to abort parsing early in case they want single-specs only
|
||||
insta::assert_snapshot!(err, @"delegate.kind(ExcludeReachable) failed");
|
||||
}
|
||||
|
||||
mod include_parents {
|
||||
use gix_revision::spec;
|
||||
|
||||
use crate::spec::parse::{kind::prefix, parse, try_parse, Call};
|
||||
|
||||
#[test]
|
||||
fn trailing_caret_at_symbol() {
|
||||
let rec = parse("HEAD^@");
|
||||
assert_eq!(rec.kind.unwrap(), spec::Kind::IncludeReachableFromParents);
|
||||
assert_eq!(rec.get_ref(0), "HEAD");
|
||||
assert_eq!(rec.prefix[0], None);
|
||||
assert_eq!(rec.order, [Call::FindRef, Call::Kind]);
|
||||
assert!(rec.done);
|
||||
|
||||
let rec = parse("abcd^@");
|
||||
assert_eq!(rec.kind.unwrap(), spec::Kind::IncludeReachableFromParents);
|
||||
assert_eq!(rec.prefix[0], prefix("abcd").into());
|
||||
assert_eq!(rec.order, [Call::DisambiguatePrefix, Call::Kind]);
|
||||
assert!(rec.done);
|
||||
|
||||
let rec = parse("r1^@");
|
||||
assert_eq!(rec.kind.unwrap(), spec::Kind::IncludeReachableFromParents);
|
||||
assert_eq!(rec.get_ref(0), "r1");
|
||||
assert_eq!(rec.prefix[0], None);
|
||||
assert_eq!(rec.order, [Call::FindRef, Call::Kind]);
|
||||
assert!(rec.done);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trailing_caret_exclamation_mark_must_end_the_input() {
|
||||
let err = try_parse("r1^@~1").unwrap_err().into_inner();
|
||||
assert!(err.message.contains("unconsumed input"));
|
||||
}
|
||||
}
|
||||
|
||||
mod exclude_parents {
|
||||
use gix_revision::spec;
|
||||
|
||||
use crate::spec::parse::{kind::prefix, parse, try_parse, Call};
|
||||
|
||||
#[test]
|
||||
fn freestanding() {
|
||||
let rec = parse("^!");
|
||||
assert_eq!(
|
||||
rec.kind,
|
||||
Some(gix_revision::spec::Kind::ExcludeReachable),
|
||||
"the delegate has to be able to deal with this"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trailing_caret_exclamation_mark() {
|
||||
let rec = parse("HEAD^!");
|
||||
assert_eq!(rec.kind.unwrap(), spec::Kind::ExcludeReachableFromParents);
|
||||
assert_eq!(rec.get_ref(0), "HEAD");
|
||||
assert_eq!(rec.prefix[0], None);
|
||||
assert_eq!(rec.order, [Call::FindRef, Call::Kind]);
|
||||
assert!(rec.done);
|
||||
|
||||
let rec = parse("abcd^!");
|
||||
assert_eq!(rec.kind.unwrap(), spec::Kind::ExcludeReachableFromParents);
|
||||
assert_eq!(rec.prefix[0], prefix("abcd").into());
|
||||
assert_eq!(rec.order, [Call::DisambiguatePrefix, Call::Kind]);
|
||||
assert!(rec.done);
|
||||
|
||||
let rec = parse("r1^!");
|
||||
assert_eq!(rec.kind.unwrap(), spec::Kind::ExcludeReachableFromParents);
|
||||
assert_eq!(rec.get_ref(0), "r1");
|
||||
assert_eq!(rec.prefix[0], None);
|
||||
assert_eq!(rec.order, [Call::FindRef, Call::Kind]);
|
||||
assert!(rec.done);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trailing_caret_exclamation_mark_must_end_the_input() {
|
||||
let err = try_parse("r1^!~1").unwrap_err().into_inner();
|
||||
assert!(err.message.contains("unconsumed input"));
|
||||
}
|
||||
}
|
||||
|
||||
mod exclusive {
|
||||
use gix_revision::spec;
|
||||
|
||||
use crate::spec::parse::{kind::prefix, parse};
|
||||
|
||||
#[test]
|
||||
fn freestanding() {
|
||||
let rec = parse("^");
|
||||
assert_eq!(
|
||||
rec.kind,
|
||||
Some(gix_revision::spec::Kind::ExcludeReachable),
|
||||
"the delegate has to be able to deal with this"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leading_caret() {
|
||||
let rec = parse("^HEAD");
|
||||
assert_eq!(rec.kind.unwrap(), spec::Kind::ExcludeReachable);
|
||||
assert_eq!(rec.get_ref(0), "HEAD");
|
||||
assert_eq!(rec.prefix[0], None);
|
||||
assert_eq!(rec.calls, 2);
|
||||
|
||||
let rec = parse("^abcd");
|
||||
assert_eq!(rec.kind.unwrap(), spec::Kind::ExcludeReachable);
|
||||
assert_eq!(rec.find_ref[0], None);
|
||||
assert_eq!(rec.prefix[0], prefix("abcd").into());
|
||||
assert_eq!(rec.calls, 2);
|
||||
|
||||
let rec = parse("^r1");
|
||||
assert_eq!(rec.kind.unwrap(), spec::Kind::ExcludeReachable);
|
||||
assert_eq!(rec.get_ref(0), "r1");
|
||||
assert_eq!(rec.prefix[0], None);
|
||||
assert_eq!(rec.calls, 2);
|
||||
|
||||
let rec = parse("^hello-0-gabcd-dirty");
|
||||
assert_eq!(rec.kind.unwrap(), spec::Kind::ExcludeReachable);
|
||||
assert_eq!(rec.find_ref[0], None);
|
||||
assert_eq!(rec.prefix[0], prefix("abcd").into());
|
||||
assert_eq!(rec.calls, 2);
|
||||
}
|
||||
}
|
||||
|
||||
mod range {
|
||||
use gix_revision::{spec, spec::parse::delegate::Traversal};
|
||||
|
||||
use crate::spec::parse::{kind::prefix, parse, try_parse, Call};
|
||||
|
||||
#[test]
|
||||
fn minus_with_n_omitted() {
|
||||
let rec = parse("r1^-");
|
||||
assert_eq!(rec.kind.unwrap(), spec::Kind::RangeBetween);
|
||||
assert_eq!(rec.get_ref(0), "r1");
|
||||
assert_eq!(rec.traversal, [Traversal::NthParent(1)], "default is 1");
|
||||
assert_eq!(rec.get_ref(1), "r1");
|
||||
assert_eq!(rec.prefix[0], None);
|
||||
assert_eq!(rec.order, [Call::FindRef, Call::Traverse, Call::Kind, Call::FindRef]);
|
||||
assert!(rec.done);
|
||||
|
||||
let rec = parse("@^-");
|
||||
assert_eq!(rec.kind.unwrap(), spec::Kind::RangeBetween);
|
||||
assert_eq!(rec.get_ref(0), "HEAD");
|
||||
assert_eq!(rec.traversal, [Traversal::NthParent(1)], "default is 1");
|
||||
assert_eq!(rec.get_ref(1), "HEAD");
|
||||
assert_eq!(rec.prefix[0], None);
|
||||
assert_eq!(rec.order, [Call::FindRef, Call::Traverse, Call::Kind, Call::FindRef]);
|
||||
assert!(rec.done);
|
||||
|
||||
let rec = parse("abcd^-");
|
||||
assert_eq!(rec.kind.unwrap(), spec::Kind::RangeBetween);
|
||||
assert_eq!(rec.prefix[0], prefix("abcd").into());
|
||||
assert_eq!(rec.traversal, [Traversal::NthParent(1)], "default is 1");
|
||||
assert_eq!(rec.prefix[1], prefix("abcd").into());
|
||||
assert_eq!(
|
||||
rec.order,
|
||||
[
|
||||
Call::DisambiguatePrefix,
|
||||
Call::Traverse,
|
||||
Call::Kind,
|
||||
Call::DisambiguatePrefix
|
||||
]
|
||||
);
|
||||
assert!(rec.done);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn minus_with_n() {
|
||||
let rec = parse("r1^-42");
|
||||
assert_eq!(rec.kind.unwrap(), spec::Kind::RangeBetween);
|
||||
assert_eq!(rec.get_ref(0), "r1");
|
||||
assert_eq!(rec.traversal, [Traversal::NthParent(42)]);
|
||||
assert_eq!(rec.get_ref(1), "r1");
|
||||
assert_eq!(rec.prefix[0], None);
|
||||
assert_eq!(rec.order, [Call::FindRef, Call::Traverse, Call::Kind, Call::FindRef]);
|
||||
assert!(rec.done);
|
||||
|
||||
let rec = parse("@^-42");
|
||||
assert_eq!(rec.kind.unwrap(), spec::Kind::RangeBetween);
|
||||
assert_eq!(rec.get_ref(0), "HEAD");
|
||||
assert_eq!(rec.traversal, [Traversal::NthParent(42)]);
|
||||
assert_eq!(rec.get_ref(1), "HEAD");
|
||||
assert_eq!(rec.prefix[0], None);
|
||||
assert_eq!(rec.order, [Call::FindRef, Call::Traverse, Call::Kind, Call::FindRef]);
|
||||
assert!(rec.done);
|
||||
|
||||
let rec = parse("abcd^-42");
|
||||
assert_eq!(rec.kind.unwrap(), spec::Kind::RangeBetween);
|
||||
assert_eq!(rec.prefix[0], prefix("abcd").into());
|
||||
assert_eq!(rec.traversal, [Traversal::NthParent(42)]);
|
||||
assert_eq!(rec.prefix[1], prefix("abcd").into());
|
||||
assert_eq!(
|
||||
rec.order,
|
||||
[
|
||||
Call::DisambiguatePrefix,
|
||||
Call::Traverse,
|
||||
Call::Kind,
|
||||
Call::DisambiguatePrefix
|
||||
]
|
||||
);
|
||||
assert!(rec.done);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn minus_with_n_omitted_has_to_end_there() {
|
||||
let err = try_parse("r1^-^").unwrap_err().into_inner();
|
||||
assert!(err.message.contains("unconsumed input"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn minus_with_n_has_to_end_there() {
|
||||
let err = try_parse("r1^-42^").unwrap_err().into_inner();
|
||||
assert!(err.message.contains("unconsumed input"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn minus_with_n_has_to_end_there_and_handle_range_suffix() {
|
||||
let err = try_parse("r1^-42..").unwrap_err().into_inner();
|
||||
assert!(err.message.contains("unconsumed input"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn minus_with_n_omitted_has_to_end_there_and_handle_range_suffix() {
|
||||
let err = try_parse("r1^-..").unwrap_err().into_inner();
|
||||
assert!(err.message.contains("unconsumed input"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn freestanding_dot_dot() {
|
||||
let rec = parse("..");
|
||||
assert_eq!(
|
||||
rec.kind,
|
||||
Some(gix_revision::spec::Kind::RangeBetween),
|
||||
"the delegate has to be able to deal with this"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trailing_dot_dot() {
|
||||
let rec = parse("r1..");
|
||||
assert_eq!(rec.kind.unwrap(), spec::Kind::RangeBetween);
|
||||
assert_eq!(rec.get_ref(0), "r1");
|
||||
assert_eq!(rec.get_ref(1), "HEAD");
|
||||
assert_eq!(rec.prefix[0], None);
|
||||
assert_eq!(rec.order, [Call::FindRef, Call::Kind, Call::FindRef]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leading_dot_dot() {
|
||||
let rec = parse("..r2");
|
||||
assert_eq!(rec.kind.unwrap(), spec::Kind::RangeBetween);
|
||||
assert_eq!(rec.get_ref(0), "HEAD");
|
||||
assert_eq!(rec.get_ref(1), "r2");
|
||||
assert_eq!(rec.prefix[0], None);
|
||||
assert_eq!(rec.order, [Call::FindRef, Call::Kind, Call::FindRef]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn middle_dot_dot() {
|
||||
let rec = parse("@..r2");
|
||||
assert_eq!(rec.kind.unwrap(), spec::Kind::RangeBetween);
|
||||
assert_eq!(rec.get_ref(0), "HEAD");
|
||||
assert_eq!(rec.get_ref(1), "r2");
|
||||
assert_eq!(rec.calls, 3);
|
||||
|
||||
let rec = parse("r1..r2");
|
||||
assert_eq!(rec.kind.unwrap(), spec::Kind::RangeBetween);
|
||||
assert_eq!(rec.get_ref(0), "r1");
|
||||
assert_eq!(rec.get_ref(1), "r2");
|
||||
assert_eq!(rec.calls, 3);
|
||||
|
||||
let rec = parse("abcd..1234");
|
||||
assert_eq!(rec.kind.unwrap(), spec::Kind::RangeBetween);
|
||||
assert_eq!(rec.prefix[0], prefix("abcd").into());
|
||||
assert_eq!(rec.prefix[1], prefix("1234").into());
|
||||
assert_eq!(rec.calls, 3);
|
||||
|
||||
let rec = parse("r1..abcd");
|
||||
assert_eq!(rec.kind.unwrap(), spec::Kind::RangeBetween);
|
||||
assert_eq!(rec.get_ref(0), "r1");
|
||||
assert_eq!(rec.prefix[0], prefix("abcd").into());
|
||||
assert_eq!(rec.calls, 3);
|
||||
|
||||
let rec = parse("abcd-dirty..v1.2-42-g1234");
|
||||
assert_eq!(rec.kind.unwrap(), spec::Kind::RangeBetween);
|
||||
assert_eq!(rec.find_ref[0], None);
|
||||
assert_eq!(rec.prefix[0], prefix("abcd").into());
|
||||
assert_eq!(rec.prefix[1], prefix("1234").into());
|
||||
assert_eq!(rec.calls, 3);
|
||||
|
||||
let rec = parse("v1.2-42-g1234..abcd-dirty");
|
||||
assert_eq!(rec.kind.unwrap(), spec::Kind::RangeBetween);
|
||||
assert_eq!(rec.find_ref[0], None);
|
||||
assert_eq!(rec.prefix[0], prefix("1234").into());
|
||||
assert_eq!(rec.prefix[1], prefix("abcd").into());
|
||||
assert_eq!(rec.calls, 3);
|
||||
|
||||
let rec = parse("v1.2.4@{1}~~^10..r1@{2}~10^2");
|
||||
assert_eq!(rec.kind.unwrap(), spec::Kind::RangeBetween);
|
||||
assert_eq!(rec.get_ref(0), "v1.2.4");
|
||||
assert_eq!(rec.get_ref(1), "r1");
|
||||
assert_eq!(&rec.prefix, &[None, None]);
|
||||
assert_eq!(
|
||||
rec.traversal,
|
||||
[
|
||||
Traversal::NthAncestor(1),
|
||||
Traversal::NthAncestor(1),
|
||||
Traversal::NthParent(10),
|
||||
Traversal::NthAncestor(10),
|
||||
Traversal::NthParent(2)
|
||||
]
|
||||
);
|
||||
assert_eq!(rec.calls, 10);
|
||||
assert!(rec.done);
|
||||
}
|
||||
}
|
||||
|
||||
mod mergebase {
|
||||
use gix_revision::{spec, spec::parse::delegate::Traversal};
|
||||
|
||||
use crate::spec::parse::{kind::prefix, parse};
|
||||
|
||||
#[test]
|
||||
fn freestanding_dot_dot_dot() {
|
||||
let rec = parse("...");
|
||||
assert_eq!(
|
||||
rec.kind,
|
||||
Some(gix_revision::spec::Kind::ReachableToMergeBase),
|
||||
"the delegate has to be able to deal with this"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trailing_dot_dot_dot() {
|
||||
let rec = parse("HEAD...");
|
||||
assert_eq!(rec.kind.unwrap(), spec::Kind::ReachableToMergeBase);
|
||||
assert_eq!(rec.get_ref(0), "HEAD");
|
||||
assert_eq!(rec.prefix[0], None);
|
||||
assert_eq!(rec.calls, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leading_dot_dot_dot() {
|
||||
let rec = parse("...r2");
|
||||
assert_eq!(rec.kind.unwrap(), spec::Kind::ReachableToMergeBase);
|
||||
assert_eq!(rec.get_ref(0), "HEAD");
|
||||
assert_eq!(rec.get_ref(1), "r2");
|
||||
assert_eq!(rec.prefix[0], None);
|
||||
assert_eq!(rec.calls, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn middle_dot_dot_dot() {
|
||||
let rec = parse("HEAD...@");
|
||||
assert_eq!(rec.kind.unwrap(), spec::Kind::ReachableToMergeBase);
|
||||
assert_eq!(rec.get_ref(0), "HEAD");
|
||||
assert_eq!(rec.get_ref(1), "HEAD");
|
||||
assert_eq!(rec.calls, 3);
|
||||
|
||||
let rec = parse("@...HEAD");
|
||||
assert_eq!(rec.kind.unwrap(), spec::Kind::ReachableToMergeBase);
|
||||
assert_eq!(rec.get_ref(0), "HEAD");
|
||||
assert_eq!(rec.get_ref(1), "HEAD");
|
||||
assert_eq!(rec.calls, 3);
|
||||
|
||||
let rec = parse("r1...abcd");
|
||||
assert_eq!(rec.kind.unwrap(), spec::Kind::ReachableToMergeBase);
|
||||
assert_eq!(rec.get_ref(0), "r1");
|
||||
assert_eq!(rec.prefix[0], prefix("abcd").into());
|
||||
assert_eq!(rec.calls, 3);
|
||||
|
||||
let rec = parse("v1.2.3-beta.1-42-g1234-dirty...abcd-dirty");
|
||||
assert_eq!(rec.kind.unwrap(), spec::Kind::ReachableToMergeBase);
|
||||
assert_eq!(rec.find_ref[0], None);
|
||||
assert_eq!(rec.prefix[0], prefix("1234").into());
|
||||
assert_eq!(rec.prefix[1], prefix("abcd").into());
|
||||
assert_eq!(rec.calls, 3);
|
||||
|
||||
let rec = parse("r1@{1}~~^10...@{2}~10^2");
|
||||
assert_eq!(rec.kind.unwrap(), spec::Kind::ReachableToMergeBase);
|
||||
assert_eq!(rec.get_ref(0), "r1");
|
||||
assert_eq!(rec.find_ref[1], None, "HEAD is implied");
|
||||
assert_eq!(&rec.prefix, &[None, None]);
|
||||
assert_eq!(
|
||||
rec.traversal,
|
||||
[
|
||||
Traversal::NthAncestor(1),
|
||||
Traversal::NthAncestor(1),
|
||||
Traversal::NthParent(10),
|
||||
Traversal::NthAncestor(10),
|
||||
Traversal::NthParent(2)
|
||||
]
|
||||
);
|
||||
assert_eq!(rec.calls, 9);
|
||||
assert!(rec.done);
|
||||
}
|
||||
}
|
||||
|
||||
fn prefix(hex: &str) -> gix_hash::Prefix {
|
||||
gix_hash::Prefix::from_hex(hex).unwrap()
|
||||
}
|
||||
273
src-revision/tests/revision/spec/parse/mod.rs
Normal file
273
src-revision/tests/revision/spec/parse/mod.rs
Normal file
@@ -0,0 +1,273 @@
|
||||
use gix_error::{bail, message, ErrorExt, Exn};
|
||||
use gix_object::bstr::{BStr, BString};
|
||||
use gix_revision::{
|
||||
spec,
|
||||
spec::parse::{delegate, Delegate},
|
||||
};
|
||||
|
||||
#[derive(Default, Debug, Eq, PartialEq, Ord, PartialOrd)]
|
||||
struct Options {
|
||||
reject_kind: bool,
|
||||
reject_prefix: bool,
|
||||
no_internal_assertions: bool,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Eq, PartialEq, Ord, PartialOrd)]
|
||||
struct Recorder {
|
||||
// anchors
|
||||
find_ref: [Option<BString>; 2],
|
||||
prefix: [Option<gix_hash::Prefix>; 2],
|
||||
prefix_hint: [Option<PrefixHintOwned>; 2],
|
||||
current_branch_reflog_entry: [Option<String>; 2],
|
||||
nth_checked_out_branch: [Option<usize>; 2],
|
||||
sibling_branch: [Option<String>; 2],
|
||||
index_lookups: Vec<(BString, u8)>,
|
||||
|
||||
// navigation
|
||||
traversal: Vec<delegate::Traversal>,
|
||||
peel_to: Vec<PeelToOwned>,
|
||||
patterns: Vec<(BString, bool)>,
|
||||
|
||||
// range
|
||||
kind: Option<spec::Kind>,
|
||||
|
||||
order: Vec<Call>,
|
||||
calls: usize,
|
||||
opts: Options,
|
||||
done: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
|
||||
pub enum Call {
|
||||
FindRef,
|
||||
DisambiguatePrefix,
|
||||
Reflog,
|
||||
NthCheckedOutBranch,
|
||||
SiblingBranch,
|
||||
Traverse,
|
||||
PeelUntil,
|
||||
Find,
|
||||
IndexLookup,
|
||||
Kind,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
|
||||
pub enum PeelToOwned {
|
||||
ObjectKind(gix_object::Kind),
|
||||
ExistingObject,
|
||||
RecursiveTagObject,
|
||||
Path(BString),
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
|
||||
pub enum PrefixHintOwned {
|
||||
MustBeCommit,
|
||||
DescribeAnchor { ref_name: BString, generation: usize },
|
||||
}
|
||||
|
||||
impl Recorder {
|
||||
fn with(options: Options) -> Self {
|
||||
Recorder {
|
||||
opts: options,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn get_ref(&self, idx: usize) -> &BStr {
|
||||
self.find_ref[idx].as_ref().map(AsRef::as_ref).unwrap()
|
||||
}
|
||||
|
||||
fn called(&mut self, f: Call) {
|
||||
self.calls += 1;
|
||||
self.order.push(f);
|
||||
}
|
||||
}
|
||||
|
||||
fn set_val<T: std::fmt::Debug>(fn_name: &str, store: &mut [Option<T>; 2], val: T) -> Result<(), Exn> {
|
||||
for entry in store.iter_mut() {
|
||||
if entry.is_none() {
|
||||
*entry = Some(val);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
panic!("called {fn_name}() more than twice with '{val:?}'");
|
||||
}
|
||||
|
||||
impl delegate::Revision for Recorder {
|
||||
fn find_ref(&mut self, input: &BStr) -> Result<(), Exn> {
|
||||
self.called(Call::FindRef);
|
||||
set_val("find_ref", &mut self.find_ref, input.into())
|
||||
}
|
||||
|
||||
fn disambiguate_prefix(
|
||||
&mut self,
|
||||
input: gix_hash::Prefix,
|
||||
hint: Option<delegate::PrefixHint<'_>>,
|
||||
) -> Result<(), Exn> {
|
||||
self.called(Call::DisambiguatePrefix);
|
||||
if self.opts.reject_prefix {
|
||||
bail!(message!("disambiguate_prefix rejected").raise_erased());
|
||||
}
|
||||
set_val("disambiguate_prefix", &mut self.prefix, input)?;
|
||||
if let Some(hint) = hint {
|
||||
set_val(
|
||||
"disambiguate_prefix",
|
||||
&mut self.prefix_hint,
|
||||
match hint {
|
||||
delegate::PrefixHint::DescribeAnchor { ref_name, generation } => PrefixHintOwned::DescribeAnchor {
|
||||
ref_name: ref_name.into(),
|
||||
generation,
|
||||
},
|
||||
delegate::PrefixHint::MustBeCommit => PrefixHintOwned::MustBeCommit,
|
||||
},
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn reflog(&mut self, entry: delegate::ReflogLookup) -> Result<(), Exn> {
|
||||
self.called(Call::Reflog);
|
||||
set_val(
|
||||
"current_branch_reflog",
|
||||
&mut self.current_branch_reflog_entry,
|
||||
match entry {
|
||||
delegate::ReflogLookup::Entry(no) => no.to_string(),
|
||||
delegate::ReflogLookup::Date(time) => {
|
||||
let mut buf = Vec::new();
|
||||
time.write_to(&mut buf).unwrap();
|
||||
BString::from(buf).to_string()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn nth_checked_out_branch(&mut self, branch: usize) -> Result<(), Exn> {
|
||||
assert_ne!(branch, 0);
|
||||
self.called(Call::NthCheckedOutBranch);
|
||||
set_val("nth_checked_out_branch", &mut self.nth_checked_out_branch, branch)
|
||||
}
|
||||
|
||||
fn sibling_branch(&mut self, kind: delegate::SiblingBranch) -> Result<(), Exn> {
|
||||
self.called(Call::SiblingBranch);
|
||||
set_val("sibling_branch", &mut self.sibling_branch, format!("{kind:?}"))
|
||||
}
|
||||
}
|
||||
|
||||
impl delegate::Navigate for Recorder {
|
||||
fn traverse(&mut self, kind: delegate::Traversal) -> Result<(), Exn> {
|
||||
self.called(Call::Traverse);
|
||||
self.traversal.push(kind);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn peel_until(&mut self, kind: delegate::PeelTo) -> Result<(), Exn> {
|
||||
self.called(Call::PeelUntil);
|
||||
self.peel_to.push(match kind {
|
||||
delegate::PeelTo::ObjectKind(kind) => PeelToOwned::ObjectKind(kind),
|
||||
delegate::PeelTo::ValidObject => PeelToOwned::ExistingObject,
|
||||
delegate::PeelTo::Path(path) => PeelToOwned::Path(path.into()),
|
||||
delegate::PeelTo::RecursiveTagObject => PeelToOwned::RecursiveTagObject,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn find(&mut self, regex: &BStr, negated: bool) -> Result<(), Exn> {
|
||||
self.called(Call::Find);
|
||||
self.patterns.push((regex.into(), negated));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn index_lookup(&mut self, path: &BStr, stage: u8) -> Result<(), Exn> {
|
||||
self.called(Call::IndexLookup);
|
||||
self.index_lookups.push((path.into(), stage));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl delegate::Kind for Recorder {
|
||||
fn kind(&mut self, kind: spec::Kind) -> Result<(), Exn> {
|
||||
self.called(Call::Kind);
|
||||
if self.opts.reject_kind {
|
||||
bail!(message!("kind() was rejected").raise_erased());
|
||||
}
|
||||
if self.kind.is_none() {
|
||||
self.kind = Some(kind);
|
||||
} else if !self.opts.no_internal_assertions {
|
||||
panic!("called kind more than once with '{kind:?}'");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Delegate for Recorder {
|
||||
fn done(&mut self) -> Result<(), Exn> {
|
||||
self.done = true;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn parse(spec: &str) -> Recorder {
|
||||
try_parse_opts(spec, Options::default()).unwrap()
|
||||
}
|
||||
|
||||
fn try_parse(spec: &str) -> Result<Recorder, Exn<spec::parse::Error>> {
|
||||
try_parse_opts(spec, Default::default())
|
||||
}
|
||||
|
||||
fn try_parse_opts(spec: &str, options: Options) -> Result<Recorder, Exn<spec::parse::Error>> {
|
||||
let mut rec = Recorder::with(options);
|
||||
spec::parse(spec.into(), &mut rec)?;
|
||||
Ok(rec)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_specs_are_valid() {
|
||||
// they should of course be invalid for the delegate. CLIs may pre-process the input as well if they wish
|
||||
// but git itself doesn't do that.
|
||||
for spec in [" ", "\n\t"] {
|
||||
let rec = parse(spec);
|
||||
assert_eq!(rec.calls, 1);
|
||||
}
|
||||
let rec = parse("");
|
||||
assert_eq!(rec.calls, 0, "but we do not bother to call the delegate with nothing");
|
||||
assert!(rec.done);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_characters_are_taken_verbatim_which_includes_whitespace() {
|
||||
let spec = " HEAD \n";
|
||||
let rec = parse(spec);
|
||||
assert!(rec.kind.is_none());
|
||||
assert_eq!(rec.get_ref(0), spec);
|
||||
}
|
||||
|
||||
mod fuzz {
|
||||
use crate::spec::parse::{try_parse_opts, Options};
|
||||
|
||||
#[test]
|
||||
fn failures() {
|
||||
for spec in [
|
||||
"@{6255520 day ago}: ",
|
||||
"|^--",
|
||||
"^^-^",
|
||||
"^^-",
|
||||
":/!-",
|
||||
"A6a^-09223372036854775808",
|
||||
"^^^^^^-(",
|
||||
] {
|
||||
drop(
|
||||
try_parse_opts(
|
||||
spec,
|
||||
Options {
|
||||
no_internal_assertions: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.unwrap_err(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
mod anchor;
|
||||
mod kind;
|
||||
mod navigate;
|
||||
255
src-revision/tests/revision/spec/parse/navigate/caret_symbol.rs
Normal file
255
src-revision/tests/revision/spec/parse/navigate/caret_symbol.rs
Normal file
@@ -0,0 +1,255 @@
|
||||
use gix_revision::spec::parse::delegate::Traversal;
|
||||
|
||||
use crate::spec::parse::{parse, try_parse, PeelToOwned as PeelTo};
|
||||
|
||||
#[test]
|
||||
fn single_is_first_parent() {
|
||||
let rec = parse("@^");
|
||||
|
||||
assert!(rec.kind.is_none());
|
||||
assert_eq!(rec.get_ref(0), "HEAD");
|
||||
assert_eq!(rec.prefix[0], None);
|
||||
assert_eq!(rec.traversal[0], Traversal::NthParent(1));
|
||||
assert_eq!(rec.calls, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_calls_stack() {
|
||||
let rec = parse("@^^^10^0^{tag}^020");
|
||||
|
||||
assert!(rec.kind.is_none());
|
||||
assert_eq!(rec.get_ref(0), "HEAD");
|
||||
assert_eq!(rec.prefix[0], None);
|
||||
assert_eq!(
|
||||
rec.traversal,
|
||||
vec![
|
||||
Traversal::NthParent(1),
|
||||
Traversal::NthParent(1),
|
||||
Traversal::NthParent(10),
|
||||
Traversal::NthParent(20),
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
rec.peel_to,
|
||||
vec![
|
||||
PeelTo::ObjectKind(gix_object::Kind::Commit),
|
||||
PeelTo::ObjectKind(gix_object::Kind::Tag)
|
||||
]
|
||||
);
|
||||
assert_eq!(rec.calls, 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn followed_by_zero_is_peeling_to_commit() {
|
||||
let rec = parse("@^0");
|
||||
|
||||
assert!(rec.kind.is_none());
|
||||
assert_eq!(rec.get_ref(0), "HEAD");
|
||||
assert_eq!(rec.prefix[0], None);
|
||||
assert_eq!(rec.traversal.len(), 0, "traversals by parent are never zero");
|
||||
assert_eq!(
|
||||
rec.peel_to,
|
||||
vec![PeelTo::ObjectKind(gix_object::Kind::Commit)],
|
||||
"instead 0 serves as shortcut"
|
||||
);
|
||||
assert_eq!(rec.calls, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicitly_positive_numbers_are_invalid() {
|
||||
let err = try_parse("@^+1").unwrap_err().into_inner();
|
||||
assert_eq!(err.input.as_ref().map(AsRef::as_ref), Some(b"+1".as_ref()));
|
||||
assert!(err.message.contains("positive numbers are invalid"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_parent_number() {
|
||||
for (spec, expected) in [
|
||||
("HEAD^1", 1),
|
||||
("abcd^10", 10),
|
||||
("v1.3.4^123", 123),
|
||||
("v1.3.4-12-g1234^1000", 1000),
|
||||
] {
|
||||
let rec = parse(spec);
|
||||
|
||||
assert!(rec.kind.is_none());
|
||||
assert!(rec.find_ref[0].as_ref().is_some() || rec.prefix[0].is_some());
|
||||
assert_eq!(rec.traversal, vec![Traversal::NthParent(expected)]);
|
||||
assert_eq!(rec.calls, 2);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn peel_to_object_type() {
|
||||
for (spec, expected) in [
|
||||
("HEAD^{commit}", PeelTo::ObjectKind(gix_object::Kind::Commit)),
|
||||
("abcd^{tree}", PeelTo::ObjectKind(gix_object::Kind::Tree)),
|
||||
("v1.3.4^{blob}", PeelTo::ObjectKind(gix_object::Kind::Blob)),
|
||||
("v1.3.4-12-g1234^{tag}", PeelTo::ObjectKind(gix_object::Kind::Tag)),
|
||||
("v1.3.4-12-g1234^{object}", PeelTo::ExistingObject),
|
||||
] {
|
||||
let rec = parse(spec);
|
||||
|
||||
assert!(rec.kind.is_none());
|
||||
assert!(rec.find_ref[0].as_ref().is_some() || rec.prefix[0].is_some());
|
||||
assert_eq!(rec.peel_to, vec![expected]);
|
||||
assert_eq!(rec.calls, 2);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn regex_backslash_rules() {
|
||||
for (spec, regex, msg) in [
|
||||
(
|
||||
r#"@^{/with count{1}}"#,
|
||||
r#"with count{1}"#,
|
||||
"matching inner parens do not need escaping",
|
||||
),
|
||||
(
|
||||
r"@^{/with count\{1\}}",
|
||||
r#"with count{1}"#,
|
||||
"escaped parens are entirely ignored",
|
||||
),
|
||||
(r"@^{/1\}}", r#"1}"#, "unmatched closing parens need to be escaped"),
|
||||
(r"@^{/2\{}", r#"2{"#, "unmatched opening parens need to be escaped"),
|
||||
(
|
||||
r"@^{/3{\{}}",
|
||||
r#"3{{}"#,
|
||||
"unmatched nested opening parens need to be escaped",
|
||||
),
|
||||
(
|
||||
r"@^{/4{\}}}",
|
||||
r#"4{}}"#,
|
||||
"unmatched nested closing parens need to be escaped",
|
||||
),
|
||||
(r"@^{/a\b\c}", r"a\b\c", "single backslashes do not need to be escaped"),
|
||||
(
|
||||
r"@^{/a\b\c\\}",
|
||||
r"a\b\c\",
|
||||
"single backslashes do not need to be escaped, trailing",
|
||||
),
|
||||
(
|
||||
r"@^{/a\\b\\c\\}",
|
||||
r"a\b\c\",
|
||||
"backslashes can be escaped nonetheless, trailing",
|
||||
),
|
||||
(
|
||||
r"@^{/5\\{}}",
|
||||
r"5\{}",
|
||||
"backslashes in front of parens must be escaped or they would unbalance the brace pair",
|
||||
),
|
||||
] {
|
||||
let rec = try_parse(spec).expect(msg);
|
||||
|
||||
assert!(rec.kind.is_none());
|
||||
assert!(rec.find_ref[0].as_ref().is_some() || rec.prefix[0].is_some());
|
||||
assert_eq!(rec.patterns, vec![(regex.into(), false)], "{msg}");
|
||||
assert_eq!(rec.calls, 2);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn regex_with_revision_starting_point_and_negation() {
|
||||
for (spec, (regex, negated)) in [
|
||||
("HEAD^{/simple}", ("simple", false)),
|
||||
("abcd^{/!-negated}", ("negated", true)),
|
||||
("v1.3.4^{/^from start}", ("^from start", false)),
|
||||
(
|
||||
"v1.3.4-12-g1234^{/!!leading exclamation mark}",
|
||||
("!leading exclamation mark", false),
|
||||
),
|
||||
("v1.3.4-12-g1234^{/with count{1}}", ("with count{1}", false)),
|
||||
] {
|
||||
let rec = parse(spec);
|
||||
|
||||
assert!(rec.kind.is_none());
|
||||
assert!(rec.find_ref[0].as_ref().is_some() || rec.prefix[0].is_some());
|
||||
assert_eq!(rec.patterns, vec![(regex.into(), negated)]);
|
||||
assert_eq!(rec.calls, 2);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_braces_deref_a_tag() {
|
||||
let rec = parse("v1.2^{}");
|
||||
|
||||
assert!(rec.kind.is_none());
|
||||
assert_eq!(rec.get_ref(0), "v1.2");
|
||||
assert_eq!(rec.peel_to, vec![PeelTo::RecursiveTagObject]);
|
||||
assert_eq!(rec.calls, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_object_type() {
|
||||
let err = try_parse("@^{invalid}").unwrap_err().into_inner();
|
||||
assert_eq!(err.input.as_ref().map(AsRef::as_ref), Some(b"invalid".as_ref()));
|
||||
assert!(err.message.contains("cannot peel"));
|
||||
|
||||
let err = try_parse("@^{Commit}").unwrap_err().into_inner();
|
||||
assert!(
|
||||
err.input.as_ref().map(AsRef::as_ref) == Some(b"Commit".as_ref()) && err.message.contains("cannot peel"),
|
||||
"these types are case sensitive"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_caret_without_previous_refname() {
|
||||
let rec = parse(r"^^");
|
||||
assert_eq!(rec.calls, 2);
|
||||
assert_eq!(rec.kind, Some(gix_revision::spec::Kind::ExcludeReachable));
|
||||
assert_eq!(
|
||||
rec.traversal,
|
||||
[Traversal::NthParent(1)],
|
||||
"This can trip off an implementation as it's actually invalid, but looks valid"
|
||||
);
|
||||
|
||||
for revspec in ["^^^HEAD", "^^HEAD"] {
|
||||
let err = try_parse(revspec).unwrap_err().into_inner();
|
||||
assert_eq!(err.input.as_ref().map(AsRef::as_ref), Some(b"HEAD".as_ref()));
|
||||
assert!(err.message.contains("unconsumed input"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn incomplete_escaped_braces_in_regex_are_invalid() {
|
||||
let err = try_parse(r"@^{/a\{1}}").unwrap_err().into_inner();
|
||||
assert_eq!(err.input.as_ref().map(AsRef::as_ref), Some(b"}".as_ref()));
|
||||
assert!(err.message.contains("unconsumed input"));
|
||||
|
||||
let err = try_parse(r"@^{/a{1\}}").unwrap_err().into_inner();
|
||||
assert!(
|
||||
err.input.as_ref().map(AsRef::as_ref) == Some(br"{/a{1\}}".as_ref()) && err.message.contains("unclosed brace")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn regex_with_empty_exclamation_mark_prefix_is_invalid() {
|
||||
let err = try_parse(r#"@^{/!hello}"#).unwrap_err().into_inner();
|
||||
assert_eq!(err.input.as_ref().map(AsRef::as_ref), Some(b"!hello".as_ref()));
|
||||
assert!(err.message.contains("need one character after"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_escapes_can_cause_brace_mismatch() {
|
||||
let err = try_parse(r"@^{\}").unwrap_err().into_inner();
|
||||
assert!(err.input.as_ref().map(AsRef::as_ref) == Some(br"{\}".as_ref()) && err.message.contains("unclosed brace"));
|
||||
|
||||
let err = try_parse(r"@^{{\}}").unwrap_err().into_inner();
|
||||
// The raw string r"{{\}}" contains actual backslashes, so the input would be r"{{\}}"
|
||||
assert!(
|
||||
err.input.as_ref().map(AsRef::as_ref) == Some(br"{{\}}".as_ref()) && err.message.contains("unclosed brace")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_top_revision_regex_are_skipped_as_they_match_everything() {
|
||||
let rec = parse("@^{/}");
|
||||
|
||||
assert!(rec.kind.is_none());
|
||||
assert_eq!(rec.get_ref(0), "HEAD");
|
||||
assert!(
|
||||
rec.patterns.is_empty(),
|
||||
"The delegate won't be called with empty regexes"
|
||||
);
|
||||
assert_eq!(rec.calls, 1);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
use gix_revision::spec::parse::delegate::Traversal;
|
||||
|
||||
use crate::spec::parse::{parse, PeelToOwned as PeelTo};
|
||||
|
||||
#[test]
|
||||
fn paths_consume_all_remaining_input_as_they_refer_to_blobs() {
|
||||
let rec = parse("@:../relative/path...@^^~~");
|
||||
|
||||
assert!(rec.kind.is_none());
|
||||
assert_eq!(rec.get_ref(0), "HEAD");
|
||||
assert_eq!(rec.prefix[0], None);
|
||||
assert_eq!(rec.traversal.len(), 0);
|
||||
assert_eq!(rec.peel_to, vec![PeelTo::Path("../relative/path...@^^~~".into())]);
|
||||
assert_eq!(rec.calls, 2);
|
||||
|
||||
let rec = parse("@:absolute/path^{object}");
|
||||
assert_eq!(
|
||||
rec.peel_to,
|
||||
vec![PeelTo::Path("absolute/path^{object}".into())],
|
||||
"this includes useful navigation like object-existence, a shortcoming git shares, proper implementation needs escaping as well."
|
||||
);
|
||||
|
||||
let rec = parse("@:absolute/path^{tree}");
|
||||
assert_eq!(
|
||||
rec.peel_to,
|
||||
vec![PeelTo::Path("absolute/path^{tree}".into())],
|
||||
"this includes useful navigation like assertion of trees/blobs, we may make this possible in future but for now are as open as git"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_paths_refer_to_the_root_tree() {
|
||||
let rec = parse("@:");
|
||||
|
||||
assert!(rec.kind.is_none());
|
||||
assert_eq!(rec.get_ref(0), "HEAD");
|
||||
assert_eq!(rec.peel_to, vec![PeelTo::Path("".into())]);
|
||||
assert_eq!(rec.calls, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn paths_have_to_be_last_but_stack_with_other_navigation() {
|
||||
let rec = parse("HEAD@{1}~10^2^{commit}:README.md");
|
||||
|
||||
assert!(rec.kind.is_none());
|
||||
assert_eq!(rec.get_ref(0), "HEAD");
|
||||
assert_eq!(rec.current_branch_reflog_entry[0], Some("1".to_string()));
|
||||
assert_eq!(rec.traversal, vec![Traversal::NthAncestor(10), Traversal::NthParent(2)]);
|
||||
assert_eq!(
|
||||
rec.peel_to,
|
||||
vec![
|
||||
PeelTo::ObjectKind(gix_object::Kind::Commit),
|
||||
PeelTo::Path("README.md".into())
|
||||
]
|
||||
);
|
||||
assert_eq!(rec.calls, 6);
|
||||
}
|
||||
3
src-revision/tests/revision/spec/parse/navigate/mod.rs
Normal file
3
src-revision/tests/revision/spec/parse/navigate/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
mod caret_symbol;
|
||||
mod colon_symbol;
|
||||
mod tilde_symbol;
|
||||
@@ -0,0 +1,49 @@
|
||||
use gix_revision::spec::parse::delegate::Traversal;
|
||||
|
||||
use crate::spec::parse::{parse, try_parse};
|
||||
|
||||
#[test]
|
||||
fn without_anchor_is_invalid() {
|
||||
let err = try_parse("~").unwrap_err().into_inner();
|
||||
assert!(err.message.contains("tilde needs to follow an anchor"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_is_first_ancestor() {
|
||||
let rec = parse("@~");
|
||||
|
||||
assert!(rec.kind.is_none());
|
||||
assert_eq!(rec.get_ref(0), "HEAD");
|
||||
assert_eq!(rec.prefix[0], None);
|
||||
assert_eq!(rec.traversal[0], Traversal::NthAncestor(1));
|
||||
assert_eq!(rec.calls, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn followed_by_zero_is_no_op() {
|
||||
let rec = parse("@~0");
|
||||
|
||||
assert!(rec.kind.is_none());
|
||||
assert_eq!(rec.get_ref(0), "HEAD");
|
||||
assert_eq!(rec.prefix[0], None);
|
||||
assert_eq!(rec.calls, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_calls_stack() {
|
||||
let rec = parse("@~~~10~0~020");
|
||||
|
||||
assert!(rec.kind.is_none());
|
||||
assert_eq!(rec.get_ref(0), "HEAD");
|
||||
assert_eq!(rec.prefix[0], None);
|
||||
assert_eq!(
|
||||
rec.traversal,
|
||||
vec![
|
||||
Traversal::NthAncestor(1),
|
||||
Traversal::NthAncestor(1),
|
||||
Traversal::NthAncestor(10),
|
||||
Traversal::NthAncestor(20),
|
||||
]
|
||||
);
|
||||
assert_eq!(rec.calls, 5);
|
||||
}
|
||||
Reference in New Issue
Block a user