// Copyright 2024 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. #[cfg(unix)] use std::os::unix::fs::PermissionsExt as _; use std::path::PathBuf; use indoc::formatdoc; use indoc::indoc; use jj_lib::file_util::symlink_file; use testutils::TestResult; use crate::common::TestEnvironment; use crate::common::to_toml_value; fn set_up_fake_formatter(test_env: &mut TestEnvironment, args: &[&str]) { let formatter_path = assert_cmd::cargo::cargo_bin!("fake-formatter"); assert!(formatter_path.is_file()); test_env.add_config(formatdoc! {" [fix.tools.fake-formatter] command = {command} patterns = ['all()'] ", command = toml_edit::Value::from_iter( [formatter_path.to_str().unwrap()] .iter() .chain(args) .copied() ) }); test_env.add_paths_to_normalize(formatter_path, "$FAKE_FORMATTER_PATH"); } #[test] fn test_config_no_tools() { let test_env = TestEnvironment::default(); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); let work_dir = test_env.work_dir("repo"); work_dir.write_file("file", "content\n"); let output = work_dir.run_jj(["fix"]); insta::assert_snapshot!(output, @" ------- stderr ------- Config error: No `fix.tools` are configured For help, see https://docs.jj-vcs.dev/latest/config/ or use `jj help -k config`. [EOF] [exit status: 1] "); let output = work_dir.run_jj(["file", "show", "file", "-r", "@"]); insta::assert_snapshot!(output, @" content [EOF] "); } #[test] fn test_config_nonexistent_tool() { 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###" [fix.tools.my-tool] command = ["nonexistent-fix-tool-binary"] patterns = ["glob:**"] "###, ); work_dir.write_file("file", "content\n"); let output = work_dir.run_jj(["fix"]); // We inform the user about the non-existent tool insta::assert_snapshot!(output, @r" ------- stderr ------- Warning: Failed to start `nonexistent-fix-tool-binary`. Fixed 0 commits of 1 checked. Nothing changed. [EOF] "); } #[test] fn test_config_multiple_tools() { let test_env = TestEnvironment::default(); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); let work_dir = test_env.work_dir("repo"); let formatter_path = assert_cmd::cargo::cargo_bin!("fake-formatter"); assert!(formatter_path.is_file()); let formatter = to_toml_value(formatter_path.to_str().unwrap()); test_env.add_config(format!( r###" [fix.tools.tool-1] command = [{formatter}, "--uppercase"] patterns = ["foo"] [fix.tools.tool-2] command = [{formatter}, "--lowercase"] patterns = ["bar"] "###, )); work_dir.write_file("foo", "Foo\n"); work_dir.write_file("bar", "Bar\n"); work_dir.write_file("baz", "Baz\n"); work_dir.run_jj(["fix"]).success(); let output = work_dir.run_jj(["file", "show", "foo", "-r", "@"]); insta::assert_snapshot!(output, @" FOO [EOF] "); let output = work_dir.run_jj(["file", "show", "bar", "-r", "@"]); insta::assert_snapshot!(output, @" bar [EOF] "); let output = work_dir.run_jj(["file", "show", "baz", "-r", "@"]); insta::assert_snapshot!(output, @" Baz [EOF] "); } #[test] fn test_config_multiple_tools_with_same_name() { let mut test_env = TestEnvironment::default(); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); let work_dir = test_env.work_dir("repo"); let formatter_path = assert_cmd::cargo::cargo_bin!("fake-formatter"); assert!(formatter_path.is_file()); let formatter = to_toml_value(formatter_path.to_str().unwrap()); // Multiple definitions with the same `name` are not allowed, because it is // likely to be a mistake, and mistakes are risky when they rewrite files. test_env.add_config(format!( r###" [fix.tools.my-tool] command = [{formatter}, "--uppercase"] patterns = ["foo"] [fix.tools.my-tool] command = [{formatter}, "--lowercase"] patterns = ["bar"] "###, )); work_dir.write_file("foo", "Foo\n"); work_dir.write_file("bar", "Bar\n"); let output = work_dir.run_jj(["fix"]); insta::assert_snapshot!(output, @" ------- stderr ------- Config error: Configuration cannot be parsed as TOML document Caused by: TOML parse error at line 6, column 20 | 6 | [fix.tools.my-tool] | ^^^^^^^ duplicate key Hint: Check the config file: $TEST_ENV/config/config0002.toml For help, see https://docs.jj-vcs.dev/latest/config/ or use `jj help -k config`. [EOF] [exit status: 1] "); test_env.set_config_path("/dev/null"); let work_dir = test_env.work_dir("repo"); let output = work_dir.run_jj(["file", "show", "foo", "-r", "@"]); insta::assert_snapshot!(output, @" Foo [EOF] "); let output = work_dir.run_jj(["file", "show", "bar", "-r", "@"]); insta::assert_snapshot!(output, @" Bar [EOF] "); } #[test] fn test_config_disabled_tools() { let test_env = TestEnvironment::default(); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); let work_dir = test_env.work_dir("repo"); let formatter_path = assert_cmd::cargo::cargo_bin!("fake-formatter"); assert!(formatter_path.is_file()); let formatter = to_toml_value(formatter_path.to_str().unwrap()); test_env.add_config(format!( r###" [fix.tools.tool-1] # default is enabled command = [{formatter}, "--uppercase"] patterns = ["foo"] [fix.tools.tool-2] enabled = true command = [{formatter}, "--lowercase"] patterns = ["bar"] [fix.tools.tool-3] enabled = false command = [{formatter}, "--lowercase"] patterns = ["baz"] "### )); work_dir.write_file("foo", "Foo\n"); work_dir.write_file("bar", "Bar\n"); work_dir.write_file("baz", "Baz\n"); work_dir.run_jj(["fix"]).success(); let output = work_dir.run_jj(["file", "show", "foo", "-r", "@"]); insta::assert_snapshot!(output, @" FOO [EOF] "); let output = work_dir.run_jj(["file", "show", "bar", "-r", "@"]); insta::assert_snapshot!(output, @" bar [EOF] "); let output = work_dir.run_jj(["file", "show", "baz", "-r", "@"]); insta::assert_snapshot!(output, @" Baz [EOF] "); } #[test] fn test_config_disabled_tools_warning_when_all_tools_are_disabled() { let test_env = TestEnvironment::default(); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); let work_dir = test_env.work_dir("repo"); let formatter_path = assert_cmd::cargo::cargo_bin!("fake-formatter"); assert!(formatter_path.is_file()); let formatter = to_toml_value(formatter_path.to_str().unwrap()); test_env.add_config(format!( r###" [fix.tools.tool-2] enabled = false command = [{formatter}, "--lowercase"] patterns = ["bar"] "### )); work_dir.write_file("bar", "Bar\n"); let output = work_dir.run_jj(["fix"]); insta::assert_snapshot!(output, @" ------- stderr ------- Config error: At least one entry of `fix.tools` must be enabled. For help, see https://docs.jj-vcs.dev/latest/config/ or use `jj help -k config`. [EOF] [exit status: 1] "); } #[test] fn test_config_tables_overlapping_patterns() { let test_env = TestEnvironment::default(); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); let work_dir = test_env.work_dir("repo"); let formatter_path = assert_cmd::cargo::cargo_bin!("fake-formatter"); assert!(formatter_path.is_file()); let formatter = to_toml_value(formatter_path.to_str().unwrap()); test_env.add_config(format!( r###" [fix.tools.tool-1] command = [{formatter}, "--append", "tool-1"] patterns = ["foo", "bar"] [fix.tools.tool-2] command = [{formatter}, "--append", "tool-2"] patterns = ["bar", "baz"] "###, )); work_dir.write_file("foo", "foo\n"); work_dir.write_file("bar", "bar\n"); work_dir.write_file("baz", "baz\n"); work_dir.run_jj(["fix"]).success(); let output = work_dir.run_jj(["file", "show", "foo", "-r", "@"]); insta::assert_snapshot!(output, @" foo tool-1[EOF] "); let output = work_dir.run_jj(["file", "show", "bar", "-r", "@"]); insta::assert_snapshot!(output, @" bar tool-1tool-2[EOF] "); let output = work_dir.run_jj(["file", "show", "baz", "-r", "@"]); insta::assert_snapshot!(output, @" baz tool-2[EOF] "); } #[test] fn test_config_tables_all_commands_missing() { 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###" [fix.tools.my-tool-missing-command-1] patterns = ["foo"] [fix.tools.my-tool-missing-command-2] patterns = ['glob:"ba*"'] "###, ); work_dir.write_file("foo", "foo\n"); let output = work_dir.run_jj(["fix"]); insta::assert_snapshot!(output.normalize_backslash(), @" ------- stderr ------- Config error: Invalid type or value for fix.tools.my-tool-missing-command-1 Caused by: missing field `command` Hint: Check the config file: $TEST_ENV/config/config0002.toml For help, see https://docs.jj-vcs.dev/latest/config/ or use `jj help -k config`. [EOF] [exit status: 1] "); let output = work_dir.run_jj(["file", "show", "foo", "-r", "@"]); insta::assert_snapshot!(output, @" foo [EOF] "); } #[test] fn test_config_tables_some_commands_missing() { let test_env = TestEnvironment::default(); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); let work_dir = test_env.work_dir("repo"); let formatter_path = assert_cmd::cargo::cargo_bin!("fake-formatter"); assert!(formatter_path.is_file()); let formatter = to_toml_value(formatter_path.to_str().unwrap()); test_env.add_config(format!( r###" [fix.tools.tool-1] command = [{formatter}, "--uppercase"] patterns = ["foo"] [fix.tools.my-tool-missing-command] patterns = ['bar'] "###, )); work_dir.write_file("foo", "foo\n"); let output = work_dir.run_jj(["fix"]); insta::assert_snapshot!(output.normalize_backslash(), @" ------- stderr ------- Config error: Invalid type or value for fix.tools.my-tool-missing-command Caused by: missing field `command` Hint: Check the config file: $TEST_ENV/config/config0002.toml For help, see https://docs.jj-vcs.dev/latest/config/ or use `jj help -k config`. [EOF] [exit status: 1] "); let output = work_dir.run_jj(["file", "show", "foo", "-r", "@"]); insta::assert_snapshot!(output, @" foo [EOF] "); } #[test] fn test_config_tables_empty_patterns_list() { let test_env = TestEnvironment::default(); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); let work_dir = test_env.work_dir("repo"); let formatter_path = assert_cmd::cargo::cargo_bin!("fake-formatter"); assert!(formatter_path.is_file()); let formatter = to_toml_value(formatter_path.to_str().unwrap()); test_env.add_config(format!( r###" [fix.tools.my-tool-empty-patterns] command = [{formatter}, "--uppercase"] patterns = [] "###, )); work_dir.write_file("foo", "foo\n"); let output = work_dir.run_jj(["fix"]); insta::assert_snapshot!(output, @" ------- stderr ------- Fixed 0 commits of 1 checked. Nothing changed. [EOF] "); let output = work_dir.run_jj(["file", "show", "foo", "-r", "@"]); insta::assert_snapshot!(output, @" foo [EOF] "); } #[test] fn test_config_filesets() { let test_env = TestEnvironment::default(); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); let work_dir = test_env.work_dir("repo"); let formatter_path = assert_cmd::cargo::cargo_bin!("fake-formatter"); assert!(formatter_path.is_file()); let formatter = to_toml_value(formatter_path.to_str().unwrap()); test_env.add_config(format!( r###" [fix.tools.my-tool-match-one] command = [{formatter}, "--uppercase"] patterns = ['glob:"a*"'] [fix.tools.my-tool-match-two] command = [{formatter}, "--reverse"] patterns = ['glob:"b*"'] [fix.tools.my-tool-match-none] command = [{formatter}, "--append", "SHOULD NOT APPEAR"] patterns = ['glob:"this-doesnt-match-anything-*"'] "###, )); work_dir.write_file("a1", "a1\n"); work_dir.write_file("b1", "b1\n"); work_dir.write_file("b2", "b2\n"); work_dir.run_jj(["fix"]).success(); let output = work_dir.run_jj(["file", "show", "a1", "-r", "@"]); insta::assert_snapshot!(output, @" A1 [EOF] "); let output = work_dir.run_jj(["file", "show", "b1", "-r", "@"]); insta::assert_snapshot!(output, @" 1b[EOF] "); let output = work_dir.run_jj(["file", "show", "b2", "-r", "@"]); insta::assert_snapshot!(output, @" 2b[EOF] "); } #[test] fn test_relative_paths() { let test_env = TestEnvironment::default(); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); let work_dir = test_env.work_dir("repo"); let formatter_path = assert_cmd::cargo::cargo_bin!("fake-formatter"); assert!(formatter_path.is_file()); let formatter = to_toml_value(formatter_path.to_str().unwrap()); test_env.add_config(format!( r###" [fix.tools.tool] command = [{formatter}, "--stdout", "Fixed!"] patterns = ['glob:"foo*"'] "###, )); let sub_dir = work_dir.create_dir("dir"); work_dir.write_file("foo1", "unfixed\n"); work_dir.write_file("foo2", "unfixed\n"); work_dir.write_file("dir/foo3", "unfixed\n"); // Positional arguments are cwd-relative, but the configured patterns are // repo-relative, so this command fixes the empty intersection of those // filesets. sub_dir.run_jj(["fix", "foo3"]).success(); let output = work_dir.run_jj(["file", "show", "foo1", "-r", "@"]); insta::assert_snapshot!(output, @" unfixed [EOF] "); let output = work_dir.run_jj(["file", "show", "foo2", "-r", "@"]); insta::assert_snapshot!(output, @" unfixed [EOF] "); let output = work_dir.run_jj(["file", "show", "dir/foo3", "-r", "@"]); insta::assert_snapshot!(output, @" unfixed [EOF] "); // Positional arguments can specify a subset of the configured fileset. sub_dir.run_jj(["fix", "../foo1"]).success(); let output = work_dir.run_jj(["file", "show", "foo1", "-r", "@"]); insta::assert_snapshot!(output, @"Fixed![EOF]"); let output = work_dir.run_jj(["file", "show", "foo2", "-r", "@"]); insta::assert_snapshot!(output, @" unfixed [EOF] "); let output = work_dir.run_jj(["file", "show", "dir/foo3", "-r", "@"]); insta::assert_snapshot!(output, @" unfixed [EOF] "); // The current directory does not change the interpretation of the config, so // foo2 is fixed but not dir/foo3. sub_dir.run_jj(["fix"]).success(); let output = work_dir.run_jj(["file", "show", "foo1", "-r", "@"]); insta::assert_snapshot!(output, @"Fixed![EOF]"); let output = work_dir.run_jj(["file", "show", "foo2", "-r", "@"]); insta::assert_snapshot!(output, @"Fixed![EOF]"); let output = work_dir.run_jj(["file", "show", "dir/foo3", "-r", "@"]); insta::assert_snapshot!(output, @" unfixed [EOF] "); // The output filtered to a non-existent file should display a warning. let output = work_dir.run_jj(["fix", "nonexistent"]); insta::assert_snapshot!(output, @" ------- stderr ------- Warning: No matching entries for paths: nonexistent Fixed 0 commits of 1 checked. Nothing changed. [EOF] "); } #[test] fn test_relative_tool_path_from_subdirectory() -> TestResult { let test_env = TestEnvironment::default(); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); let work_dir = test_env.work_dir("repo"); // Copy the fake-formatter into the workspace as a relative tool let formatter_path = assert_cmd::cargo::cargo_bin!("fake-formatter"); let formatter_name = formatter_path.file_name().unwrap().to_str().unwrap(); let tool_dir = work_dir.create_dir("tools"); let workspace_formatter_path = tool_dir.root().join(formatter_name); std::fs::copy(formatter_path, &workspace_formatter_path)?; work_dir.write_file(".gitignore", "tools/\n"); let formatter_relative_path = PathBuf::from_iter(["$root", "tools", formatter_name]); test_env.add_config(format!( r###" [fix.tools.a] command = [{path}, "--uppercase"] patterns = ['glob:"**/*.txt"'] "###, path = to_toml_value(formatter_relative_path.to_str().unwrap()) )); // Create a test file and subdirectory work_dir.write_file("test.txt", "hello world\n"); let sub_dir = work_dir.create_dir("subdir"); work_dir.write_file("subdir/nested.txt", "nested content\n"); work_dir.run_jj(["debug", "snapshot"]).success(); let setup_opid = work_dir.current_operation_id(); // Run fix from workspace root let output = work_dir.run_jj(["fix"]); insta::assert_snapshot!(output, @" ------- stderr ------- Fixed 1 commits of 1 checked. Working copy (@) now at: qpvuntsm 57a27b36 (no description set) Parent commit (@-) : zzzzzzzz 00000000 (empty) (no description set) Added 0 files, modified 2 files, removed 0 files [EOF] "); let output = work_dir.run_jj(["file", "show", "test.txt", "-r", "@"]); insta::assert_snapshot!(output, @" HELLO WORLD [EOF] "); let output = work_dir.run_jj(["file", "show", "subdir/nested.txt", "-r", "@"]); insta::assert_snapshot!(output, @" NESTED CONTENT [EOF] "); // Reset so the fix tools should have an effect again work_dir.run_jj(["op", "restore", &setup_opid]).success(); // Run fix from the subdirectory let output = sub_dir.run_jj(["fix"]); insta::assert_snapshot!(output, @" ------- stderr ------- Fixed 1 commits of 1 checked. Working copy (@) now at: qpvuntsm 05404d5b (no description set) Parent commit (@-) : zzzzzzzz 00000000 (empty) (no description set) Added 0 files, modified 2 files, removed 0 files [EOF] "); let output = work_dir.run_jj(["file", "show", "test.txt", "-r", "@"]); insta::assert_snapshot!(output, @" HELLO WORLD [EOF] "); let output = work_dir.run_jj(["file", "show", "subdir/nested.txt", "-r", "@"]); insta::assert_snapshot!(output, @" NESTED CONTENT [EOF] "); Ok(()) } #[test] fn test_fix_empty_commit() { let mut test_env = TestEnvironment::default(); set_up_fake_formatter(&mut test_env, &["--uppercase"]); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); let work_dir = test_env.work_dir("repo"); let output = work_dir.run_jj(["fix", "-s", "@"]); insta::assert_snapshot!(output, @" ------- stderr ------- Fixed 0 commits of 1 checked. Nothing changed. [EOF] "); } #[test] fn test_fix_leaf_commit() { let mut test_env = TestEnvironment::default(); set_up_fake_formatter(&mut test_env, &["--uppercase"]); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); let work_dir = test_env.work_dir("repo"); work_dir.write_file("file", "unaffected"); work_dir.run_jj(["new"]).success(); work_dir.write_file("file", "affected"); let output = work_dir.run_jj(["fix", "-s", "@"]); insta::assert_snapshot!(output, @" ------- stderr ------- Fixed 1 commits of 1 checked. Working copy (@) now at: rlvkpnrz f5c11961 (no description set) Parent commit (@-) : qpvuntsm b37955c0 (no description set) Added 0 files, modified 1 files, removed 0 files [EOF] "); let output = work_dir.run_jj(["file", "show", "file", "-r", "@-"]); insta::assert_snapshot!(output, @"unaffected[EOF]"); let output = work_dir.run_jj(["file", "show", "file", "-r", "@"]); insta::assert_snapshot!(output, @"AFFECTED[EOF]"); } #[test] fn test_fix_parent_commit() { let mut test_env = TestEnvironment::default(); set_up_fake_formatter(&mut test_env, &["--uppercase"]); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); let work_dir = test_env.work_dir("repo"); // Using one file name for all commits adds coverage of some possible bugs. work_dir.write_file("file", "parent"); work_dir .run_jj(["bookmark", "create", "-r@", "parent"]) .success(); work_dir.run_jj(["new"]).success(); work_dir.write_file("file", "child1"); work_dir .run_jj(["bookmark", "create", "-r@", "child1"]) .success(); work_dir.run_jj(["new", "-r", "parent"]).success(); work_dir.write_file("file", "child2"); work_dir .run_jj(["bookmark", "create", "-r@", "child2"]) .success(); let output = work_dir.run_jj(["fix", "-s", "parent"]); insta::assert_snapshot!(output, @" ------- stderr ------- Fixed 3 commits of 3 checked. Working copy (@) now at: mzvwutvl e7ba6d31 child2 | (no description set) Parent commit (@-) : qpvuntsm 49f1ddd5 parent | (no description set) Added 0 files, modified 1 files, removed 0 files [EOF] "); let output = work_dir.run_jj(["file", "show", "file", "-r", "parent"]); insta::assert_snapshot!(output, @"PARENT[EOF]"); let output = work_dir.run_jj(["file", "show", "file", "-r", "child1"]); insta::assert_snapshot!(output, @"CHILD1[EOF]"); let output = work_dir.run_jj(["file", "show", "file", "-r", "child2"]); insta::assert_snapshot!(output, @"CHILD2[EOF]"); } #[test] fn test_fix_sibling_commit() { let mut test_env = TestEnvironment::default(); set_up_fake_formatter(&mut test_env, &["--uppercase"]); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); let work_dir = test_env.work_dir("repo"); work_dir.write_file("file", "parent"); work_dir .run_jj(["bookmark", "create", "-r@", "parent"]) .success(); work_dir.run_jj(["new"]).success(); work_dir.write_file("file", "child1"); work_dir .run_jj(["bookmark", "create", "-r@", "child1"]) .success(); work_dir.run_jj(["new", "-r", "parent"]).success(); work_dir.write_file("file", "child2"); work_dir .run_jj(["bookmark", "create", "-r@", "child2"]) .success(); let output = work_dir.run_jj(["fix", "-s", "child1"]); insta::assert_snapshot!(output, @" ------- stderr ------- Fixed 1 commits of 1 checked. [EOF] "); let output = work_dir.run_jj(["file", "show", "file", "-r", "parent"]); insta::assert_snapshot!(output, @"parent[EOF]"); let output = work_dir.run_jj(["file", "show", "file", "-r", "child1"]); insta::assert_snapshot!(output, @"CHILD1[EOF]"); let output = work_dir.run_jj(["file", "show", "file", "-r", "child2"]); insta::assert_snapshot!(output, @"child2[EOF]"); } #[test] fn test_fix_descendant_commits() { let mut test_env = TestEnvironment::default(); set_up_fake_formatter(&mut test_env, &["--uppercase"]); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); let work_dir = test_env.work_dir("repo"); work_dir.write_file("parent", "parent"); work_dir .run_jj(["bookmark", "create", "-r@", "parent"]) .success(); work_dir.run_jj(["new"]).success(); work_dir.write_file("child1", "child1"); work_dir .run_jj(["bookmark", "create", "-r@", "child1"]) .success(); work_dir.run_jj(["new", "-r", "parent"]).success(); work_dir.write_file("child2", "child2"); work_dir .run_jj(["bookmark", "create", "-r@", "child2"]) .success(); let output = work_dir.run_jj(["fix", "-s", "parent", "child1", "child2", "nonexistent"]); insta::assert_snapshot!(output, @" ------- stderr ------- Warning: No matching entries for paths: nonexistent Fixed 2 commits of 3 checked. Working copy (@) now at: mzvwutvl afe0ade0 child2 | (no description set) Parent commit (@-) : qpvuntsm c9cb6288 parent | (no description set) Added 0 files, modified 1 files, removed 0 files [EOF] "); let output = work_dir.run_jj(["file", "show", "parent", "-r", "parent"]); insta::assert_snapshot!(output, @"parent[EOF]"); let output = work_dir.run_jj(["file", "show", "child1", "-r", "child1"]); insta::assert_snapshot!(output, @"CHILD1[EOF]"); let output = work_dir.run_jj(["file", "show", "child2", "-r", "child2"]); insta::assert_snapshot!(output, @"CHILD2[EOF]"); } #[test] fn test_default_revset() { let mut test_env = TestEnvironment::default(); set_up_fake_formatter(&mut test_env, &["--uppercase"]); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); let work_dir = test_env.work_dir("repo"); work_dir.write_file("file", "trunk1"); work_dir .run_jj(["bookmark", "create", "-r@", "trunk1"]) .success(); work_dir.run_jj(["new"]).success(); work_dir.write_file("file", "trunk2"); work_dir .run_jj(["bookmark", "create", "-r@", "trunk2"]) .success(); work_dir.run_jj(["new", "trunk1"]).success(); work_dir.write_file("file", "foo"); work_dir .run_jj(["bookmark", "create", "-r@", "foo"]) .success(); work_dir.run_jj(["new", "trunk1"]).success(); work_dir.write_file("file", "bar1"); work_dir .run_jj(["bookmark", "create", "-r@", "bar1"]) .success(); work_dir.run_jj(["new"]).success(); work_dir.write_file("file", "bar2"); work_dir .run_jj(["bookmark", "create", "-r@", "bar2"]) .success(); work_dir.run_jj(["new"]).success(); work_dir.write_file("file", "bar3"); work_dir .run_jj(["bookmark", "create", "-r@", "bar3"]) .success(); work_dir.run_jj(["edit", "bar2"]).success(); // With no args and no revset configuration, we fix `reachable(@, mutable())`, // which includes bar{1,2,3} and excludes trunk{1,2} (which is immutable) and // foo (which is mutable but not reachable). test_env.add_config(r#"revset-aliases."immutable_heads()" = "trunk2""#); let output = work_dir.run_jj(["fix"]); insta::assert_snapshot!(output, @" ------- stderr ------- Fixed 3 commits of 3 checked. Working copy (@) now at: yostqsxw 932b950d bar2 | (no description set) Parent commit (@-) : yqosqzyt 8a37ed67 bar1 | (no description set) Added 0 files, modified 1 files, removed 0 files [EOF] "); let output = work_dir.run_jj(["file", "show", "file", "-r", "trunk1"]); insta::assert_snapshot!(output, @"trunk1[EOF]"); let output = work_dir.run_jj(["file", "show", "file", "-r", "trunk2"]); insta::assert_snapshot!(output, @"trunk2[EOF]"); let output = work_dir.run_jj(["file", "show", "file", "-r", "foo"]); insta::assert_snapshot!(output, @"foo[EOF]"); let output = work_dir.run_jj(["file", "show", "file", "-r", "bar1"]); insta::assert_snapshot!(output, @"BAR1[EOF]"); let output = work_dir.run_jj(["file", "show", "file", "-r", "bar2"]); insta::assert_snapshot!(output, @"BAR2[EOF]"); let output = work_dir.run_jj(["file", "show", "file", "-r", "bar3"]); insta::assert_snapshot!(output, @"BAR3[EOF]"); } #[test] fn test_custom_default_revset() { let mut test_env = TestEnvironment::default(); set_up_fake_formatter(&mut test_env, &["--uppercase"]); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); let work_dir = test_env.work_dir("repo"); work_dir.write_file("file", "foo"); work_dir .run_jj(["bookmark", "create", "-r@", "foo"]) .success(); work_dir.run_jj(["new"]).success(); work_dir.write_file("file", "bar"); work_dir .run_jj(["bookmark", "create", "-r@", "bar"]) .success(); // Check out a different commit so that the schema default `reachable(@, // mutable())` would behave differently from our customized default. work_dir.run_jj(["new", "-r", "foo"]).success(); test_env.add_config(r#"revsets.fix = "bar""#); let output = work_dir.run_jj(["fix"]); insta::assert_snapshot!(output, @" ------- stderr ------- Fixed 1 commits of 1 checked. [EOF] "); let output = work_dir.run_jj(["file", "show", "file", "-r", "foo"]); insta::assert_snapshot!(output, @"foo[EOF]"); let output = work_dir.run_jj(["file", "show", "file", "-r", "bar"]); insta::assert_snapshot!(output, @"BAR[EOF]"); } #[test] fn test_fix_immutable_commit() { let mut test_env = TestEnvironment::default(); set_up_fake_formatter(&mut test_env, &["--uppercase"]); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); let work_dir = test_env.work_dir("repo"); work_dir.write_file("file", "immutable"); work_dir .run_jj(["bookmark", "create", "-r@", "immutable"]) .success(); work_dir.run_jj(["new"]).success(); work_dir.write_file("file", "mutable"); work_dir .run_jj(["bookmark", "create", "-r@", "mutable"]) .success(); test_env.add_config(r#"revset-aliases."immutable_heads()" = "immutable""#); let output = work_dir.run_jj(["fix", "-s", "immutable"]); insta::assert_snapshot!(output, @r#" ------- stderr ------- Error: Commit a86b2eccaaab is immutable Hint: Could not modify commit: qpvuntsm a86b2ecc immutable | (no description set) Hint: Immutable commits are used to protect shared history. Hint: For more information, see: - https://docs.jj-vcs.dev/latest/config/#set-of-immutable-commits - `jj help -k config`, "Set of immutable commits" Hint: This operation would rewrite 1 immutable commits. [EOF] [exit status: 1] "#); let output = work_dir.run_jj(["file", "show", "file", "-r", "immutable"]); insta::assert_snapshot!(output, @"immutable[EOF]"); let output = work_dir.run_jj(["file", "show", "file", "-r", "mutable"]); insta::assert_snapshot!(output, @"mutable[EOF]"); } #[test] fn test_fix_empty_file() { let mut test_env = TestEnvironment::default(); set_up_fake_formatter(&mut test_env, &["--uppercase"]); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); let work_dir = test_env.work_dir("repo"); work_dir.write_file("file", ""); let output = work_dir.run_jj(["fix", "-s", "@"]); insta::assert_snapshot!(output, @" ------- stderr ------- Fixed 0 commits of 1 checked. Nothing changed. [EOF] "); let output = work_dir.run_jj(["file", "show", "file", "-r", "@"]); insta::assert_snapshot!(output, @""); } #[test] fn test_fix_large_file() { let mut test_env = TestEnvironment::default(); // Set to byte mode, so that fake-formatter doesn't read from the stdin all at // once. set_up_fake_formatter(&mut test_env, &["--lowercase", "--byte-mode"]); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); let work_dir = test_env.work_dir("repo"); // 512KB should be larger than most default page size, so that the stdin pipe // will be blocked if fake-formatter doesn't read, and the stdout pipe will // be blocked if jj doesn't read. let mut large_contents = b"A".repeat(0x800_000); large_contents.push(b'\n'); work_dir.write_file("file", &large_contents); work_dir .run_jj([ "config", "set", "--repo", "snapshot.max-new-file-size", &large_contents.len().to_string(), ]) .success(); let output = work_dir.run_jj(["fix", "-s", "@"]); insta::assert_snapshot!(output, @" ------- stderr ------- Fixed 1 commits of 1 checked. Working copy (@) now at: qpvuntsm d500c021 (no description set) Parent commit (@-) : zzzzzzzz 00000000 (empty) (no description set) Added 0 files, modified 1 files, removed 0 files [EOF] "); let output = work_dir.run_jj(["file", "show", "file", "-r", "@"]); let stdout_bytes = output.stdout.raw().as_bytes(); assert_eq!(stdout_bytes.len(), large_contents.len()); assert!(stdout_bytes == large_contents.to_ascii_lowercase()); } #[test] fn test_fix_some_paths() { let mut test_env = TestEnvironment::default(); set_up_fake_formatter(&mut test_env, &["--uppercase"]); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); let work_dir = test_env.work_dir("repo"); work_dir.write_file("file1", "foo"); work_dir.write_file("file2", "bar"); let output = work_dir.run_jj(["fix", "-s", "@", "file1"]); insta::assert_snapshot!(output, @" ------- stderr ------- Fixed 1 commits of 1 checked. Working copy (@) now at: qpvuntsm 0279baba (no description set) Parent commit (@-) : zzzzzzzz 00000000 (empty) (no description set) Added 0 files, modified 1 files, removed 0 files [EOF] "); let output = work_dir.run_jj(["file", "show", "file1"]); insta::assert_snapshot!(output, @"FOO[EOF]"); let output = work_dir.run_jj(["file", "show", "file2"]); insta::assert_snapshot!(output, @"bar[EOF]"); } #[test] fn test_fix_cyclic() { let mut test_env = TestEnvironment::default(); set_up_fake_formatter(&mut test_env, &["--reverse"]); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); let work_dir = test_env.work_dir("repo"); work_dir.write_file("file", "content\n"); let output = work_dir.run_jj(["fix"]); insta::assert_snapshot!(output, @" ------- stderr ------- Fixed 1 commits of 1 checked. Working copy (@) now at: qpvuntsm ce361156 (no description set) Parent commit (@-) : zzzzzzzz 00000000 (empty) (no description set) Added 0 files, modified 1 files, removed 0 files [EOF] "); let output = work_dir.run_jj(["file", "show", "file", "-r", "@"]); insta::assert_snapshot!(output, @" tnetnoc[EOF] "); let output = work_dir.run_jj(["fix"]); insta::assert_snapshot!(output, @" ------- stderr ------- Fixed 1 commits of 1 checked. Working copy (@) now at: qpvuntsm 547f589b (no description set) Parent commit (@-) : zzzzzzzz 00000000 (empty) (no description set) Added 0 files, modified 1 files, removed 0 files [EOF] "); let output = work_dir.run_jj(["file", "show", "file", "-r", "@"]); insta::assert_snapshot!(output, @" content[EOF] "); } #[test] fn test_deduplication() { // Append all fixed content to a log file. Note that fix tools are always run // from the workspace root, so this will always write to $root/$path-fixlog. let mut test_env = TestEnvironment::default(); set_up_fake_formatter(&mut test_env, &["--uppercase", "--tee", "$path-fixlog"]); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); let work_dir = test_env.work_dir("repo"); // There are at least two interesting cases: the content is repeated immediately // in the child commit, or later in another descendant. work_dir.write_file("file", "foo\n"); work_dir .run_jj(["bookmark", "create", "-r@", "a"]) .success(); work_dir.run_jj(["new"]).success(); work_dir.write_file("file", "bar\n"); work_dir .run_jj(["bookmark", "create", "-r@", "b"]) .success(); work_dir.run_jj(["new"]).success(); work_dir.write_file("file", "bar\n"); work_dir .run_jj(["bookmark", "create", "-r@", "c"]) .success(); work_dir.run_jj(["new"]).success(); work_dir.write_file("file", "foo\n"); work_dir .run_jj(["bookmark", "create", "-r@", "d"]) .success(); let output = work_dir.run_jj(["fix", "-s", "a"]); insta::assert_snapshot!(output, @" ------- stderr ------- Fixed 4 commits of 4 checked. Working copy (@) now at: yqosqzyt 9849a250 d | (no description set) Parent commit (@-) : mzvwutvl 9544f381 c | (empty) (no description set) Added 0 files, modified 1 files, removed 0 files [EOF] "); let output = work_dir.run_jj(["file", "show", "file", "-r", "a"]); insta::assert_snapshot!(output, @" FOO [EOF] "); let output = work_dir.run_jj(["file", "show", "file", "-r", "b"]); insta::assert_snapshot!(output, @" BAR [EOF] "); let output = work_dir.run_jj(["file", "show", "file", "-r", "c"]); insta::assert_snapshot!(output, @" BAR [EOF] "); let output = work_dir.run_jj(["file", "show", "file", "-r", "d"]); insta::assert_snapshot!(output, @" FOO [EOF] "); // Each new content string only appears once in the log, because all the other // inputs (like file name) were identical, and so the results were reused. We // sort the log because the order of execution inside `jj fix` is undefined. insta::assert_snapshot!(sorted_lines(work_dir.root().join("file-fixlog")), @" BAR FOO "); } fn sorted_lines(path: PathBuf) -> String { let mut log: Vec<_> = std::fs::read_to_string(path.as_os_str()) .unwrap() .lines() .map(String::from) .collect(); log.sort(); log.join("\n") } #[test] fn test_executed_but_nothing_changed() { // Show that the tool ran by causing a side effect with --tee, and test that we // do the right thing when the tool's output is exactly equal to its input. let mut test_env = TestEnvironment::default(); set_up_fake_formatter(&mut test_env, &["--tee", "$path-copy"]); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); let work_dir = test_env.work_dir("repo"); work_dir.write_file("file", "content\n"); let output = work_dir.run_jj(["fix", "-s", "@"]); insta::assert_snapshot!(output, @" ------- stderr ------- Fixed 0 commits of 1 checked. Nothing changed. [EOF] "); let output = work_dir.run_jj(["file", "show", "file", "-r", "@"]); insta::assert_snapshot!(output, @" content [EOF] "); let copy_content = work_dir.read_file("file-copy"); insta::assert_snapshot!(copy_content, @"content"); // fix tools are always run from the workspace root, regardless of working // directory at time of invocation. let sub_dir = work_dir.create_dir("dir"); let output = sub_dir.run_jj(["fix"]); insta::assert_snapshot!(output, @" ------- stderr ------- Fixed 0 commits of 1 checked. Nothing changed. [EOF] "); let copy_content = work_dir.read_file("file-copy"); insta::assert_snapshot!(copy_content, @" content content "); assert!(!sub_dir.root().join("file-copy").exists()); } #[test] fn test_failure() { let mut test_env = TestEnvironment::default(); set_up_fake_formatter(&mut test_env, &["--fail"]); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); let work_dir = test_env.work_dir("repo"); work_dir.write_file("file", "content"); let output = work_dir.run_jj(["fix", "-s", "@"]); insta::assert_snapshot!(output, @" ------- stderr ------- Warning: Fix tool `$FAKE_FORMATTER_PATH` exited with non-zero exit code for `file` Fixed 0 commits of 1 checked. Nothing changed. [EOF] "); let output = work_dir.run_jj(["file", "show", "file", "-r", "@"]); insta::assert_snapshot!(output, @"content[EOF]"); } #[test] fn test_stderr_success() { let mut test_env = TestEnvironment::default(); set_up_fake_formatter( &mut test_env, &["--stderr", "error", "--stdout", "new content"], ); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); let work_dir = test_env.work_dir("repo"); work_dir.write_file("file", "old content"); // TODO: Associate the stderr lines with the relevant tool/file/commit instead // of passing it through directly. let output = work_dir.run_jj(["fix", "-s", "@"]); insta::assert_snapshot!(output, @" ------- stderr ------- file: error Fixed 1 commits of 1 checked. Working copy (@) now at: qpvuntsm cb75cbcb (no description set) Parent commit (@-) : zzzzzzzz 00000000 (empty) (no description set) Added 0 files, modified 1 files, removed 0 files [EOF] "); let output = work_dir.run_jj(["file", "show", "file", "-r", "@"]); insta::assert_snapshot!(output, @"new content[EOF]"); } #[test] fn test_stderr_failure() { let mut test_env = TestEnvironment::default(); set_up_fake_formatter( &mut test_env, &["--stderr", "error", "--stdout", "new content", "--fail"], ); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); let work_dir = test_env.work_dir("repo"); work_dir.write_file("file", "old content"); let output = work_dir.run_jj(["fix", "-s", "@"]); insta::assert_snapshot!(output, @" ------- stderr ------- file: error Warning: Fix tool `$FAKE_FORMATTER_PATH` exited with non-zero exit code for `file` Fixed 0 commits of 1 checked. Nothing changed. [EOF] "); let output = work_dir.run_jj(["file", "show", "file", "-r", "@"]); insta::assert_snapshot!(output, @"old content[EOF]"); } #[test] fn test_missing_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(indoc! {" [fix.tools.bad-tool] command = ['this_executable_shouldnt_exist'] patterns = ['all()'] "}); // TODO: We should display a warning about invalid tool configurations. When we // support multiple tools, we should also keep going to see if any of the other // executions succeed. let output = work_dir.run_jj(["fix", "-s", "@"]); insta::assert_snapshot!(output, @" ------- stderr ------- Fixed 0 commits of 1 checked. Nothing changed. [EOF] "); } #[test] fn test_fix_file_types() -> TestResult { let mut test_env = TestEnvironment::default(); set_up_fake_formatter(&mut test_env, &["--uppercase"]); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); let work_dir = test_env.work_dir("repo"); work_dir.write_file("file", "content"); work_dir.create_dir("dir"); symlink_file("file", work_dir.root().join("link"))?; let output = work_dir.run_jj(["fix", "-s", "@"]); insta::assert_snapshot!(output, @" ------- stderr ------- Fixed 1 commits of 1 checked. Working copy (@) now at: qpvuntsm 0184b215 (no description set) Parent commit (@-) : zzzzzzzz 00000000 (empty) (no description set) Added 0 files, modified 1 files, removed 0 files [EOF] "); let output = work_dir.run_jj(["file", "show", "file", "-r", "@"]); insta::assert_snapshot!(output, @"CONTENT[EOF]"); Ok(()) } #[cfg(unix)] #[test] fn test_fix_executable() -> TestResult { let mut test_env = TestEnvironment::default(); set_up_fake_formatter(&mut test_env, &["--uppercase"]); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); let work_dir = test_env.work_dir("repo"); let path = work_dir.root().join("file"); work_dir.write_file("file", "content"); let mut permissions = std::fs::metadata(&path)?.permissions(); permissions.set_mode(permissions.mode() | 0o111); std::fs::set_permissions(&path, permissions)?; let output = work_dir.run_jj(["fix", "-s", "@"]); insta::assert_snapshot!(output, @" ------- stderr ------- Fixed 1 commits of 1 checked. Working copy (@) now at: qpvuntsm 5293bf26 (no description set) Parent commit (@-) : zzzzzzzz 00000000 (empty) (no description set) Added 0 files, modified 1 files, removed 0 files [EOF] "); let output = work_dir.run_jj(["file", "show", "file", "-r", "@"]); insta::assert_snapshot!(output, @"CONTENT[EOF]"); let executable = std::fs::metadata(&path)?.permissions().mode() & 0o111; assert_eq!(executable, 0o111); Ok(()) } #[test] fn test_fix_trivial_merge_commit() { // All the changes are attributable to a parent, so none are fixed (in the same // way that none would be shown in `jj diff -r @`). let mut test_env = TestEnvironment::default(); set_up_fake_formatter(&mut test_env, &["--uppercase"]); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); let work_dir = test_env.work_dir("repo"); work_dir.write_file("file_a", "content a"); work_dir.write_file("file_c", "content c"); work_dir .run_jj(["bookmark", "create", "-r@", "a"]) .success(); work_dir.run_jj(["new", "@-"]).success(); work_dir.write_file("file_b", "content b"); work_dir.write_file("file_c", "content c"); work_dir .run_jj(["bookmark", "create", "-r@", "b"]) .success(); work_dir.run_jj(["new", "a", "b"]).success(); let output = work_dir.run_jj(["fix", "-s", "@"]); insta::assert_snapshot!(output, @" ------- stderr ------- Fixed 0 commits of 1 checked. Nothing changed. [EOF] "); let output = work_dir.run_jj(["file", "show", "file_a", "-r", "@"]); insta::assert_snapshot!(output, @"content a[EOF]"); let output = work_dir.run_jj(["file", "show", "file_b", "-r", "@"]); insta::assert_snapshot!(output, @"content b[EOF]"); let output = work_dir.run_jj(["file", "show", "file_c", "-r", "@"]); insta::assert_snapshot!(output, @"content c[EOF]"); } #[test] fn test_fix_adding_merge_commit() { // None of the changes are attributable to a parent, so they are all fixed (in // the same way that they would be shown in `jj diff -r @`). let mut test_env = TestEnvironment::default(); set_up_fake_formatter(&mut test_env, &["--uppercase"]); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); let work_dir = test_env.work_dir("repo"); work_dir.write_file("file_a", "content a"); work_dir.write_file("file_c", "content c"); work_dir .run_jj(["bookmark", "create", "-r@", "a"]) .success(); work_dir.run_jj(["new", "@-"]).success(); work_dir.write_file("file_b", "content b"); work_dir.write_file("file_c", "content c"); work_dir .run_jj(["bookmark", "create", "-r@", "b"]) .success(); work_dir.run_jj(["new", "a", "b"]).success(); work_dir.write_file("file_a", "change a"); work_dir.write_file("file_b", "change b"); work_dir.write_file("file_c", "change c"); work_dir.write_file("file_d", "change d"); let output = work_dir.run_jj(["fix", "-s", "@"]); insta::assert_snapshot!(output, @" ------- stderr ------- Fixed 1 commits of 1 checked. Working copy (@) now at: mzvwutvl 9f580aac (no description set) Parent commit (@-) : qpvuntsm 93f04460 a | (no description set) Parent commit (@-) : kkmpptxz ad4fc36c b | (no description set) Added 0 files, modified 4 files, removed 0 files [EOF] "); let output = work_dir.run_jj(["file", "show", "file_a", "-r", "@"]); insta::assert_snapshot!(output, @"CHANGE A[EOF]"); let output = work_dir.run_jj(["file", "show", "file_b", "-r", "@"]); insta::assert_snapshot!(output, @"CHANGE B[EOF]"); let output = work_dir.run_jj(["file", "show", "file_c", "-r", "@"]); insta::assert_snapshot!(output, @"CHANGE C[EOF]"); let output = work_dir.run_jj(["file", "show", "file_d", "-r", "@"]); insta::assert_snapshot!(output, @"CHANGE D[EOF]"); } #[test] fn test_fix_both_sides_of_conflict() { let mut test_env = TestEnvironment::default(); set_up_fake_formatter(&mut test_env, &["--uppercase"]); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); let work_dir = test_env.work_dir("repo"); work_dir.write_file("file", "content a\n"); work_dir .run_jj(["bookmark", "create", "-r@", "a"]) .success(); work_dir.run_jj(["new", "@-"]).success(); work_dir.write_file("file", "content b\n"); work_dir .run_jj(["bookmark", "create", "-r@", "b"]) .success(); work_dir.run_jj(["new", "a", "b"]).success(); // The conflicts are not different from the merged parent, so they would not be // fixed if we didn't fix the parents also. let output = work_dir.run_jj(["fix", "-s", "a", "-s", "b"]); insta::assert_snapshot!(output, @" ------- stderr ------- Fixed 3 commits of 3 checked. Working copy (@) now at: mzvwutvl 1bfa5dc3 (conflict) (empty) (no description set) Parent commit (@-) : qpvuntsm 0eae0dae a | (no description set) Parent commit (@-) : kkmpptxz eb61ba8d b | (no description set) Added 0 files, modified 1 files, removed 0 files Warning: There are unresolved conflicts at these paths: file 2-sided conflict [EOF] "); let output = work_dir.run_jj(["file", "show", "file", "-r", "a"]); insta::assert_snapshot!(output, @" CONTENT A [EOF] "); let output = work_dir.run_jj(["file", "show", "file", "-r", "b"]); insta::assert_snapshot!(output, @" CONTENT B [EOF] "); let output = work_dir.run_jj(["file", "show", "file", "-r", "@"]); insta::assert_snapshot!(output, @r" <<<<<<< conflict 1 of 1 %%%%%%% diff from: zzzzzzzz 00000000 \\\\\\\ to: qpvuntsm c8346c1c +CONTENT A +++++++ kkmpptxz 7c4518cb CONTENT B >>>>>>> conflict 1 of 1 ends [EOF] "); } #[test] fn test_fix_resolve_conflict() { // If both sides of the conflict look the same after being fixed, the conflict // will be resolved. let mut test_env = TestEnvironment::default(); set_up_fake_formatter(&mut test_env, &["--uppercase"]); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); let work_dir = test_env.work_dir("repo"); work_dir.write_file("file", "Content\n"); work_dir .run_jj(["bookmark", "create", "-r@", "a"]) .success(); work_dir.run_jj(["new", "@-"]).success(); work_dir.write_file("file", "cOnTeNt\n"); work_dir .run_jj(["bookmark", "create", "-r@", "b"]) .success(); work_dir.run_jj(["new", "a", "b"]).success(); // The conflicts are not different from the merged parent, so they would not be // fixed if we didn't fix the parents also. let output = work_dir.run_jj(["fix", "-s", "a", "-s", "b"]); insta::assert_snapshot!(output, @" ------- stderr ------- Fixed 3 commits of 3 checked. Working copy (@) now at: mzvwutvl c4e4665e (empty) (no description set) Parent commit (@-) : qpvuntsm 7a0dbb95 a | (no description set) Parent commit (@-) : kkmpptxz 5d9510ab b | (no description set) Added 0 files, modified 1 files, removed 0 files [EOF] "); let output = work_dir.run_jj(["file", "show", "file", "-r", "@"]); insta::assert_snapshot!(output, @" CONTENT [EOF] "); } #[test] fn test_all_files() { let test_env = TestEnvironment::default(); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); let work_dir = test_env.work_dir("repo"); let formatter_path = assert_cmd::cargo::cargo_bin!("fake-formatter"); assert!(formatter_path.is_file()); let formatter = to_toml_value(formatter_path.to_str().unwrap()); // Consider a few cases: // File A: in patterns, changed in child // File B: in patterns, NOT changed in child // File C: NOT in patterns, NOT changed in child // File D: NOT in patterns, changed in child // Some files will be in subdirectories to make sure we're covering that aspect // of matching. test_env.add_config(format!( r###" [fix.tools.tool] command = [{formatter}, "--append", "fixed"] patterns = ["a/a", "b/b"] "###, )); work_dir.create_dir("a"); work_dir.create_dir("b"); work_dir.create_dir("c"); work_dir.write_file("a/a", "parent aaa\n"); work_dir.write_file("b/b", "parent bbb\n"); work_dir.write_file("c/c", "parent ccc\n"); work_dir.write_file("ddd", "parent ddd\n"); work_dir.run_jj(["commit", "-m", "parent"]).success(); work_dir.write_file("a/a", "child aaa\n"); work_dir.write_file("ddd", "child ddd\n"); work_dir.run_jj(["describe", "-m", "child"]).success(); // Specifying files means exactly those files will be fixed in each revision, // although some like file C won't have any tools configured to make changes to // them. Specified but unfixed files are silently skipped, whether they lack // configuration, are ignored, don't exist, aren't normal files, etc. let output = work_dir.run_jj([ "fix", "--include-unchanged-files", "b/b", "c/c", "does_not.exist", ]); insta::assert_snapshot!(output, @" ------- stderr ------- Warning: No matching entries for paths: does_not.exist Fixed 2 commits of 2 checked. Working copy (@) now at: rlvkpnrz d8503fee child Parent commit (@-) : qpvuntsm 62c6ee98 parent Added 0 files, modified 1 files, removed 0 files [EOF] "); let output = work_dir.run_jj(["file", "show", "a/a", "-r", "@-"]); insta::assert_snapshot!(output, @" parent aaa [EOF] "); let output = work_dir.run_jj(["file", "show", "b/b", "-r", "@-"]); insta::assert_snapshot!(output, @" parent bbb fixed[EOF] "); let output = work_dir.run_jj(["file", "show", "c/c", "-r", "@-"]); insta::assert_snapshot!(output, @" parent ccc [EOF] "); let output = work_dir.run_jj(["file", "show", "ddd", "-r", "@-"]); insta::assert_snapshot!(output, @" parent ddd [EOF] "); let output = work_dir.run_jj(["file", "show", "a/a", "-r", "@"]); insta::assert_snapshot!(output, @" child aaa [EOF] "); let output = work_dir.run_jj(["file", "show", "b/b", "-r", "@"]); insta::assert_snapshot!(output, @" parent bbb fixed[EOF] "); let output = work_dir.run_jj(["file", "show", "c/c", "-r", "@"]); insta::assert_snapshot!(output, @" parent ccc [EOF] "); let output = work_dir.run_jj(["file", "show", "ddd", "-r", "@"]); insta::assert_snapshot!(output, @" child ddd [EOF] "); // Not specifying files means all files will be fixed in each revision. let output = work_dir.run_jj(["fix", "--include-unchanged-files"]); insta::assert_snapshot!(output, @" ------- stderr ------- Fixed 2 commits of 2 checked. Working copy (@) now at: rlvkpnrz 16aeb14c child Parent commit (@-) : qpvuntsm 5257b8ec parent Added 0 files, modified 2 files, removed 0 files [EOF] "); let output = work_dir.run_jj(["file", "show", "a/a", "-r", "@-"]); insta::assert_snapshot!(output, @" parent aaa fixed[EOF] "); let output = work_dir.run_jj(["file", "show", "b/b", "-r", "@-"]); insta::assert_snapshot!(output, @" parent bbb fixedfixed[EOF] "); let output = work_dir.run_jj(["file", "show", "c/c", "-r", "@-"]); insta::assert_snapshot!(output, @" parent ccc [EOF] "); let output = work_dir.run_jj(["file", "show", "ddd", "-r", "@-"]); insta::assert_snapshot!(output, @" parent ddd [EOF] "); let output = work_dir.run_jj(["file", "show", "a/a", "-r", "@"]); insta::assert_snapshot!(output, @" child aaa fixed[EOF] "); let output = work_dir.run_jj(["file", "show", "b/b", "-r", "@"]); insta::assert_snapshot!(output, @" parent bbb fixedfixed[EOF] "); let output = work_dir.run_jj(["file", "show", "c/c", "-r", "@"]); insta::assert_snapshot!(output, @" parent ccc [EOF] "); let output = work_dir.run_jj(["file", "show", "ddd", "-r", "@"]); insta::assert_snapshot!(output, @" child ddd [EOF] "); } #[test] fn test_fix_with_line_ranges() { let test_env = TestEnvironment::default(); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); let work_dir = test_env.work_dir("repo"); let formatter_path = assert_cmd::cargo::cargo_bin!("fake-formatter"); assert!(formatter_path.is_file()); let formatter = to_toml_value(formatter_path.to_str().unwrap()); test_env.add_config(format!( r###" [fix.tools.tool-1] command = [{formatter}, "--uppercase"] patterns = ["foo", "baz", "qux"] line-range-arg = "--line-ranges=$first-$last" [fix.tools.tool-2] command = [{formatter}, "--lowercase"] patterns = ["bar", "baz"] line-range-arg = "--line-ranges=$first-$last" "###, )); // Initial commit. work_dir.write_file("foo", "Foo1\nFoo2\nFoo3\n"); work_dir.write_file("bar", "Bar1\nBar2\nBar3\n"); work_dir.write_file("baz", "unmodified\n"); work_dir .run_jj(["bookmark", "create", "-r@", "c1"]) .success(); // Create new commit modifying foo and bar. work_dir.run_jj(["new"]).success(); work_dir.write_file("foo", "Foo1\nFoo2-Mod\nFoo3\n"); work_dir.write_file("bar", "Bar1-Mod\nBar2\nBar3\n"); work_dir.write_file("baz", "unmodified\n"); work_dir .run_jj(["bookmark", "create", "-r@", "c2"]) .success(); // Commit c3 doing more modifications to foo and bar. work_dir.run_jj(["new"]).success(); work_dir.write_file("foo", "Foo1\nFoo2\nFoo3-Mod\n"); work_dir.remove_file("bar"); work_dir.write_file("baz", ""); work_dir.write_file("qux", "newfile\n"); work_dir .run_jj(["bookmark", "create", "-r@", "c3"]) .success(); // Run `jj fix` on the second commit. let output = work_dir.run_jj(["fix", "-s", "c2"]).success(); insta::assert_snapshot!(output, @" ------- stderr ------- Fixed 2 commits of 2 checked. Working copy (@) now at: mzvwutvl 7d52a6e8 c3 | (no description set) Parent commit (@-) : kkmpptxz a8e4c2e8 c2 | (no description set) Added 0 files, modified 2 files, removed 0 files [EOF] "); // Check that the formatter was not applied to the first commit. let output = work_dir.run_jj(["file", "show", "foo", "-r", "c1"]); insta::assert_snapshot!(output, @r" Foo1 Foo2 Foo3 [EOF] "); let output = work_dir.run_jj(["file", "show", "bar", "-r", "c1"]); insta::assert_snapshot!(output, @r" Bar1 Bar2 Bar3 [EOF] "); let output = work_dir.run_jj(["file", "show", "baz", "-r", "c1"]); insta::assert_snapshot!(output, @r" unmodified [EOF] "); // Check that the formatter was applied to the second commit. let output = work_dir.run_jj(["file", "show", "foo", "-r", "c2"]); insta::assert_snapshot!(output, @r" Foo1 FOO2-MOD Foo3 [EOF] "); let output = work_dir.run_jj(["file", "show", "bar", "-r", "c2"]); insta::assert_snapshot!(output, @r" bar1-mod Bar2 Bar3 [EOF] "); let output = work_dir.run_jj(["file", "show", "baz", "-r", "c2"]); insta::assert_snapshot!(output, @r" unmodified [EOF] "); // Check that the formatter was applied to the third commit but in different // line ranges. let output = work_dir.run_jj(["file", "show", "foo", "-r", "c3"]); insta::assert_snapshot!(output, @r" Foo1 Foo2 FOO3-MOD [EOF] "); let output = work_dir.run_jj(["file", "show", "baz", "-r", "c3"]); insta::assert_snapshot!(output, @""); let output = work_dir.run_jj(["file", "show", "qux", "-r", "c3"]); insta::assert_snapshot!(output, @r" NEWFILE [EOF] "); } #[test] fn test_fix_with_run_tool_if_zero_line_ranges() { let test_env = TestEnvironment::default(); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); let work_dir = test_env.work_dir("repo"); let formatter_path = assert_cmd::cargo::cargo_bin!("fake-formatter"); assert!(formatter_path.is_file()); let formatter = to_toml_value(formatter_path.to_str().unwrap()); test_env.add_config(format!( r###" [fix.tools.tool-1] command = [{formatter}, "--uppercase"] patterns = ["foo", "baz"] line-range-arg = "--line-ranges=$first-$last" run-tool-if-zero-line-ranges = true [fix.tools.tool-2] command = [{formatter}, "--lowercase"] patterns = ["bar"] line-range-arg = "--line-ranges=$first-$last" run-tool-if-zero-line-ranges = false [fix.tools.tool-3] command = [{formatter}, "--uppercase", "--split-even-length-lines"] patterns = ["qux"] line-range-arg = "--line-ranges=$first-$last" run-tool-if-zero-line-ranges = true "###, )); // Initial commit. work_dir.write_file("foo", "Foo1\nFoo2\nFoo3\n"); work_dir.write_file("bar", "Bar1\nBar2\nBar3\n"); work_dir.write_file("qux", "Qux1\nQux2\nQux3\n"); work_dir.write_file("baz", "unmodified\n"); work_dir .run_jj(["bookmark", "create", "-r@", "c1"]) .success(); // Create a new commit doing a deletion of a line in foo and bar (which should // produce empty line ranges). work_dir.run_jj(["new"]).success(); work_dir.write_file("foo", "Foo1\nFoo3\n"); work_dir.write_file("bar", "Bar1\nBar3\n"); work_dir.write_file("baz", "unmodified\n"); work_dir .run_jj(["bookmark", "create", "-r@", "c2"]) .success(); // Run `jj fix` on the second commit. let output = work_dir.run_jj(["fix", "-s", "c2"]).success(); insta::assert_snapshot!(output, @" ------- stderr ------- Fixed 1 commits of 1 checked. Working copy (@) now at: kkmpptxz 4613d090 c2 | (no description set) Parent commit (@-) : qpvuntsm 074c5dba c1 | (no description set) Added 0 files, modified 1 files, removed 0 files [EOF] "); // Check that the formatter was not applied to the first commit. let output = work_dir.run_jj(["file", "show", "foo", "-r", "c1"]); insta::assert_snapshot!(output, @r" Foo1 Foo2 Foo3 [EOF] "); let output = work_dir.run_jj(["file", "show", "bar", "-r", "c1"]); insta::assert_snapshot!(output, @r" Bar1 Bar2 Bar3 [EOF] "); let output = work_dir.run_jj(["file", "show", "baz", "-r", "c1"]); insta::assert_snapshot!(output, @r" unmodified [EOF] "); // Check that the formatter was applied to the second commit where // run-tool-if-zero-line-ranges is true. let output = work_dir.run_jj(["file", "show", "foo", "-r", "c2"]); insta::assert_snapshot!(output, @r" FOO1 FOO3 [EOF] "); let output = work_dir.run_jj(["file", "show", "bar", "-r", "c2"]); insta::assert_snapshot!(output, @r" Bar1 Bar3 [EOF] "); let output = work_dir.run_jj(["file", "show", "baz", "-r", "c2"]); insta::assert_snapshot!(output, @r" unmodified [EOF] "); // Create a new commit doing a deletion of a line in qux. This commit's parent // is c1. work_dir.run_jj(["new", "-r", "c1"]).success(); work_dir.write_file("qux", "Qux1\nQux3\n"); work_dir .run_jj(["bookmark", "create", "-r@", "c3"]) .success(); // Run `jj fix` on the third commit. This command should pass, but the // fake-formatter should fail to format qux because `--split-even-length-lines` // requires line-ranges to be passed into the fake formatter and qux should // not have any line ranges passed due to run-tool-if-zero-line-ranges. // This should result in no changes to qux. let output = work_dir.run_jj(["fix", "-s", "c3"]).success(); let output = output.normalize_stderr_with(|s| { s.replace(formatter_path.to_str().unwrap(), "fake-formatter") .replace(".exe", "") }); insta::assert_snapshot!(output, @" ------- stderr ------- qux: error: the following required arguments were not provided: --line-ranges Usage: fake-formatter --line-ranges --uppercase --split-even-length-lines For more information, try '--help'. Warning: Fix tool `fake-formatter` exited with non-zero exit code for `qux` Fixed 0 commits of 1 checked. Nothing changed. [EOF] "); // Check that the formatter was not applied to the third commit. let output = work_dir.run_jj(["file", "show", "qux", "-r", "c3"]); insta::assert_snapshot!(output, @r" Qux1 Qux3 [EOF] "); } #[test] fn test_fix_with_all_lines_arg() { let test_env = TestEnvironment::default(); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); let work_dir = test_env.work_dir("repo"); let formatter_path = assert_cmd::cargo::cargo_bin!("fake-formatter"); assert!(formatter_path.is_file()); let formatter = to_toml_value(formatter_path.to_str().unwrap()); test_env.add_config(format!( r###" [fix.tools.tool-1] command = [{formatter}, "--uppercase"] patterns = ["foo", "baz", "qux"] line-range-arg = "--line-ranges=$first-$last" [fix.tools.tool-2] command = [{formatter}, "--lowercase"] patterns = ["bar"] "###, )); // Initial commit. work_dir.write_file("foo", "Foo1\nFoo2\nFoo3\n"); work_dir.write_file("bar", "Bar1\nBar2\nBar3\n"); work_dir.write_file("baz", "unmodified\n"); work_dir .run_jj(["bookmark", "create", "-r@", "c1"]) .success(); // Create new commit modifying foo and bar. work_dir.run_jj(["new"]).success(); work_dir.write_file("foo", "Foo1\nFoo2-Mod\nFoo3\n"); work_dir.write_file("bar", "Bar1\nBar2-Mod\nBar3\n"); work_dir.write_file("baz", "unmodified\n"); work_dir.write_file("qux", "newfile\n"); work_dir .run_jj(["bookmark", "create", "-r@", "c2"]) .success(); // Run `jj fix` on the second commit with --all-lines arg. let output = work_dir .run_jj(["fix", "--all-lines", "-s", "c2"]) .success(); insta::assert_snapshot!(output, @" ------- stderr ------- Fixed 1 commits of 1 checked. Working copy (@) now at: kkmpptxz d875fd1f c2 | (no description set) Parent commit (@-) : qpvuntsm 81234495 c1 | (no description set) Added 0 files, modified 3 files, removed 0 files [EOF] "); // Check that the formatter was not applied to the first commit. let output = work_dir.run_jj(["file", "show", "foo", "-r", "c1"]); insta::assert_snapshot!(output, @r" Foo1 Foo2 Foo3 [EOF] "); let output = work_dir.run_jj(["file", "show", "bar", "-r", "c1"]); insta::assert_snapshot!(output, @r" Bar1 Bar2 Bar3 [EOF] "); let output = work_dir.run_jj(["file", "show", "baz", "-r", "c1"]); insta::assert_snapshot!(output, @r" unmodified [EOF] "); // Check that the formatter was applied to the second commit. And that the // whole-file was formatted for changed files. And whole file was formatted // for formatters that don't support line ranges. let output = work_dir.run_jj(["file", "show", "foo", "-r", "c2"]); insta::assert_snapshot!(output, @r" FOO1 FOO2-MOD FOO3 [EOF] "); let output = work_dir.run_jj(["file", "show", "bar", "-r", "c2"]); insta::assert_snapshot!(output, @r" bar1 bar2-mod bar3 [EOF] "); let output = work_dir.run_jj(["file", "show", "baz", "-r", "c2"]); insta::assert_snapshot!(output, @r" unmodified [EOF] "); let output = work_dir.run_jj(["file", "show", "qux", "-r", "c2"]); insta::assert_snapshot!(output, @r" NEWFILE [EOF] "); } #[test] fn test_fix_with_line_ranges_multiple_formatters() { let test_env = TestEnvironment::default(); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); let work_dir = test_env.work_dir("repo"); let formatter_path = assert_cmd::cargo::cargo_bin!("fake-formatter"); assert!(formatter_path.is_file()); let formatter = to_toml_value(formatter_path.to_str().unwrap()); test_env.add_config(format!( r###" [fix.tools.tool-1] command = [{formatter}, "--split-even-length-lines"] patterns = ["foo", "boo"] line-range-arg = "--line-ranges=$first-$last" [fix.tools.tool-2] command = [{formatter}, "--uppercase"] patterns = ["foo", "boo"] line-range-arg = "--line-ranges=$first-$last" "###, )); // Initial commit. work_dir.write_file("foo", "ab\ncd\nefg\n"); work_dir .run_jj(["bookmark", "create", "-r@", "c1"]) .success(); // Create new commit to deal with split even lines and uppercase. work_dir.run_jj(["new"]).success(); work_dir.write_file("foo", "abcd\nefgh\n"); work_dir.write_file("boo", "abcd\nefgh\n"); work_dir .run_jj(["bookmark", "create", "-r@", "c2"]) .success(); // Run `jj fix` on the second commit. let output = work_dir.run_jj(["fix", "-s", "c2"]).success(); insta::assert_snapshot!(output, @" ------- stderr ------- Fixed 1 commits of 1 checked. Working copy (@) now at: kkmpptxz 4c5f12f5 c2 | (no description set) Parent commit (@-) : qpvuntsm 37f1e5c6 c1 | (no description set) Added 0 files, modified 2 files, removed 0 files [EOF] "); // Check that the formatter was not applied to the first commit. let output = work_dir.run_jj(["file", "show", "foo", "-r", "c1"]); insta::assert_snapshot!(output, @r" ab cd efg [EOF] "); // Check that split lines was applied first and then upper was only applied to // the second half. let output = work_dir.run_jj(["file", "show", "foo", "-r", "c2"]); insta::assert_snapshot!(output, @r" ab cd EF GH [EOF] "); let output = work_dir.run_jj(["file", "show", "boo", "-r", "c2"]); insta::assert_snapshot!(output, @r" AB CD EF GH [EOF] "); } #[test] fn test_fix_with_line_ranges_and_include_unchanged_files_all_lines() { let test_env = TestEnvironment::default(); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); let work_dir = test_env.work_dir("repo"); let formatter_path = assert_cmd::cargo::cargo_bin!("fake-formatter"); assert!(formatter_path.is_file()); let formatter = to_toml_value(formatter_path.to_str().unwrap()); test_env.add_config(format!( r###" [fix.tools.tool-1] command = [{formatter}, "--uppercase"] patterns = ["all()"] line-range-arg = "--line-ranges=$first-$last" "###, )); // Initial commit. work_dir.write_file("changed.txt", "Foo1\nFoo2\nFoo3\n"); work_dir.write_file("unchanged.txt", "baz1\nbaz2\nbaz3\n"); work_dir.write_file("empty.txt", "content\n"); work_dir .run_jj(["bookmark", "create", "-r@", "c1"]) .success(); // Create new commit modifying changed.txt. work_dir.run_jj(["new"]).success(); work_dir.write_file("changed.txt", "Foo1\nFoo2-Mod\nFoo3\n"); work_dir.write_file("unchanged.txt", "baz1\nbaz2\nbaz3\n"); work_dir.write_file("newfile.txt", "new\n"); work_dir.write_file("empty.txt", ""); work_dir .run_jj(["bookmark", "create", "-r@", "c2"]) .success(); // Run `jj fix` on the second commit, including unchanged files. let output = work_dir .run_jj([ "fix", "--include-unchanged-files", "--all-lines", "-s", "c2", ]) .success(); insta::assert_snapshot!(output, @" ------- stderr ------- Fixed 1 commits of 1 checked. Working copy (@) now at: kkmpptxz 1fb2bf8f c2 | (no description set) Parent commit (@-) : qpvuntsm 42db21c2 c1 | (no description set) Added 0 files, modified 3 files, removed 0 files [EOF] "); // Check that the formatter was not applied to the first commit. let output = work_dir.run_jj(["file", "show", "changed.txt", "-r", "c1"]); insta::assert_snapshot!(output, @r" Foo1 Foo2 Foo3 [EOF] "); let output = work_dir.run_jj(["file", "show", "unchanged.txt", "-r", "c1"]); insta::assert_snapshot!(output, @r" baz1 baz2 baz3 [EOF] "); let output = work_dir.run_jj(["file", "show", "empty.txt", "-r", "c1"]); insta::assert_snapshot!(output, @r" content [EOF] "); // Check that the formatter was applied to the second commit and all lines were // affected. let output = work_dir.run_jj(["file", "show", "changed.txt", "-r", "c2"]); insta::assert_snapshot!(output, @" FOO1 FOO2-MOD FOO3 [EOF] "); let output = work_dir.run_jj(["file", "show", "unchanged.txt", "-r", "c2"]); insta::assert_snapshot!(output, @r" BAZ1 BAZ2 BAZ3 [EOF] "); let output = work_dir.run_jj(["file", "show", "newfile.txt", "-r", "c2"]); insta::assert_snapshot!(output, @r" NEW [EOF] "); let output = work_dir.run_jj(["file", "show", "empty.txt", "-r", "c2"]); insta::assert_snapshot!(output, @r""); }