From 9ba9ecd70855d1e964a1fd0673fa7d2220e23ddd Mon Sep 17 00:00:00 2001 From: Emily Fox Date: Sun, 13 Aug 2023 19:35:11 -0500 Subject: [PATCH] revset: add function mine() --- CHANGELOG.md | 2 + cli/src/cli_util.rs | 1 + cli/src/commands/debug.rs | 2 + cli/src/commands/mod.rs | 16 ++++++- docs/github.md | 26 +++++------ docs/revsets.md | 2 + lib/src/revset.rs | 27 ++++++++++- lib/tests/test_revset.rs | 94 +++++++++++++++++++++++++++++++++++++-- 8 files changed, 150 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e1a1f2b48..76a20beaf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,6 +72,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `jj split` will now leave the description empty on the second part if the description was empty on the input commit. +* Revsets gained a new function `mine()` that aliases `author([your_email])`. + ### Fixed bugs * `jj config set --user` and `jj config edit --user` can now be used outside of any repository. diff --git a/cli/src/cli_util.rs b/cli/src/cli_util.rs index 634b4b4ca..8887c8176 100644 --- a/cli/src/cli_util.rs +++ b/cli/src/cli_util.rs @@ -1053,6 +1053,7 @@ impl WorkspaceCommandHelper { let expression = revset::parse( revision_str, &self.revset_aliases_map, + &self.settings.user_email(), Some(&self.revset_context()), )?; if let Some(ui) = ui { diff --git a/cli/src/commands/debug.rs b/cli/src/commands/debug.rs index ecdeb8b68..1c0d33e64 100644 --- a/cli/src/commands/debug.rs +++ b/cli/src/commands/debug.rs @@ -199,6 +199,7 @@ fn cmd_debug_revset( command: &CommandHelper, args: &DebugRevsetArgs, ) -> Result<(), CommandError> { + let user_email = command.settings().user_email(); let workspace_command = command.workspace_helper(ui)?; let workspace_ctx = workspace_command.revset_context(); let repo = workspace_command.repo().as_ref(); @@ -206,6 +207,7 @@ fn cmd_debug_revset( let expression = revset::parse( &args.revision, workspace_command.revset_aliases_map(), + &user_email, Some(&workspace_ctx), )?; writeln!(ui, "-- Parsed:")?; diff --git a/cli/src/commands/mod.rs b/cli/src/commands/mod.rs index c4dce01bd..a8711ad37 100644 --- a/cli/src/commands/mod.rs +++ b/cli/src/commands/mod.rs @@ -1723,7 +1723,13 @@ fn cmd_log(ui: &mut Ui, command: &CommandHelper, args: &LogArgs) -> Result<(), C the working copy commit, pass -r '@' instead." )?; } else if revset.is_empty() - && revset::parse(only_path, &RevsetAliasesMap::new(), None).is_ok() + && revset::parse( + only_path, + &RevsetAliasesMap::new(), + &command.settings().user_email(), + None, + ) + .is_ok() { writeln!( ui.warning(), @@ -2512,7 +2518,13 @@ from the source will be moved into the parent. if let [only_path] = &args.paths[..] { let (_, matches) = command.matches().subcommand().unwrap(); if matches.value_source("revision").unwrap() == ValueSource::DefaultValue - && revset::parse(only_path, &RevsetAliasesMap::new(), None).is_ok() + && revset::parse( + only_path, + &RevsetAliasesMap::new(), + &command.settings().user_email(), + None, + ) + .is_ok() { writeln!( ui.warning(), diff --git a/docs/github.md b/docs/github.md index 5666be516..b30af3681 100644 --- a/docs/github.md +++ b/docs/github.md @@ -51,8 +51,8 @@ $ # Do some more work. $ jj commit -m "Update tutorial" $ jj branch create doc-update $ # Move the previous revision to doc-update. -$ jj branch set doc-update -r @- -$ jj git push +$ jj branch set doc-update -r @- +$ jj git push ``` ## Working in a Jujutsu repository @@ -63,7 +63,7 @@ able to create a branch for a revision. ```shell script $ # Do your work -$ jj commit +$ jj commit $ # Jujutsu automatically creates a branch $ jj git push --change $revision ``` @@ -101,11 +101,11 @@ something like this: ```shell script $ # Create a new commit on top of the second-to-last commit in `your-feature`, -$ # as reviews requested a fix there. -$ jj new your-feature- +$ # as reviews requested a fix there. +$ jj new your-feature- $ # Address the comments by updating the code $ # Review the changes -$ jj diff +$ jj diff $ # Squash the changes into the parent commit $ jj squash $ # Push the updated branch to the remote. Jujutsu automatically makes it a force push @@ -140,14 +140,14 @@ commands like `gh issue list` normally. ## Useful Revsets Log all revisions across all local branches, which aren't on the main branch nor -on any remote -`jj log -r 'branches() & ~(main | remote_branches())'` +on any remote +`jj log -r 'branches() & ~(main | remote_branches())'` Log all revisions which you authored, across all branches which aren't on any -remote -`jj log -r 'author(your@email.com) & branches() & ~remote_branches()'` -Log all remote branches, which you authored or committed to -`jj log -r 'remote_branches() & (committer(your@email.com) | author(your@email.com))'` -Log all descendants of the current working copy, which aren't on a remote +remote +`jj log -r 'mine() & branches() & ~remote_branches()'` +Log all remote branches, which you authored or committed to +`jj log -r 'remote_branches() & (mine() | committer(your@email.com))'` +Log all descendants of the current working copy, which aren't on a remote `jj log -r ':@ & ~remote_branches()'` ## Merge conflicts diff --git a/docs/revsets.md b/docs/revsets.md index 019c74a6b..0d6f42a88 100644 --- a/docs/revsets.md +++ b/docs/revsets.md @@ -122,6 +122,8 @@ revsets (expressions) as arguments. description. * `author(needle)`: Commits with the given string in the author's name or email. +* `mine()`: Commits where the author's email matches the email of the current + user. * `committer(needle)`: Commits with the given string in the committer's name or email. * `empty()`: Commits modifying no files. This also includes `merges()` without diff --git a/lib/src/revset.rs b/lib/src/revset.rs index 2e3af26af..0f090a338 100644 --- a/lib/src/revset.rs +++ b/lib/src/revset.rs @@ -103,6 +103,8 @@ pub enum RevsetParseErrorKind { }, #[error("Invalid arguments to revset function \"{name}\": {message}")] InvalidFunctionArguments { name: String, message: String }, + #[error("Revset function \"mine\" requires a user name")] + MineWithoutUserName, #[error("Invalid file pattern: {0}")] FsPathParseError(#[source] FsPathParseError), #[error("Cannot resolve file pattern without workspace")] @@ -681,6 +683,7 @@ struct ParseState<'a> { aliases_map: &'a RevsetAliasesMap, aliases_expanding: &'a [RevsetAliasId<'a>], locals: &'a HashMap<&'a str, Rc>, + user_email: &'a str, workspace_ctx: Option<&'a RevsetWorkspaceContext<'a>>, } @@ -705,6 +708,7 @@ impl ParseState<'_> { aliases_map: self.aliases_map, aliases_expanding: &aliases_expanding, locals, + user_email: self.user_email, workspace_ctx: self.workspace_ctx, }; f(expanding_state).map_err(|e| { @@ -1049,6 +1053,12 @@ static BUILTIN_FUNCTION_MAP: Lazy> = Lazy: needle, ))) }); + map.insert("mine", |name, arguments_pair, state| { + expect_no_arguments(name, arguments_pair)?; + Ok(RevsetExpression::filter(RevsetFilterPredicate::Author( + state.user_email.to_owned(), + ))) + }); map.insert("committer", |name, arguments_pair, state| { let arg = expect_one_argument(name, arguments_pair)?; let needle = parse_function_argument_to_string(name, arg, state)?; @@ -1267,12 +1277,14 @@ fn parse_function_argument_as_literal( pub fn parse( revset_str: &str, aliases_map: &RevsetAliasesMap, + user_email: &str, workspace_ctx: Option<&RevsetWorkspaceContext>, ) -> Result, RevsetParseError> { let state = ParseState { aliases_map, aliases_expanding: &[], locals: &HashMap::new(), + user_email, workspace_ctx, }; parse_program(revset_str, state) @@ -2301,7 +2313,13 @@ mod tests { workspace_root: Path::new("/"), }; // Map error to comparable object - super::parse(revset_str, &aliases_map, Some(&workspace_ctx)).map_err(|e| e.kind) + super::parse( + revset_str, + &aliases_map, + "test.user@example.com", + Some(&workspace_ctx), + ) + .map_err(|e| e.kind) } #[test] @@ -2705,6 +2723,13 @@ mod tests { RevsetFilterPredicate::Description("(foo)".to_string()) )) ); + assert!(parse("mine(foo)").is_err()); + assert_eq!( + parse("mine()"), + Ok(RevsetExpression::filter(RevsetFilterPredicate::Author( + "test.user@example.com".to_string() + ))) + ); assert_eq!( parse("empty()"), Ok(RevsetExpression::filter(RevsetFilterPredicate::File(None)).negated()) diff --git a/lib/tests/test_revset.rs b/lib/tests/test_revset.rs index ea4175c2c..062facdfc 100644 --- a/lib/tests/test_revset.rs +++ b/lib/tests/test_revset.rs @@ -192,7 +192,7 @@ fn test_resolve_symbol_commit_id() { assert_eq!(resolve_commit_ids(repo.as_ref(), "present(foo)"), []); let symbol_resolver = DefaultSymbolResolver::new(repo.as_ref(), None); assert_matches!( - optimize(parse("present(04)", &RevsetAliasesMap::new(), None).unwrap()).resolve_user_expression(repo.as_ref(), &symbol_resolver), + optimize(parse("present(04)", &RevsetAliasesMap::new(), &settings.user_email(), None).unwrap()).resolve_user_expression(repo.as_ref(), &symbol_resolver), Err(RevsetResolutionError::AmbiguousCommitIdPrefix(s)) if s == "04" ); assert_eq!( @@ -775,7 +775,16 @@ fn test_resolve_symbol_git_refs() { } fn resolve_commit_ids(repo: &dyn Repo, revset_str: &str) -> Vec { - let expression = optimize(parse(revset_str, &RevsetAliasesMap::new(), None).unwrap()); + let settings = testutils::user_settings(); + let expression = optimize( + parse( + revset_str, + &RevsetAliasesMap::new(), + &settings.user_email(), + None, + ) + .unwrap(), + ); let symbol_resolver = DefaultSymbolResolver::new(repo, None); let expression = expression .resolve_user_expression(repo, &symbol_resolver) @@ -789,13 +798,21 @@ fn resolve_commit_ids_in_workspace( workspace: &Workspace, cwd: Option<&Path>, ) -> Vec { + let settings = testutils::user_settings(); let workspace_ctx = RevsetWorkspaceContext { cwd: cwd.unwrap_or_else(|| workspace.workspace_root()), workspace_id: workspace.workspace_id(), workspace_root: workspace.workspace_root(), }; - let expression = - optimize(parse(revset_str, &RevsetAliasesMap::new(), Some(&workspace_ctx)).unwrap()); + let expression = optimize( + parse( + revset_str, + &RevsetAliasesMap::new(), + &settings.user_email(), + Some(&workspace_ctx), + ) + .unwrap(), + ); let symbol_resolver = DefaultSymbolResolver::new(repo, Some(workspace_ctx.workspace_id)); let expression = expression .resolve_user_expression(repo, &symbol_resolver) @@ -2075,6 +2092,75 @@ fn test_evaluate_expression_author(use_git: bool) { ); } +#[test_case(false ; "local backend")] +#[test_case(true ; "git backend")] +fn test_evaluate_expression_mine(use_git: bool) { + let settings = testutils::user_settings(); + let test_repo = TestRepo::init(use_git); + let repo = &test_repo.repo; + + let mut tx = repo.start_transaction(&settings, "test"); + let mut_repo = tx.mut_repo(); + + let timestamp = Timestamp { + timestamp: MillisSinceEpoch(0), + tz_offset: 0, + }; + let commit1 = create_random_commit(mut_repo, &settings) + .set_author(Signature { + name: "name1".to_string(), + email: "email1".to_string(), + timestamp: timestamp.clone(), + }) + .write() + .unwrap(); + let commit2 = create_random_commit(mut_repo, &settings) + .set_parents(vec![commit1.id().clone()]) + .set_author(Signature { + name: "name2".to_string(), + email: settings.user_email(), + timestamp: timestamp.clone(), + }) + .write() + .unwrap(); + // Can find a unique match by name + assert_eq!( + resolve_commit_ids(mut_repo, "mine()"), + vec![commit2.id().clone()] + ); + let commit3 = create_random_commit(mut_repo, &settings) + .set_parents(vec![commit2.id().clone()]) + .set_author(Signature { + name: "name3".to_string(), + email: settings.user_email(), + timestamp, + }) + .write() + .unwrap(); + // Can find multiple matches by name + assert_eq!( + resolve_commit_ids(mut_repo, "mine()"), + vec![commit3.id().clone(), commit2.id().clone()] + ); + // Searches only among candidates if specified + assert_eq!( + resolve_commit_ids(mut_repo, "visible_heads() & mine()"), + vec![commit3.id().clone()], + ); + // Filter by union of pure predicate and set + assert_eq!( + resolve_commit_ids( + mut_repo, + &format!("root.. & (mine() | {})", commit1.id().hex()) + ), + vec![ + commit3.id().clone(), + commit2.id().clone(), + commit1.id().clone() + ] + ); +} + #[test_case(false ; "local backend")] #[test_case(true ; "git backend")] fn test_evaluate_expression_committer(use_git: bool) {