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]
");
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]
");
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();
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]
"
);
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();
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("seed.txt", "seed");
work_dir.run_jj(&["commit", "-m", "no-sub"]).success();
work_dir.write_file("sub/file.txt", "x");
work_dir.run_jj(&["commit", "-m", "with-sub"]).success();
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]
");
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();
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();
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();
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("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""#);
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]
"
);
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]
");
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");
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")]);
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]
"
);
insta::assert_snapshot!(
work_dir.run_jj(&["file", "list", "-r=b"]),
@r"
a.txt
b.txt
ran.txt
[EOF]
"
);
}
#[test]
fn test_run_ignore_errors_all_fail() {
let 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();
let work_dir = test_env.work_dir("repo");
work_dir.write_file("A.txt", "A");
work_dir.run_jj(&["commit", "-m", "A"]).success();
let log_before = get_log_output(&work_dir);
let output = work_dir.run_jj(&[
"run",
"--ignore-errors",
"-r",
"@-",
"--",
&fake_formatter_path,
"--fail",
]);
assert_snapshot!(
output.success(), @r"
------- stderr -------
Nothing changed.
[EOF]
"
);
assert_eq!(get_log_output(&work_dir), log_before);
}
#[test]
fn test_run_recovers_after_failure() {
let 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();
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 first = work_dir.run_jj(&["run", "-r", "..@", "--", &fake_formatter_path, "--fail"]);
assert!(!first.status.success(), "expected first `jj run` to fail");
work_dir
.run_jj(&[
"run",
"-r",
"..@",
"--",
&fake_formatter_path,
"--tee",
"ran.txt",
])
.success();
insta::assert_snapshot!(
work_dir.run_jj(&["file", "list", "-r", "@-"]),
@r"
A.txt
b.txt
ran.txt
[EOF]
"
);
insta::assert_snapshot!(
work_dir.run_jj(&["file", "list", "-r", "@--"]),
@r"
A.txt
ran.txt
[EOF]
"
);
}
#[test]
fn test_run_shell_command() {
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();
work_dir.write_file("c.txt", "test to replace");
work_dir.run_jj(&["commit", "-m", "C"]).success();
let log_template = r#"change_id ++ " " ++ commit_id ++ " " ++ description ++ "\n""#;
insta::assert_snapshot!(
work_dir.run_jj(&["log", "-T", log_template, "-r", "..@"]),
@r"
@ zsuskulnrvyrovkzqrwmxqlsskqntxvp 8d0cb96bac2cfefd56a8691b9301ef44cc94a368
○ kkmpptxzrspxrzommnulwmwkkqwworpl 3406218c99ce8076f3a28434ebda109cbd84de9e C
│
○ rlvkpnrzqnoowoytxnquwvuryrwnrmlp 9453b0f03bbda20fa849b10eb051d1e3eed1ec5d B
│
○ qpvuntsmwlqtpsluzzsnyyzlmlwvmlnu 26d8ff9bba4faa4da6735ced959c57280e49afa7 A
│
~
[EOF]
"
);
let jj_args: &[&str] = if cfg!(windows) {
&["run", "-r", "..@", "--", "cmd", "/c", "echo %JJ_COMMIT_ID%"]
} else {
&[
"run",
"-r",
"..@",
"--",
"sh",
"-c",
r#"echo "$JJ_COMMIT_ID""#,
]
};
let output = work_dir.run_jj(jj_args).success();
let mut lines: Vec<&str> = output.stdout.raw().lines().collect();
lines.sort_unstable();
let sorted_stdout = lines.join("\n");
insta::assert_snapshot!(sorted_stdout, @r"
26d8ff9bba4faa4da6735ced959c57280e49afa7
3406218c99ce8076f3a28434ebda109cbd84de9e
8d0cb96bac2cfefd56a8691b9301ef44cc94a368
9453b0f03bbda20fa849b10eb051d1e3eed1ec5d
");
}
#[test]
fn test_run_sets_workspace_root_env_var() {
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();
let jj_args: &[&str] = if cfg!(windows) {
&[
"run",
"-r",
"@-",
"--",
"cmd",
"/c",
"echo %JJ_WORKSPACE_ROOT%>workspace_root.txt",
]
} else {
&[
"run",
"-r",
"@-",
"--",
"sh",
"-c",
"echo $JJ_WORKSPACE_ROOT > workspace_root.txt",
]
};
work_dir.run_jj(jj_args).success();
let normalize_whitespace = |s: String| {
s.replace("\r\n", "\n")
.lines()
.map(|line| line.trim_end().to_string())
.collect::<Vec<_>>()
.join("\n")
+ "\n"
};
insta::assert_snapshot!(
work_dir
.run_jj(&["file", "show", "-r", "@-", "workspace_root.txt"])
.normalize_stdout_with(normalize_whitespace),
@r"
$TEST_ENV/repo/.jj/run/default/1/working_copy
[EOF]
"
);
}
#[test]
fn test_run_pool_reuses_slot_after_failure() {
let 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();
let work_dir = test_env.work_dir("repo");
work_dir.write_file(".gitignore", "cache/\n");
work_dir.write_file("seed.txt", "seed");
work_dir.run_jj(&["commit", "-m", "seed"]).success();
let first = work_dir.run_jj(&[
"run",
"--config",
"run.jobs=1",
"-r",
"@-",
"--",
"sh",
"-c",
&format!(
"mkdir -p cache && echo artifact > cache/artifact.txt && touch stray.txt && \
{fake_formatter_path} --fail"
),
]);
assert!(!first.status.success(), "expected first run to fail");
let slot_state = work_dir.root().join(".jj/run/default/1/state/tree_state");
assert!(
slot_state.exists(),
"tree_state must be persisted even after failure"
);
work_dir
.run_jj(&[
"run",
"--config",
"run.jobs=1",
"-r",
"@-",
"--",
"sh",
"-c",
"if [ -f cache/artifact.txt ]; then cp cache/artifact.txt result.txt; else echo \
MISSING > result.txt; fi",
])
.success();
assert_snapshot!(
work_dir.run_jj(&["file", "show", "-r", "@-", "result.txt"]),@r"
artifact
[EOF]
"
);
assert_snapshot!(
work_dir
.run_jj(&["file", "list", "-r", "@-"]),
@r"
.gitignore
result.txt
seed.txt
[EOF]
"
);
}
#[test]
fn test_run_pool_persists_between_runs() {
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();
work_dir
.run_jj(&[
"run",
"--config",
"run.jobs=1",
"-r",
"@-",
"--",
"touch",
"ran.txt",
])
.success();
let pool_slot = work_dir.root().join(".jj/run/default/1");
assert!(
pool_slot.exists(),
"expected pool slot 1 to persist between runs at {pool_slot:?}",
);
assert!(pool_slot.join("working_copy").exists());
assert!(pool_slot.join("state").exists());
work_dir
.run_jj(&[
"run",
"--config",
"run.jobs=1",
"-r",
"@-",
"--",
"touch",
"ran2.txt",
])
.success();
assert!(pool_slot.join("working_copy").exists());
}
#[test]
fn test_run_pool_size_from_config() {
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();
work_dir.write_file("c.txt", "c");
work_dir.run_jj(&["commit", "-m", "C"]).success();
work_dir
.run_jj(&[
"run",
"--config",
"run.jobs=2",
"-r",
"..@",
"--",
"sh",
"-c",
"touch ran-$JJ_CHANGE_ID.txt",
])
.success();
let pool_dir = work_dir.root().join(".jj/run/default");
assert!(pool_dir.join("1").exists(), "pool/1 should exist");
assert!(pool_dir.join("2").exists(), "pool/2 should exist");
assert!(
!pool_dir.join("3").exists(),
"pool/3 should NOT exist with size=2",
);
assert_snapshot!(work_dir.run_jj(&["file", "list", "-r", "@---"]), @r"
a.txt
ran-qpvuntsmwlqtpsluzzsnyyzlmlwvmlnu.txt
[EOF]
");
assert_snapshot!(work_dir.run_jj(&["file", "list", "-r", "@--"]), @r"
a.txt
b.txt
ran-qpvuntsmwlqtpsluzzsnyyzlmlwvmlnu.txt
ran-rlvkpnrzqnoowoytxnquwvuryrwnrmlp.txt
[EOF]
");
assert_snapshot!(work_dir.run_jj(&["file", "list", "-r", "@-"]), @r"
a.txt
b.txt
c.txt
ran-kkmpptxzrspxrzommnulwmwkkqwworpl.txt
ran-qpvuntsmwlqtpsluzzsnyyzlmlwvmlnu.txt
ran-rlvkpnrzqnoowoytxnquwvuryrwnrmlp.txt
[EOF]
");
}
#[test]
fn test_run_pool_size_from_jobs_flag() {
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();
work_dir.write_file("c.txt", "c");
work_dir.run_jj(&["commit", "-m", "C"]).success();
work_dir
.run_jj(&["run", "--jobs", "2", "-r", "..@", "--", "touch", "ran.txt"])
.success();
let pool_dir = work_dir.root().join(".jj/run/default");
assert!(pool_dir.join("1").exists(), "pool/1 should exist");
assert!(pool_dir.join("2").exists(), "pool/2 should exist");
assert!(
!pool_dir.join("3").exists(),
"pool/3 must NOT exist with --jobs 2",
);
}
#[test]
fn test_run_pool_preserves_untracked_artifacts() {
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(".gitignore", "cache/\n");
work_dir.write_file("seed.txt", "seed");
work_dir.run_jj(&["commit", "-m", "seed"]).success();
work_dir
.run_jj(&[
"run",
"--config",
"run.jobs=1",
"-r",
"@-",
"--",
"sh",
"-c",
"mkdir -p cache && echo run1 > cache/marker && touch ran1.txt",
])
.success();
work_dir
.run_jj(&[
"run",
"--config",
"run.jobs=1",
"-r",
"@-",
"--",
"sh",
"-c",
"if [ -f cache/marker ]; then cp cache/marker result.txt; else echo MISSING > \
result.txt; fi",
])
.success();
assert_snapshot!(
work_dir
.run_jj(&["file", "show", "-r", "@-", "result.txt"])
.success()
.stdout,
@r"
run1
[EOF]
",
);
}
#[test]
fn test_run_pool_removes_file_absent_in_next_commit() {
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("only_in_a.txt", "a");
work_dir.run_jj(&["commit", "-m", "A"]).success();
std::fs::remove_file(work_dir.root().join("only_in_a.txt")).unwrap();
work_dir.run_jj(&["commit", "-m", "B"]).success();
work_dir
.run_jj(&[
"run",
"--config",
"run.jobs=1",
"-r",
"@--::@-",
"--",
"touch",
"ran.txt",
])
.success();
assert_snapshot!(
work_dir
.run_jj(&["file", "list", "-r", "@--"])
.success()
.stdout,
@r"
only_in_a.txt
ran.txt
[EOF]
",
);
assert_snapshot!(
work_dir
.run_jj(&["file", "list", "-r", "@-"])
.success()
.stdout,
@r"
ran.txt
[EOF]
",
);
}
#[test]
fn test_run_pool_removes_previously_ignored_files() {
let 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();
let work_dir = test_env.work_dir("repo");
work_dir.write_file("seed.txt", "seed");
work_dir.write_file(".gitignore", "always_ignored.txt\n");
work_dir
.run_jj(&["bookmark", "create", "-r@", "a"])
.success();
work_dir.run_jj(&["commit", "-m", "a"]).success();
work_dir.write_file(".gitignore", "always_ignored.txt\nignored.txt\n");
work_dir
.run_jj(&["bookmark", "create", "-r@", "b"])
.success();
work_dir.run_jj(&["commit", "-m", "b"]).success();
let run_wc = work_dir.root().join(".jj/run/default/1/working_copy");
work_dir
.run_jj(&[
"run",
"-r=b",
"--",
&fake_formatter_path,
"--tee=always_ignored.txt",
"--tee=ignored.txt",
])
.success();
assert_snapshot!(
work_dir.run_jj(&["file", "list", "-r", "b"]),
@r"
.gitignore
seed.txt
[EOF]
"
);
assert!(fs::exists(run_wc.join("always_ignored.txt")).unwrap());
assert!(fs::exists(run_wc.join("ignored.txt")).unwrap());
work_dir
.run_jj(&[
"run",
"-r=a",
"--",
&fake_formatter_path,
"--stdout",
"done",
])
.success();
assert_snapshot!(
work_dir.run_jj(&["file", "list", "-r", "a"]),
@r"
.gitignore
seed.txt
[EOF]
"
);
assert!(fs::exists(run_wc.join("always_ignored.txt")).unwrap());
assert!(!fs::exists(run_wc.join("ignored.txt")).unwrap());
work_dir
.run_jj(&[
"run",
"-r=a",
"--clean",
"--",
&fake_formatter_path,
"--stdout",
"done",
])
.success();
assert_snapshot!(
work_dir.run_jj(&["file", "list", "-r", "a"]),
@r"
.gitignore
seed.txt
[EOF]
"
);
assert!(!fs::exists(run_wc.join("always_ignored.txt")).unwrap());
assert!(!fs::exists(run_wc.join("ignored.txt")).unwrap());
}
#[test]
fn test_run_pool_recovers_from_missing_tree_state() {
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();
work_dir
.run_jj(&["run", "--config", "run.jobs=1", "-r", "@-", "--", "true"])
.success();
let slot = work_dir.root().join(".jj/run/default/1");
std::fs::write(slot.join("working_copy/stale.txt"), "crash leftovers").unwrap();
std::fs::remove_file(slot.join("state/tree_state")).unwrap();
work_dir
.run_jj(&[
"run",
"--config",
"run.jobs=1",
"-r",
"@-",
"--",
"touch",
"ran.txt",
])
.success();
assert_snapshot!(
work_dir.run_jj(&["file", "list", "-r", "@-"]).success().stdout,
@r"
ran.txt
seed.txt
[EOF]
",
);
}
#[test]
fn test_run_pool_failed_command_does_not_poison_slot() {
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();
drop(work_dir.run_jj(&[
"run",
"--config",
"run.jobs=1",
"-r",
"@-",
"--",
"sh",
"-c",
"touch poison.txt; exit 1",
]));
work_dir
.run_jj(&[
"run",
"--config",
"run.jobs=1",
"-r",
"@-",
"--",
"touch",
"ran.txt",
])
.success();
assert_snapshot!(
work_dir.run_jj(&["file", "list", "-r", "@-"]).success().stdout,
@r"
ran.txt
seed.txt
[EOF]
",
);
}
#[test]
fn test_run_restore_descendants_preserves_content() {
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");
create_commit_with_files(&work_dir, "a", &[], &[("file", "a\n")]);
create_commit_with_files(&work_dir, "b", &["a"], &[("file", "b\n")]);
create_commit_with_files(&work_dir, "c", &["b"], &[("file", "c\n")]);
let command = if cfg!(windows) {
format!("{fake_formatter_path} --tee ran-%JJ_CHANGE_ID%.txt")
} else {
format!("{fake_formatter_path} --tee ran-$JJ_CHANGE_ID.txt")
};
let args: &[&str] = if cfg!(windows) {
&[
"run",
"-r",
"a::b",
"--restore-descendants",
"--",
"cmd",
"/c",
command.as_str(),
]
} else {
&[
"run",
"-r",
"a::b",
"--restore-descendants",
"--",
"sh",
"-c",
command.as_str(),
]
};
let output = work_dir.run_jj(args).success();
assert_snapshot!(output.stderr, @r"
Rewrote 2 commits.
Rebased 1 descendant commits (while preserving their content).
Working copy (@) now at: royxmykx a741a7d3 c | c
Parent commit (@-) : zsuskuln 43c5a714 b | b
[EOF]
");
assert_snapshot!(
work_dir
.run_jj(&["file", "list", "-r", "a"])
.success()
.stdout,
@r"
file
ran-rlvkpnrzqnoowoytxnquwvuryrwnrmlp.txt
[EOF]
"
);
assert_snapshot!(
work_dir
.run_jj(&["file", "list", "-r", "b"])
.success()
.stdout,
@r"
file
ran-zsuskulnrvyrovkzqrwmxqlsskqntxvp.txt
[EOF]
"
);
assert_snapshot!(
work_dir
.run_jj(&["file", "list", "-r", "c"])
.success()
.stdout,
@r"
file
[EOF]
"
);
assert_snapshot!(
work_dir.run_jj(&["diff", "--from=a", "--to=b", "--git"]).success().stdout,
@r"
diff --git a/file b/file
index 7898192261..6178079822 100644
--- a/file
+++ b/file
@@ -1,1 +1,1 @@
-a
+b
diff --git a/ran-rlvkpnrzqnoowoytxnquwvuryrwnrmlp.txt b/ran-rlvkpnrzqnoowoytxnquwvuryrwnrmlp.txt
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/ran-zsuskulnrvyrovkzqrwmxqlsskqntxvp.txt b/ran-zsuskulnrvyrovkzqrwmxqlsskqntxvp.txt
new file mode 100644
index 0000000000..e69de29bb2
[EOF]
"
);
assert_snapshot!(
work_dir.run_jj(&["diff", "--from=b", "--to=c", "--git"]).success().stdout,
@r"
diff --git a/file b/file
index 6178079822..f2ad6c76f0 100644
--- a/file
+++ b/file
@@ -1,1 +1,1 @@
-b
+c
diff --git a/ran-zsuskulnrvyrovkzqrwmxqlsskqntxvp.txt b/ran-zsuskulnrvyrovkzqrwmxqlsskqntxvp.txt
deleted file mode 100644
index e69de29bb2..0000000000
[EOF]
"
);
}
#[test]
fn test_run_failure_shows_output() {
let 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();
let work_dir = test_env.work_dir("repo");
work_dir.write_file("A.txt", "A");
work_dir.run_jj(&["commit", "-m", "A"]).success();
let output = work_dir.run_jj(&[
"run",
"-r",
"@-",
"--",
&fake_formatter_path,
"--stdout",
"hello stdout\n",
"--stderr",
"hello stderr\n",
"--fail",
]);
assert!(!output.status.success());
insta::with_settings!({
filters => [
("exit code", "exit status"), ],
}, {
insta::assert_snapshot!(&output.normalize_stderr_with(|stderr| stderr.replace(&fake_formatter_path.clone(), "fake-formatter")), @"
hello stdout
[EOF]
------- stderr -------
hello stderr
Error: the command 'fake-formatter --stdout hello stdout
--stderr hello stderr
--fail' failed with exit status: 1
Hint: Failed revision: qpvuntsm 26d8ff9b A
[EOF]
[exit status: 1]
");
});
}
#[test]
fn test_run_parallel_changes_propagate_to_descendants() {
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();
work_dir.write_file("c.txt", "c");
work_dir.run_jj(&["commit", "-m", "C"]).success();
work_dir
.run_jj(&[
"run",
"--jobs=3",
"-r=..@",
"--",
"sh",
"-c",
"touch newfile-$JJ_CHANGE_ID.txt",
])
.success();
assert_snapshot!(work_dir.run_jj(&["file", "list", "-r=@---"]).success().stdout,@r"
a.txt
newfile-qpvuntsmwlqtpsluzzsnyyzlmlwvmlnu.txt
[EOF]
");
assert_snapshot!(work_dir.run_jj(&["file", "list", "-r=@--"]).success().stdout,@r"
a.txt
b.txt
newfile-qpvuntsmwlqtpsluzzsnyyzlmlwvmlnu.txt
newfile-rlvkpnrzqnoowoytxnquwvuryrwnrmlp.txt
[EOF]
");
assert_snapshot!(work_dir.run_jj(&["file", "list", "-r=@-"]).success().stdout,@r"
a.txt
b.txt
c.txt
newfile-kkmpptxzrspxrzommnulwmwkkqwworpl.txt
newfile-qpvuntsmwlqtpsluzzsnyyzlmlwvmlnu.txt
newfile-rlvkpnrzqnoowoytxnquwvuryrwnrmlp.txt
[EOF]
");
}
#[test]
fn test_run_ignore_changes_does_not_rewrite() {
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");
crate::common::create_commit_with_files(&work_dir, "a", &[], &[("file.txt", "a\n")]);
crate::common::create_commit_with_files(&work_dir, "b", &["a"], &[("file.txt", "b\n")]);
let log_before = get_log_output(&work_dir);
let output = work_dir
.run_jj(&[
"run",
"--ignore-changes",
"--config",
"run.jobs=1",
"-r",
"a",
"--",
&fake_formatter_path,
"--tee",
"touched.txt",
])
.success();
insta::assert_snapshot!(output.stderr, @r"
Nothing changed.
[EOF]
");
assert_eq!(get_log_output(&work_dir), log_before);
assert_snapshot!(
work_dir
.run_jj(&["file", "list", "-r", "a"])
.success(),
@r"
file.txt
[EOF]
"
);
}
#[test]
fn test_run_ignore_changes_does_not_leak_between_invocations() {
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", "commit1"]).success();
work_dir.run_jj(&["commit", "-m", "commit2"]).success();
let touch_cmd: &[&str] = if cfg!(windows) {
&[
"run",
"--ignore-changes",
"--config",
"run.jobs=1",
"-r",
"@-",
"--",
"cmd",
"/c",
"echo. > artifact.txt",
]
} else {
&[
"run",
"--ignore-changes",
"--config",
"run.jobs=1",
"-r",
"@-",
"--",
"touch",
"artifact.txt",
]
};
work_dir.run_jj(touch_cmd).success();
work_dir
.run_jj(&["run", "--config", "run.jobs=1", "-r", "@--", "--", "true"])
.success();
let files = work_dir
.run_jj(&["file", "list", "-r", "@--"])
.success()
.stdout
.to_string();
assert!(
!files.contains("artifact.txt"),
"artifact.txt must not leak into commit1 from --ignore-changes slot state; got:\n{files}",
);
assert!(
files.contains("seed.txt"),
"expected seed.txt in commit1; got:\n{files}",
);
}
#[test]
fn test_run_ignore_changes_works_on_immutable() {
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.txt", "content");
work_dir.run_jj(&["commit", "-m", "the commit"]).success();
work_dir
.run_jj(&["bookmark", "create", "-r", "@-", "main"])
.success();
test_env.add_config(r#"revset-aliases."immutable_heads()" = "main""#);
let err_output = work_dir.run_jj(&["run", "-r", "main", "--", "true"]);
assert!(
!err_output.status.success(),
"expected failure on immutable commit"
);
let stderr = err_output.stderr.to_string();
assert!(
stderr.contains("immutable"),
"expected 'immutable' in error; got:\n{stderr}",
);
work_dir
.run_jj(&["run", "--ignore-changes", "-r", "main", "--", "true"])
.success();
}
#[test]
fn test_run_ignore_changes_conflicts_with_restore_descendants() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
let output = work_dir.run_jj(&[
"run",
"--ignore-changes",
"--restore-descendants",
"--",
"true",
]);
assert!(!output.status.success());
insta::assert_snapshot!(output.stderr, @r"
error: the argument '--ignore-changes' cannot be used with '--restore-descendants'
Usage: jj run --ignore-changes <COMMAND> [ARGS]...
For more information, try '--help'.
[EOF]
");
}
fn get_log_output(work_dir: &TestWorkDir) -> String {
work_dir
.run_jj(&["log", "-T", r#"change_id ++ description ++ "\n""#])
.success()
.stdout
.to_string()
}
#[test]
fn test_run_order() {
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.txt", "a");
work_dir.run_jj(&["commit", "-m", "A"]).success();
work_dir.write_file("file.txt", "b");
work_dir.run_jj(&["commit", "-m", "B"]).success();
work_dir.write_file("file.txt", "c");
work_dir.run_jj(&["commit", "-m", "C"]).success();
let jj_args: &[&str] = if cfg!(windows) {
&["run", "-r=..@", "--", "cmd", "/c", "echo %JJ_CHANGE_ID%"]
} else {
&["run", "-r=..@", "--", "sh", "-c", "echo $JJ_CHANGE_ID"]
};
assert_snapshot!(work_dir.run_jj(jj_args).success().stdout,@"
qpvuntsmwlqtpsluzzsnyyzlmlwvmlnu
rlvkpnrzqnoowoytxnquwvuryrwnrmlp
kkmpptxzrspxrzommnulwmwkkqwworpl
zsuskulnrvyrovkzqrwmxqlsskqntxvp
[EOF]
");
}
#[test]
fn test_run_passthrough() {
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();
insta::assert_snapshot!(
work_dir.run_jj(&["log", "-T", r#"change_id ++ " " ++ description ++ "\n""#, "-r", "..@"]),
@r"
@ kkmpptxzrspxrzommnulwmwkkqwworpl
○ rlvkpnrzqnoowoytxnquwvuryrwnrmlp B
│
○ qpvuntsmwlqtpsluzzsnyyzlmlwvmlnu A
│
~
[EOF]
"
);
let jj_args: &[&str] = if cfg!(windows) {
&[
"run",
"--passthrough",
"-r",
"..@",
"--",
"cmd",
"/c",
"echo hello from passthrough",
]
} else {
&[
"run",
"--passthrough",
"-r",
"..@",
"--",
"echo",
"hello from passthrough",
]
};
let output = work_dir.run_jj(jj_args).success();
insta::assert_snapshot!(output.stdout, @"hello from passthrough
hello from passthrough
hello from passthrough
[EOF]
");
}
#[test]
fn test_run_passthrough_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]
");
let output = work_dir.run_jj(&["run", "--passthrough", "-r", "..@", "--", "false"]);
assert!(!output.status.success(), "expected `jj run` to fail");
assert_eq!(get_log_output(&work_dir), log_before);
}
#[test]
fn test_run_passthrough_rejects_multi_job() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
let output = work_dir.run_jj(&[
"run",
"--passthrough",
"--jobs",
"2",
"-r",
"@",
"--",
"true",
]);
assert!(!output.status.success());
insta::assert_snapshot!(output.stderr, @r"
Error: cannot use --passthrough with more than one job
[EOF]
");
}
#[test]
fn test_run_on_conflicted_commit() {
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");
create_commit_with_files(&work_dir, "base", &[], &[("file", "base\n")]);
create_commit_with_files(&work_dir, "a", &["base"], &[("file", "a\n")]);
create_commit_with_files(&work_dir, "b", &["base"], &[("file", "b\n")]);
create_commit_with_files(&work_dir, "conflict", &["a", "b"], &[]);
insta::assert_snapshot!(work_dir.run_jj(&["resolve", "--list"]).success().stdout, @r"
file 2-sided conflict
[EOF]
");
let output = work_dir.run_jj(&[
"run",
"-r",
"conflict",
"--",
&fake_formatter_path,
"--stdout",
"x",
"--tee",
"touched.txt",
]);
insta::assert_snapshot!(output, @r"
x[EOF]
------- stderr -------
Rewrote 1 commits.
Working copy (@) now at: vruxwmqv 74643194 conflict | (conflict) conflict
Parent commit (@-) : zsuskuln 45537d53 a | a
Parent commit (@-) : royxmykx 89d1b299 b | b
Added 1 files, modified 0 files, removed 0 files
Warning: There are unresolved conflicts at these paths:
file 2-sided conflict
New conflicts appeared in 1 commits:
vruxwmqv 74643194 conflict | (conflict) conflict
Hint: To resolve the conflicts, start by creating a commit on top of
the conflicted commit:
jj new vruxwmqv
Then use `jj resolve`, or edit the conflict markers in the file directly.
Once the conflicts are resolved, you can inspect the result with `jj diff`.
Then run `jj squash` to move the resolution into the conflicted commit.
[EOF]
");
insta::assert_snapshot!(work_dir.run_jj(&["resolve", "--list", "-r", "conflict"]).success().stdout, @r"
file 2-sided conflict
[EOF]
");
insta::assert_snapshot!(work_dir.run_jj(&["file", "list", "-r", "conflict"]).success().stdout, @r"
file
touched.txt
[EOF]
");
}