cli: load revset aliases from config file

Aliases are loaded at WorkspaceCommandHelper::new() as it's easier to warn
invalid declarations there. Not all commands use revsets, but many do, so
I think it's okay to always pay the loading cost. Parsing the declaration
part (i.e. a symbol) should be fast anyway.

The nested error message isn't super readable, but seems good enough.

Config syntax to bikeshed:
- naming: [revset-alias] vs [revset-aliases] ?
- function alias will need quotes: 'f(x)' = 'x'
This commit is contained in:
Yuya Nishihara 2022-11-25 19:27:13 +09:00
parent 11ee2f22c4
commit 8b00a64ab2
4 changed files with 136 additions and 5 deletions

View file

@ -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.<name> = <expression>`.
* It is now possible to specify configuration options on the command line
with the new `--config-toml` global option.

View file

@ -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`):

View file

@ -307,7 +307,7 @@ jj init --git-repo=.",
pub fn for_loaded_repo(
&self,
ui: &Ui,
ui: &mut Ui,
workspace: Workspace,
repo: Arc<ReadonlyRepo>,
) -> Result<WorkspaceCommandHelper, CommandError> {
@ -330,13 +330,14 @@ pub struct WorkspaceCommandHelper {
settings: UserSettings,
workspace: Workspace,
repo: Arc<ReadonlyRepo>,
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<String>,
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<Rc<RevsetExpression>, 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<RevsetAliasesMap, CommandError> {
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],

View file

@ -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
"###);
}