// 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_alias_basic() { 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#"aliases.bk = ["log", "-r", "@", "-T", "bookmarks"]"#); work_dir .run_jj(["bookmark", "create", "my-bookmark", "-r", "@"]) .success(); let output = work_dir.run_jj(["bk"]); insta::assert_snapshot!(output, @r" @ my-bookmark │ ~ [EOF] "); } #[test] fn test_alias_bad_name() { 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(["foo."]); insta::assert_snapshot!(output, @r" ------- stderr ------- error: unrecognized subcommand 'foo.' Usage: jj [OPTIONS] For more information, try '--help'. [EOF] [exit status: 2] "); } #[test] fn test_alias_calls_empty_command() { 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#" aliases.empty = [] aliases.empty_command_with_opts = ["--no-pager"] "#, ); let output = work_dir.run_jj(["empty"]); insta::assert_snapshot!( output.normalize_stderr_with(|s| s.split_inclusive('\n').take(3).collect()), @r" ------- stderr ------- Jujutsu (An experimental VCS) Usage: jj [OPTIONS] [EOF] [exit status: 2] "); let output = work_dir.run_jj(["empty", "--no-pager"]); insta::assert_snapshot!( output.normalize_stderr_with(|s| s.split_inclusive('\n').take(1).collect()), @r" ------- stderr ------- error: 'jj' requires a subcommand but one was not provided [EOF] [exit status: 2] "); let output = work_dir.run_jj(["empty_command_with_opts"]); insta::assert_snapshot!( output.normalize_stderr_with(|s| s.split_inclusive('\n').take(1).collect()), @r" ------- stderr ------- error: 'jj' requires a subcommand but one was not provided [EOF] [exit status: 2] "); } #[test] fn test_alias_calls_unknown_command() { 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#"aliases.foo = ["nonexistent"]"#); let output = work_dir.run_jj(["foo"]); insta::assert_snapshot!(output, @r" ------- stderr ------- error: unrecognized subcommand 'nonexistent' tip: a similar subcommand exists: 'next' Usage: jj [OPTIONS] For more information, try '--help'. [EOF] [exit status: 2] "); } #[test] fn test_alias_calls_command_with_invalid_option() { 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#"aliases.foo = ["log", "--nonexistent"]"#); let output = work_dir.run_jj(["foo"]); insta::assert_snapshot!(output, @r" ------- stderr ------- error: unexpected argument '--nonexistent' found tip: to pass '--nonexistent' as a value, use '-- --nonexistent' Usage: jj log [OPTIONS] [FILESETS]... For more information, try '--help'. [EOF] [exit status: 2] "); } #[test] fn test_alias_calls_help() { 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#"aliases.h = ["--help"]"#); let output = work_dir.run_jj(&["h"]); insta::assert_snapshot!( output.normalize_stdout_with(|s| s.split_inclusive('\n').take(7).collect()), @r" Jujutsu (An experimental VCS) To get started, see the tutorial [`jj help -k tutorial`]. [`jj help -k tutorial`]: https://jj-vcs.github.io/jj/latest/tutorial/ Usage: jj [OPTIONS] [EOF] "); } #[test] fn test_alias_cannot_override_builtin() { 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#"aliases.log = ["rebase"]"#); // Alias should give a warning let output = work_dir.run_jj(["log", "-r", "root()"]); insta::assert_snapshot!(output, @r" ◆ zzzzzzzz root() 00000000 [EOF] ------- stderr ------- Warning: Cannot define an alias that overrides the built-in command 'log' [EOF] "); } #[test] fn test_alias_recursive() { 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#"[aliases] foo = ["foo"] bar = ["baz"] baz = ["bar"] "#, ); // Alias should not cause infinite recursion or hang let output = work_dir.run_jj(["foo"]); insta::assert_snapshot!(output, @r" ------- stderr ------- Error: Recursive alias definition involving `foo` [EOF] [exit status: 1] "); // Also test with mutual recursion let output = work_dir.run_jj(["bar"]); insta::assert_snapshot!(output, @r" ------- stderr ------- Error: Recursive alias definition involving `bar` [EOF] [exit status: 1] "); } #[test] fn test_alias_global_args_before_and_after() { 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#"aliases.l = ["log", "-T", "commit_id", "-r", "all()"]"#); // Test the setup let output = work_dir.run_jj(["l"]); insta::assert_snapshot!(output, @r" @ 230dd059e1b059aefc0da06a2e5a7dbf22362f22 ◆ 0000000000000000000000000000000000000000 [EOF] "); // Can pass global args before let output = work_dir.run_jj(["l", "--at-op", "@-"]); insta::assert_snapshot!(output, @r" ◆ 0000000000000000000000000000000000000000 [EOF] "); // Can pass global args after let output = work_dir.run_jj(["--at-op", "@-", "l"]); insta::assert_snapshot!(output, @r" ◆ 0000000000000000000000000000000000000000 [EOF] "); // Test passing global args both before and after let output = work_dir.run_jj(["--at-op", "abc123", "l", "--at-op", "@-"]); insta::assert_snapshot!(output, @r" ◆ 0000000000000000000000000000000000000000 [EOF] "); let output = work_dir.run_jj(["-R", "../nonexistent", "l", "-R", "."]); insta::assert_snapshot!(output, @r" @ 230dd059e1b059aefc0da06a2e5a7dbf22362f22 ◆ 0000000000000000000000000000000000000000 [EOF] "); } #[test] fn test_alias_global_args_in_definition() { 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#"aliases.l = ["log", "-T", "commit_id", "--at-op", "@-", "-r", "all()", "--color=always"]"#, ); // The global argument in the alias is respected let output = work_dir.run_jj(["l"]); insta::assert_snapshot!(output, @r" ◆ 0000000000000000000000000000000000000000 [EOF] "); } #[test] fn test_alias_invalid_definition() { let test_env = TestEnvironment::default(); test_env.add_config( r#"[aliases] non-list = 5 non-string-list = [0] "#, ); let output = test_env.run_jj_in(".", ["non-list"]); insta::assert_snapshot!(output.normalize_backslash(), @r" ------- stderr ------- Config error: Invalid type or value for aliases.non-list Caused by: invalid type: integer `5`, expected a sequence Hint: Check the config file: $TEST_ENV/config/config0002.toml For help, see https://jj-vcs.github.io/jj/latest/config/ or use `jj help -k config`. [EOF] [exit status: 1] "); let output = test_env.run_jj_in(".", ["non-string-list"]); insta::assert_snapshot!(output, @r" ------- stderr ------- Config error: Invalid type or value for aliases.non-string-list Caused by: invalid type: integer `0`, expected a string Hint: Check the config file: $TEST_ENV/config/config0002.toml For help, see https://jj-vcs.github.io/jj/latest/config/ or use `jj help -k config`. [EOF] [exit status: 1] "); } #[test] fn test_alias_in_repo_config() { let test_env = TestEnvironment::default(); test_env.run_jj_in(".", ["git", "init", "repo1"]).success(); let work_dir1 = test_env.work_dir("repo1"); work_dir1.create_dir("sub"); test_env.run_jj_in(".", ["git", "init", "repo2"]).success(); let work_dir2 = test_env.work_dir("repo2"); work_dir2.create_dir("sub"); test_env.add_config(r#"aliases.l = ['log', '-r@', '--no-graph', '-T"user alias\n"']"#); work_dir1.write_file( ".jj/repo/config.toml", r#"aliases.l = ['log', '-r@', '--no-graph', '-T"repo1 alias\n"']"#, ); // In repo1 sub directory, aliases can be loaded from the repo1 config. let output = test_env.run_jj_in(work_dir1.root().join("sub"), ["l"]); insta::assert_snapshot!(output, @r" repo1 alias [EOF] "); // In repo2 directory, no repo-local aliases exist. let output = work_dir2.run_jj(["l"]); insta::assert_snapshot!(output, @r" user alias [EOF] "); // Aliases can't be loaded from the -R path due to chicken and egg problem. let output = work_dir2.run_jj(["l", "-R", work_dir1.root().to_str().unwrap()]); insta::assert_snapshot!(output, @r" user alias [EOF] ------- stderr ------- Warning: Command aliases cannot be loaded from -R/--repository path or --config/--config-file arguments. [EOF] "); // Aliases are loaded from the cwd-relative workspace even with -R. let output = work_dir1.run_jj(["l", "-R", work_dir2.root().to_str().unwrap()]); insta::assert_snapshot!(output, @r" repo1 alias [EOF] ------- stderr ------- Warning: Command aliases cannot be loaded from -R/--repository path or --config/--config-file arguments. [EOF] "); // No warning if the expanded command is identical. let output = work_dir1.run_jj(["file", "list", "-R", work_dir2.root().to_str().unwrap()]); insta::assert_snapshot!(output, @""); // Config loaded from the cwd-relative workspace shouldn't persist. It's // used only for command arguments expansion. let output = work_dir1.run_jj([ "config", "list", "aliases", "-R", work_dir2.root().to_str().unwrap(), ]); insta::assert_snapshot!(output, @r#" aliases.l = ['log', '-r@', '--no-graph', '-T"user alias\n"'] [EOF] "#); } #[test] fn test_alias_in_config_arg() { 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#"aliases.l = ['log', '-r@', '--no-graph', '-T"user alias\n"']"#); let output = work_dir.run_jj(["l"]); insta::assert_snapshot!(output, @r" user alias [EOF] "); let alias_arg = r#"--config=aliases.l=['log', '-r@', '--no-graph', '-T"arg alias\n"']"#; let output = work_dir.run_jj([alias_arg, "l"]); insta::assert_snapshot!(output, @r" user alias [EOF] ------- stderr ------- Warning: Command aliases cannot be loaded from -R/--repository path or --config/--config-file arguments. [EOF] "); // should print warning about aliases even if cli parsing fails let alias_arg = r#"--config=aliases.this-command-not-exist=['log', '-r@', '--no-graph', '-T"arg alias\n"']"#; let output = work_dir.run_jj([alias_arg, "this-command-not-exist"]); insta::assert_snapshot!(output, @r" ------- stderr ------- Warning: Command aliases cannot be loaded from -R/--repository path or --config/--config-file arguments. error: unrecognized subcommand 'this-command-not-exist' Usage: jj [OPTIONS] For more information, try '--help'. [EOF] [exit status: 2] "); } #[test] fn test_aliases_overriding_friendly_errors() { let test_env = TestEnvironment::default(); // Test with color let output = test_env.run_jj_in(".", ["--color=always", "init", "repo"]); insta::assert_snapshot!(output, @r#" ------- stderr ------- error: unrecognized subcommand 'init' For more information, try '--help'. Hint: You probably want `jj git init`. See also `jj help git`. Hint: You can configure `aliases.init = ["git", "init"]` if you want `jj init` to work and always use the Git backend. [EOF] [exit status: 2] "#); let output = test_env.run_jj_in(".", ["init", "repo"]); insta::assert_snapshot!(output, @r#" ------- stderr ------- error: unrecognized subcommand 'init' For more information, try '--help'. Hint: You probably want `jj git init`. See also `jj help git`. Hint: You can configure `aliases.init = ["git", "init"]` if you want `jj init` to work and always use the Git backend. [EOF] [exit status: 2] "#); let output = test_env.run_jj_in(".", ["clone", "https://example.org/repo"]); insta::assert_snapshot!(output, @r#" ------- stderr ------- error: unrecognized subcommand 'clone' For more information, try '--help'. Hint: You probably want `jj git clone`. See also `jj help git`. Hint: You can configure `aliases.clone = ["git", "clone"]` if you want `jj clone` to work and always use the Git backend. [EOF] [exit status: 2] "#); let output = test_env.run_jj_in(".", ["init", "--help"]); insta::assert_snapshot!(output, @r#" ------- stderr ------- error: unrecognized subcommand 'init' For more information, try '--help'. Hint: You probably want `jj git init`. See also `jj help git`. Hint: You can configure `aliases.init = ["git", "init"]` if you want `jj init` to work and always use the Git backend. [EOF] [exit status: 2] "#); // Test that `init` can be overridden as an alias. (We use `jj config get` // as a command with a predictable output) test_env.add_config(r#"aliases.init=["config", "get", "user.name"]"#); let output = test_env.run_jj_in(".", ["init"]); insta::assert_snapshot!(output, @r" Test User [EOF] "); }