#![allow(clippy::unwrap_used, clippy::expect_used)]
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use tempfile::TempDir;
fn git(dir: &Path, args: &[&str]) -> String {
let output = Command::new("git")
.current_dir(dir)
.args(args)
.env("GIT_CONFIG_GLOBAL", "/dev/null")
.env("GIT_CONFIG_SYSTEM", "/dev/null")
.env("GIT_AUTHOR_NAME", "pristine")
.env("GIT_AUTHOR_EMAIL", "pristine@example.invalid")
.env("GIT_COMMITTER_NAME", "pristine")
.env("GIT_COMMITTER_EMAIL", "pristine@example.invalid")
.env_remove("GIT_DIR")
.env_remove("GIT_INDEX_FILE")
.env_remove("GIT_WORK_TREE")
.stdin(Stdio::null())
.output()
.unwrap();
assert!(
output.status.success(),
"git {args:?} in {}: {}",
dir.display(),
String::from_utf8_lossy(&output.stderr)
);
String::from_utf8_lossy(&output.stdout).into_owned()
}
fn write(path: &Path, contents: &str) {
fs::create_dir_all(path.parent().unwrap()).unwrap();
fs::write(path, contents).unwrap();
}
fn checkout() -> (TempDir, PathBuf) {
let tmp = TempDir::new().unwrap();
let root = fs::canonicalize(tmp.path()).unwrap();
git(&root, &["init", "--quiet"]);
write(&root.join("tracked.txt"), "the original\n");
git(&root, &["add", "tracked.txt"]);
git(&root, &["commit", "--quiet", "-m", "first"]);
(tmp, root)
}
struct Run {
stdout: String,
stderr: String,
ok: bool,
}
fn run(root: &Path, args: &[&str], answer: &str) -> Run {
let mut child = Command::new(env!("CARGO_BIN_EXE_pristine"))
.arg("repo")
.arg(root)
.args(args)
.env("LANGUAGE", "de")
.env("LC_ALL", "de_DE.UTF-8")
.env("GIT_CONFIG_GLOBAL", "/dev/null")
.env("GIT_CONFIG_SYSTEM", "/dev/null")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
child
.stdin
.take()
.unwrap()
.write_all(answer.as_bytes())
.unwrap();
let output = child.wait_with_output().unwrap();
Run {
stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
ok: output.status.success(),
}
}
fn succeeds(root: &Path, args: &[&str], answer: &str) -> String {
let run = run(root, args, answer);
assert!(
run.ok,
"pristine repo {args:?} failed:\n{}\n{}",
run.stdout, run.stderr
);
run.stdout
}
fn mixed_directory(root: &Path) {
write(&root.join(".gitignore"), ".nx/cache/\n");
git(root, &["add", ".gitignore"]);
git(root, &["commit", "--quiet", "-m", "ignore the cache"]);
write(&root.join(".nx/cache/hash.bin"), "expensive to rebuild\n");
write(&root.join(".nx/workspace-data/state.json"), "{}\n");
}
#[test]
fn removing_untracked_files_does_not_take_the_ignored_cache_beside_them() {
let (_tmp, root) = checkout();
mixed_directory(&root);
let printed = succeeds(&root, &["--untracked", "--yes"], "");
assert!(
!root.join(".nx/workspace-data").exists(),
"the untracked half survived:\n{printed}"
);
assert!(
root.join(".nx/cache/hash.bin").exists(),
"removing untracked files took the ignored cache with them:\n{printed}"
);
}
#[test]
fn removing_ignored_files_does_not_take_the_untracked_data_beside_them() {
let (_tmp, root) = checkout();
mixed_directory(&root);
let printed = succeeds(&root, &["--ignored", "--yes"], "");
assert!(!root.join(".nx/cache").exists(), "{printed}");
assert!(
root.join(".nx/workspace-data/state.json").exists(),
"removing ignored files took the untracked data with them:\n{printed}"
);
}
#[test]
fn a_tracked_file_is_never_removed_whatever_was_asked_for() {
let (_tmp, root) = checkout();
write(&root.join(".gitignore"), "dist/\n");
write(&root.join("dist/bundle.js"), "built\n");
write(&root.join("scratch.txt"), "untracked\n");
succeeds(
&root,
&[
"--untracked",
"--ignored",
"--node-modules",
"--env",
"--yes",
],
"",
);
assert!(root.join("tracked.txt").exists(), "a tracked file went");
assert!(!root.join("dist").exists());
assert!(!root.join("scratch.txt").exists());
}
fn sediment(root: &Path) {
write(
&root.join(".gitignore"),
"dist/\nnode_modules/\n.env.local\n",
);
git(root, &["add", ".gitignore"]);
git(root, &["commit", "--quiet", "-m", "ignore the usual"]);
write(&root.join("dist/bundle.js"), "built\n");
write(&root.join("node_modules/left-pad/index.js"), "module\n");
write(&root.join(".env.local"), "SECRET=1\n");
write(&root.join(".env"), "SECRET=2\n");
}
#[test]
fn vendor_and_env_survive_a_run_that_did_not_ask_for_them() {
let (_tmp, root) = checkout();
sediment(&root);
let printed = succeeds(&root, &["--untracked", "--ignored", "--yes"], "");
assert!(!root.join("dist").exists(), "{printed}");
assert!(
root.join("node_modules/left-pad/index.js").exists(),
"{printed}"
);
assert!(root.join(".env.local").exists(), "{printed}");
assert!(root.join(".env").exists(), "{printed}");
assert!(printed.contains("--node-modules"), "{printed}");
assert!(printed.contains("--env"), "{printed}");
}
#[test]
fn opting_in_takes_them() {
let (_tmp, root) = checkout();
sediment(&root);
let printed = succeeds(
&root,
&[
"--untracked",
"--ignored",
"--node-modules",
"--env",
"--yes",
],
"",
);
assert!(!root.join("node_modules").exists(), "{printed}");
assert!(!root.join(".env.local").exists(), "{printed}");
assert!(!root.join(".env").exists(), "{printed}");
assert!(root.join("tracked.txt").exists(), "{printed}");
}
#[test]
fn an_untracked_directory_that_hides_an_env_file_is_held_back() {
let (_tmp, root) = checkout();
write(&root.join("docker/compose.yaml"), "services: {}\n");
write(&root.join("docker/.env"), "SECRET=1\n");
let offered = git(&root, &["clean", "-n", "-d"]);
assert!(offered.contains("docker/"), "{offered}");
assert!(
!offered.contains(".env"),
"the fixture did not collapse: {offered}"
);
let printed = succeeds(&root, &["--untracked", "--yes"], "");
assert!(
root.join("docker/.env").exists(),
"an env file was deleted from behind a collapsed directory:\n{printed}"
);
assert!(root.join("docker/compose.yaml").exists(), "{printed}");
assert!(printed.contains("docker"), "{printed}");
assert!(printed.contains("--env"), "{printed}");
}
#[test]
fn an_untracked_directory_that_hides_node_modules_is_held_back() {
let (_tmp, root) = checkout();
write(&root.join("pkg/index.js"), "source\n");
write(&root.join("pkg/node_modules/left-pad/index.js"), "module\n");
let printed = succeeds(&root, &["--untracked", "--yes"], "");
assert!(
root.join("pkg/node_modules/left-pad/index.js").exists(),
"a vendored tree was deleted from behind a collapsed directory:\n{printed}"
);
assert!(printed.contains("--node-modules"), "{printed}");
}
#[test]
fn an_ignored_directory_that_hides_an_env_file_is_held_back() {
let (_tmp, root) = checkout();
write(&root.join(".gitignore"), "build/\n");
git(&root, &["add", ".gitignore"]);
git(&root, &["commit", "--quiet", "-m", "ignore build"]);
write(&root.join("build/out.js"), "built\n");
write(&root.join("build/.env"), "SECRET=1\n");
let offered = git(&root, &["clean", "-n", "-d", "-X"]);
assert!(offered.contains("build/"), "{offered}");
assert!(
!offered.contains(".env"),
"the fixture did not collapse: {offered}"
);
let printed = succeeds(&root, &["--ignored", "--yes"], "");
assert!(
root.join("build/.env").exists(),
"an env file was deleted from behind a collapsed ignored directory:\n{printed}"
);
}
#[test]
fn opting_in_releases_a_directory_that_was_only_held_back_by_what_it_hides() {
let (_tmp, root) = checkout();
write(&root.join("docker/compose.yaml"), "services: {}\n");
write(&root.join("docker/.env"), "SECRET=1\n");
write(&root.join("pkg/node_modules/left-pad/index.js"), "module\n");
let printed = succeeds(
&root,
&["--untracked", "--node-modules", "--env", "--yes"],
"",
);
assert!(!root.join("docker").exists(), "{printed}");
assert!(!root.join("pkg").exists(), "{printed}");
}
#[cfg(unix)]
#[test]
fn a_directory_that_cannot_be_read_is_not_a_directory_that_was_cleared() {
use std::os::unix::fs::PermissionsExt;
let (_tmp, root) = checkout();
write(&root.join("opaque/inner/thing.txt"), "who knows\n");
let sealed = root.join("opaque/inner");
fs::set_permissions(&sealed, fs::Permissions::from_mode(0o000)).unwrap();
if fs::read_dir(&sealed).is_ok() {
fs::set_permissions(&sealed, fs::Permissions::from_mode(0o755)).unwrap();
return; }
let offered = git(&root, &["clean", "-n", "-d"]);
assert!(offered.contains("opaque/"), "{offered}");
let run = run(&root, &["--untracked", "--yes"], "");
fs::set_permissions(&sealed, fs::Permissions::from_mode(0o755)).unwrap();
assert!(
root.join("opaque/inner/thing.txt").exists(),
"a subtree nothing could read was removed anyway:\n{}",
run.stdout
);
assert!(
run.stdout.contains("held back"),
"the unreadable subtree was not caught while planning:\n{}",
run.stdout
);
assert!(run.ok, "declining to act is not a failure:\n{}", run.stderr);
}
#[test]
fn an_action_flag_without_yes_still_refuses_to_delete() {
let (_tmp, root) = checkout();
sediment(&root);
let run = run(&root, &["--ignored"], "");
assert!(run.ok, "{}", run.stderr);
assert!(root.join("dist/bundle.js").exists(), "{}", run.stdout);
assert!(run.stdout.contains("[y/N]"), "{}", run.stdout);
assert!(run.stdout.contains("nothing was"), "{}", run.stdout);
}
#[test]
fn a_bare_enter_at_the_confirmation_removes_nothing() {
let (_tmp, root) = checkout();
sediment(&root);
let run = run(&root, &["--ignored"], "\n");
assert!(run.ok, "{}", run.stderr);
assert!(root.join("dist/bundle.js").exists(), "enter was consent");
}
#[test]
fn saying_yes_at_the_confirmation_removes_what_the_plan_listed() {
let (_tmp, root) = checkout();
sediment(&root);
let run = run(&root, &["--ignored"], "y\n");
assert!(run.ok, "{}", run.stderr);
assert!(!root.join("dist").exists(), "{}", run.stdout);
}
#[test]
fn yes_on_its_own_selects_nothing() {
let (_tmp, root) = checkout();
sediment(&root);
let printed = succeeds(&root, &["--yes"], "");
assert!(root.join("dist/bundle.js").exists(), "{printed}");
assert!(printed.contains("nothing selected"), "{printed}");
}
#[test]
fn yes_does_not_ask_what_to_do_however_eagerly_the_input_answers() {
let (_tmp, root) = checkout();
sediment(&root);
write(&root.join("tracked.txt"), "changed\n");
let printed = succeeds(&root, &["--yes"], "3\ny\ny\ny\ny\ny\n");
assert!(
!printed.contains("[y/N]"),
"--yes reached a prompt:\n{printed}"
);
assert!(!printed.contains("Reset changed"), "{printed}");
assert!(printed.contains("nothing selected"), "{printed}");
assert!(root.join("dist/bundle.js").exists(), "{printed}");
assert!(root.join("node_modules").exists(), "{printed}");
assert_eq!(
fs::read_to_string(root.join("tracked.txt")).unwrap(),
"changed\n",
"--yes reset a work tree nobody asked it to"
);
}
#[test]
fn a_dry_run_prints_the_plan_and_touches_nothing() {
let (_tmp, root) = checkout();
sediment(&root);
write(&root.join("tracked.txt"), "changed\n");
let printed = succeeds(
&root,
&[
"--reset=hard",
"--untracked",
"--ignored",
"--dry-run",
"--yes",
],
"",
);
assert!(root.join("dist/bundle.js").exists(), "{printed}");
assert_eq!(
fs::read_to_string(root.join("tracked.txt")).unwrap(),
"changed\n",
"a dry run reset the work tree:\n{printed}"
);
assert!(
printed.lines().any(|line| line.ends_with(" dist")),
"{printed}"
);
assert!(printed.contains("git reset --hard HEAD"), "{printed}");
assert!(!printed.contains(root.to_str().unwrap()), "{printed}");
assert!(printed.contains("dry run"), "{printed}");
}
#[test]
fn reset_worktree_discards_the_working_copy_and_leaves_the_index() {
let (_tmp, root) = checkout();
write(&root.join("staged.txt"), "staged\n");
git(&root, &["add", "staged.txt"]);
write(&root.join("tracked.txt"), "changed\n");
succeeds(&root, &["--reset=worktree", "--yes"], "");
assert_eq!(
fs::read_to_string(root.join("tracked.txt")).unwrap(),
"the original\n",
"`git restore -- .` did not restore the working tree"
);
assert!(
git(&root, &["diff", "--cached", "--name-only"]).contains("staged.txt"),
"the index went with the working tree"
);
}
#[test]
fn reset_hard_discards_the_index_too() {
let (_tmp, root) = checkout();
write(&root.join("staged.txt"), "staged\n");
git(&root, &["add", "staged.txt"]);
write(&root.join("tracked.txt"), "changed\n");
succeeds(&root, &["--reset=hard", "--yes"], "");
assert_eq!(
fs::read_to_string(root.join("tracked.txt")).unwrap(),
"the original\n"
);
assert!(
git(&root, &["diff", "--cached", "--name-only"])
.trim()
.is_empty(),
"a hard reset left the index alone"
);
assert!(
!root.join("staged.txt").exists(),
"a hard reset kept a file that is not in HEAD"
);
}
#[test]
fn a_bare_reset_is_a_hard_one() {
let (_tmp, root) = checkout();
write(&root.join("staged.txt"), "staged\n");
git(&root, &["add", "staged.txt"]);
succeeds(&root, &["--reset", "--yes"], "");
assert!(
git(&root, &["diff", "--cached", "--name-only"])
.trim()
.is_empty(),
"a bare --reset was not a hard one"
);
}
#[test]
fn a_reset_that_makes_a_planned_target_tracked_does_not_delete_it() {
let (_tmp, root) = checkout();
git(&root, &["rm", "--cached", "--quiet", "tracked.txt"]);
write(&root.join("scratch.txt"), "untracked\n");
assert!(
git(&root, &["clean", "-n", "-d"]).contains("tracked.txt"),
"the fixture did not reach the state the bug needs"
);
let printed = succeeds(&root, &["--reset=hard", "--untracked", "--yes"], "");
assert!(
root.join("tracked.txt").exists(),
"a file the reset made tracked was deleted by a plan built before it:\n{printed}"
);
assert_eq!(
fs::read_to_string(root.join("tracked.txt")).unwrap(),
"the original\n"
);
assert!(!root.join("scratch.txt").exists(), "{printed}");
}
#[test]
fn a_reset_that_makes_git_collapse_a_directory_does_not_widen_the_plan() {
let (_tmp, root) = checkout();
write(&root.join("dir/a.txt"), "untracked\n");
write(&root.join("dir/staged.txt"), "staged\n");
git(&root, &["add", "dir/staged.txt"]);
let before = git(&root, &["clean", "-n", "-d"]);
assert!(before.contains("dir/a.txt"), "{before}");
assert!(!before.contains("Would remove dir/\n"), "{before}");
let printed = succeeds(&root, &["--reset=hard", "--untracked", "--yes"], "");
assert!(
printed.contains("withdrawn after the reset"),
"the widening was not reported:\n{printed}"
);
assert!(
root.join("dir/a.txt").exists(),
"the withdrawn target was removed anyway:\n{printed}"
);
assert!(!root.join("dir/staged.txt").exists(), "{printed}");
}
#[test]
fn a_reset_that_uncovers_an_env_file_holds_the_directory_back_on_this_run_and_the_next() {
let (_tmp, root) = checkout();
write(&root.join("dir/a.txt"), "untracked\n");
write(&root.join("dir/.env"), "SECRET=1\n");
write(&root.join("dir/staged.txt"), "staged\n");
git(&root, &["add", "dir/staged.txt"]);
let printed = succeeds(&root, &["--reset=hard", "--untracked", "--yes"], "");
assert!(
root.join("dir/.env").exists(),
"the reset widened the plan onto a file that was deliberately excluded:\n{printed}"
);
let again = succeeds(&root, &["--untracked", "--yes"], "");
assert!(
root.join("dir/.env").exists(),
"the rerun deleted the env file the first run protected:\n{again}"
);
assert!(root.join("dir/a.txt").exists(), "{again}");
assert!(again.contains("held back"), "{again}");
assert!(again.contains("--env includes it"), "{again}");
}
#[test]
fn the_reset_happens_before_anything_is_removed() {
let (_tmp, root) = checkout();
fs::remove_file(root.join("tracked.txt")).unwrap();
write(&root.join("scratch.txt"), "untracked\n");
succeeds(&root, &["--reset=hard", "--untracked", "--yes"], "");
assert!(
root.join("tracked.txt").exists(),
"the reset did not restore the tracked file, or the removal took it back off"
);
assert!(!root.join("scratch.txt").exists());
}
#[test]
fn a_nested_checkout_is_left_alone_and_named() {
let (_tmp, root) = checkout();
write(&root.join(".gitignore"), "sandboxes/\n");
git(&root, &["add", ".gitignore"]);
git(&root, &["commit", "--quiet", "-m", "ignore the sandboxes"]);
let inner = root.join("sandboxes/work");
fs::create_dir_all(&inner).unwrap();
git(&inner, &["init", "--quiet"]);
write(&inner.join("uncommitted.txt"), "exists nowhere else\n");
let printed = succeeds(&root, &["--ignored", "--yes"], "");
assert!(
inner.join("uncommitted.txt").exists(),
"a nested checkout was removed:\n{printed}"
);
assert!(printed.contains("nested repositor"), "{printed}");
assert!(printed.contains("sandboxes/work"), "{printed}");
}
#[test]
fn outside_a_work_tree_it_says_so_and_exits_non_zero() {
let tmp = TempDir::new().unwrap();
let run = run(tmp.path(), &["--ignored", "--yes"], "");
assert!(!run.ok, "a directory that is not a checkout succeeded");
assert!(run.stderr.contains("git work tree"), "{}", run.stderr);
}
#[test]
fn a_path_inside_the_checkout_cleans_the_whole_checkout() {
let (_tmp, root) = checkout();
write(&root.join(".gitignore"), "dist/\n");
git(&root, &["add", ".gitignore"]);
git(&root, &["commit", "--quiet", "-m", "ignore dist"]);
write(&root.join("dist/a.js"), "built\n");
write(&root.join("packages/web/dist/b.js"), "built\n");
let printed = succeeds(&root.join("packages/web"), &["--ignored", "--yes"], "");
assert!(!root.join("dist").exists(), "{printed}");
assert!(!root.join("packages/web/dist").exists(), "{printed}");
}
#[test]
fn a_run_with_no_flags_and_no_input_does_nothing() {
let (_tmp, root) = checkout();
sediment(&root);
write(&root.join("tracked.txt"), "changed\n");
let printed = succeeds(&root, &[], "");
assert!(root.join("dist/bundle.js").exists(), "{printed}");
assert_eq!(
fs::read_to_string(root.join("tracked.txt")).unwrap(),
"changed\n"
);
assert!(
printed.contains("Reset changed (tracked) files?"),
"{printed}"
);
}
#[test]
fn the_cascade_reaches_the_deleter_when_it_is_answered() {
let (_tmp, root) = checkout();
sediment(&root);
let printed = succeeds(&root, &[], "1\nn\ny\nn\nn\ny\n");
assert!(!root.join("dist").exists(), "{printed}");
assert!(
root.join("node_modules").exists(),
"vendor was not held back"
);
assert!(root.join(".env.local").exists(), "env was not held back");
assert!(root.join(".env").exists(), "{printed}");
}