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
Author dates and committer dates can be filtered like so:

    committer_date(before:"1 hour ago") # more than 1 hour ago
    committer_date(after:"1 hour ago")  # 1 hour ago or less

A date range can be created by combining revsets. For example, to see any
revisions committed yesterday:

    committer_date(after:"yesterday") & committer_date(before:"today")
  • Loading branch information
jennings committed Aug 1, 2024
1 parent 9ae71d3 commit a3d80b3
Show file tree
Hide file tree
Showing 8 changed files with 381 additions and 3 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
This simplifies the use case of configuring code formatters for specific file
types. See `jj help fix` for details.

* Added revset functions `author_date` and `committer_date`.

### Fixed bugs

* `jj diff --git` no longer shows the contents of binary files.
Expand Down
9 changes: 9 additions & 0 deletions cli/src/cli_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ use std::time::SystemTime;
use std::{fs, mem, str};

use bstr::ByteVec as _;
use chrono::TimeZone;
use clap::builder::{
MapValueParser, NonEmptyStringValueParser, TypedValueParser, ValueParserFactory,
};
Expand Down Expand Up @@ -1010,9 +1011,17 @@ impl WorkspaceCommandHelper {
path_converter: &self.path_converter,
workspace_id: self.workspace_id(),
};
let now = if let Some(timestamp) = self.settings.commit_timestamp() {
chrono::Local
.timestamp_millis_opt(timestamp.timestamp.0)
.unwrap()
} else {
chrono::Local::now()
};
RevsetParseContext::new(
&self.revset_aliases_map,
self.settings.user_email(),
now.into(),
&self.revset_extensions,
Some(workspace_context),
)
Expand Down
129 changes: 128 additions & 1 deletion cli/tests/test_revset_output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,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 Expand Up @@ -629,3 +629,130 @@ fn test_all_modifier() {
For help, see https://github.com/martinvonz/jj/blob/main/docs/config.md.
"###);
}

/// Verifies that the committer_date revset honors the local time zone.
/// This test cannot run on Windows because The TZ env var does not control
/// chrono::Local on that platform.
#[test]
#[cfg(not(target_os = "windows"))]
fn test_revset_committer_date_with_time_zone() {
let mut test_env = TestEnvironment::default();
test_env.add_env_var("TZ", "America/New_York");
test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "repo"]);
let repo_path = test_env.env_root().join("repo");

test_env.jj_cmd_ok(
&repo_path,
&[
"--config-toml",
"debug.commit-timestamp='2023-01-25T11:30:00-05:00'",
"describe",
"-m",
"first",
],
);
test_env.jj_cmd_ok(
&repo_path,
&[
"--config-toml",
"debug.commit-timestamp='2023-01-25T12:30:00-05:00'",
"new",
"-m",
"second",
],
);
test_env.jj_cmd_ok(
&repo_path,
&[
"--config-toml",
"debug.commit-timestamp='2023-01-25T13:30:00-05:00'",
"new",
"-m",
"third",
],
);

let mut log_commits_before_and_after =
|committer_date: &str, now: &str, tz: &str| -> (String, String) {
test_env.add_env_var("TZ", tz);
let config = format!("debug.commit-timestamp='{now}'");
let before_log = test_env.jj_cmd_success(
&repo_path,
&[
"--config-toml",
config.as_str(),
"log",
"--no-graph",
"-T",
"description.first_line() ++ ' ' ++ committer.timestamp() ++ '\n'",
"-r",
format!("committer_date(before:'{committer_date}') ~ root()").as_str(),
],
);
let after_log = test_env.jj_cmd_success(
&repo_path,
&[
"--config-toml",
config.as_str(),
"log",
"--no-graph",
"-T",
"description.first_line() ++ ' ' ++ committer.timestamp() ++ '\n'",
"-r",
format!("committer_date(after:'{committer_date}')").as_str(),
],
);
(before_log, after_log)
};

let (before_log, after_log) = log_commits_before_and_after(
"2023-01-25 12:00",
"2023-02-01T00:00:00-05:00",
"America/New_York",
);
insta::assert_snapshot!(before_log, @r###"
first 2023-01-25 11:30:00.000 -05:00
"###);
insta::assert_snapshot!(after_log, @r###"
third 2023-01-25 13:30:00.000 -05:00
second 2023-01-25 12:30:00.000 -05:00
"###);

// Switch to DST and ensure we get the same results, because it should
// evaluate 12:00 on commit date, not the current date
let (before_log, after_log) = log_commits_before_and_after(
"2023-01-25 12:00",
"2023-06-01T00:00:00-04:00",
"America/New_York",
);
insta::assert_snapshot!(before_log, @r###"
first 2023-01-25 11:30:00.000 -05:00
"###);
insta::assert_snapshot!(after_log, @r###"
third 2023-01-25 13:30:00.000 -05:00
second 2023-01-25 12:30:00.000 -05:00
"###);

// Change the local time zone and ensure the result changes
let (before_log, after_log) = log_commits_before_and_after(
"2023-01-25 12:00",
"2023-06-01T00:00:00-06:00",
"America/Chicago",
);
insta::assert_snapshot!(before_log, @r###"
second 2023-01-25 12:30:00.000 -05:00
first 2023-01-25 11:30:00.000 -05:00
"###);
insta::assert_snapshot!(after_log, @"third 2023-01-25 13:30:00.000 -05:00");

// Time zone far outside USA with no DST
let (before_log, after_log) =
log_commits_before_and_after("2023-01-26 03:00", "2023-06-01T00:00:00+10:00", "AEST-10");
insta::assert_snapshot!(before_log, @r###"
first 2023-01-25 11:30:00.000 -05:00
"###);
insta::assert_snapshot!(after_log, @r###"
third 2023-01-25 13:30:00.000 -05:00
second 2023-01-25 12:30:00.000 -05:00
"###);
}
26 changes: 26 additions & 0 deletions docs/revsets.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,12 @@ 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 with author dates matching the specified [date
pattern](#date-patterns).

* `committer_date(pattern)`: Commits with committer dates matching the specified
[date pattern](#date-patterns).

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

Expand Down Expand Up @@ -359,6 +365,26 @@ Functions that perform string matching support the following pattern syntax:
You can append `-i` after the kind to match case‐insensitively (e.g.
`glob-i:"fix*jpeg*"`).

## Date patterns

Functions that perform date matching support the following pattern syntax:

* `after:"string"`: Matches dates exactly at or after the given date.
* `before:"string"`: Matches dates before, but not including, the given date.

Date strings 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
18 changes: 18 additions & 0 deletions lib/src/default_index/revset_engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1074,6 +1074,24 @@ 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;
expression.matches(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;
expression.matches(committer_date)
})
}
RevsetFilterPredicate::File(expr) => {
let matcher: Rc<dyn Matcher> = expr.to_matcher().into();
box_pure_predicate_fn(move |index, pos| {
Expand Down
43 changes: 43 additions & 0 deletions lib/src/revset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ pub use crate::revset_parser::{
};
use crate::store::Store;
use crate::str_util::StringPattern;
use crate::time_util::{DatePattern, DatePatternContext};
use crate::{dsl_util, fileset, revset_parser};

/// Error occurred during symbol resolution.
Expand Down Expand Up @@ -132,6 +133,10 @@ pub enum RevsetFilterPredicate {
Author(StringPattern),
/// Commits with committer name or email matching the pattern.
Committer(StringPattern),
/// Commits with author dates matching the given date pattern.
AuthorDate(DatePattern),
/// Commits with committer dates matching the given date pattern.
CommitterDate(DatePattern),
/// Commits modifying the paths specified by the fileset.
File(FilesetExpression),
/// Commits containing diffs matching the `text` pattern within the `files`.
Expand Down Expand Up @@ -684,6 +689,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_date_pattern(arg, context.date_pattern_context())?;
Ok(RevsetExpression::filter(RevsetFilterPredicate::AuthorDate(
pattern,
)))
});
map.insert("mine", |function, context| {
function.expect_no_arguments()?;
// Email address domains are inherently case‐insensitive, and the local‐parts
Expand All @@ -700,6 +712,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_date_pattern(arg, context.date_pattern_context())?;
Ok(RevsetExpression::filter(
RevsetFilterPredicate::CommitterDate(pattern),
))
});
map.insert("empty", |function, _context| {
function.expect_no_arguments()?;
Ok(RevsetExpression::is_empty())
Expand Down Expand Up @@ -774,6 +793,20 @@ pub fn expect_string_pattern(node: &ExpressionNode) -> Result<StringPattern, Rev
revset_parser::expect_pattern_with("string pattern", node, parse_pattern)
}

pub fn expect_date_pattern(
node: &ExpressionNode,
context: &DatePatternContext,
) -> Result<DatePattern, RevsetParseError> {
let parse_pattern =
|value: &str, kind: Option<&str>| -> Result<_, Box<dyn std::error::Error + Send + Sync>> {
match kind {
None => Err("Date pattern must specify 'after' or 'before'".into()),
Some(kind) => Ok(context.parse_relative(value, kind)?),
}
};
revset_parser::expect_pattern_with("date pattern", node, parse_pattern)
}

fn parse_remote_branches_arguments(
function: &FunctionCallNode,
remote_ref_state: Option<RemoteRefState>,
Expand Down Expand Up @@ -2035,6 +2068,7 @@ impl RevsetExtensions {
pub struct RevsetParseContext<'a> {
aliases_map: &'a RevsetAliasesMap,
user_email: String,
date_pattern_context: DatePatternContext,
extensions: &'a RevsetExtensions,
workspace: Option<RevsetWorkspaceContext<'a>>,
}
Expand All @@ -2043,12 +2077,14 @@ impl<'a> RevsetParseContext<'a> {
pub fn new(
aliases_map: &'a RevsetAliasesMap,
user_email: String,
date_pattern_context: DatePatternContext,
extensions: &'a RevsetExtensions,
workspace: Option<RevsetWorkspaceContext<'a>>,
) -> Self {
Self {
aliases_map,
user_email,
date_pattern_context,
extensions,
workspace,
}
Expand All @@ -2062,6 +2098,10 @@ impl<'a> RevsetParseContext<'a> {
&self.user_email
}

pub fn date_pattern_context(&self) -> &DatePatternContext {
&self.date_pattern_context
}

pub fn symbol_resolvers(&self) -> &[impl AsRef<dyn SymbolResolverExtension>] {
self.extensions.symbol_resolvers()
}
Expand Down Expand Up @@ -2105,6 +2145,7 @@ mod tests {
let context = RevsetParseContext::new(
&aliases_map,
"[email protected]".to_string(),
chrono::Utc::now().fixed_offset().into(),
&extensions,
None,
);
Expand Down Expand Up @@ -2133,6 +2174,7 @@ mod tests {
let context = RevsetParseContext::new(
&aliases_map,
"[email protected]".to_string(),
chrono::Utc::now().fixed_offset().into(),
&extensions,
Some(workspace_ctx),
);
Expand All @@ -2157,6 +2199,7 @@ mod tests {
let context = RevsetParseContext::new(
&aliases_map,
"[email protected]".to_string(),
chrono::Utc::now().fixed_offset().into(),
&extensions,
None,
);
Expand Down
4 changes: 4 additions & 0 deletions lib/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,10 @@ impl UserSettings {
// address
pub const USER_EMAIL_PLACEHOLDER: &'static str = "(no email configured)";

pub fn commit_timestamp(&self) -> Option<Timestamp> {
self.timestamp.to_owned()
}

pub fn operation_timestamp(&self) -> Option<Timestamp> {
get_timestamp_config(&self.config, "debug.operation-timestamp")
}
Expand Down
Loading

0 comments on commit a3d80b3

Please sign in to comment.