use super::*;
use crate::subvolume_test_support::{PlainPlatform, backdate, checkout_with_worktree};
use crate::test_support::{git, remove};
use std::path::{Path, PathBuf};
const DRY: ConvertOptions = ConvertOptions { dry_run: true, abandon: false, command_line_attributes: false };
fn dry_run(worktree: &Path) -> Verdict {
return convert_worktree(&PlainPlatform::new(), worktree, DRY, &Interrupt::never(), &mut |_text: &str| {}).expect("no failure");
}
fn refusal_reasons(worktree: &Path) -> String {
return match dry_run(worktree) {
Verdict::Skipped(reasons) => reasons.join("\n"),
other => panic!("expected a skip, got {other:?}"),
};
}
fn assert_converts(worktree: &Path) {
assert_eq!(dry_run(worktree), Verdict::WouldConvert);
}
fn settle(worktree: &Path) {
backdate(worktree);
git(worktree, &["update-index", "--refresh"]);
}
fn admin_of(worktree: &Path) -> PathBuf {
return inspect(worktree).expect("registered").admin;
}
fn settled_worktree(name: &str) -> (PathBuf, PathBuf, PathBuf) {
let (parent, root) = checkout_with_worktree(name);
let worktree: PathBuf = std::fs::canonicalize(root.join("wt")).expect("canonical worktree");
settle(worktree.as_path());
return (parent, root, worktree);
}
fn make_every_entry_racy(worktree: &Path) {
let index = std::fs::OpenOptions::new().write(true).open(admin_of(worktree).join("index")).expect("index");
index.set_modified(std::time::SystemTime::now() - std::time::Duration::from_secs(3_600)).expect("old index time");
}
fn rewrite_keeping_mtime(path: &Path, bytes: &[u8]) {
let before = std::fs::metadata(path).expect("metadata").modified().expect("mtime");
std::fs::write(path, bytes).expect("rewrite");
std::fs::OpenOptions::new().write(true).open(path).expect("open").set_modified(before).expect("restore mtime");
}
#[test]
fn a_detached_head_naming_a_commit_converts() {
let (parent, root) = checkout_with_worktree("content-detached");
git(root.as_path(), &["tag", "@scope/package@1.0.0"]);
git(root.as_path(), &["pack-refs", "--all"]);
let worktree: PathBuf = std::fs::canonicalize(root.join("wt")).expect("canonical worktree");
assert!(std::fs::read_to_string(admin_of(worktree.as_path()).join("HEAD")).expect("HEAD").len() == 41, "HEAD holds a bare commit name");
settle(worktree.as_path());
assert_converts(worktree.as_path());
remove(parent.as_path());
}
#[test]
fn a_packed_branch_resolves_beside_a_tag_with_an_unusual_name() {
let (parent, root) = checkout_with_worktree("content-packed");
git(root.as_path(), &["tag", "@scope/package@1.0.0"]);
git(root.as_path(), &["worktree", "add", "--quiet", "-b", "topic", "topic"]);
git(root.as_path(), &["pack-refs", "--all"]);
let worktree: PathBuf = std::fs::canonicalize(root.join("topic")).expect("canonical worktree");
assert!(!root.join(".git/refs/heads/topic").exists(), "the branch is only in packed-refs");
settle(worktree.as_path());
assert_converts(worktree.as_path());
remove(parent.as_path());
}
#[test]
fn a_head_naming_no_commit_refuses() {
let (parent, _root, worktree) = settled_worktree("content-nohead");
std::fs::write(admin_of(worktree.as_path()).join("HEAD"), format!("{}\n", "1".repeat(40))).expect("HEAD");
let reasons: String = refusal_reasons(worktree.as_path());
assert!(reasons.contains("the commit of HEAD is unreadable"), "{reasons}");
remove(parent.as_path());
}
#[test]
fn a_racy_entry_with_equal_content_converts() {
let (parent, _root, worktree) = settled_worktree("content-racy-equal");
make_every_entry_racy(worktree.as_path());
assert_converts(worktree.as_path());
remove(parent.as_path());
}
#[test]
fn a_racy_entry_with_different_content_refuses() {
let (parent, _root, worktree) = settled_worktree("content-racy-different");
rewrite_keeping_mtime(worktree.join("README.md").as_path(), b"README\n");
make_every_entry_racy(worktree.as_path());
let reasons: String = refusal_reasons(worktree.as_path());
assert!(reasons.contains("README.md differs from the committed content"), "{reasons}");
remove(parent.as_path());
}
#[test]
fn a_stale_stat_is_decided_by_content() {
let (parent, _root, worktree) = settled_worktree("content-stale");
std::fs::write(worktree.join("README.md"), b"readme\n").expect("same bytes, new times");
assert_converts(worktree.as_path());
std::fs::write(worktree.join("README.md"), b"README\n").expect("other bytes of the same size");
let reasons: String = refusal_reasons(worktree.as_path());
assert!(reasons.contains("README.md differs from the committed content"), "{reasons}");
remove(parent.as_path());
}
#[test]
fn a_symbolic_link_is_compared_by_its_target() {
let (parent, _root, worktree) = settled_worktree("content-link");
std::fs::remove_file(worktree.join("link")).expect("remove link");
std::os::unix::fs::symlink("src/main.rs", worktree.join("link")).expect("same target");
assert_converts(worktree.as_path());
std::fs::remove_file(worktree.join("link")).expect("remove link");
std::os::unix::fs::symlink("README.md", worktree.join("link")).expect("other target");
let reasons: String = refusal_reasons(worktree.as_path());
assert!(reasons.contains("link differs from the committed content"), "{reasons}");
remove(parent.as_path());
}
fn attributed_worktree(name: &str, attributes: &str) -> (PathBuf, PathBuf) {
let (parent, root) = checkout_with_worktree(name);
std::fs::write(root.join("data.dat"), b"data\n").expect("data");
std::fs::write(root.join("crlf.txt"), b"one\r\ntwo\r\n").expect("crlf");
git(root.as_path(), &["add", "data.dat", "crlf.txt"]);
git(root.as_path(), &["commit", "--quiet", "--message=files"]);
std::fs::write(root.join(".gitattributes"), attributes).expect("attributes");
git(root.as_path(), &["add", ".gitattributes"]);
git(root.as_path(), &["commit", "--quiet", "--message=attributes"]);
git(root.as_path(), &["worktree", "add", "--quiet", "--detach", "attr"]);
let worktree: PathBuf = std::fs::canonicalize(root.join("attr")).expect("canonical worktree");
settle(worktree.as_path());
return (parent, worktree);
}
#[test]
fn a_filtered_path_refuses_and_an_unfiltered_one_converts() {
let (parent, worktree) = attributed_worktree("content-filter", "*.dat filter=scrambler\n");
std::fs::write(worktree.join("README.md"), b"readme\n").expect("same bytes, new times");
assert_converts(worktree.as_path());
std::fs::write(worktree.join("data.dat"), b"data\n").expect("same bytes, new times");
let reasons: String = refusal_reasons(worktree.as_path());
assert!(reasons.contains("data.dat") && reasons.contains("filter") && reasons.contains(".gitattributes"), "{reasons}");
remove(parent.as_path());
}
#[test]
fn every_attribute_that_can_change_bytes_refuses() {
for (label, attributes) in [("ident", "*.dat ident\n"), ("encoding", "*.dat working-tree-encoding=ISO-8859-1\n"), ("macro", "[attr]scrambled filter=scrambler\n*.dat scrambled\n")] {
let (parent, worktree) = attributed_worktree(format!("content-attribute-{label}").as_str(), attributes);
std::fs::write(worktree.join("data.dat"), b"data\n").expect("same bytes, new times");
let reasons: String = refusal_reasons(worktree.as_path());
assert!(reasons.contains("data.dat") && reasons.contains(".gitattributes"), "{label}: {reasons}");
remove(parent.as_path());
}
}
#[test]
fn a_carriage_return_refuses() {
let (parent, worktree) = attributed_worktree("content-cr", "* text=auto eol=lf\n");
std::fs::write(worktree.join("crlf.txt"), b"one\r\ntwo\r\n").expect("same bytes, new times");
let reasons: String = refusal_reasons(worktree.as_path());
assert!(reasons.contains("crlf.txt") && reasons.contains("carriage return"), "{reasons}");
remove(parent.as_path());
}
#[test]
fn line_ending_attributes_do_not_refuse_a_file_without_a_carriage_return() {
let (parent, worktree) = attributed_worktree("content-eol", "* text=auto eol=lf\n");
std::fs::write(worktree.join("data.dat"), b"data\n").expect("same bytes, new times");
assert_converts(worktree.as_path());
remove(parent.as_path());
}
#[test]
fn attributes_from_every_other_source_refuse() {
let (parent, worktree) = attributed_worktree("content-sources", "# nothing converts here\n");
let root: PathBuf = std::fs::canonicalize(parent.join("checkout")).expect("canonical root");
std::fs::write(worktree.join("data.dat"), b"data\n").expect("same bytes, new times");
assert_converts(worktree.as_path());
std::fs::create_dir_all(root.join(".git/info")).expect("info");
std::fs::write(root.join(".git/info/attributes"), "*.dat filter=scrambler\n").expect("info attributes");
let reasons: String = refusal_reasons(worktree.as_path());
assert!(reasons.contains("data.dat") && reasons.contains("info/attributes"), "{reasons}");
std::fs::remove_file(root.join(".git/info/attributes")).expect("remove");
let global: PathBuf = parent.join("global-attributes");
std::fs::write(&global, "data.dat ident\n").expect("global attributes");
git(root.as_path(), &["config", "core.attributesFile", global.to_str().expect("UTF-8")]);
let from_global: String = refusal_reasons(worktree.as_path());
assert!(from_global.contains("data.dat") && from_global.contains(global.to_str().expect("UTF-8")), "{from_global}");
git(root.as_path(), &["config", "core.attributesFile", "relative-attributes"]);
let from_relative: String = refusal_reasons(worktree.as_path());
assert!(from_relative.contains("core.attributesFile") && from_relative.contains("not an absolute path"), "{from_relative}");
git(root.as_path(), &["config", "--unset", "core.attributesFile"]);
assert_converts(worktree.as_path());
remove(parent.as_path());
}
#[test]
fn a_nested_attributes_file_applies_below_its_directory() {
let (parent, root) = checkout_with_worktree("content-nested");
std::fs::write(root.join("src/.gitattributes"), "*.rs filter=scrambler\n").expect("nested attributes");
git(root.as_path(), &["add", "src/.gitattributes"]);
git(root.as_path(), &["commit", "--quiet", "--message=nested attributes"]);
git(root.as_path(), &["worktree", "add", "--quiet", "--detach", "nested"]);
let worktree: PathBuf = std::fs::canonicalize(root.join("nested")).expect("canonical worktree");
settle(worktree.as_path());
std::fs::write(worktree.join("README.md"), b"readme\n").expect("same bytes, new times");
assert_converts(worktree.as_path());
std::fs::write(worktree.join("src/main.rs"), b"fn main() {}\n").expect("same bytes, new times");
let reasons: String = refusal_reasons(worktree.as_path());
assert!(reasons.contains("src/main.rs") && reasons.contains("src/.gitattributes"), "{reasons}");
remove(parent.as_path());
}
#[test]
fn a_file_larger_than_the_limit_refuses() {
let (parent, _root, worktree) = settled_worktree("content-large");
let large = std::fs::OpenOptions::new().write(true).open(worktree.join("README.md")).expect("open");
large.set_len(crate::worktree_clean_content::CONTENT_LIMIT + 1).expect("sparse growth");
drop(large);
let reasons: String = refusal_reasons(worktree.as_path());
assert!(reasons.contains("README.md is larger than"), "{reasons}");
remove(parent.as_path());
}
#[test]
fn a_mode_or_type_change_is_not_settled_by_content() {
let (parent, _root, worktree) = settled_worktree("content-mode");
make_every_entry_racy(worktree.as_path());
std::fs::set_permissions(worktree.join("README.md"), std::os::unix::fs::PermissionsExt::from_mode(0o755)).expect("chmod");
let reasons: String = refusal_reasons(worktree.as_path());
assert!(reasons.contains("README.md changed its executable bit"), "{reasons}");
std::fs::set_permissions(worktree.join("README.md"), std::os::unix::fs::PermissionsExt::from_mode(0o644)).expect("chmod back");
std::fs::remove_file(worktree.join("link")).expect("remove link");
std::fs::write(worktree.join("link"), b"src/main.rs").expect("a file in place of the link");
let type_reasons: String = refusal_reasons(worktree.as_path());
assert!(type_reasons.contains("link changed its type"), "{type_reasons}");
remove(parent.as_path());
}