Skip to content

Commit

Permalink
revset: add author_date and committer_date revset functions
Browse files Browse the repository at this point in the history
These functions always filter for dates at or after the specified date. To
invert the filter, combine it with the ~ operator:

    # anything authored before yesterday at midnight
    ~author_date("yesterday")
  • Loading branch information
jennings committed Jul 2, 2024
1 parent bc81167 commit 8275ba0
Show file tree
Hide file tree
Showing 6 changed files with 242 additions and 4 deletions.
1 change: 1 addition & 0 deletions cli/src/cli_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -994,6 +994,7 @@ impl WorkspaceCommandHelper {
RevsetParseContext::new(
&self.revset_aliases_map,
self.settings.user_email(),
chrono::Local::now(),
&self.revset_extensions,
Some(workspace_context),
)
Expand Down
2 changes: 1 addition & 1 deletion cli/tests/test_revset_output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ fn test_function_name_hint() {
| ^-----^
|
= Function "author_" doesn't exist
Hint: Did you mean "author", "my_author"?
Hint: Did you mean "author", "author_date", "my_author"?
"###);

insta::assert_snapshot!(evaluate_err("my_branches"), @r###"
Expand Down
19 changes: 19 additions & 0 deletions docs/revsets.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,10 @@ revsets (expressions) as arguments.
* `committer(pattern)`: Commits with the committer's name or email matching the
given [string pattern](#string-patterns).

* `author_date(pattern)`: Commits authored within at or after specified [date](#date-patterns).

* `committer_date(pattern)`: Commits committed at or after the specified [date](#date-patterns).

* `empty()`: Commits modifying no files. This also includes `merges()` without
user modifications and `root()`.

Expand Down Expand Up @@ -333,6 +337,21 @@ Functions that perform string matching support the following pattern syntax:
* `glob:"pattern"`: Matches strings with Unix-style shell [wildcard
`pattern`](https://docs.rs/glob/latest/glob/struct.Pattern.html).

## Date patterns

Date patterns can be specified in several forms, including:

* 2024-02-01
* 2024-02-01T12:00:00
* 2024-02-01T12:00:00-08:00
* 2024-02-01 12:00:00
* 2 days ago
* 5 minutes ago
* yesterday
* yesterday 5pm
* yesterday 10:30
* yesterday 15:30

## Aliases

New symbols and functions can be defined in the config file, by using any
Expand Down
23 changes: 23 additions & 0 deletions lib/src/default_index/revset_engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ use crate::revset::{
RevsetFilterPredicate, GENERATION_RANGE_FULL,
};
use crate::store::Store;
use crate::time_expression::TimeExpression;
use crate::{rewrite, union_find};

type BoxedPredicateFn<'a> = Box<dyn FnMut(&CompositeIndex, IndexPosition) -> bool + 'a>;
Expand Down Expand Up @@ -1069,6 +1070,28 @@ fn build_predicate_fn(
|| pattern.matches(&commit.committer().email)
})
}
RevsetFilterPredicate::AuthorDate(expression) => {
let expression = expression.clone();
box_pure_predicate_fn(move |index, pos| {
let entry = index.entry_by_pos(pos);
let commit = store.get_commit(&entry.commit_id()).unwrap();
let author_date = &commit.author().timestamp;
match &expression {
TimeExpression::AtOrAfter(ts) => ts.le(author_date),
}
})
}
RevsetFilterPredicate::CommitterDate(expression) => {
let expression = expression.clone();
box_pure_predicate_fn(move |index, pos| {
let entry = index.entry_by_pos(pos);
let commit = store.get_commit(&entry.commit_id()).unwrap();
let committer_date = &commit.committer().timestamp;
match &expression {
TimeExpression::AtOrAfter(ts) => ts.le(committer_date),
}
})
}
RevsetFilterPredicate::File(expr) => {
let matcher: Rc<dyn Matcher> = expr.to_matcher().into();
box_pure_predicate_fn(move |index, pos| {
Expand Down
52 changes: 51 additions & 1 deletion lib/src/revset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ use std::ops::Range;
use std::rc::Rc;
use std::sync::Arc;

use chrono::{DateTime, FixedOffset, NaiveDateTime, Offset, TimeZone};
use itertools::Itertools;
use once_cell::sync::Lazy;
use thiserror::Error;
Expand All @@ -43,6 +44,7 @@ pub use crate::revset_parser::{
};
use crate::store::Store;
use crate::str_util::StringPattern;
use crate::time_expression::TimeExpression;
use crate::{dsl_util, revset_parser};

/// Error occurred during symbol resolution.
Expand Down Expand Up @@ -131,6 +133,10 @@ pub enum RevsetFilterPredicate {
Author(StringPattern),
/// Commits with committer's name or email containing the needle.
Committer(StringPattern),
/// Commits authored after the given date.
AuthorDate(TimeExpression),
/// Commits committed after the given date.
CommitterDate(TimeExpression),
/// Commits modifying the paths specified by the fileset.
File(FilesetExpression),
/// Commits with conflicts
Expand Down Expand Up @@ -685,6 +691,13 @@ static BUILTIN_FUNCTION_MAP: Lazy<HashMap<&'static str, RevsetFunction>> = Lazy:
pattern,
)))
});
map.insert("author_date", |function, context| {
let [arg] = function.expect_exact_arguments()?;
let pattern = expect_time_expression(arg, context.now().to_owned())?;
Ok(RevsetExpression::filter(RevsetFilterPredicate::AuthorDate(
pattern,
)))
});
map.insert("mine", |function, context| {
function.expect_no_arguments()?;
Ok(RevsetExpression::filter(RevsetFilterPredicate::Author(
Expand All @@ -698,6 +711,13 @@ static BUILTIN_FUNCTION_MAP: Lazy<HashMap<&'static str, RevsetFunction>> = Lazy:
pattern,
)))
});
map.insert("committer_date", |function, context| {
let [arg] = function.expect_exact_arguments()?;
let pattern = expect_time_expression(arg, context.now().to_owned())?;
Ok(RevsetExpression::filter(
RevsetFilterPredicate::CommitterDate(pattern),
))
});
map.insert("empty", |function, _context| {
function.expect_no_arguments()?;
Ok(RevsetExpression::is_empty())
Expand Down Expand Up @@ -749,6 +769,20 @@ pub fn expect_string_pattern(node: &ExpressionNode) -> Result<StringPattern, Rev
revset_parser::expect_pattern_with("string pattern", node, parse_pattern)
}

fn expect_time_expression<Tz: TimeZone>(
node: &ExpressionNode,
now: DateTime<Tz>,
) -> Result<TimeExpression, RevsetParseError>
where
Tz::Offset: Copy,
{
let expression = revset_parser::expect_literal::<String>("time expression", node)?;
TimeExpression::parse(&expression, now).map_err(|err| {
RevsetParseError::expression(format!("Unable to parse time expression: {err}"), node.span)
.with_source(err)
})
}

/// Resolves function call by using the given function map.
fn lower_function_call(
function: &FunctionCallNode,
Expand Down Expand Up @@ -1977,20 +2011,29 @@ impl RevsetExtensions {
pub struct RevsetParseContext<'a> {
aliases_map: &'a RevsetAliasesMap,
user_email: String,
/// The current local time when the revset expression was written.
now: NaiveDateTime,
/// The offset from UTC at the time the revset expression was written. TODO:
/// It would be better if this was the TimeZone so times could be computed
/// correctly across DST shifts.
offset: FixedOffset,
extensions: &'a RevsetExtensions,
workspace: Option<RevsetWorkspaceContext<'a>>,
}

impl<'a> RevsetParseContext<'a> {
pub fn new(
pub fn new<Tz: TimeZone>(
aliases_map: &'a RevsetAliasesMap,
user_email: String,
now: DateTime<Tz>,
extensions: &'a RevsetExtensions,
workspace: Option<RevsetWorkspaceContext<'a>>,
) -> Self {
Self {
aliases_map,
user_email,
now: now.naive_local(),
offset: now.offset().fix(),
extensions,
workspace,
}
Expand All @@ -2004,6 +2047,10 @@ impl<'a> RevsetParseContext<'a> {
&self.user_email
}

pub fn now(&self) -> DateTime<FixedOffset> {
DateTime::from_naive_utc_and_offset(self.now, self.offset)
}

pub fn symbol_resolvers(&self) -> &[impl AsRef<dyn SymbolResolverExtension>] {
self.extensions.symbol_resolvers()
}
Expand Down Expand Up @@ -2047,6 +2094,7 @@ mod tests {
let context = RevsetParseContext::new(
&aliases_map,
"[email protected]".to_string(),
chrono::Utc::now(),
&extensions,
None,
);
Expand Down Expand Up @@ -2076,6 +2124,7 @@ mod tests {
let context = RevsetParseContext::new(
&aliases_map,
"[email protected]".to_string(),
chrono::Utc::now(),
&extensions,
Some(workspace_ctx),
);
Expand All @@ -2101,6 +2150,7 @@ mod tests {
let context = RevsetParseContext::new(
&aliases_map,
"[email protected]".to_string(),
chrono::Utc::now(),
&extensions,
None,
);
Expand Down
Loading

0 comments on commit 8275ba0

Please sign in to comment.