mirror of
https://github.com/jj-vcs/jj.git
synced 2025-12-23 06:01:01 +00:00
git: add function to extract positive/negative refspecs from StringExpression
This allows us to express negative refspecs in jj's syntax. We'll need something like `git.fetch-tags = <patterns>` to fetch tags into remote views and merge them accordingly #7528. The default tag patterns can't be set to .git/config because Git processes remote tags differently.
This commit is contained in:
parent
8615362689
commit
d9be0dbfeb
1 changed files with 188 additions and 0 deletions
188
lib/src/git.rs
188
lib/src/git.rs
|
|
@ -2310,6 +2310,110 @@ pub fn expand_fetch_refspecs(
|
|||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum GitRefExpressionError {
|
||||
#[error("Cannot use `~` in sub expression")]
|
||||
NestedNotIn,
|
||||
#[error("Cannot use `&` in sub expression")]
|
||||
NestedIntersection,
|
||||
#[error("Cannot use `&` for positive expressions")]
|
||||
PositiveIntersection,
|
||||
}
|
||||
|
||||
/// Splits string matcher expression into Git-compatible `(positive | ...) &
|
||||
/// ~(negative | ...)` form.
|
||||
#[cfg_attr(not(test), expect(dead_code))] // TODO
|
||||
fn split_into_positive_negative_patterns(
|
||||
expr: &StringExpression,
|
||||
) -> Result<(Vec<&StringPattern>, Vec<&StringPattern>), GitRefExpressionError> {
|
||||
static ALL: StringPattern = StringPattern::all();
|
||||
|
||||
// Outer expression is considered an intersection of
|
||||
// - zero or one union of positive expressions
|
||||
// - zero or more unions of negative expressions
|
||||
// e.g.
|
||||
// - `a` (1+)
|
||||
// - `~a&~b` (1-, 1-)
|
||||
// - `(a|b)&~(c|d)&~e` (2+, 2-, 1-)
|
||||
//
|
||||
// No negation nor intersection is allowed under union or not-in nodes.
|
||||
// - `a|~b` (incompatible with Git refspecs)
|
||||
// - `~(~a&~b)` (equivalent to `a|b`, but unsupported)
|
||||
// - `(a&~b)&(~c&~d)` (equivalent to `a&~b&~c&~d`, but unsupported)
|
||||
|
||||
fn visit_positive<'a>(
|
||||
expr: &'a StringExpression,
|
||||
positives: &mut Vec<&'a StringPattern>,
|
||||
negatives: &mut Vec<&'a StringPattern>,
|
||||
) -> Result<(), GitRefExpressionError> {
|
||||
match expr {
|
||||
StringExpression::Pattern(pattern) => {
|
||||
positives.push(pattern);
|
||||
Ok(())
|
||||
}
|
||||
StringExpression::NotIn(complement) => {
|
||||
positives.push(&ALL);
|
||||
visit_negative(complement, negatives)
|
||||
}
|
||||
StringExpression::Union(expr1, expr2) => visit_union(expr1, expr2, positives),
|
||||
StringExpression::Intersection(expr1, expr2) => {
|
||||
match (expr1.as_ref(), expr2.as_ref()) {
|
||||
(other, StringExpression::NotIn(complement))
|
||||
| (StringExpression::NotIn(complement), other) => {
|
||||
visit_positive(other, positives, negatives)?;
|
||||
visit_negative(complement, negatives)
|
||||
}
|
||||
_ => Err(GitRefExpressionError::PositiveIntersection),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_negative<'a>(
|
||||
expr: &'a StringExpression,
|
||||
negatives: &mut Vec<&'a StringPattern>,
|
||||
) -> Result<(), GitRefExpressionError> {
|
||||
match expr {
|
||||
StringExpression::Pattern(pattern) => {
|
||||
negatives.push(pattern);
|
||||
Ok(())
|
||||
}
|
||||
StringExpression::NotIn(_) => Err(GitRefExpressionError::NestedNotIn),
|
||||
StringExpression::Union(expr1, expr2) => visit_union(expr1, expr2, negatives),
|
||||
StringExpression::Intersection(_, _) => Err(GitRefExpressionError::NestedIntersection),
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_union<'a>(
|
||||
expr1: &'a StringExpression,
|
||||
expr2: &'a StringExpression,
|
||||
patterns: &mut Vec<&'a StringPattern>,
|
||||
) -> Result<(), GitRefExpressionError> {
|
||||
visit_union_sub(expr1, patterns)?;
|
||||
visit_union_sub(expr2, patterns)
|
||||
}
|
||||
|
||||
fn visit_union_sub<'a>(
|
||||
expr: &'a StringExpression,
|
||||
patterns: &mut Vec<&'a StringPattern>,
|
||||
) -> Result<(), GitRefExpressionError> {
|
||||
match expr {
|
||||
StringExpression::Pattern(pattern) => {
|
||||
patterns.push(pattern);
|
||||
Ok(())
|
||||
}
|
||||
StringExpression::NotIn(_) => Err(GitRefExpressionError::NestedNotIn),
|
||||
StringExpression::Union(expr1, expr2) => visit_union(expr1, expr2, patterns),
|
||||
StringExpression::Intersection(_, _) => Err(GitRefExpressionError::NestedIntersection),
|
||||
}
|
||||
}
|
||||
|
||||
let mut positives = Vec::new();
|
||||
let mut negatives = Vec::new();
|
||||
visit_positive(expr, &mut positives, &mut negatives)?;
|
||||
Ok((positives, negatives))
|
||||
}
|
||||
|
||||
/// A list of fetch refspecs configured within a remote that were ignored during
|
||||
/// a expansion. Callers should consider displaying these in the UI as
|
||||
/// appropriate.
|
||||
|
|
@ -2729,3 +2833,87 @@ pub enum FetchTagsOverride {
|
|||
/// remote's `tagOpt` is configured to
|
||||
NoTags,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use assert_matches::assert_matches;
|
||||
|
||||
use super::*;
|
||||
use crate::revset;
|
||||
use crate::revset::RevsetDiagnostics;
|
||||
|
||||
#[test]
|
||||
fn test_split_positive_negative_patterns() {
|
||||
fn split(text: &str) -> (Vec<StringPattern>, Vec<StringPattern>) {
|
||||
try_split(text).unwrap()
|
||||
}
|
||||
|
||||
fn try_split(
|
||||
text: &str,
|
||||
) -> Result<(Vec<StringPattern>, Vec<StringPattern>), GitRefExpressionError> {
|
||||
let mut diagnostics = RevsetDiagnostics::new();
|
||||
let expr = revset::parse_string_expression(&mut diagnostics, text).unwrap();
|
||||
let (positives, negatives) = split_into_positive_negative_patterns(&expr)?;
|
||||
Ok((
|
||||
positives.into_iter().cloned().collect(),
|
||||
negatives.into_iter().cloned().collect(),
|
||||
))
|
||||
}
|
||||
|
||||
insta::assert_compact_debug_snapshot!(
|
||||
split("a"),
|
||||
@r#"([Exact("a")], [])"#);
|
||||
insta::assert_compact_debug_snapshot!(
|
||||
split("~a"),
|
||||
@r#"([Substring("")], [Exact("a")])"#);
|
||||
insta::assert_compact_debug_snapshot!(
|
||||
split("~a~b"),
|
||||
@r#"([Substring("")], [Exact("a"), Exact("b")])"#);
|
||||
insta::assert_compact_debug_snapshot!(
|
||||
split("~(a|b)"),
|
||||
@r#"([Substring("")], [Exact("a"), Exact("b")])"#);
|
||||
insta::assert_compact_debug_snapshot!(
|
||||
split("a|b"),
|
||||
@r#"([Exact("a"), Exact("b")], [])"#);
|
||||
insta::assert_compact_debug_snapshot!(
|
||||
split("(a|b)&~c"),
|
||||
@r#"([Exact("a"), Exact("b")], [Exact("c")])"#);
|
||||
insta::assert_compact_debug_snapshot!(
|
||||
split("~a&b"),
|
||||
@r#"([Exact("b")], [Exact("a")])"#);
|
||||
insta::assert_compact_debug_snapshot!(
|
||||
split("a&~b&~c"),
|
||||
@r#"([Exact("a")], [Exact("b"), Exact("c")])"#);
|
||||
insta::assert_compact_debug_snapshot!(
|
||||
split("~a&b&~c"),
|
||||
@r#"([Exact("b")], [Exact("a"), Exact("c")])"#);
|
||||
insta::assert_compact_debug_snapshot!(
|
||||
split("a&~(b|c)"),
|
||||
@r#"([Exact("a")], [Exact("b"), Exact("c")])"#);
|
||||
insta::assert_compact_debug_snapshot!(
|
||||
split("((a|b)|c)&~(d|(e|f))"),
|
||||
@r#"([Exact("a"), Exact("b"), Exact("c")], [Exact("d"), Exact("e"), Exact("f")])"#);
|
||||
assert_matches!(
|
||||
try_split("a&b"),
|
||||
Err(GitRefExpressionError::PositiveIntersection)
|
||||
);
|
||||
assert_matches!(try_split("a|~b"), Err(GitRefExpressionError::NestedNotIn));
|
||||
assert_matches!(
|
||||
try_split("a&~(b&~c)"),
|
||||
Err(GitRefExpressionError::NestedIntersection)
|
||||
);
|
||||
assert_matches!(
|
||||
try_split("(a|b)&c"),
|
||||
Err(GitRefExpressionError::PositiveIntersection)
|
||||
);
|
||||
assert_matches!(
|
||||
try_split("(a&~b)&(~c&~d)"),
|
||||
Err(GitRefExpressionError::PositiveIntersection)
|
||||
);
|
||||
assert_matches!(try_split("a&~~b"), Err(GitRefExpressionError::NestedNotIn));
|
||||
assert_matches!(
|
||||
try_split("a&~b|c&~d"),
|
||||
Err(GitRefExpressionError::NestedIntersection)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue