revsets: add author() and committer() functions (#46)

Filtering by the author or committer is quite common.
This commit is contained in:
Martin von Zweigbergk 2021-12-15 22:16:22 -08:00
parent 7d3d0fe83c
commit 277f42d98a
3 changed files with 216 additions and 2 deletions

View file

@ -105,6 +105,12 @@ revsets (expressions) as arguments.
* `description(needle[, x])`: Commits with the given string in their
description. If a second argument was provided, then only commits in that set
are considered, otherwise all visible commits are considered.
* `author(needle[, x])`: Commits with the given string in the author's name or
email. If a second argument was provided, then only commits in that set
are considered, otherwise all visible commits are considered.
* `committer(needle[, x])`: Commits with the given string in the committer's
name or email. If a second argument was provided, then only commits in that
set are considered, otherwise all visible commits are considered.
## Examples
@ -139,3 +145,9 @@ those commits:
```
jj log -r '(remote_branches()..@):'
```
Show commits authored by "martinvonz" and containing the word "reset" in the
description:
```
jj log -r 'author(martinvonz) & description(reset)'
```

View file

@ -211,6 +211,16 @@ pub enum RevsetExpression {
needle: String,
candidates: Rc<RevsetExpression>,
},
Author {
// Matches against both name and email
needle: String,
candidates: Rc<RevsetExpression>,
},
Committer {
// Matches against both name and email
needle: String,
candidates: Rc<RevsetExpression>,
},
Union(Rc<RevsetExpression>, Rc<RevsetExpression>),
Intersection(Rc<RevsetExpression>, Rc<RevsetExpression>),
Difference(Rc<RevsetExpression>, Rc<RevsetExpression>),
@ -334,6 +344,22 @@ impl RevsetExpression {
})
}
/// Commits in `self` with author's name or email containing `needle`.
pub fn with_author(self: &Rc<RevsetExpression>, needle: String) -> Rc<RevsetExpression> {
Rc::new(RevsetExpression::Author {
candidates: self.clone(),
needle,
})
}
/// Commits in `self` with committer's name or email containing `needle`.
pub fn with_committer(self: &Rc<RevsetExpression>, needle: String) -> Rc<RevsetExpression> {
Rc::new(RevsetExpression::Committer {
candidates: self.clone(),
needle,
})
}
/// Commits that are in `self` or in `other` (or both).
pub fn union(
self: &Rc<RevsetExpression>,
@ -665,7 +691,7 @@ fn parse_function_expression(
};
Ok(candidates.with_parent_count(2..u32::MAX))
}
"description" => {
"description" | "author" | "committer" => {
if !(1..=2).contains(&arg_count) {
return Err(RevsetParseError::InvalidFunctionArguments {
name,
@ -681,7 +707,14 @@ fn parse_function_expression(
} else {
parse_expression_rule(argument_pairs.next().unwrap().into_inner())?
};
Ok(candidates.with_description(needle))
match name.as_str() {
"description" => Ok(candidates.with_description(needle)),
"author" => Ok(candidates.with_author(needle)),
"committer" => Ok(candidates.with_committer(needle)),
_ => {
panic!("unexpected function name: {}", name)
}
}
}
_ => Err(RevsetParseError::NoSuchFunction(name)),
}
@ -1182,6 +1215,35 @@ pub fn evaluate_expression<'repo>(
}),
}))
}
RevsetExpression::Author { needle, candidates } => {
let candidates = candidates.evaluate(repo)?;
let repo = repo;
let needle = needle.clone();
// TODO: Make these functions that take a needle to search for accept some
// syntax for specifying whether it's a regex and whether it's
// case-sensitive.
Ok(Box::new(FilterRevset {
candidates,
predicate: Box::new(move |entry| {
let commit = repo.store().get_commit(&entry.commit_id()).unwrap();
commit.author().name.contains(needle.as_str())
|| commit.author().email.contains(needle.as_str())
}),
}))
}
RevsetExpression::Committer { needle, candidates } => {
let candidates = candidates.evaluate(repo)?;
let repo = repo;
let needle = needle.clone();
Ok(Box::new(FilterRevset {
candidates,
predicate: Box::new(move |entry| {
let commit = repo.store().get_commit(&entry.commit_id()).unwrap();
commit.committer().name.contains(needle.as_str())
|| commit.committer().email.contains(needle.as_str())
}),
}))
}
RevsetExpression::Union(expression1, expression2) => {
let set1 = expression1.evaluate(repo)?;
let set2 = expression2.evaluate(repo)?;
@ -1292,6 +1354,20 @@ mod tests {
needle: "needle".to_string()
})
);
assert_eq!(
foo_symbol.with_author("needle".to_string()),
Rc::new(RevsetExpression::Author {
candidates: foo_symbol.clone(),
needle: "needle".to_string()
})
);
assert_eq!(
foo_symbol.with_committer("needle".to_string()),
Rc::new(RevsetExpression::Committer {
candidates: foo_symbol.clone(),
needle: "needle".to_string()
})
);
assert_eq!(
foo_symbol.union(&checkout_symbol),
Rc::new(RevsetExpression::Union(

View file

@ -1227,6 +1227,132 @@ fn test_evaluate_expression_description(use_git: bool) {
);
}
#[test_case(false ; "local backend")]
#[test_case(true ; "git backend")]
fn test_evaluate_expression_author(use_git: bool) {
let settings = testutils::user_settings();
let test_workspace = testutils::init_repo(&settings, use_git);
let repo = &test_workspace.repo;
let mut tx = repo.start_transaction("test");
let mut_repo = tx.mut_repo();
let timestamp = Timestamp {
timestamp: MillisSinceEpoch(0),
tz_offset: 0,
};
let commit1 = testutils::create_random_commit(&settings, repo)
.set_author(Signature {
name: "name1".to_string(),
email: "email1".to_string(),
timestamp: timestamp.clone(),
})
.write_to_repo(mut_repo);
let commit2 = testutils::create_random_commit(&settings, repo)
.set_parents(vec![commit1.id().clone()])
.set_author(Signature {
name: "name2".to_string(),
email: "email2".to_string(),
timestamp: timestamp.clone(),
})
.write_to_repo(mut_repo);
let commit3 = testutils::create_random_commit(&settings, repo)
.set_parents(vec![commit2.id().clone()])
.set_author(Signature {
name: "name3".to_string(),
email: "email3".to_string(),
timestamp,
})
.write_to_repo(mut_repo);
// Can find multiple matches
assert_eq!(
resolve_commit_ids(mut_repo.as_repo_ref(), "author(name)"),
vec![
commit3.id().clone(),
commit2.id().clone(),
commit1.id().clone()
]
);
// Can find a unique match by either name or email
assert_eq!(
resolve_commit_ids(mut_repo.as_repo_ref(), "author(\"name2\")"),
vec![commit2.id().clone()]
);
assert_eq!(
resolve_commit_ids(mut_repo.as_repo_ref(), "author(\"name3\")"),
vec![commit3.id().clone()]
);
// Searches only among candidates if specified
assert_eq!(
resolve_commit_ids(mut_repo.as_repo_ref(), "author(\"name2\",heads())"),
vec![]
);
}
#[test_case(false ; "local backend")]
#[test_case(true ; "git backend")]
fn test_evaluate_expression_committer(use_git: bool) {
let settings = testutils::user_settings();
let test_workspace = testutils::init_repo(&settings, use_git);
let repo = &test_workspace.repo;
let mut tx = repo.start_transaction("test");
let mut_repo = tx.mut_repo();
let timestamp = Timestamp {
timestamp: MillisSinceEpoch(0),
tz_offset: 0,
};
let commit1 = testutils::create_random_commit(&settings, repo)
.set_committer(Signature {
name: "name1".to_string(),
email: "email1".to_string(),
timestamp: timestamp.clone(),
})
.write_to_repo(mut_repo);
let commit2 = testutils::create_random_commit(&settings, repo)
.set_parents(vec![commit1.id().clone()])
.set_committer(Signature {
name: "name2".to_string(),
email: "email2".to_string(),
timestamp: timestamp.clone(),
})
.write_to_repo(mut_repo);
let commit3 = testutils::create_random_commit(&settings, repo)
.set_parents(vec![commit2.id().clone()])
.set_committer(Signature {
name: "name3".to_string(),
email: "email3".to_string(),
timestamp,
})
.write_to_repo(mut_repo);
// Can find multiple matches
assert_eq!(
resolve_commit_ids(mut_repo.as_repo_ref(), "committer(name)"),
vec![
commit3.id().clone(),
commit2.id().clone(),
commit1.id().clone()
]
);
// Can find a unique match by either name or email
assert_eq!(
resolve_commit_ids(mut_repo.as_repo_ref(), "committer(\"name2\")"),
vec![commit2.id().clone()]
);
assert_eq!(
resolve_commit_ids(mut_repo.as_repo_ref(), "committer(\"name3\")"),
vec![commit3.id().clone()]
);
// Searches only among candidates if specified
assert_eq!(
resolve_commit_ids(mut_repo.as_repo_ref(), "committer(\"name2\",heads())"),
vec![]
);
}
#[test_case(false ; "local backend")]
#[test_case(true ; "git backend")]
fn test_evaluate_expression_union(use_git: bool) {