// 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.jj_cmd_ok(test_env.env_root(), &["git", "init", "repo"]); let repo_path = test_env.env_root().join("repo"); let stderr = test_env.jj_cmd_failure(&repo_path, &["log", "-r", ":x"]); insta::assert_snapshot!(stderr, @r" 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? "); let stderr = test_env.jj_cmd_failure(&repo_path, &["log", "-r", "x &"]); insta::assert_snapshot!(stderr, @r###" Error: Failed to parse revset: Syntax error Caused by: --> 1:4 | 1 | x & | ^--- | = expected `::`, `..`, `~`, or "###); let stderr = test_env.jj_cmd_failure(&repo_path, &["log", "-r", "x - y"]); insta::assert_snapshot!(stderr, @r" 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? "); let stderr = test_env.jj_cmd_failure(&repo_path, &["log", "-r", "HEAD^"]); insta::assert_snapshot!(stderr, @r" 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? "); } #[test] fn test_bad_function_call() { let test_env = TestEnvironment::default(); test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "repo"]); let repo_path = test_env.env_root().join("repo"); let stderr = test_env.jj_cmd_failure(&repo_path, &["log", "-r", "all(or::nothing)"]); insta::assert_snapshot!(stderr, @r" Error: Failed to parse revset: Function `all`: Expected 0 arguments Caused by: --> 1:5 | 1 | all(or::nothing) | ^---------^ | = Function `all`: Expected 0 arguments "); let stderr = test_env.jj_cmd_failure(&repo_path, &["log", "-r", "parents()"]); insta::assert_snapshot!(stderr, @r" Error: Failed to parse revset: Function `parents`: Expected 1 arguments Caused by: --> 1:9 | 1 | parents() | ^ | = Function `parents`: Expected 1 arguments "); let stderr = test_env.jj_cmd_failure(&repo_path, &["log", "-r", "parents(foo, bar)"]); insta::assert_snapshot!(stderr, @r" Error: Failed to parse revset: Function `parents`: Expected 1 arguments Caused by: --> 1:9 | 1 | parents(foo, bar) | ^------^ | = Function `parents`: Expected 1 arguments "); let stderr = test_env.jj_cmd_failure(&repo_path, &["log", "-r", "heads(foo, bar)"]); insta::assert_snapshot!(stderr, @r" Error: Failed to parse revset: Function `heads`: Expected 1 arguments Caused by: --> 1:7 | 1 | heads(foo, bar) | ^------^ | = Function `heads`: Expected 1 arguments "); let stderr = test_env.jj_cmd_failure(&repo_path, &["log", "-r", "latest(a, not_an_integer)"]); insta::assert_snapshot!(stderr, @r###" 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 "###); let stderr = test_env.jj_cmd_failure(&repo_path, &["log", "-r", "files()"]); insta::assert_snapshot!(stderr, @r" Error: Failed to parse revset: Function `files`: Expected at least 1 arguments Caused by: --> 1:7 | 1 | files() | ^ | = Function `files`: Expected at least 1 arguments "); let stderr = test_env.jj_cmd_failure(&repo_path, &["log", "-r", "files(not::a-fileset)"]); insta::assert_snapshot!(stderr, @r#" 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 , , or Hint: See https://jj-vcs.github.io/jj/latest/filesets/ for filesets syntax, or for how to match file paths. "#); let stderr = test_env.jj_cmd_failure(&repo_path, &["log", "-r", r#"files(foo:"bar")"#]); insta::assert_snapshot!(stderr, @r#" 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:` "#); let stderr = test_env.jj_cmd_failure(&repo_path, &["log", "-r", r#"files(a, "../out")"#]); insta::assert_snapshot!(stderr.replace('\\', "/"), @r###" Error: Failed to parse revset: In fileset expression Caused by: 1: --> 1:10 | 1 | files(a, "../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" "###); let stderr = test_env.jj_cmd_failure(&repo_path, &["log", "-r", "bookmarks(bad:pattern)"]); insta::assert_snapshot!(stderr, @r" 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:`, or `substring:` "); let stderr = test_env.jj_cmd_failure(&repo_path, &["log", "-r", "bookmarks(regex:'(')"]); insta::assert_snapshot!(stderr, @r###" 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 "###); let stderr = test_env.jj_cmd_failure(&repo_path, &["log", "-r", "root()::whatever()"]); insta::assert_snapshot!(stderr, @r" Error: Failed to parse revset: Function `whatever` doesn't exist Caused by: --> 1:9 | 1 | root()::whatever() | ^------^ | = Function `whatever` doesn't exist "); let stderr = test_env.jj_cmd_failure( &repo_path, &["log", "-r", "remote_bookmarks(a, b, remote=c)"], ); insta::assert_snapshot!(stderr, @r#" 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" "#); let stderr = test_env.jj_cmd_failure(&repo_path, &["log", "-r", "remote_bookmarks(remote=a, b)"]); insta::assert_snapshot!(stderr, @r" 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 "); let stderr = test_env.jj_cmd_failure(&repo_path, &["log", "-r", "remote_bookmarks(=foo)"]); insta::assert_snapshot!(stderr, @r" Error: Failed to parse revset: Syntax error Caused by: --> 1:18 | 1 | remote_bookmarks(=foo) | ^--- | = expected or "); let stderr = test_env.jj_cmd_failure(&repo_path, &["log", "-r", "remote_bookmarks(remote=)"]); insta::assert_snapshot!(stderr, @r###" Error: Failed to parse revset: Syntax error Caused by: --> 1:25 | 1 | remote_bookmarks(remote=) | ^--- | = expected "###); } #[test] fn test_parse_warning() { let test_env = TestEnvironment::default(); test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "repo"]); let repo_path = test_env.env_root().join("repo"); let (stdout, stderr) = test_env.jj_cmd_ok( &repo_path, &[ "log", "-r", "branches() | remote_branches() | tracked_remote_branches() | \ untracked_remote_branches()", ], ); insta::assert_snapshot!(stdout, @""); insta::assert_snapshot!(stderr, @r#" Warning: In revset expression --> 1:1 | 1 | branches() | remote_branches() | tracked_remote_branches() | untracked_remote_branches() | ^------^ | = branches() is deprecated; use bookmarks() instead Warning: In revset expression --> 1:14 | 1 | branches() | remote_branches() | tracked_remote_branches() | untracked_remote_branches() | ^-------------^ | = remote_branches() is deprecated; use remote_bookmarks() instead Warning: In revset expression --> 1:34 | 1 | branches() | remote_branches() | tracked_remote_branches() | untracked_remote_branches() | ^---------------------^ | = tracked_remote_branches() is deprecated; use tracked_remote_bookmarks() instead Warning: In revset expression --> 1:62 | 1 | branches() | remote_branches() | tracked_remote_branches() | untracked_remote_branches() | ^-----------------------^ | = untracked_remote_branches() is deprecated; use untracked_remote_bookmarks() instead "#); let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["log", "-r", "files(foo, bar)"]); insta::assert_snapshot!(stdout, @""); insta::assert_snapshot!(stderr, @r###" Warning: In revset expression --> 1:7 | 1 | files(foo, bar) | ^------^ | = Multi-argument patterns syntax is deprecated; separate them with | "###); } #[test] fn test_function_name_hint() { let test_env = TestEnvironment::default(); test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "repo"]); let repo_path = test_env.env_root().join("repo"); let evaluate_err = |expr| test_env.jj_cmd_failure(&repo_path, &["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_err("bookmark()"), @r" 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`? "); // Both builtin function and function alias should be suggested insta::assert_snapshot!(evaluate_err("author_()"), @r" 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`? "); insta::assert_snapshot!(evaluate_err("my_bookmarks"), @r" 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`? "); } #[test] fn test_alias() { let test_env = TestEnvironment::default(); test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "repo"]); let repo_path = test_env.env_root().join("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)' 'deprecated()' = 'branches()' "###, ); let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-r", "my-root"]); insta::assert_snapshot!(stdout, @r###" ◆ zzzzzzzz root() 00000000 "###); let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-r", "identity(my-root)"]); insta::assert_snapshot!(stdout, @r###" ◆ zzzzzzzz root() 00000000 "###); let stderr = test_env.jj_cmd_failure(&repo_path, &["log", "-r", "root() & syntax-error"]); insta::assert_snapshot!(stderr, @r" 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 "); let stderr = test_env.jj_cmd_failure(&repo_path, &["log", "-r", "identity()"]); insta::assert_snapshot!(stderr, @r" Error: Failed to parse revset: Function `identity`: Expected 1 arguments Caused by: --> 1:10 | 1 | identity() | ^ | = Function `identity`: Expected 1 arguments "); let stderr = test_env.jj_cmd_failure(&repo_path, &["log", "-r", "my_author(none())"]); insta::assert_snapshot!(stderr, @r" 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 "); let stderr = test_env.jj_cmd_failure(&repo_path, &["log", "-r", "root() & recurse"]); insta::assert_snapshot!(stderr, @r" 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 "); let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["log", "-r", "deprecated()"]); insta::assert_snapshot!(stdout, @""); insta::assert_snapshot!(stderr, @r" Warning: In revset expression --> 1:1 | 1 | deprecated() | ^----------^ | = In alias `deprecated()` --> 1:1 | 1 | branches() | ^------^ | = branches() is deprecated; use bookmarks() instead "); let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["log", "-r", "all:deprecated()"]); insta::assert_snapshot!(stdout, @""); insta::assert_snapshot!(stderr, @r" Warning: In revset expression --> 1:5 | 1 | all:deprecated() | ^----------^ | = In alias `deprecated()` --> 1:1 | 1 | branches() | ^------^ | = branches() is deprecated; use bookmarks() instead "); } #[test] fn test_alias_override() { let test_env = TestEnvironment::default(); test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "repo"]); let repo_path = test_env.env_root().join("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 stderr = test_env.jj_cmd_failure( &repo_path, &["log", "-r", "f(_)", "--config=revset-aliases.'f(a)'=arg"], ); insta::assert_snapshot!(stderr, @"Error: Revision `arg` doesn't exist"); } #[test] fn test_bad_alias_decl() { let test_env = TestEnvironment::default(); test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "repo"]); let repo_path = test_env.env_root().join("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 (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["log", "-r", "my-root"]); insta::assert_snapshot!(stdout, @r###" ◆ zzzzzzzz root() 00000000 "###); insta::assert_snapshot!(stderr, @r#" Warning: Failed to load `revset-aliases."bad"`: --> 1:1 | 1 | "bad" | ^--- | = expected or Warning: Failed to load `revset-aliases.badfn(a, a)`: --> 1:7 | 1 | badfn(a, a) | ^--^ | = Redefinition of function parameter "#); } #[test] fn test_all_modifier() { let test_env = TestEnvironment::default(); test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "repo"]); let repo_path = test_env.env_root().join("repo"); // Command that accepts single revision by default let stderr = test_env.jj_cmd_failure(&repo_path, &["new", "all()"]); insta::assert_snapshot!(stderr, @r" 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()`). "); let stderr = test_env.jj_cmd_failure(&repo_path, &["new", "all:all()"]); insta::assert_snapshot!(stderr, @r###" Error: The Git backend does not support creating merge commits with the root commit as one of the parents. "###); // Command that accepts multiple revisions by default let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-rall:all()"]); insta::assert_snapshot!(stdout, @r###" @ qpvuntsm test.user@example.com 2001-02-03 08:05:07 230dd059 │ (empty) (no description set) ◆ zzzzzzzz root() 00000000 "###); // Command that accepts only single revision let (_stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["bookmark", "create", "-rall:@", "x"]); insta::assert_snapshot!(stderr, @r###" Created 1 bookmarks pointing to qpvuntsm 230dd059 x | (empty) (no description set) "###); let stderr = test_env.jj_cmd_failure(&repo_path, &["bookmark", "set", "-rall:all()", "x"]); insta::assert_snapshot!(stderr, @r" 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) "); // Template expression that accepts multiple revisions by default let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-Tself.contained_in('all:all()')"]); insta::assert_snapshot!(stdout, @r###" @ true ◆ true "###); // Typo let stderr = test_env.jj_cmd_failure(&repo_path, &["new", "ale:x"]); insta::assert_snapshot!(stderr, @r" Error: Failed to parse revset: Modifier `ale` doesn't exist Caused by: --> 1:1 | 1 | ale:x | ^-^ | = Modifier `ale` doesn't exist "); // Modifier shouldn't be allowed in sub expression let stderr = test_env.jj_cmd_failure( &repo_path, &["new", "x..", "--config=revset-aliases.x='all:@'"], ); insta::assert_snapshot!(stderr, @r" 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 "); // immutable_heads() alias may be parsed as a top-level expression, but // still, modifier shouldn't be allowed there. let stderr = test_env.jj_cmd_failure( &repo_path, &[ "new", "--config=revset-aliases.'immutable_heads()'='all:@'", "--config=revsets.short-prefixes='none()'", ], ); insta::assert_snapshot!(stderr, @r" 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/. "); } /// 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.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=debug.commit-timestamp=2023-01-25T11:30:00-05:00", "describe", "-m", "first", ], ); test_env.jj_cmd_ok( &repo_path, &[ "--config=debug.commit-timestamp=2023-01-25T12:30:00-05:00", "new", "-m", "second", ], ); test_env.jj_cmd_ok( &repo_path, &[ "--config=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", 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", 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 "###); 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", 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", 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", AUSTRALIA); 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 "###); }