// 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 std::env::join_paths; use std::path::PathBuf; use indoc::indoc; use regex::Regex; use crate::common::fake_editor_path; use crate::common::force_interactive; use crate::common::to_toml_value; use crate::common::TestEnvironment; #[test] fn test_config_list_single() { let test_env = TestEnvironment::default(); test_env.add_config( r#" [test-table] somekey = "some value" "#, ); let output = test_env.run_jj_in(".", ["config", "list", "test-table.somekey"]); insta::assert_snapshot!(output, @r#" test-table.somekey = "some value" [EOF] "#); let output = test_env.run_jj_in( ".", ["config", "list", r#"-Tname ++ "\n""#, "test-table.somekey"], ); insta::assert_snapshot!(output, @r" test-table.somekey [EOF] "); } #[test] fn test_config_list_nonexistent() { let test_env = TestEnvironment::default(); let output = test_env.run_jj_in(".", ["config", "list", "nonexistent-test-key"]); insta::assert_snapshot!(output, @r" ------- stderr ------- Warning: No matching config key for nonexistent-test-key [EOF] "); } #[test] fn test_config_list_table() { let test_env = TestEnvironment::default(); test_env.add_config( r#" [test-table] x = true y.foo = "abc" y.bar = 123 "z"."with space"."function()" = 5 "#, ); let output = test_env.run_jj_in(".", ["config", "list", "test-table"]); insta::assert_snapshot!(output, @r#" test-table.x = true test-table.y.foo = "abc" test-table.y.bar = 123 test-table.z."with space"."function()" = 5 [EOF] "#); } #[test] fn test_config_list_inline_table() { let test_env = TestEnvironment::default(); test_env.add_config( r#" test-table = { x = true, y = 1 } "#, ); // Inline tables are expanded let output = test_env.run_jj_in(".", ["config", "list", "test-table"]); insta::assert_snapshot!(output, @r" test-table.x = true test-table.y = 1 [EOF] "); // Inner value can also be addressed by a dotted name path let output = test_env.run_jj_in(".", ["config", "list", "test-table.x"]); insta::assert_snapshot!(output, @r" test-table.x = true [EOF] "); } #[test] fn test_config_list_array() { let test_env = TestEnvironment::default(); test_env.add_config( r#" test-array = [1, "b", 3.4] "#, ); let output = test_env.run_jj_in(".", ["config", "list", "test-array"]); insta::assert_snapshot!(output, @r#" test-array = [1, "b", 3.4] [EOF] "#); } #[test] fn test_config_list_array_of_tables() { let test_env = TestEnvironment::default(); test_env.add_config( r#" [[test-table]] x = 1 [[test-table]] y = ["z"] z."key=with whitespace" = [] "#, ); // Array is a value, so is array of tables let output = test_env.run_jj_in(".", ["config", "list", "test-table"]); insta::assert_snapshot!(output, @r#" test-table = [{ x = 1 }, { y = ["z"], z = { "key=with whitespace" = [] } }] [EOF] "#); } #[test] fn test_config_list_all() { let test_env = TestEnvironment::default(); test_env.add_config( r#" test-val = [1, 2, 3] [test-table] x = true y.foo = "abc" y.bar = 123 "#, ); let output = test_env.run_jj_in(".", ["config", "list"]); insta::assert_snapshot!( output.normalize_stdout_with(|s| find_stdout_lines(r"(test-val|test-table\b[^=]*)", &s)), @r#" test-val = [1, 2, 3] test-table.x = true test-table.y.foo = "abc" test-table.y.bar = 123 [EOF] "#); } #[test] fn test_config_list_multiline_string() { let test_env = TestEnvironment::default(); test_env.add_config( r#" multiline = ''' foo bar ''' "#, ); let output = test_env.run_jj_in(".", ["config", "list", "multiline"]); insta::assert_snapshot!(output, @r" multiline = ''' foo bar ''' [EOF] "); let output = test_env.run_jj_in( ".", [ "config", "list", "multiline", "--include-overridden", "--config=multiline='single'", ], ); insta::assert_snapshot!(output, @r" # multiline = ''' # foo # bar # ''' multiline = 'single' [EOF] "); } #[test] fn test_config_list_layer() { let mut test_env = TestEnvironment::default(); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); // Test with fresh new config file let user_config_path = test_env.config_path().join("config.toml"); test_env.set_config_path(&user_config_path); let work_dir = test_env.work_dir("repo"); // User work_dir .run_jj(["config", "set", "--user", "test-key", "test-val"]) .success(); work_dir .run_jj([ "config", "set", "--user", "test-layered-key", "test-original-val", ]) .success(); let output = work_dir.run_jj(["config", "list", "--user"]); insta::assert_snapshot!(output, @r#" "$schema" = "https://jj-vcs.github.io/jj/latest/config-schema.json" test-key = "test-val" test-layered-key = "test-original-val" [EOF] "#); // Repo work_dir .run_jj([ "config", "set", "--repo", "test-layered-key", "test-layered-val", ]) .success(); let output = work_dir.run_jj(["config", "list", "--user"]); insta::assert_snapshot!(output, @r#" test-key = "test-val" [EOF] "#); let output = work_dir.run_jj(["config", "list", "--repo"]); insta::assert_snapshot!(output, @r#" "$schema" = "https://jj-vcs.github.io/jj/latest/config-schema.json" test-layered-key = "test-layered-val" [EOF] "#); } #[test] fn test_config_list_origin() { let mut test_env = TestEnvironment::default(); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); // Test with fresh new config file let user_config_path = test_env.config_path().join("config.toml"); test_env.set_config_path(&user_config_path); let work_dir = test_env.work_dir("repo"); // User work_dir .run_jj(["config", "set", "--user", "test-key", "test-val"]) .success(); work_dir .run_jj([ "config", "set", "--user", "test-layered-key", "test-original-val", ]) .success(); // Repo work_dir .run_jj([ "config", "set", "--repo", "test-layered-key", "test-layered-val", ]) .success(); let output = work_dir.run_jj([ "config", "list", "-Tbuiltin_config_list_detailed", "--config", "test-cli-key=test-cli-val", ]); insta::assert_snapshot!(output, @r#" test-key = "test-val" # user $TEST_ENV/config/config.toml "$schema" = "https://jj-vcs.github.io/jj/latest/config-schema.json" # repo $TEST_ENV/repo/.jj/repo/config.toml test-layered-key = "test-layered-val" # repo $TEST_ENV/repo/.jj/repo/config.toml user.name = "Test User" # env user.email = "test.user@example.com" # env debug.commit-timestamp = "2001-02-03T04:05:11+07:00" # env debug.randomness-seed = 5 # env debug.operation-timestamp = "2001-02-03T04:05:11+07:00" # env operation.hostname = "host.example.com" # env operation.username = "test-username" # env test-cli-key = "test-cli-val" # cli [EOF] "#); let output = work_dir.run_jj([ "config", "list", "-Tbuiltin_config_list_detailed", "--color=debug", "--include-defaults", "--include-overridden", "--config=test-key=test-cli-val", "test-key", ]); insta::assert_snapshot!(output, @r#" <><><><><><><><> <><><><><><> [EOF] "#); } #[test] fn test_config_layer_override_default() { let test_env = TestEnvironment::default(); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); let work_dir = test_env.work_dir("repo"); let config_key = "merge-tools.vimdiff.program"; // Default let output = work_dir.run_jj(["config", "list", config_key, "--include-defaults"]); insta::assert_snapshot!(output, @r#" merge-tools.vimdiff.program = "vim" [EOF] "#); // User test_env.add_config(format!( "{config_key} = {value}\n", value = to_toml_value("user") )); let output = work_dir.run_jj(["config", "list", config_key]); insta::assert_snapshot!(output, @r#" merge-tools.vimdiff.program = "user" [EOF] "#); // Repo work_dir.write_file( ".jj/repo/config.toml", format!("{config_key} = {value}\n", value = to_toml_value("repo")), ); let output = work_dir.run_jj(["config", "list", config_key]); insta::assert_snapshot!(output, @r#" merge-tools.vimdiff.program = "repo" [EOF] "#); // Command argument let output = work_dir.run_jj([ "config", "list", config_key, "--config", &format!("{config_key}={value}", value = to_toml_value("command-arg")), ]); insta::assert_snapshot!(output, @r#" merge-tools.vimdiff.program = "command-arg" [EOF] "#); // Allow printing overridden values let output = work_dir.run_jj([ "config", "list", config_key, "--include-overridden", "--config", &format!("{config_key}={value}", value = to_toml_value("command-arg")), ]); insta::assert_snapshot!(output, @r##" # merge-tools.vimdiff.program = "user" # merge-tools.vimdiff.program = "repo" merge-tools.vimdiff.program = "command-arg" [EOF] "##); let output = work_dir.run_jj([ "config", "list", "--color=always", config_key, "--include-overridden", ]); insta::assert_snapshot!(output, @r#" # merge-tools.vimdiff.program = "user" merge-tools.vimdiff.program = "repo" [EOF] "#); } #[test] fn test_config_layer_override_env() { let mut test_env = TestEnvironment::default(); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); let config_key = "ui.editor"; // Environment base test_env.add_env_var("EDITOR", "env-base"); let work_dir = test_env.work_dir("repo"); let output = work_dir.run_jj(["config", "list", config_key]); insta::assert_snapshot!(output, @r#" ui.editor = "env-base" [EOF] "#); // User test_env.add_config(format!( "{config_key} = {value}\n", value = to_toml_value("user") )); let output = work_dir.run_jj(["config", "list", config_key]); insta::assert_snapshot!(output, @r#" ui.editor = "user" [EOF] "#); // Repo work_dir.write_file( ".jj/repo/config.toml", format!("{config_key} = {value}\n", value = to_toml_value("repo")), ); let output = work_dir.run_jj(["config", "list", config_key]); insta::assert_snapshot!(output, @r#" ui.editor = "repo" [EOF] "#); // Environment override test_env.add_env_var("JJ_EDITOR", "env-override"); let work_dir = test_env.work_dir("repo"); let output = work_dir.run_jj(["config", "list", config_key]); insta::assert_snapshot!(output, @r#" ui.editor = "env-override" [EOF] "#); // Command argument let output = work_dir.run_jj([ "config", "list", config_key, "--config", &format!("{config_key}={value}", value = to_toml_value("command-arg")), ]); insta::assert_snapshot!(output, @r#" ui.editor = "command-arg" [EOF] "#); // Allow printing overridden values let output = work_dir.run_jj([ "config", "list", config_key, "--include-overridden", "--config", &format!("{config_key}={value}", value = to_toml_value("command-arg")), ]); insta::assert_snapshot!(output, @r##" # ui.editor = "env-base" # ui.editor = "user" # ui.editor = "repo" # ui.editor = "env-override" ui.editor = "command-arg" [EOF] "##); } #[test] fn test_config_layer_workspace() { let test_env = TestEnvironment::default(); test_env.run_jj_in(".", ["git", "init", "main"]).success(); let main_dir = test_env.work_dir("main"); let secondary_dir = test_env.work_dir("secondary"); let config_key = "ui.editor"; main_dir.write_file("file", "contents"); main_dir.run_jj(["new"]).success(); main_dir .run_jj(["workspace", "add", "--name", "second", "../secondary"]) .success(); // Repo main_dir.write_file( ".jj/repo/config.toml", format!( "{config_key} = {value}\n", value = to_toml_value("main-repo") ), ); let output = main_dir.run_jj(["config", "list", config_key]); insta::assert_snapshot!(output, @r#" ui.editor = "main-repo" [EOF] "#); let output = secondary_dir.run_jj(["config", "list", config_key]); insta::assert_snapshot!(output, @r#" ui.editor = "main-repo" [EOF] "#); } #[test] fn test_config_set_bad_opts() { let test_env = TestEnvironment::default(); let output = test_env.run_jj_in(".", ["config", "set"]); insta::assert_snapshot!(output, @r" ------- stderr ------- error: the following required arguments were not provided: <--user|--repo> Usage: jj config set <--user|--repo> For more information, try '--help'. [EOF] [exit status: 2] "); let output = test_env.run_jj_in(".", ["config", "set", "--user", "", "x"]); insta::assert_snapshot!(output, @r" ------- stderr ------- error: invalid value '' for '': TOML parse error at line 1, column 1 | 1 | | ^ invalid key For more information, try '--help'. [EOF] [exit status: 2] "); let output = test_env.run_jj_in(".", ["config", "set", "--user", "x", "['typo'}"]); insta::assert_snapshot!(output, @r" ------- stderr ------- error: invalid value '['typo'}' for '': TOML parse error at line 1, column 8 | 1 | ['typo'} | ^ invalid array expected `]` For more information, try '--help'. [EOF] [exit status: 2] "); } #[test] fn test_config_set_for_user() { let mut test_env = TestEnvironment::default(); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); // Test with fresh new config file let user_config_path = test_env.config_path().join("config.toml"); test_env.set_config_path(&user_config_path); let work_dir = test_env.work_dir("repo"); work_dir .run_jj(["config", "set", "--user", "test-key", "test-val"]) .success(); work_dir .run_jj(["config", "set", "--user", "test-table.foo", "true"]) .success(); work_dir .run_jj(["config", "set", "--user", "test-table.'bar()'", "0"]) .success(); // Ensure test-key successfully written to user config. let user_config_toml = std::fs::read_to_string(&user_config_path) .unwrap_or_else(|_| panic!("Failed to read file {}", user_config_path.display())); insta::assert_snapshot!(user_config_toml, @r#" "$schema" = "https://jj-vcs.github.io/jj/latest/config-schema.json" test-key = "test-val" [test-table] foo = true 'bar()' = 0 "#); } #[test] fn test_config_set_for_user_directory() { let test_env = TestEnvironment::default(); test_env .run_jj_in(".", ["config", "set", "--user", "test-key", "test-val"]) .success(); insta::assert_snapshot!( std::fs::read_to_string(test_env.last_config_file_path()).unwrap(), @r#" test-key = "test-val" [template-aliases] 'format_time_range(time_range)' = 'time_range.start() ++ " - " ++ time_range.end()' "#); // Add one more config file to the directory test_env.add_config(""); let output = test_env.run_jj_in( ".", ["config", "set", "--user", "test-key", "test-other-val"], ); insta::assert_snapshot!(output, @r" ------- stderr ------- 1: $TEST_ENV/config/config0001.toml 2: $TEST_ENV/config/config0002.toml Choose a config file (default 1): 1 [EOF] "); insta::assert_snapshot!( std::fs::read_to_string(test_env.first_config_file_path()).unwrap(), @r#" test-key = "test-other-val" [template-aliases] 'format_time_range(time_range)' = 'time_range.start() ++ " - " ++ time_range.end()' "#); insta::assert_snapshot!( std::fs::read_to_string(test_env.last_config_file_path()).unwrap(), @""); } #[test] fn test_config_set_for_repo() { let test_env = TestEnvironment::default(); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); let work_dir = test_env.work_dir("repo"); work_dir .run_jj(["config", "set", "--repo", "test-key", "test-val"]) .success(); work_dir .run_jj(["config", "set", "--repo", "test-table.foo", "true"]) .success(); // Ensure test-key successfully written to user config. let repo_config_toml = work_dir.read_file(".jj/repo/config.toml"); insta::assert_snapshot!(repo_config_toml, @r#" "$schema" = "https://jj-vcs.github.io/jj/latest/config-schema.json" test-key = "test-val" [test-table] foo = true "#); } #[test] fn test_config_set_toml_types() { let mut test_env = TestEnvironment::default(); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); // Test with fresh new config file let user_config_path = test_env.config_path().join("config.toml"); test_env.set_config_path(&user_config_path); let work_dir = test_env.work_dir("repo"); let set_value = |key, value| { work_dir .run_jj(["config", "set", "--user", key, value]) .success(); }; set_value("test-table.integer", "42"); set_value("test-table.float", "3.14"); set_value("test-table.array", r#"["one", "two"]"#); set_value("test-table.boolean", "true"); set_value("test-table.string", r#""foo""#); set_value("test-table.invalid", r"a + b"); insta::assert_snapshot!(std::fs::read_to_string(&user_config_path).unwrap(), @r#" "$schema" = "https://jj-vcs.github.io/jj/latest/config-schema.json" [test-table] integer = 42 float = 3.14 array = ["one", "two"] boolean = true string = "foo" invalid = "a + b" "#); } #[test] fn test_config_set_type_mismatch() { let test_env = TestEnvironment::default(); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); let work_dir = test_env.work_dir("repo"); work_dir .run_jj(["config", "set", "--user", "test-table.foo", "test-val"]) .success(); let output = work_dir.run_jj(["config", "set", "--user", "test-table", "not-a-table"]); insta::assert_snapshot!(output, @r" ------- stderr ------- Error: Failed to set test-table Caused by: Would overwrite entire table test-table [EOF] [exit status: 1] "); // But it's fine to overwrite arrays and inline tables work_dir .run_jj(["config", "set", "--user", "test-table.array", "[1,2,3]"]) .success(); work_dir .run_jj(["config", "set", "--user", "test-table.array", "[4,5,6]"]) .success(); work_dir .run_jj(["config", "set", "--user", "test-table.inline", "{ x = 42}"]) .success(); work_dir .run_jj(["config", "set", "--user", "test-table.inline", "42"]) .success(); } #[test] fn test_config_set_nontable_parent() { let test_env = TestEnvironment::default(); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); let work_dir = test_env.work_dir("repo"); work_dir .run_jj(["config", "set", "--user", "test-nontable", "test-val"]) .success(); let output = work_dir.run_jj(["config", "set", "--user", "test-nontable.foo", "test-val"]); insta::assert_snapshot!(output, @r" ------- stderr ------- Error: Failed to set test-nontable.foo Caused by: Would overwrite non-table value with parent table test-nontable [EOF] [exit status: 1] "); } #[test] fn test_config_unset_non_existent_key() { 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(["config", "unset", "--user", "nonexistent"]); insta::assert_snapshot!(output, @r#" ------- stderr ------- Error: "nonexistent" doesn't exist [EOF] [exit status: 1] "#); } #[test] fn test_config_unset_inline_table_key() { let mut test_env = TestEnvironment::default(); // Test with fresh new config file let user_config_path = test_env.config_path().join("config.toml"); test_env.set_config_path(&user_config_path); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); let work_dir = test_env.work_dir("repo"); work_dir .run_jj(["config", "set", "--user", "inline-table", "{ foo = true }"]) .success(); work_dir .run_jj(["config", "unset", "--user", "inline-table.foo"]) .success(); let user_config_toml = std::fs::read_to_string(&user_config_path).unwrap(); insta::assert_snapshot!(user_config_toml, @r#" "$schema" = "https://jj-vcs.github.io/jj/latest/config-schema.json" inline-table = {} "#); } #[test] fn test_config_unset_table_like() { let mut test_env = TestEnvironment::default(); // Test with fresh new config file let user_config_path = test_env.config_path().join("config.toml"); test_env.set_config_path(&user_config_path); std::fs::write( &user_config_path, indoc! {b" inline-table = { foo = true } [non-inline-table] foo = true "}, ) .unwrap(); // Inline table is syntactically a "value", so it can be deleted. test_env .run_jj_in(".", ["config", "unset", "--user", "inline-table"]) .success(); // Non-inline table cannot be deleted. let output = test_env.run_jj_in(".", ["config", "unset", "--user", "non-inline-table"]); insta::assert_snapshot!(output, @r" ------- stderr ------- Error: Failed to unset non-inline-table Caused by: Would delete entire table non-inline-table [EOF] [exit status: 1] "); let user_config_toml = std::fs::read_to_string(&user_config_path).unwrap(); insta::assert_snapshot!(user_config_toml, @r" [non-inline-table] foo = true "); } #[test] fn test_config_unset_for_user() { let mut test_env = TestEnvironment::default(); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); // Test with fresh new config file let user_config_path = test_env.config_path().join("config.toml"); test_env.set_config_path(&user_config_path); let work_dir = test_env.work_dir("repo"); work_dir .run_jj(["config", "set", "--user", "foo", "true"]) .success(); work_dir .run_jj(["config", "unset", "--user", "foo"]) .success(); work_dir .run_jj(["config", "set", "--user", "table.foo", "true"]) .success(); work_dir .run_jj(["config", "unset", "--user", "table.foo"]) .success(); work_dir .run_jj(["config", "set", "--user", "table.inline", "{ foo = true }"]) .success(); work_dir .run_jj(["config", "unset", "--user", "table.inline"]) .success(); let user_config_toml = std::fs::read_to_string(&user_config_path).unwrap(); insta::assert_snapshot!(user_config_toml, @r#" "$schema" = "https://jj-vcs.github.io/jj/latest/config-schema.json" [table] "#); } #[test] fn test_config_unset_for_repo() { let test_env = TestEnvironment::default(); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); let work_dir = test_env.work_dir("repo"); work_dir .run_jj(["config", "set", "--repo", "test-key", "test-val"]) .success(); work_dir .run_jj(["config", "unset", "--repo", "test-key"]) .success(); let repo_config_toml = work_dir.read_file(".jj/repo/config.toml"); insta::assert_snapshot!(repo_config_toml, @r#""$schema" = "https://jj-vcs.github.io/jj/latest/config-schema.json""#); } #[test] fn test_config_edit_missing_opt() { let test_env = TestEnvironment::default(); let output = test_env.run_jj_in(".", ["config", "edit"]); insta::assert_snapshot!(output, @r" ------- stderr ------- error: the following required arguments were not provided: <--user|--repo> Usage: jj config edit <--user|--repo> For more information, try '--help'. [EOF] [exit status: 2] "); } #[test] fn test_config_edit_user() { let mut test_env = TestEnvironment::default(); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); // Remove one of the config file to disambiguate std::fs::remove_file(test_env.last_config_file_path()).unwrap(); let edit_script = test_env.set_up_fake_editor(); let work_dir = test_env.work_dir("repo"); std::fs::write(edit_script, "dump-path path").unwrap(); work_dir.run_jj(["config", "edit", "--user"]).success(); let edited_path = PathBuf::from(std::fs::read_to_string(test_env.env_root().join("path")).unwrap()); assert_eq!( edited_path, dunce::simplified(&test_env.last_config_file_path()) ); } #[test] fn test_config_edit_user_new_file() { let mut test_env = TestEnvironment::default(); let user_config_path = test_env.config_path().join("config").join("file.toml"); test_env.set_up_fake_editor(); // set $EDIT_SCRIPT, but added configuration is ignored test_env.add_env_var("EDITOR", fake_editor_path()); test_env.set_config_path(&user_config_path); assert!(!user_config_path.exists()); test_env .run_jj_in(".", ["config", "edit", "--user"]) .success(); assert!( user_config_path.exists(), "new file and directory should be created" ); } #[test] fn test_config_edit_repo() { let mut test_env = TestEnvironment::default(); let edit_script = test_env.set_up_fake_editor(); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); let work_dir = test_env.work_dir("repo"); let repo_config_path = work_dir .root() .join(PathBuf::from_iter([".jj", "repo", "config.toml"])); assert!(!repo_config_path.exists()); std::fs::write(edit_script, "dump-path path").unwrap(); work_dir.run_jj(["config", "edit", "--repo"]).success(); let edited_path = PathBuf::from(std::fs::read_to_string(test_env.env_root().join("path")).unwrap()); assert_eq!(edited_path, dunce::simplified(&repo_config_path)); assert!(repo_config_path.exists(), "new file should be created"); } #[test] fn test_config_edit_invalid_config() { let mut test_env = TestEnvironment::default(); let edit_script = test_env.set_up_fake_editor(); // Test re-edit std::fs::write( &edit_script, "write\ninvalid config here\0next invocation\n\0write\ntest=\"success\"", ) .unwrap(); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); let work_dir = test_env.work_dir("repo"); let output = work_dir.run_jj_with(|cmd| { force_interactive(cmd) .args(["config", "edit", "--repo"]) .write_stdin("Y\n") }); insta::assert_snapshot!(output, @r" ------- stderr ------- Warning: An error has been found inside the config: Caused by: 1: Configuration cannot be parsed as TOML document 2: TOML parse error at line 1, column 9 | 1 | invalid config here | ^ expected `.`, `=` Do you want to keep editing the file? If not, previous config will be restored. (Yn): [EOF] "); let output = work_dir.run_jj(["config", "get", "test"]); insta::assert_snapshot!(output, @r" success [EOF]" ); // Test the restore previous config std::fs::write(&edit_script, "write\ninvalid config here").unwrap(); let work_dir = test_env.work_dir("repo"); let output = work_dir.run_jj_with(|cmd| { force_interactive(cmd) .args(["config", "edit", "--repo"]) .write_stdin("n\n") }); insta::assert_snapshot!(output, @r" ------- stderr ------- Warning: An error has been found inside the config: Caused by: 1: Configuration cannot be parsed as TOML document 2: TOML parse error at line 1, column 9 | 1 | invalid config here | ^ expected `.`, `=` Do you want to keep editing the file? If not, previous config will be restored. (Yn): [EOF] "); let output = work_dir.run_jj(["config", "get", "test"]); insta::assert_snapshot!(output, @r" success [EOF]" ); } #[test] fn test_config_path() { let mut test_env = TestEnvironment::default(); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); let work_dir = test_env.work_dir("repo"); let user_config_path = test_env.env_root().join("config.toml"); let repo_config_path = work_dir .root() .join(PathBuf::from_iter([".jj", "repo", "config.toml"])); test_env.set_config_path(&user_config_path); let work_dir = test_env.work_dir("repo"); insta::assert_snapshot!(work_dir.run_jj(["config", "path", "--user"]), @r" $TEST_ENV/config.toml [EOF] "); assert!( !user_config_path.exists(), "jj config path shouldn't create new file" ); insta::assert_snapshot!(work_dir.run_jj(["config", "path", "--repo"]), @r" $TEST_ENV/repo/.jj/repo/config.toml [EOF] "); assert!( !repo_config_path.exists(), "jj config path shouldn't create new file" ); insta::assert_snapshot!(test_env.run_jj_in(".", ["config", "path", "--repo"]), @r" ------- stderr ------- Error: No repo config path found [EOF] [exit status: 1] "); } #[test] fn test_config_path_multiple() { let mut test_env = TestEnvironment::default(); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); let config_path = test_env.config_path().join("config.toml"); let work_config_path = test_env.config_path().join("conf.d"); let user_config_path = join_paths([config_path, work_config_path]).unwrap(); test_env.set_config_path(&user_config_path); let work_dir = test_env.work_dir("repo"); insta::assert_snapshot!(work_dir.run_jj(["config", "path", "--user"]), @r" $TEST_ENV/config/config.toml $TEST_ENV/config/conf.d [EOF] "); } #[test] fn test_config_only_loads_toml_files() { let mut test_env = TestEnvironment::default(); test_env.set_up_fake_editor(); std::fs::File::create(test_env.config_path().join("is-not.loaded")).unwrap(); insta::assert_snapshot!(test_env.run_jj_in(".", ["config", "edit", "--user"]), @r" ------- stderr ------- 1: $TEST_ENV/config/config0001.toml 2: $TEST_ENV/config/config0002.toml Choose a config file (default 1): 1 [EOF] "); } #[test] fn test_config_edit_repo_outside_repo() { let test_env = TestEnvironment::default(); let output = test_env.run_jj_in(".", ["config", "edit", "--repo"]); insta::assert_snapshot!(output, @r" ------- stderr ------- Error: No repo config path found to edit [EOF] [exit status: 1] "); } #[test] fn test_config_get() { let test_env = TestEnvironment::default(); test_env.add_config( r#" [table] string = "some value 1" int = 123 list = ["list", "value"] overridden = "foo" "#, ); test_env.add_config( r#" [table] overridden = "bar" "#, ); let output = test_env.run_jj_in(".", ["config", "get", "nonexistent"]); insta::assert_snapshot!(output, @r" ------- stderr ------- Config error: Value not found for nonexistent 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(".", ["config", "get", "table.string"]); insta::assert_snapshot!(output, @r" some value 1 [EOF] "); let output = test_env.run_jj_in(".", ["config", "get", "table.int"]); insta::assert_snapshot!(output, @r" 123 [EOF] "); let output = test_env.run_jj_in(".", ["config", "get", "table.list"]); insta::assert_snapshot!(output, @r" ------- stderr ------- Config error: Invalid type or value for table.list Caused by: Expected a value convertible to a string, but is an array 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(".", ["config", "get", "table"]); insta::assert_snapshot!(output, @r" ------- stderr ------- Config error: Invalid type or value for table Caused by: Expected a value convertible to a string, but is a table Hint: Check the config file: $TEST_ENV/config/config0003.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(".", ["config", "get", "table.overridden"]); insta::assert_snapshot!(output, @r" bar [EOF] "); } #[test] fn test_config_path_syntax() { let test_env = TestEnvironment::default(); test_env.add_config( r#" a.'b()' = 0 'b c'.d = 1 'b c'.e.'f[]' = 2 - = 3 _ = 4 '.' = 5 "#, ); let output = test_env.run_jj_in(".", ["config", "list", "a.'b()'"]); insta::assert_snapshot!(output, @r" a.'b()' = 0 [EOF] "); let output = test_env.run_jj_in(".", ["config", "list", "'b c'"]); insta::assert_snapshot!(output, @r#" 'b c'.d = 1 'b c'.e."f[]" = 2 [EOF] "#); let output = test_env.run_jj_in(".", ["config", "list", "'b c'.d"]); insta::assert_snapshot!(output, @r" 'b c'.d = 1 [EOF] "); let output = test_env.run_jj_in(".", ["config", "list", "'b c'.e.'f[]'"]); insta::assert_snapshot!(output, @r" 'b c'.e.'f[]' = 2 [EOF] "); let output = test_env.run_jj_in(".", ["config", "get", "'b c'.e.'f[]'"]); insta::assert_snapshot!(output, @r" 2 [EOF] "); // Not a table let output = test_env.run_jj_in(".", ["config", "list", "a.'b()'.x"]); insta::assert_snapshot!(output, @r" ------- stderr ------- Warning: No matching config key for a.'b()'.x [EOF] "); let output = test_env.run_jj_in(".", ["config", "get", "a.'b()'.x"]); insta::assert_snapshot!(output, @r" ------- stderr ------- Config error: Value not found for a.'b()'.x For help, see https://jj-vcs.github.io/jj/latest/config/ or use `jj help -k config`. [EOF] [exit status: 1] "); // "-" and "_" are valid TOML keys let output = test_env.run_jj_in(".", ["config", "list", "-"]); insta::assert_snapshot!(output, @r" - = 3 [EOF] "); let output = test_env.run_jj_in(".", ["config", "list", "_"]); insta::assert_snapshot!(output, @r" _ = 4 [EOF] "); // "." requires quoting let output = test_env.run_jj_in(".", ["config", "list", "'.'"]); insta::assert_snapshot!(output, @r" '.' = 5 [EOF] "); let output = test_env.run_jj_in(".", ["config", "get", "'.'"]); insta::assert_snapshot!(output, @r" 5 [EOF] "); let output = test_env.run_jj_in(".", ["config", "get", "."]); insta::assert_snapshot!(output, @r" ------- stderr ------- error: invalid value '.' for '': TOML parse error at line 1, column 1 | 1 | . | ^ invalid key For more information, try '--help'. [EOF] [exit status: 2] "); // Invalid TOML keys let output = test_env.run_jj_in(".", ["config", "list", "b c"]); insta::assert_snapshot!(output, @r" ------- stderr ------- error: invalid value 'b c' for '[NAME]': TOML parse error at line 1, column 3 | 1 | b c | ^ For more information, try '--help'. [EOF] [exit status: 2] "); let output = test_env.run_jj_in(".", ["config", "list", ""]); insta::assert_snapshot!(output, @r" ------- stderr ------- error: invalid value '' for '[NAME]': TOML parse error at line 1, column 1 | 1 | | ^ invalid key For more information, try '--help'. [EOF] [exit status: 2] "); } #[test] #[cfg_attr(windows, ignore = "dirs::home_dir() can't be overridden by $HOME")] // TODO fn test_config_conditional() { let mut test_env = TestEnvironment::default(); let home_dir = test_env.work_dir(test_env.home_dir()); home_dir.run_jj(["git", "init", "repo1"]).success(); home_dir.run_jj(["git", "init", "repo2"]).success(); // Test with fresh new config file let user_config_path = test_env.env_root().join("config.toml"); test_env.set_config_path(&user_config_path); std::fs::write( &user_config_path, indoc! {" foo = 'global' baz = 'global' qux = 'global' [[--scope]] --when.repositories = ['~/repo1'] foo = 'repo1' [[--scope]] --when.repositories = ['~/repo2'] foo = 'repo2' [[--scope]] --when.commands = ['config'] baz = 'config' [[--scope]] --when.commands = ['config get'] qux = 'get' [[--scope]] --when.commands = ['config list'] qux = 'list' "}, ) .unwrap(); let home_dir = test_env.work_dir(test_env.home_dir()); let work_dir1 = home_dir.dir("repo1"); let work_dir2 = home_dir.dir("repo2"); // get and list should refer to the resolved config let output = test_env.run_jj_in(".", ["config", "get", "foo"]); insta::assert_snapshot!(output, @r" global [EOF] "); let output = work_dir1.run_jj(["config", "get", "foo"]); insta::assert_snapshot!(output, @r" repo1 [EOF] "); // baz should be the same for `jj config get` and `jj config list` // qux should be different let output = work_dir1.run_jj(["config", "get", "baz"]); insta::assert_snapshot!(output, @r" config [EOF] "); let output = work_dir1.run_jj(["config", "get", "qux"]); insta::assert_snapshot!(output, @r" get [EOF] "); let output = test_env.run_jj_in(".", ["config", "list", "--user"]); insta::assert_snapshot!(output, @r" foo = 'global' baz = 'config' qux = 'list' [EOF] "); let output = work_dir1.run_jj(["config", "list", "--user"]); insta::assert_snapshot!(output, @r" foo = 'repo1' baz = 'config' qux = 'list' [EOF] "); let output = work_dir2.run_jj(["config", "list", "--user"]); insta::assert_snapshot!(output, @r" foo = 'repo2' baz = 'config' qux = 'list' [EOF] "); // relative workspace path let output = work_dir2.run_jj(["config", "list", "--user", "-R../repo1"]); insta::assert_snapshot!(output, @r" foo = 'repo1' baz = 'config' qux = 'list' [EOF] "); // set and unset should refer to the source config // (there's no option to update scoped table right now.) let output = test_env.run_jj_in(".", ["config", "set", "--user", "bar", "new value"]); insta::assert_snapshot!(output, @""); insta::assert_snapshot!(std::fs::read_to_string(&user_config_path).unwrap(), @r#" foo = 'global' baz = 'global' qux = 'global' bar = "new value" [[--scope]] --when.repositories = ['~/repo1'] foo = 'repo1' [[--scope]] --when.repositories = ['~/repo2'] foo = 'repo2' [[--scope]] --when.commands = ['config'] baz = 'config' [[--scope]] --when.commands = ['config get'] qux = 'get' [[--scope]] --when.commands = ['config list'] qux = 'list' "#); let output = work_dir1.run_jj(["config", "unset", "--user", "foo"]); insta::assert_snapshot!(output, @""); insta::assert_snapshot!(std::fs::read_to_string(&user_config_path).unwrap(), @r#" baz = 'global' qux = 'global' bar = "new value" [[--scope]] --when.repositories = ['~/repo1'] foo = 'repo1' [[--scope]] --when.repositories = ['~/repo2'] foo = 'repo2' [[--scope]] --when.commands = ['config'] baz = 'config' [[--scope]] --when.commands = ['config get'] qux = 'get' [[--scope]] --when.commands = ['config list'] qux = 'list' "#); } // Minimal test for Windows where the home directory can't be switched. // (Can be removed if test_config_conditional() is enabled on Windows.) #[test] fn test_config_conditional_without_home_dir() { let mut test_env = TestEnvironment::default(); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); // Test with fresh new config file let user_config_path = test_env.env_root().join("config.toml"); test_env.set_config_path(&user_config_path); let work_dir = test_env.work_dir("repo"); std::fs::write( &user_config_path, format!( indoc! {" foo = 'global' [[--scope]] --when.repositories = [{repo_path}] foo = 'repo' "}, // "\\?\" paths shouldn't be required on Windows repo_path = to_toml_value(dunce::simplified(work_dir.root()).to_str().unwrap()) ), ) .unwrap(); let output = test_env.run_jj_in(".", ["config", "get", "foo"]); insta::assert_snapshot!(output, @r" global [EOF] "); let output = work_dir.run_jj(["config", "get", "foo"]); insta::assert_snapshot!(output, @r" repo [EOF] "); } #[test] fn test_config_show_paths() { let test_env = TestEnvironment::default(); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); let work_dir = test_env.work_dir("repo"); work_dir .run_jj(["config", "set", "--user", "ui.paginate", ":builtin"]) .success(); let output = test_env.run_jj_in(".", ["st"]); insta::assert_snapshot!(output, @r" ------- stderr ------- Config error: Invalid type or value for ui.paginate Caused by: unknown variant `:builtin`, expected `never` or `auto` Hint: Check the config file: $TEST_ENV/config/config0001.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_config_author_change_warning() { 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(["config", "set", "--repo", "user.email", "'Foo'"]); insta::assert_snapshot!(output, @r#" ------- stderr ------- Warning: This setting will only impact future commits. The author of the working copy will stay "Test User ". To change the working copy author, use "jj describe --reset-author --no-edit" [EOF] "#); // test_env.run_jj*() resets state for every invocation // for this test, the state (user.email) is needed work_dir .run_jj_with(|cmd| { cmd.args(["describe", "--reset-author", "--no-edit"]) .env_remove("JJ_EMAIL") }) .success(); let output = work_dir.run_jj(["log"]); insta::assert_snapshot!(output, @r" @ qpvuntsm Foo 2001-02-03 08:05:09 ed1febd8 │ (empty) (no description set) ◆ zzzzzzzz root() 00000000 [EOF] "); } #[test] fn test_config_author_change_warning_root_env() { let test_env = TestEnvironment::default(); let output = test_env.run_jj_in(".", ["config", "set", "--user", "user.email", "'Foo'"]); insta::assert_snapshot!(output, @""); } fn find_stdout_lines(keyname_pattern: &str, stdout: &str) -> String { let key_line_re = Regex::new(&format!(r"(?m)^{keyname_pattern} = .*\n")).unwrap(); key_line_re.find_iter(stdout).map(|m| m.as_str()).collect() }