// 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.// use std::fs; use insta::assert_snapshot; use crate::common::TestEnvironment;use crate::common::TestWorkDir;use crate::common::create_commit_with_files; #[test]fn test_run_simple() { let mut test_env = TestEnvironment::default(); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); let fake_formatter = assert_cmd::cargo::cargo_bin("fake-formatter"); assert!(fake_formatter.is_file()); let fake_formatter_path = fake_formatter.to_string_lossy().into_owned(); test_env.add_paths_to_normalize(fake_formatter.clone(), "$FAKE_FORMATTER_PATH"); let work_dir = test_env.work_dir("repo"); work_dir.write_file("A.txt", "A"); work_dir.run_jj(&["commit", "-m", "A"]).success(); work_dir.write_file("b.txt", "b"); work_dir.run_jj(&["commit", "-m", "B"]).success(); work_dir.write_file("c.txt", "test to replace"); work_dir.run_jj(&["commit", "-m", "C"]).success(); insta::assert_snapshot!(get_log_output(&work_dir), @r" @ zsuskulnrvyrovkzqrwmxqlsskqntxvp ○ kkmpptxzrspxrzommnulwmwkkqwworplC │ ○ rlvkpnrzqnoowoytxnquwvuryrwnrmlpB │ ○ qpvuntsmwlqtpsluzzsnyyzlmlwvmlnuA │ ◆ zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz [EOF] "); // `--tee touched.txt` creates a file in each working copy, so every commit's // tree gets rewritten. let stdout = work_dir .run_jj(&[ "run", "-r", "..@", "--", &fake_formatter_path, "--stdout", "x", "--tee", "touched.txt", ]) .success() .stdout; insta::assert_snapshot!(stdout, @"xxxx[EOF]");} #[test]fn test_run_on_immutable() { let test_env = TestEnvironment::default(); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); let work_dir = test_env.work_dir("repo"); let fake_formatter = assert_cmd::cargo::cargo_bin("fake-formatter"); assert!(fake_formatter.is_file()); let fake_formatter_path = fake_formatter.to_string_lossy(); work_dir.write_file("A.txt", "A"); work_dir.run_jj(&["commit", "-m", "A"]).success(); work_dir.write_file("b.txt", "b"); work_dir.run_jj(&["commit", "-m", "B"]).success(); work_dir.write_file("c.txt", "test to replace"); work_dir.run_jj(&["commit", "-m", "C"]).success(); insta::assert_snapshot!(get_log_output(&work_dir), @r" @ zsuskulnrvyrovkzqrwmxqlsskqntxvp ○ kkmpptxzrspxrzommnulwmwkkqwworplC │ ○ rlvkpnrzqnoowoytxnquwvuryrwnrmlpB │ ○ qpvuntsmwlqtpsluzzsnyyzlmlwvmlnuA │ ◆ zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz [EOF] "); let output = work_dir.run_jj(&[ "run", "-r", "all()", "--", &fake_formatter_path, "--uppercase", ]); insta::assert_snapshot!(output, @r" ------- stderr ------- Error: The root commit 000000000000 is immutable [EOF] [exit status: 1] ");} #[test]fn test_run_noop() { let mut test_env = TestEnvironment::default(); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); let fake_formatter = assert_cmd::cargo::cargo_bin("fake-formatter"); assert!(fake_formatter.is_file()); let fake_formatter_path = fake_formatter.to_string_lossy().into_owned(); test_env.add_paths_to_normalize(fake_formatter.clone(), "$FAKE_FORMATTER_PATH"); let work_dir = test_env.work_dir("repo"); work_dir.write_file("A.txt", "A"); work_dir.run_jj(&["commit", "-m", "A"]).success(); work_dir.write_file("b.txt", "b"); work_dir.run_jj(&["commit", "-m", "B"]).success(); work_dir.write_file("c.txt", "test to replace"); work_dir.run_jj(&["commit", "-m", "C"]).success(); insta::assert_snapshot!(get_log_output(&work_dir), @r" @ zsuskulnrvyrovkzqrwmxqlsskqntxvp ○ kkmpptxzrspxrzommnulwmwkkqwworplC │ ○ rlvkpnrzqnoowoytxnquwvuryrwnrmlpB │ ○ qpvuntsmwlqtpsluzzsnyyzlmlwvmlnuA │ ◆ zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz [EOF] "); // `--stdout foo` writes to the subprocess's stdout, which `jj run` buffers // and emits to its own stdout. No tracked files in the working copy change, // so no commits get rewritten. Using a fixed string keeps the per-commit // output identical, so the concatenated stdout is stable regardless of the // (non-deterministic) order in which the parallel jobs finish. let output = work_dir .run_jj(&[ "run", "-r", "..@", "--", &fake_formatter_path, "--stdout", "foo", ]) .success(); insta::assert_snapshot!(output.stdout, @"foofoofoofoo[EOF]"); insta::assert_snapshot!(output.stderr, @r" Nothing changed. [EOF] ");} #[test]fn test_run_sets_env_vars() { 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("seed.txt", "seed"); work_dir.run_jj(&["commit", "-m", "seed"]).success(); // Show the change_id and commit_id so the reader can match them against // the values the subprocess writes into the per-commit working copy. let log_template = r#"change_id ++ " " ++ commit_id ++ " " ++ description ++ "\n""#; insta::assert_snapshot!( work_dir.run_jj(&["log", "-T", log_template]), @r" @ rlvkpnrzqnoowoytxnquwvuryrwnrmlp fc4c875c9bc90128cbb9e8084dd5f5f336b383d9 ○ qpvuntsmwlqtpsluzzsnyyzlmlwvmlnu 5fbe90560fed1c39d46a46a672ba98abd53bdc6d seed │ ◆ zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz 0000000000000000000000000000000000000000 [EOF] " ); // Each subprocess echoes its JJ_CHANGE_ID and JJ_COMMIT_ID into files in // the per-commit working copy, modifying the tree so the commit gets // rewritten with those files. let jj_args: &[&str] = if cfg!(windows) { &[ "run", "-r", "@-", "--", "cmd", "/c", "echo %JJ_CHANGE_ID%>change_id.txt && echo %JJ_COMMIT_ID%>commit_id.txt", ] } else { &[ "run", "-r", "@-", "--", "sh", "-c", "echo $JJ_CHANGE_ID > change_id.txt && echo $JJ_COMMIT_ID > commit_id.txt", ] }; work_dir.run_jj(jj_args).success(); let normalize_whitespace = |s: String| { s.replace("\r\n", "\n") .lines() .map(|line| line.trim_end()) .collect::<Vec<_>>() .join("\n") + "\n" }; insta::assert_snapshot!( work_dir .run_jj(&["file", "show", "-r", "@-", "change_id.txt"]) .normalize_stdout_with(normalize_whitespace), @r" qpvuntsmwlqtpsluzzsnyyzlmlwvmlnu [EOF] " ); insta::assert_snapshot!( work_dir .run_jj(&["file", "show", "-r", "@-", "commit_id.txt"]) .normalize_stdout_with(normalize_whitespace), @r" 5fbe90560fed1c39d46a46a672ba98abd53bdc6d [EOF] " );} #[test]fn test_run_from_subdir_skips_commits_without_it() { let mut test_env = TestEnvironment::default(); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); // `fake-formatter --tee ran.txt` is a portable way to create an empty // `ran.txt`, equivalent to `touch ran.txt` but available on all platforms. let fake_formatter = assert_cmd::cargo::cargo_bin("fake-formatter"); assert!(fake_formatter.is_file()); let fake_formatter_path = fake_formatter.to_string_lossy().into_owned(); test_env.add_paths_to_normalize(fake_formatter.clone(), "$FAKE_FORMATTER_PATH"); let work_dir = test_env.work_dir("repo"); // First commit has only root-level files; no `sub/` exists yet. work_dir.write_file("seed.txt", "seed"); work_dir.run_jj(&["commit", "-m", "no-sub"]).success(); // Second commit adds `sub/file.txt`, so `sub/` exists from here on. work_dir.write_file("sub/file.txt", "x"); work_dir.run_jj(&["commit", "-m", "with-sub"]).success(); // Run from inside sub/ on both ancestors. The command creates `ran.txt` // in cwd, so we can later tell where it ran. The `no-sub` commit has no // `sub/` directory and should be skipped; the `with-sub` commit has // `sub/` and should be rewritten with `sub/ran.txt` added. let sub_dir = work_dir.dir("sub"); let output = sub_dir .run_jj(&[ "run", "-r", "@-|@--", "--", &fake_formatter_path, "--tee", "ran.txt", ]) .success() .normalize_backslash(); insta::assert_snapshot!(output.stderr, @r" Skipped commit 3bb1f1ca3c09a8e6be46ef48515803464b16b426: directory does not exist: sub Rewrote 1 commits. Working copy (@) now at: kkmpptxz 3548431a (empty) (no description set) Parent commit (@-) : rlvkpnrz 3aa9a235 with-sub Added 1 files, modified 0 files, removed 0 files [EOF] "); // The rewritten `with-sub` commit has `sub/ran.txt`, alongside the // pre-existing `sub/file.txt`. insta::assert_snapshot!( work_dir .run_jj(&["file", "list", "-r", "@-"]) .normalize_backslash(), @r" seed.txt sub/file.txt sub/ran.txt [EOF] " );} #[test]fn test_run_root_flag() { let test_env = TestEnvironment::default(); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); // `fake-formatter --tee ran.txt` is a portable way to create an empty // `ran.txt`, equivalent to `touch ran.txt` but available on all platforms. let fake_formatter = assert_cmd::cargo::cargo_bin("fake-formatter"); assert!(fake_formatter.is_file()); let fake_formatter_path = fake_formatter.to_string_lossy().into_owned(); let work_dir = test_env.work_dir("repo"); work_dir.write_file("sub/file.txt", "x"); work_dir.run_jj(&["commit", "-m", "with-sub"]).success(); // Invoke `jj run` from inside sub/, but pass `--root` so the command // executes from the workspace root and `ran.txt` lands at the top level. let sub_dir = work_dir.dir("sub"); sub_dir .run_jj(&[ "run", "--root", "-r", "@-", "--", &fake_formatter_path, "--tee", "ran.txt", ]) .success(); insta::assert_snapshot!( work_dir .run_jj(&["file", "list", "-r", "@-"]) .normalize_backslash(), @r" ran.txt sub/file.txt [EOF] " );} #[test]fn test_run_uses_revsets_run_as_default() { let test_env = TestEnvironment::default(); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); // `fake-formatter --tee ran.txt` is a portable way to create an empty // `ran.txt`, equivalent to `touch ran.txt` but available on all platforms. let fake_formatter = assert_cmd::cargo::cargo_bin("fake-formatter"); assert!(fake_formatter.is_file()); let fake_formatter_path = fake_formatter.to_string_lossy().into_owned(); let work_dir = test_env.work_dir("repo"); // Two sibling commits, `foo` and `bar`. work_dir.write_file("file", "foo"); work_dir .run_jj(["bookmark", "create", "-r@", "foo"]) .success(); work_dir.run_jj(["new", "root()"]).success(); work_dir.write_file("file", "bar"); work_dir .run_jj(["bookmark", "create", "-r@", "bar"]) .success(); work_dir.run_jj(["edit", "foo"]).success(); test_env.add_config(r#"revsets.run = "bar""#); // Running `jj run` with `revsets.run=bar` should only modify bar work_dir .run_jj([ "--config=revsets.run=\"bar\"", "run", "--", &fake_formatter_path, "--tee", "ran.txt", ]) .success(); insta::assert_snapshot!( work_dir.run_jj(["file", "list", "-r", "foo"]), @r" file [EOF] " ); insta::assert_snapshot!( work_dir.run_jj(["file", "list", "-r", "bar"]), @r" file ran.txt [EOF] " ); // Run again but now with foo in the config work_dir.run_jj(["undo"]).success(); work_dir .run_jj([ "--config=revsets.run=\"foo\"", "run", "--", &fake_formatter_path, "--tee", "ran.txt", ]) .success(); insta::assert_snapshot!( work_dir.run_jj(["file", "list", "-r", "foo"]), @r" file ran.txt [EOF] " ); insta::assert_snapshot!( work_dir.run_jj(["file", "list", "-r", "bar"]), @r" file [EOF] " );} #[test]fn test_run_failure_rewrites_nothing() { 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("A.txt", "A"); work_dir.run_jj(&["commit", "-m", "A"]).success(); work_dir.write_file("b.txt", "b"); work_dir.run_jj(&["commit", "-m", "B"]).success(); let log_before = get_log_output(&work_dir); insta::assert_snapshot!(log_before, @r" @ kkmpptxzrspxrzommnulwmwkkqwworpl ○ rlvkpnrzqnoowoytxnquwvuryrwnrmlpB │ ○ qpvuntsmwlqtpsluzzsnyyzlmlwvmlnuA │ ◆ zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz [EOF] "); // Fail on commit B; succeed (modify the tree) on every other commit. If // any subprocess fails, `jj run` must roll back: no commit gets rewritten, // even the ones whose commands ran to completion before B's failure // propagated. let cmd = "if [ \"$JJ_CHANGE_ID\" = 'rlvkpnrzqnoowoytxnquwvuryrwnrmlp' ]; then exit 1; fi; \ touch ran.txt"; let output = work_dir.run_jj(&["run", "-r", "..@", "--", "sh", "-c", cmd]); assert!(!output.status.success(), "expected `jj run` to fail"); // Log is unchanged: same change_ids, same shape, no descendants of B got // rebased onto a new commit. assert_eq!(get_log_output(&work_dir), log_before);} #[test]fn test_run_ignore_errors_rewrites_successes() { let test_env = TestEnvironment::default(); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); let work_dir = test_env.work_dir("repo"); create_commit_with_files(&work_dir, "a", &[], &[("a.txt", "a")]); create_commit_with_files(&work_dir, "b", &["a"], &[("b.txt", "b")]); // Fail on A; succeed on B. A should be untouched, B should be rewritten. let jj_args: &[&str] = if cfg!(windows) { &[ "run", "--ignore-errors", "-r", "..b", "--", "cmd", "/c", "type nul >ran.txt & (if not exist b.txt exit 1)", ] } else { &[ "run", "--ignore-errors", "-r", "..b", "--", "sh", "-c", "touch ran.txt; if [ ! -f b.txt ]; then exit 1; fi", ] }; let output = work_dir.run_jj(jj_args); assert_snapshot!( output.success(), @r" ------- stderr ------- Rewrote 1 commits. Working copy (@) now at: zsuskuln f3c3b6ac b | b Parent commit (@-) : rlvkpnrz 2385de9f a | a Added 1 files, modified 0 files, removed 0 files [EOF] " ); insta::assert_snapshot!( work_dir.run_jj(&["file", "list", "-r=a"]), @r" a.txt [EOF]Lines 1–500 of 1763. Load more
// 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. // use std::fs; use insta::assert_snapshot; use crate::common::TestEnvironment; use crate::common::TestWorkDir; use crate::common::create_commit_with_files; #[test] fn test_run_simple() { let mut test_env = TestEnvironment::default(); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); let fake_formatter = assert_cmd::cargo::cargo_bin("fake-formatter"); assert!(fake_formatter.is_file()); let fake_formatter_path = fake_formatter.to_string_lossy().into_owned(); test_env.add_paths_to_normalize(fake_formatter.clone(), "$FAKE_FORMATTER_PATH"); let work_dir = test_env.work_dir("repo"); work_dir.write_file("A.txt", "A"); work_dir.run_jj(&["commit", "-m", "A"]).success(); work_dir.write_file("b.txt", "b"); work_dir.run_jj(&["commit", "-m", "B"]).success(); work_dir.write_file("c.txt", "test to replace"); work_dir.run_jj(&["commit", "-m", "C"]).success(); insta::assert_snapshot!(get_log_output(&work_dir), @r" @ zsuskulnrvyrovkzqrwmxqlsskqntxvp ○ kkmpptxzrspxrzommnulwmwkkqwworplC │ ○ rlvkpnrzqnoowoytxnquwvuryrwnrmlpB │ ○ qpvuntsmwlqtpsluzzsnyyzlmlwvmlnuA │ ◆ zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz [EOF] "); // `--tee touched.txt` creates a file in each working copy, so every commit's // tree gets rewritten. let stdout = work_dir .run_jj(&[ "run", "-r", "..@", "--", &fake_formatter_path, "--stdout", "x", "--tee", "touched.txt", ]) .success() .stdout; insta::assert_snapshot!(stdout, @"xxxx[EOF]"); } #[test] fn test_run_on_immutable() { let test_env = TestEnvironment::default(); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); let work_dir = test_env.work_dir("repo"); let fake_formatter = assert_cmd::cargo::cargo_bin("fake-formatter"); assert!(fake_formatter.is_file()); let fake_formatter_path = fake_formatter.to_string_lossy(); work_dir.write_file("A.txt", "A"); work_dir.run_jj(&["commit", "-m", "A"]).success(); work_dir.write_file("b.txt", "b"); work_dir.run_jj(&["commit", "-m", "B"]).success(); work_dir.write_file("c.txt", "test to replace"); work_dir.run_jj(&["commit", "-m", "C"]).success(); insta::assert_snapshot!(get_log_output(&work_dir), @r" @ zsuskulnrvyrovkzqrwmxqlsskqntxvp ○ kkmpptxzrspxrzommnulwmwkkqwworplC │ ○ rlvkpnrzqnoowoytxnquwvuryrwnrmlpB │ ○ qpvuntsmwlqtpsluzzsnyyzlmlwvmlnuA │ ◆ zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz [EOF] "); let output = work_dir.run_jj(&[ "run", "-r", "all()", "--", &fake_formatter_path, "--uppercase", ]); insta::assert_snapshot!(output, @r" ------- stderr ------- Error: The root commit 000000000000 is immutable [EOF] [exit status: 1] "); } #[test] fn test_run_noop() { let mut test_env = TestEnvironment::default(); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); let fake_formatter = assert_cmd::cargo::cargo_bin("fake-formatter"); assert!(fake_formatter.is_file()); let fake_formatter_path = fake_formatter.to_string_lossy().into_owned(); test_env.add_paths_to_normalize(fake_formatter.clone(), "$FAKE_FORMATTER_PATH"); let work_dir = test_env.work_dir("repo"); work_dir.write_file("A.txt", "A"); work_dir.run_jj(&["commit", "-m", "A"]).success(); work_dir.write_file("b.txt", "b"); work_dir.run_jj(&["commit", "-m", "B"]).success(); work_dir.write_file("c.txt", "test to replace"); work_dir.run_jj(&["commit", "-m", "C"]).success(); insta::assert_snapshot!(get_log_output(&work_dir), @r" @ zsuskulnrvyrovkzqrwmxqlsskqntxvp ○ kkmpptxzrspxrzommnulwmwkkqwworplC │ ○ rlvkpnrzqnoowoytxnquwvuryrwnrmlpB │ ○ qpvuntsmwlqtpsluzzsnyyzlmlwvmlnuA │ ◆ zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz [EOF] "); // `--stdout foo` writes to the subprocess's stdout, which `jj run` buffers // and emits to its own stdout. No tracked files in the working copy change, // so no commits get rewritten. Using a fixed string keeps the per-commit // output identical, so the concatenated stdout is stable regardless of the // (non-deterministic) order in which the parallel jobs finish. let output = work_dir .run_jj(&[ "run", "-r", "..@", "--", &fake_formatter_path, "--stdout", "foo", ]) .success(); insta::assert_snapshot!(output.stdout, @"foofoofoofoo[EOF]"); insta::assert_snapshot!(output.stderr, @r" Nothing changed. [EOF] "); } #[test] fn test_run_sets_env_vars() { 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("seed.txt", "seed"); work_dir.run_jj(&["commit", "-m", "seed"]).success(); // Show the change_id and commit_id so the reader can match them against // the values the subprocess writes into the per-commit working copy. let log_template = r#"change_id ++ " " ++ commit_id ++ " " ++ description ++ "\n""#; insta::assert_snapshot!( work_dir.run_jj(&["log", "-T", log_template]), @r" @ rlvkpnrzqnoowoytxnquwvuryrwnrmlp fc4c875c9bc90128cbb9e8084dd5f5f336b383d9 ○ qpvuntsmwlqtpsluzzsnyyzlmlwvmlnu 5fbe90560fed1c39d46a46a672ba98abd53bdc6d seed │ ◆ zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz 0000000000000000000000000000000000000000 [EOF] " ); // Each subprocess echoes its JJ_CHANGE_ID and JJ_COMMIT_ID into files in // the per-commit working copy, modifying the tree so the commit gets // rewritten with those files. let jj_args: &[&str] = if cfg!(windows) { &[ "run", "-r", "@-", "--", "cmd", "/c", "echo %JJ_CHANGE_ID%>change_id.txt && echo %JJ_COMMIT_ID%>commit_id.txt", ] } else { &[ "run", "-r", "@-", "--", "sh", "-c", "echo $JJ_CHANGE_ID > change_id.txt && echo $JJ_COMMIT_ID > commit_id.txt", ] }; work_dir.run_jj(jj_args).success(); let normalize_whitespace = |s: String| { s.replace("\r\n", "\n") .lines() .map(|line| line.trim_end()) .collect::<Vec<_>>() .join("\n") + "\n" }; insta::assert_snapshot!( work_dir .run_jj(&["file", "show", "-r", "@-", "change_id.txt"]) .normalize_stdout_with(normalize_whitespace), @r" qpvuntsmwlqtpsluzzsnyyzlmlwvmlnu [EOF] " ); insta::assert_snapshot!( work_dir .run_jj(&["file", "show", "-r", "@-", "commit_id.txt"]) .normalize_stdout_with(normalize_whitespace), @r" 5fbe90560fed1c39d46a46a672ba98abd53bdc6d [EOF] " ); } #[test] fn test_run_from_subdir_skips_commits_without_it() { let mut test_env = TestEnvironment::default(); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); // `fake-formatter --tee ran.txt` is a portable way to create an empty // `ran.txt`, equivalent to `touch ran.txt` but available on all platforms. let fake_formatter = assert_cmd::cargo::cargo_bin("fake-formatter"); assert!(fake_formatter.is_file()); let fake_formatter_path = fake_formatter.to_string_lossy().into_owned(); test_env.add_paths_to_normalize(fake_formatter.clone(), "$FAKE_FORMATTER_PATH"); let work_dir = test_env.work_dir("repo"); // First commit has only root-level files; no `sub/` exists yet. work_dir.write_file("seed.txt", "seed"); work_dir.run_jj(&["commit", "-m", "no-sub"]).success(); // Second commit adds `sub/file.txt`, so `sub/` exists from here on. work_dir.write_file("sub/file.txt", "x"); work_dir.run_jj(&["commit", "-m", "with-sub"]).success(); // Run from inside sub/ on both ancestors. The command creates `ran.txt` // in cwd, so we can later tell where it ran. The `no-sub` commit has no // `sub/` directory and should be skipped; the `with-sub` commit has // `sub/` and should be rewritten with `sub/ran.txt` added. let sub_dir = work_dir.dir("sub"); let output = sub_dir .run_jj(&[ "run", "-r", "@-|@--", "--", &fake_formatter_path, "--tee", "ran.txt", ]) .success() .normalize_backslash(); insta::assert_snapshot!(output.stderr, @r" Skipped commit 3bb1f1ca3c09a8e6be46ef48515803464b16b426: directory does not exist: sub Rewrote 1 commits. Working copy (@) now at: kkmpptxz 3548431a (empty) (no description set) Parent commit (@-) : rlvkpnrz 3aa9a235 with-sub Added 1 files, modified 0 files, removed 0 files [EOF] "); // The rewritten `with-sub` commit has `sub/ran.txt`, alongside the // pre-existing `sub/file.txt`. insta::assert_snapshot!( work_dir .run_jj(&["file", "list", "-r", "@-"]) .normalize_backslash(), @r" seed.txt sub/file.txt sub/ran.txt [EOF] " ); } #[test] fn test_run_root_flag() { let test_env = TestEnvironment::default(); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); // `fake-formatter --tee ran.txt` is a portable way to create an empty // `ran.txt`, equivalent to `touch ran.txt` but available on all platforms. let fake_formatter = assert_cmd::cargo::cargo_bin("fake-formatter"); assert!(fake_formatter.is_file()); let fake_formatter_path = fake_formatter.to_string_lossy().into_owned(); let work_dir = test_env.work_dir("repo"); work_dir.write_file("sub/file.txt", "x"); work_dir.run_jj(&["commit", "-m", "with-sub"]).success(); // Invoke `jj run` from inside sub/, but pass `--root` so the command // executes from the workspace root and `ran.txt` lands at the top level. let sub_dir = work_dir.dir("sub"); sub_dir .run_jj(&[ "run", "--root", "-r", "@-", "--", &fake_formatter_path, "--tee", "ran.txt", ]) .success(); insta::assert_snapshot!( work_dir .run_jj(&["file", "list", "-r", "@-"]) .normalize_backslash(), @r" ran.txt sub/file.txt [EOF] " ); } #[test] fn test_run_uses_revsets_run_as_default() { let test_env = TestEnvironment::default(); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); // `fake-formatter --tee ran.txt` is a portable way to create an empty // `ran.txt`, equivalent to `touch ran.txt` but available on all platforms. let fake_formatter = assert_cmd::cargo::cargo_bin("fake-formatter"); assert!(fake_formatter.is_file()); let fake_formatter_path = fake_formatter.to_string_lossy().into_owned(); let work_dir = test_env.work_dir("repo"); // Two sibling commits, `foo` and `bar`. work_dir.write_file("file", "foo"); work_dir .run_jj(["bookmark", "create", "-r@", "foo"]) .success(); work_dir.run_jj(["new", "root()"]).success(); work_dir.write_file("file", "bar"); work_dir .run_jj(["bookmark", "create", "-r@", "bar"]) .success(); work_dir.run_jj(["edit", "foo"]).success(); test_env.add_config(r#"revsets.run = "bar""#); // Running `jj run` with `revsets.run=bar` should only modify bar work_dir .run_jj([ "--config=revsets.run=\"bar\"", "run", "--", &fake_formatter_path, "--tee", "ran.txt", ]) .success(); insta::assert_snapshot!( work_dir.run_jj(["file", "list", "-r", "foo"]), @r" file [EOF] " ); insta::assert_snapshot!( work_dir.run_jj(["file", "list", "-r", "bar"]), @r" file ran.txt [EOF] " ); // Run again but now with foo in the config work_dir.run_jj(["undo"]).success(); work_dir .run_jj([ "--config=revsets.run=\"foo\"", "run", "--", &fake_formatter_path, "--tee", "ran.txt", ]) .success(); insta::assert_snapshot!( work_dir.run_jj(["file", "list", "-r", "foo"]), @r" file ran.txt [EOF] " ); insta::assert_snapshot!( work_dir.run_jj(["file", "list", "-r", "bar"]), @r" file [EOF] " ); } #[test] fn test_run_failure_rewrites_nothing() { 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("A.txt", "A"); work_dir.run_jj(&["commit", "-m", "A"]).success(); work_dir.write_file("b.txt", "b"); work_dir.run_jj(&["commit", "-m", "B"]).success(); let log_before = get_log_output(&work_dir); insta::assert_snapshot!(log_before, @r" @ kkmpptxzrspxrzommnulwmwkkqwworpl ○ rlvkpnrzqnoowoytxnquwvuryrwnrmlpB │ ○ qpvuntsmwlqtpsluzzsnyyzlmlwvmlnuA │ ◆ zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz [EOF] "); // Fail on commit B; succeed (modify the tree) on every other commit. If // any subprocess fails, `jj run` must roll back: no commit gets rewritten, // even the ones whose commands ran to completion before B's failure // propagated. let cmd = "if [ \"$JJ_CHANGE_ID\" = 'rlvkpnrzqnoowoytxnquwvuryrwnrmlp' ]; then exit 1; fi; \ touch ran.txt"; let output = work_dir.run_jj(&["run", "-r", "..@", "--", "sh", "-c", cmd]); assert!(!output.status.success(), "expected `jj run` to fail"); // Log is unchanged: same change_ids, same shape, no descendants of B got // rebased onto a new commit. assert_eq!(get_log_output(&work_dir), log_before); } #[test] fn test_run_ignore_errors_rewrites_successes() { let test_env = TestEnvironment::default(); test_env.run_jj_in(".", ["git", "init", "repo"]).success(); let work_dir = test_env.work_dir("repo"); create_commit_with_files(&work_dir, "a", &[], &[("a.txt", "a")]); create_commit_with_files(&work_dir, "b", &["a"], &[("b.txt", "b")]); // Fail on A; succeed on B. A should be untouched, B should be rewritten. let jj_args: &[&str] = if cfg!(windows) { &[ "run", "--ignore-errors", "-r", "..b", "--", "cmd", "/c", "type nul >ran.txt & (if not exist b.txt exit 1)", ] } else { &[ "run", "--ignore-errors", "-r", "..b", "--", "sh", "-c", "touch ran.txt; if [ ! -f b.txt ]; then exit 1; fi", ] }; let output = work_dir.run_jj(jj_args); assert_snapshot!( output.success(), @r" ------- stderr ------- Rewrote 1 commits. Working copy (@) now at: zsuskuln f3c3b6ac b | b Parent commit (@-) : rlvkpnrz 2385de9f a | a Added 1 files, modified 0 files, removed 0 files [EOF] " ); insta::assert_snapshot!( work_dir.run_jj(&["file", "list", "-r=a"]), @r" a.txt [EOF]