jj/cli/tests/test_revset_output.rs
2025-04-01 15:00:17 +00:00

850 lines
24 KiB
Rust

// Copyright 2022 The Jujutsu Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use crate::common::TestEnvironment;
#[test]
fn test_syntax_error() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
let output = work_dir.run_jj(["log", "-r", ":x"]);
insta::assert_snapshot!(output, @r"
------- stderr -------
Error: Failed to parse revset: `:` is not a prefix operator
Caused by: --> 1:1
|
1 | :x
| ^
|
= `:` is not a prefix operator
Hint: Did you mean `::` for ancestors?
[EOF]
[exit status: 1]
");
let output = work_dir.run_jj(["log", "-r", "x &"]);
insta::assert_snapshot!(output, @r"
------- stderr -------
Error: Failed to parse revset: Syntax error
Caused by: --> 1:4
|
1 | x &
| ^---
|
= expected `::`, `..`, `~`, or <primary>
Hint: See https://jj-vcs.github.io/jj/latest/revsets/ or use `jj help -k revsets` for revsets syntax and how to quote symbols.
[EOF]
[exit status: 1]
");
let output = work_dir.run_jj(["log", "-r", "x - y"]);
insta::assert_snapshot!(output, @r"
------- stderr -------
Error: Failed to parse revset: `-` is not an infix operator
Caused by: --> 1:3
|
1 | x - y
| ^
|
= `-` is not an infix operator
Hint: Did you mean `~` for difference?
[EOF]
[exit status: 1]
");
let output = work_dir.run_jj(["log", "-r", "HEAD^"]);
insta::assert_snapshot!(output, @r"
------- stderr -------
Error: Failed to parse revset: `^` is not a postfix operator
Caused by: --> 1:5
|
1 | HEAD^
| ^
|
= `^` is not a postfix operator
Hint: Did you mean `-` for parents?
[EOF]
[exit status: 1]
");
}
#[test]
fn test_bad_function_call() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
let output = work_dir.run_jj(["log", "-r", "all(or::nothing)"]);
insta::assert_snapshot!(output, @r"
------- stderr -------
Error: Failed to parse revset: Function `all`: Expected 0 arguments
Caused by: --> 1:5
|
1 | all(or::nothing)
| ^---------^
|
= Function `all`: Expected 0 arguments
[EOF]
[exit status: 1]
");
let output = work_dir.run_jj(["log", "-r", "parents()"]);
insta::assert_snapshot!(output, @r"
------- stderr -------
Error: Failed to parse revset: Function `parents`: Expected 1 arguments
Caused by: --> 1:9
|
1 | parents()
| ^
|
= Function `parents`: Expected 1 arguments
[EOF]
[exit status: 1]
");
let output = work_dir.run_jj(["log", "-r", "parents(foo, bar)"]);
insta::assert_snapshot!(output, @r"
------- stderr -------
Error: Failed to parse revset: Function `parents`: Expected 1 arguments
Caused by: --> 1:9
|
1 | parents(foo, bar)
| ^------^
|
= Function `parents`: Expected 1 arguments
[EOF]
[exit status: 1]
");
let output = work_dir.run_jj(["log", "-r", "heads(foo, bar)"]);
insta::assert_snapshot!(output, @r"
------- stderr -------
Error: Failed to parse revset: Function `heads`: Expected 1 arguments
Caused by: --> 1:7
|
1 | heads(foo, bar)
| ^------^
|
= Function `heads`: Expected 1 arguments
[EOF]
[exit status: 1]
");
let output = work_dir.run_jj(["log", "-r", "latest(a, not_an_integer)"]);
insta::assert_snapshot!(output, @r"
------- stderr -------
Error: Failed to parse revset: Expected expression of type integer
Caused by: --> 1:11
|
1 | latest(a, not_an_integer)
| ^------------^
|
= Expected expression of type integer
[EOF]
[exit status: 1]
");
// "N to M arguments"
let output = work_dir.run_jj(["log", "-r", "ancestors()"]);
insta::assert_snapshot!(output, @r"
------- stderr -------
Error: Failed to parse revset: Function `ancestors`: Expected 1 to 2 arguments
Caused by: --> 1:11
|
1 | ancestors()
| ^
|
= Function `ancestors`: Expected 1 to 2 arguments
[EOF]
[exit status: 1]
");
let output = work_dir.run_jj(["log", "-r", "files(not::a-fileset)"]);
insta::assert_snapshot!(output, @r"
------- stderr -------
Error: Failed to parse revset: In fileset expression
Caused by:
1: --> 1:7
|
1 | files(not::a-fileset)
| ^------------^
|
= In fileset expression
2: --> 1:5
|
1 | not::a-fileset
| ^---
|
= expected <identifier>, <string_literal>, or <raw_string_literal>
Hint: See https://jj-vcs.github.io/jj/latest/filesets/ or use `jj help -k filesets` for filesets syntax and how to match file paths.
[EOF]
[exit status: 1]
");
let output = work_dir.run_jj(["log", "-r", r#"files(foo:"bar")"#]);
insta::assert_snapshot!(output, @r#"
------- stderr -------
Error: Failed to parse revset: In fileset expression
Caused by:
1: --> 1:7
|
1 | files(foo:"bar")
| ^-------^
|
= In fileset expression
2: --> 1:1
|
1 | foo:"bar"
| ^-------^
|
= Invalid file pattern
3: Invalid file pattern kind `foo:`
Hint: See https://jj-vcs.github.io/jj/latest/filesets/#file-patterns or `jj help -k filesets` for valid prefixes.
[EOF]
[exit status: 1]
"#);
let output = work_dir.run_jj(["log", "-r", r#"files("../out")"#]);
insta::assert_snapshot!(output.normalize_backslash(), @r#"
------- stderr -------
Error: Failed to parse revset: In fileset expression
Caused by:
1: --> 1:7
|
1 | files("../out")
| ^------^
|
= In fileset expression
2: --> 1:1
|
1 | "../out"
| ^------^
|
= Invalid file pattern
3: Path "../out" is not in the repo "."
4: Invalid component ".." in repo-relative path "../out"
[EOF]
[exit status: 1]
"#);
let output = work_dir.run_jj(["log", "-r", "bookmarks(bad:pattern)"]);
insta::assert_snapshot!(output, @r"
------- stderr -------
Error: Failed to parse revset: Invalid string pattern
Caused by:
1: --> 1:11
|
1 | bookmarks(bad:pattern)
| ^---------^
|
= Invalid string pattern
2: Invalid string pattern kind `bad:`
Hint: Try prefixing with one of `exact:`, `glob:`, `regex:`, `substring:`, or one of these with `-i` suffix added (e.g. `glob-i:`) for case-insensitive matching
[EOF]
[exit status: 1]
");
let output = work_dir.run_jj(["log", "-r", "bookmarks(regex:'(')"]);
insta::assert_snapshot!(output, @r"
------- stderr -------
Error: Failed to parse revset: Invalid string pattern
Caused by:
1: --> 1:11
|
1 | bookmarks(regex:'(')
| ^-------^
|
= Invalid string pattern
2: regex parse error:
(
^
error: unclosed group
[EOF]
[exit status: 1]
");
let output = work_dir.run_jj(["log", "-r", "root()::whatever()"]);
insta::assert_snapshot!(output, @r"
------- stderr -------
Error: Failed to parse revset: Function `whatever` doesn't exist
Caused by: --> 1:9
|
1 | root()::whatever()
| ^------^
|
= Function `whatever` doesn't exist
[EOF]
[exit status: 1]
");
let output = work_dir.run_jj(["log", "-r", "remote_bookmarks(a, b, remote=c)"]);
insta::assert_snapshot!(output, @r#"
------- stderr -------
Error: Failed to parse revset: Function `remote_bookmarks`: Got multiple values for keyword "remote"
Caused by: --> 1:24
|
1 | remote_bookmarks(a, b, remote=c)
| ^------^
|
= Function `remote_bookmarks`: Got multiple values for keyword "remote"
[EOF]
[exit status: 1]
"#);
let output = work_dir.run_jj(["log", "-r", "remote_bookmarks(remote=a, b)"]);
insta::assert_snapshot!(output, @r"
------- stderr -------
Error: Failed to parse revset: Function `remote_bookmarks`: Positional argument follows keyword argument
Caused by: --> 1:28
|
1 | remote_bookmarks(remote=a, b)
| ^
|
= Function `remote_bookmarks`: Positional argument follows keyword argument
[EOF]
[exit status: 1]
");
let output = work_dir.run_jj(["log", "-r", "remote_bookmarks(=foo)"]);
insta::assert_snapshot!(output, @r"
------- stderr -------
Error: Failed to parse revset: Syntax error
Caused by: --> 1:18
|
1 | remote_bookmarks(=foo)
| ^---
|
= expected <strict_identifier> or <expression>
Hint: See https://jj-vcs.github.io/jj/latest/revsets/ or use `jj help -k revsets` for revsets syntax and how to quote symbols.
[EOF]
[exit status: 1]
");
let output = work_dir.run_jj(["log", "-r", "remote_bookmarks(remote=)"]);
insta::assert_snapshot!(output, @r"
------- stderr -------
Error: Failed to parse revset: Syntax error
Caused by: --> 1:25
|
1 | remote_bookmarks(remote=)
| ^---
|
= expected <expression>
Hint: See https://jj-vcs.github.io/jj/latest/revsets/ or use `jj help -k revsets` for revsets syntax and how to quote symbols.
[EOF]
[exit status: 1]
");
}
#[test]
fn test_function_name_hint() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
let evaluate = |expr| work_dir.run_jj(["log", "-r", expr]);
test_env.add_config(
r###"
[revset-aliases]
'bookmarks(x)' = 'x' # override builtin function
'my_author(x)' = 'author(x)' # similar name to builtin function
'author_sym' = 'x' # not a function alias
'my_bookmarks' = 'bookmark()' # typo in alias
"###,
);
// The suggestion "bookmarks" shouldn't be duplicated
insta::assert_snapshot!(evaluate("bookmark()"), @r"
------- stderr -------
Error: Failed to parse revset: Function `bookmark` doesn't exist
Caused by: --> 1:1
|
1 | bookmark()
| ^------^
|
= Function `bookmark` doesn't exist
Hint: Did you mean `bookmarks`, `remote_bookmarks`?
[EOF]
[exit status: 1]
");
// Both builtin function and function alias should be suggested
insta::assert_snapshot!(evaluate("author_()"), @r"
------- stderr -------
Error: Failed to parse revset: Function `author_` doesn't exist
Caused by: --> 1:1
|
1 | author_()
| ^-----^
|
= Function `author_` doesn't exist
Hint: Did you mean `author`, `author_date`, `author_email`, `author_name`, `my_author`?
[EOF]
[exit status: 1]
");
insta::assert_snapshot!(evaluate("my_bookmarks"), @r"
------- stderr -------
Error: Failed to parse revset: In alias `my_bookmarks`
Caused by:
1: --> 1:1
|
1 | my_bookmarks
| ^----------^
|
= In alias `my_bookmarks`
2: --> 1:1
|
1 | bookmark()
| ^------^
|
= Function `bookmark` doesn't exist
Hint: Did you mean `bookmarks`, `remote_bookmarks`?
[EOF]
[exit status: 1]
");
}
#[test]
fn test_alias() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
test_env.add_config(
r###"
[revset-aliases]
'my-root' = 'root()'
'syntax-error' = 'whatever &'
'recurse' = 'recurse1'
'recurse1' = 'recurse2()'
'recurse2()' = 'recurse'
'identity(x)' = 'x'
'my_author(x)' = 'author(x)'
"###,
);
let output = work_dir.run_jj(["log", "-r", "my-root"]);
insta::assert_snapshot!(output, @r"
◆ zzzzzzzz root() 00000000
[EOF]
");
let output = work_dir.run_jj(["log", "-r", "identity(my-root)"]);
insta::assert_snapshot!(output, @r"
◆ zzzzzzzz root() 00000000
[EOF]
");
let output = work_dir.run_jj(["log", "-r", "root() & syntax-error"]);
insta::assert_snapshot!(output, @r"
------- stderr -------
Error: Failed to parse revset: In alias `syntax-error`
Caused by:
1: --> 1:10
|
1 | root() & syntax-error
| ^----------^
|
= In alias `syntax-error`
2: --> 1:11
|
1 | whatever &
| ^---
|
= expected `::`, `..`, `~`, or <primary>
Hint: See https://jj-vcs.github.io/jj/latest/revsets/ or use `jj help -k revsets` for revsets syntax and how to quote symbols.
[EOF]
[exit status: 1]
");
let output = work_dir.run_jj(["log", "-r", "identity()"]);
insta::assert_snapshot!(output, @r"
------- stderr -------
Error: Failed to parse revset: Function `identity`: Expected 1 arguments
Caused by: --> 1:10
|
1 | identity()
| ^
|
= Function `identity`: Expected 1 arguments
[EOF]
[exit status: 1]
");
let output = work_dir.run_jj(["log", "-r", "my_author(none())"]);
insta::assert_snapshot!(output, @r"
------- stderr -------
Error: Failed to parse revset: In alias `my_author(x)`
Caused by:
1: --> 1:1
|
1 | my_author(none())
| ^---------------^
|
= In alias `my_author(x)`
2: --> 1:8
|
1 | author(x)
| ^
|
= In function parameter `x`
3: --> 1:11
|
1 | my_author(none())
| ^----^
|
= Expected expression of string pattern
[EOF]
[exit status: 1]
");
let output = work_dir.run_jj(["log", "-r", "root() & recurse"]);
insta::assert_snapshot!(output, @r"
------- stderr -------
Error: Failed to parse revset: In alias `recurse`
Caused by:
1: --> 1:10
|
1 | root() & recurse
| ^-----^
|
= In alias `recurse`
2: --> 1:1
|
1 | recurse1
| ^------^
|
= In alias `recurse1`
3: --> 1:1
|
1 | recurse2()
| ^--------^
|
= In alias `recurse2()`
4: --> 1:1
|
1 | recurse
| ^-----^
|
= Alias `recurse` expanded recursively
[EOF]
[exit status: 1]
");
}
#[test]
fn test_alias_override() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
test_env.add_config(
r###"
[revset-aliases]
'f(x)' = 'user'
"###,
);
// 'f(x)' should be overridden by --config 'f(a)'. If aliases were sorted
// purely by name, 'f(a)' would come first.
let output = work_dir.run_jj(["log", "-r", "f(_)", "--config=revset-aliases.'f(a)'=arg"]);
insta::assert_snapshot!(output, @r"
------- stderr -------
Error: Revision `arg` doesn't exist
[EOF]
[exit status: 1]
");
}
#[test]
fn test_bad_alias_decl() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
test_env.add_config(
r#"
[revset-aliases]
'my-root' = 'root()'
'"bad"' = 'root()'
'badfn(a, a)' = 'root()'
"#,
);
// Invalid declaration should be warned and ignored.
let output = work_dir.run_jj(["log", "-r", "my-root"]);
insta::assert_snapshot!(output, @r#"
◆ zzzzzzzz root() 00000000
[EOF]
------- stderr -------
Warning: Failed to load `revset-aliases."bad"`: --> 1:1
|
1 | "bad"
| ^---
|
= expected <strict_identifier> or <function_name>
Warning: Failed to load `revset-aliases.badfn(a, a)`: --> 1:7
|
1 | badfn(a, a)
| ^--^
|
= Redefinition of function parameter
[EOF]
"#);
}
#[test]
fn test_all_modifier() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
// Command that accepts single revision by default
let output = work_dir.run_jj(["new", "all()"]);
insta::assert_snapshot!(output, @r"
------- stderr -------
Error: Revset `all()` resolved to more than one revision
Hint: The revset `all()` resolved to these revisions:
qpvuntsm 230dd059 (empty) (no description set)
zzzzzzzz 00000000 (empty) (no description set)
Hint: Prefix the expression with `all:` to allow any number of revisions (i.e. `all:all()`).
[EOF]
[exit status: 1]
");
let output = work_dir.run_jj(["new", "all:all()"]);
insta::assert_snapshot!(output, @r"
------- stderr -------
Error: The Git backend does not support creating merge commits with the root commit as one of the parents.
[EOF]
[exit status: 1]
");
// Command that accepts multiple revisions by default
let output = work_dir.run_jj(["log", "-rall:all()"]);
insta::assert_snapshot!(output, @r"
@ qpvuntsm test.user@example.com 2001-02-03 08:05:07 230dd059
│ (empty) (no description set)
◆ zzzzzzzz root() 00000000
[EOF]
");
// Command that accepts only single revision
let output = work_dir.run_jj(["bookmark", "create", "-rall:@", "x"]);
insta::assert_snapshot!(output, @r"
------- stderr -------
Created 1 bookmarks pointing to qpvuntsm 230dd059 x | (empty) (no description set)
[EOF]
");
let output = work_dir.run_jj(["bookmark", "set", "-rall:all()", "x"]);
insta::assert_snapshot!(output, @r"
------- stderr -------
Error: Revset `all:all()` resolved to more than one revision
Hint: The revset `all:all()` resolved to these revisions:
qpvuntsm 230dd059 x | (empty) (no description set)
zzzzzzzz 00000000 (empty) (no description set)
[EOF]
[exit status: 1]
");
// Template expression that accepts multiple revisions by default
let output = work_dir.run_jj(["log", "-Tself.contained_in('all:all()')"]);
insta::assert_snapshot!(output, @r"
@ true
◆ true
[EOF]
");
// Typo
let output = work_dir.run_jj(["new", "ale:x"]);
insta::assert_snapshot!(output, @r"
------- stderr -------
Error: Failed to parse revset: Modifier `ale` doesn't exist
Caused by: --> 1:1
|
1 | ale:x
| ^-^
|
= Modifier `ale` doesn't exist
[EOF]
[exit status: 1]
");
// Modifier shouldn't be allowed in sub expression
let output = work_dir.run_jj(["new", "x..", "--config=revset-aliases.x='all:@'"]);
insta::assert_snapshot!(output, @r"
------- stderr -------
Error: Failed to parse revset: In alias `x`
Caused by:
1: --> 1:1
|
1 | x..
| ^
|
= In alias `x`
2: --> 1:1
|
1 | all:@
| ^-^
|
= Modifier `all:` is not allowed in sub expression
[EOF]
[exit status: 1]
");
// immutable_heads() alias may be parsed as a top-level expression, but
// still, modifier shouldn't be allowed there.
let output = work_dir.run_jj([
"new",
"--config=revset-aliases.'immutable_heads()'='all:@'",
"--config=revsets.short-prefixes='none()'",
]);
insta::assert_snapshot!(output, @r"
------- stderr -------
Config error: Invalid `revset-aliases.immutable_heads()`
Caused by: --> 1:1
|
1 | all:@
| ^-^
|
= Modifier `all:` is not allowed in sub expression
For help, see https://jj-vcs.github.io/jj/latest/config/ or use `jj help -k config`.
[EOF]
[exit status: 1]
");
}
/// 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() {
// Use these for the test instead of tzdb identifiers like America/New_York
// because the tz database may not be installed on some build servers
const NEW_YORK: &str = "EST+5EDT+4,M3.1.0,M11.1.0";
const CHICAGO: &str = "CST+6CDT+5,M3.1.0,M11.1.0";
const AUSTRALIA: &str = "AEST-10";
let mut test_env = TestEnvironment::default();
test_env.add_env_var("TZ", NEW_YORK);
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
work_dir
.run_jj([
"--config=debug.commit-timestamp=2023-01-25T11:30:00-05:00",
"describe",
"-m",
"first",
])
.success();
work_dir
.run_jj([
"--config=debug.commit-timestamp=2023-01-25T12:30:00-05:00",
"new",
"-m",
"second",
])
.success();
work_dir
.run_jj([
"--config=debug.commit-timestamp=2023-01-25T13:30:00-05:00",
"new",
"-m",
"third",
])
.success();
let mut log_commits_before_and_after = |committer_date: &str, now: &str, tz: &str| {
test_env.add_env_var("TZ", tz);
let config = format!("debug.commit-timestamp={now}");
let work_dir = test_env.work_dir("repo");
let before_log = work_dir.run_jj([
"--config",
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 = work_dir.run_jj([
"--config",
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", NEW_YORK);
insta::assert_snapshot!(before_log, @r"
first 2023-01-25 11:30:00.000 -05:00
[EOF]
");
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
[EOF]
");
// 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", NEW_YORK);
insta::assert_snapshot!(before_log, @r"
first 2023-01-25 11:30:00.000 -05:00
[EOF]
");
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
[EOF]
");
// 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", 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
[EOF]
");
insta::assert_snapshot!(after_log, @r"
third 2023-01-25 13:30:00.000 -05:00
[EOF]
");
// 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", AUSTRALIA);
insta::assert_snapshot!(before_log, @r"
first 2023-01-25 11:30:00.000 -05:00
[EOF]
");
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
[EOF]
");
}