diff --git a/CHANGELOG.md b/CHANGELOG.md index 03381ea41..39a5ab79c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * The new revset function `empty()` finds commits modifying no files. +* Added support for revset aliases. New symbols can be configured by + `revset-aliases. = `. + * It is now possible to specify configuration options on the command line with the new `--config-toml` global option. diff --git a/docs/revsets.md b/docs/revsets.md index ebf350520..6983ae516 100644 --- a/docs/revsets.md +++ b/docs/revsets.md @@ -115,6 +115,18 @@ revsets (expressions) as arguments. in `x` doesn't exist (e.g. is an unknown branch name.) +## Aliases + +New symbols can be defined in the config file, by using any combination +of the predefined symbols/functions and other aliases. + +For example: +```toml +[revset-aliases] +'mine' = 'author(martinvonz)' +``` + + ## Examples Show the parent(s) of the working-copy commit (like `git log -1 HEAD`): diff --git a/src/cli_util.rs b/src/cli_util.rs index 130f350ba..bce77ab5c 100644 --- a/src/cli_util.rs +++ b/src/cli_util.rs @@ -307,7 +307,7 @@ jj init --git-repo=.", pub fn for_loaded_repo( &self, - ui: &Ui, + ui: &mut Ui, workspace: Workspace, repo: Arc, ) -> Result { @@ -330,13 +330,14 @@ pub struct WorkspaceCommandHelper { settings: UserSettings, workspace: Workspace, repo: Arc, + revset_aliases_map: RevsetAliasesMap, may_update_working_copy: bool, working_copy_shared_with_git: bool, } impl WorkspaceCommandHelper { pub fn new( - ui: &Ui, + ui: &mut Ui, workspace: Workspace, string_args: Vec, global_args: &GlobalArgs, @@ -360,6 +361,7 @@ impl WorkspaceCommandHelper { settings: ui.settings().clone(), workspace, repo, + revset_aliases_map: load_revset_aliases(ui)?, may_update_working_copy, working_copy_shared_with_git, }) @@ -609,8 +611,11 @@ impl WorkspaceCommandHelper { &self, revision_str: &str, ) -> Result, RevsetParseError> { - let aliases_map = RevsetAliasesMap::new(); // TODO: load from settings - let expression = revset::parse(revision_str, &aliases_map, Some(&self.revset_context()))?; + let expression = revset::parse( + revision_str, + &self.revset_aliases_map, + Some(&self.revset_context()), + )?; Ok(revset::optimize(expression)) } @@ -1024,6 +1029,23 @@ fn resolve_single_op_from_store( } } +fn load_revset_aliases(ui: &mut Ui) -> Result { + const TABLE_KEY: &str = "revset-aliases"; + let mut aliases_map = RevsetAliasesMap::new(); + if let Ok(table) = ui.settings().config().get_table(TABLE_KEY) { + for (decl, value) in table.into_iter().sorted_by(|a, b| a.0.cmp(&b.0)) { + let r = value + .into_string() + .map_err(|e| e.to_string()) + .and_then(|v| aliases_map.insert(&decl, v).map_err(|e| e.to_string())); + if let Err(s) = r { + ui.write_warn(format!("Failed to load \"{TABLE_KEY}.{decl}\": {s}\n"))?; + } + } + } + Ok(aliases_map) +} + pub fn resolve_base_revs( workspace_command: &WorkspaceCommandHelper, revisions: &[String], diff --git a/tests/test_revset_output.rs b/tests/test_revset_output.rs index 1332a7c81..c9b405f1a 100644 --- a/tests/test_revset_output.rs +++ b/tests/test_revset_output.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use common::TestEnvironment; +use common::{get_stderr_string, get_stdout_string, TestEnvironment}; pub mod common; @@ -119,3 +119,97 @@ fn test_bad_function_call() { = Revset function "whatever" doesn't exist "###); } + +#[test] +fn test_alias() { + let test_env = TestEnvironment::default(); + test_env.jj_cmd_success(test_env.env_root(), &["init", "repo", "--git"]); + let repo_path = test_env.env_root().join("repo"); + + test_env.add_config( + br###" + [revset-aliases] + 'my-root' = 'root' + 'syntax-error' = 'whatever &' + 'recurse' = 'recurse1' + 'recurse1' = 'recurse' + "###, + ); + + let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-r", "my-root"]); + insta::assert_snapshot!(stdout, @r###" + o 000000000000 1970-01-01 00:00:00.000 +00:00 000000000000 + (no description set) + "###); + + let stderr = test_env.jj_cmd_failure(&repo_path, &["log", "-r", "root & syntax-error"]); + insta::assert_snapshot!(stderr, @r###" + Error: Failed to parse revset: --> 1:8 + | + 1 | root & syntax-error + | ^----------^ + | + = Alias "syntax-error" cannot be expanded + --> 1:11 + | + 1 | whatever & + | ^--- + | + = expected dag_range_pre_op, range_pre_op, or primary + "###); + + let stderr = test_env.jj_cmd_failure(&repo_path, &["log", "-r", "root & recurse"]); + insta::assert_snapshot!(stderr, @r###" + Error: Failed to parse revset: --> 1:8 + | + 1 | root & recurse + | ^-----^ + | + = Alias "recurse" cannot be expanded + --> 1:1 + | + 1 | recurse1 + | ^------^ + | + = Alias "recurse1" cannot be expanded + --> 1:1 + | + 1 | recurse + | ^-----^ + | + = Alias "recurse" expanded recursively + "###); +} + +#[test] +fn test_bad_alias_decl() { + let test_env = TestEnvironment::default(); + test_env.jj_cmd_success(test_env.env_root(), &["init", "repo", "--git"]); + let repo_path = test_env.env_root().join("repo"); + + test_env.add_config( + br###" + [revset-aliases] + 'my-root' = 'root' + '"bad"' = 'root' + "###, + ); + + // Invalid declaration should be warned and ignored. + let assert = test_env + .jj_cmd(&repo_path, &["log", "-r", "my-root"]) + .assert() + .success(); + insta::assert_snapshot!(get_stdout_string(&assert), @r###" + o 000000000000 1970-01-01 00:00:00.000 +00:00 000000000000 + (no description set) + "###); + insta::assert_snapshot!(get_stderr_string(&assert), @r###" + Failed to load "revset-aliases."bad"": --> 1:1 + | + 1 | "bad" + | ^--- + | + = expected identifier + "###); +}