use super::*;
use crate::test_support::{fixture, git, git_output, remove, repository};
use std::os::unix::ffi::OsStringExt;
pub(super) fn hermetic_scope(root: &Path) -> ScopeEnvironment {
return ScopeEnvironment {
home: Some(root.join("no-home")),
xdg: None,
no_system: true,
system: None,
system_default: None,
global: Some(PathBuf::from("/nonexistent-global-config")),
unresolvable: false,
};
}
pub(super) fn read_here(root: &Path, directory: &Path) -> Option<RepoLayout> {
return read_layout_with(directory, &[], &hermetic_scope(root));
}
pub(super) fn git_lines(directory: &Path) -> Option<Vec<Vec<u8>>> {
let output = git_output(
directory,
&["rev-parse", "--path-format=absolute", "--is-bare-repository", "--git-dir", "--git-common-dir", "--show-toplevel", "--show-prefix"],
);
if !output.status.success() {
return None;
}
let body: &[u8] = output.stdout.strip_suffix(b"\n").unwrap_or(output.stdout.as_slice());
return Some(body.split(|byte| { return *byte == b'\n'; }).map(<[u8]>::to_vec).collect());
}
pub(super) fn spelled(layout: &RepoLayout) -> Vec<Vec<u8>> {
return vec![
b"false".to_vec(),
layout.git_dir.as_os_str().as_encoded_bytes().to_vec(),
layout.common_dir.as_os_str().as_encoded_bytes().to_vec(),
layout.worktree_root.as_os_str().as_encoded_bytes().to_vec(),
layout.prefix.clone(),
];
}
pub(super) fn answered_equal(root: &Path, directory: &Path, label: &str) -> RepoLayout {
let Some(layout) = read_here(root, directory) else {
panic!("{label}: the reader declined a plain layout");
};
assert_eq!(Some(spelled(&layout)), git_lines(directory), "{label}");
return layout;
}
pub(super) fn declined(root: &Path, directory: &Path, label: &str) {
assert!(read_here(root, directory).is_none(), "{label}: the reader answered a layout it must leave to Git");
}
#[test]
fn a_main_worktree_is_read_from_every_depth() {
let root = fixture("repo-read-main");
let repo = repository(root.as_path(), "repo");
let nested = repo.join("a/b/c");
std::fs::create_dir_all(&nested).expect("nested");
let top = answered_equal(root.as_path(), repo.as_path(), "top");
assert!(top.prefix.is_empty(), "the top level has an empty prefix");
let deep = answered_equal(root.as_path(), nested.as_path(), "nested");
assert_eq!(deep.prefix, b"a/b/c/".to_vec());
assert_eq!(deep.git_dir, top.git_dir);
remove(root.as_path());
}
#[test]
fn a_sha256_repository_reports_its_format() {
let root = fixture("repo-read-sha256");
let repo = root.join("repo");
std::fs::create_dir(&repo).expect("dir");
git(repo.as_path(), &["init", "--quiet", "--object-format=sha256", "--initial-branch=main"]);
git(repo.as_path(), &["commit", "--quiet", "--allow-empty", "--message=initial"]);
let layout = answered_equal(root.as_path(), repo.as_path(), "sha256");
assert_eq!(layout.object_format, ObjectFormat::Sha256);
let named = String::from_utf8(git_output(repo.as_path(), &["rev-parse", "--show-object-format"]).stdout).expect("UTF-8");
assert_eq!(named.trim(), layout.object_format.name());
remove(root.as_path());
}
#[test]
fn a_linked_worktree_is_read_and_its_git_paths_are_left_to_git() {
let root = fixture("repo-read-linked");
let repo = repository(root.as_path(), "repo");
let linked = root.join("linked");
git(repo.as_path(), &["worktree", "add", "--quiet", "-b", "side", linked.to_str().expect("UTF-8"), "HEAD"]);
std::fs::create_dir(linked.join("sub")).expect("sub");
let layout = answered_equal(root.as_path(), linked.join("sub").as_path(), "linked");
assert_ne!(layout.git_dir, layout.common_dir);
assert!(git_path(&layout, "index").is_none());
remove(root.as_path());
}
#[test]
fn a_gitfile_repository_is_read() {
let root = fixture("repo-read-gitfile");
let repo = root.join("repo");
std::fs::create_dir(&repo).expect("dir");
let separate = root.join("separate.git");
git(repo.as_path(), &["init", "--quiet", "--initial-branch=main", &format!("--separate-git-dir={}", separate.display())]);
git(repo.as_path(), &["commit", "--quiet", "--allow-empty", "--message=initial"]);
let layout = answered_equal(root.as_path(), repo.as_path(), "gitfile");
assert_eq!(layout.git_dir, std::fs::canonicalize(&separate).expect("separate"));
remove(root.as_path());
}
fn with_carriage_return(parent: &Path, name: &str) -> PathBuf {
let mut bytes: Vec<u8> = name.as_bytes().to_vec();
bytes.push(b'\r');
return parent.join(std::ffi::OsString::from_vec(bytes));
}
#[test]
fn a_gitfile_ending_in_crlf_names_the_directory_without_the_carriage_return() {
let root = fixture("repo-read-gitfile-crlf");
let repo = root.join("repo");
let decoy_repo = root.join("decoy-repo");
std::fs::create_dir(&repo).expect("dir");
std::fs::create_dir(&decoy_repo).expect("dir");
let admin = root.join("admin");
let decoy = with_carriage_return(root.as_path(), "admin");
git(repo.as_path(), &["init", "--quiet", "--initial-branch=main", &format!("--separate-git-dir={}", admin.display())]);
git(decoy_repo.as_path(), &["init", "--quiet", "--initial-branch=main", &format!("--separate-git-dir={}", decoy.display())]);
git(repo.as_path(), &["commit", "--quiet", "--allow-empty", "--message=initial"]);
std::fs::write(repo.join(".git"), format!("gitdir: {}\r\n", admin.display())).expect("gitfile with CRLF");
let layout = answered_equal(root.as_path(), repo.as_path(), "gitfile with CRLF");
assert_eq!(layout.git_dir, std::fs::canonicalize(&admin).expect("admin"));
remove(root.as_path());
}
#[test]
fn a_commondir_ending_in_crlf_names_the_directory_without_the_carriage_return() {
let root = fixture("repo-read-commondir-crlf");
let repo = repository(root.as_path(), "repo");
let linked = root.join("linked");
git(repo.as_path(), &["worktree", "add", "--quiet", "-b", "side", linked.to_str().expect("UTF-8"), "HEAD"]);
let common = root.join("common");
let decoy = with_carriage_return(root.as_path(), "common");
for target in [&common, &decoy] {
let copied = std::process::Command::new("cp").arg("--recursive").arg(repo.join(".git")).arg(target).status().expect("cp starts");
assert!(copied.success(), "the Git directory is copied to {}", target.display());
std::fs::remove_dir_all(target.join("worktrees")).expect("the copy has no worktrees");
}
std::fs::write(repo.join(".git/worktrees/linked/commondir"), format!("{}\r\n", common.display())).expect("commondir with CRLF");
let layout = answered_equal(root.as_path(), linked.as_path(), "commondir with CRLF");
assert_eq!(layout.common_dir, std::fs::canonicalize(&common).expect("common"));
remove(root.as_path());
}
#[test]
fn awkward_paths_are_read_as_git_reads_them() {
let root = fixture("repo-read-paths");
let repo = repository(root.as_path(), "repo");
let raw_name = std::ffi::OsString::from_vec(vec![b'r', 0xff, 0xfe, b'x']);
let raw = repo.join(raw_name);
std::fs::create_dir(&raw).expect("raw name");
let mut long = repo.clone();
for _ in 0..8 {
long.push("d".repeat(200));
}
std::fs::create_dir_all(&long).expect("long path");
let link = root.join("link");
std::os::unix::fs::symlink(&raw, &link).expect("link");
answered_equal(root.as_path(), raw.as_path(), "non-UTF-8");
answered_equal(root.as_path(), long.as_path(), "long");
let through_link = answered_equal(root.as_path(), link.as_path(), "symlinked start");
assert!(through_link.prefix.starts_with(b"r"), "the prefix is the real path below the top level");
remove(root.as_path());
}
#[test]
fn layouts_that_need_git_are_declined() {
let root = fixture("repo-read-declined");
let repo = repository(root.as_path(), "repo");
let bare = root.join("bare.git");
git(root.as_path(), &["init", "--quiet", "--bare", bare.to_str().expect("UTF-8")]);
declined(root.as_path(), bare.as_path(), "bare");
declined(root.as_path(), repo.join(".git").as_path(), "inside .git");
declined(root.as_path(), repo.join(".git/refs").as_path(), "inside .git/refs");
let outside = root.join("outside");
std::fs::create_dir(&outside).expect("outside");
declined(root.as_path(), outside.as_path(), "outside any repository");
let reftable = root.join("reftable");
std::fs::create_dir(&reftable).expect("dir");
git(reftable.as_path(), &["init", "--quiet", "--ref-format=reftable"]);
declined(root.as_path(), reftable.as_path(), "reftable");
let variable = |name: &str, value: &str| -> (std::ffi::OsString, std::ffi::OsString) {
return (std::ffi::OsString::from(name), std::ffi::OsString::from(value));
};
let scope = hermetic_scope(root.as_path());
let moved = [variable("GIT_DIR", "/elsewhere")];
assert!(read_layout_with(repo.as_path(), &moved, &scope).is_none(), "GIT_DIR in the environment");
let plain = [variable("GIT_AUTHOR_NAME", "x"), variable("PATH", "/usr/bin")];
assert!(read_layout_with(repo.as_path(), &plain, &scope).is_some(), "plain names must not decline");
let injected = [variable("GIT_CONFIG_COUNT", "1"), variable("GIT_CONFIG_KEY_0", "core.hooksPath"), variable("GIT_CONFIG_VALUE_0", "/dev/null")];
assert!(read_layout_with(repo.as_path(), &injected, &scope).is_some(), "an injected hooks path is plain");
let injected_bare = [variable("GIT_CONFIG_COUNT", "1"), variable("GIT_CONFIG_KEY_0", "Core.Bare"), variable("GIT_CONFIG_VALUE_0", "true")];
assert!(read_layout_with(repo.as_path(), &injected_bare, &scope).is_none(), "an injected core.bare");
let missing_key = [variable("GIT_CONFIG_COUNT", "1")];
assert!(read_layout_with(repo.as_path(), &missing_key, &scope).is_none(), "a count with no key");
for (label, key, value) in [
("core.bare", "core.bare", "true"),
("core.worktree", "core.worktree", "/elsewhere"),
] {
let case = repository(root.as_path(), format!("case-{label}").as_str());
git(case.as_path(), &["config", "--local", "core.repositoryformatversion", "1"]);
git(case.as_path(), &["config", "--local", key, value]);
declined(root.as_path(), case.as_path(), label);
}
let unknown = repository(root.as_path(), "unknown-extension");
git(unknown.as_path(), &["config", "--local", "core.repositoryformatversion", "1"]);
git(unknown.as_path(), &["config", "--local", "extensions.somethingnew", "1"]);
declined(root.as_path(), unknown.as_path(), "unknown extension");
let linked_dot_git = repository(root.as_path(), "linked-dot-git");
std::fs::rename(linked_dot_git.join(".git"), root.join("moved.git")).expect("move");
std::os::unix::fs::symlink(root.join("moved.git"), linked_dot_git.join(".git")).expect("link");
declined(root.as_path(), linked_dot_git.as_path(), ".git is a link");
remove(root.as_path());
}
#[test]
fn plain_entries_at_exactly_the_limit_are_read_and_one_more_declines() {
let entries = |count: u64| -> Vec<(std::ffi::OsString, std::ffi::OsString)> {
let mut variables: Vec<(std::ffi::OsString, std::ffi::OsString)> =
vec![(std::ffi::OsString::from("GIT_CONFIG_COUNT"), std::ffi::OsString::from(count.to_string()))];
for index in 0..count {
variables.push((std::ffi::OsString::from(format!("GIT_CONFIG_KEY_{index}")), std::ffi::OsString::from("core.hooksPath")));
variables.push((std::ffi::OsString::from(format!("GIT_CONFIG_VALUE_{index}")), std::ffi::OsString::from("/dev/null")));
}
return variables;
};
let declines_nothing = |_: &str| -> bool { return false; };
assert!(env_config_avoids(entries(ENV_CONFIG_ENTRY_LIMIT).as_slice(), &declines_nothing), "the limit itself is read");
assert!(!env_config_avoids(entries(ENV_CONFIG_ENTRY_LIMIT + 1).as_slice(), &declines_nothing), "one entry more declines");
}
#[test]
fn the_packages_own_variables_do_not_move_discovery() {
let root = fixture("repo-read-own-variables");
let repo = repository(root.as_path(), "repo");
let scope = hermetic_scope(root.as_path());
let named = |suffix: &str| -> [(std::ffi::OsString, std::ffi::OsString); 1] {
return [(std::ffi::OsString::from(format!("{WRAPPER_ENVIRONMENT_PREFIX}{suffix}")), std::ffi::OsString::from("tag"))];
};
for suffix in ["NATIVE_IMAGE_TAG", "E2E_BASE_TAG"] {
assert!(
read_layout_with(repo.as_path(), &named(suffix), &scope).is_some(),
"{WRAPPER_ENVIRONMENT_PREFIX}{suffix} is the package's own and Git never reads it"
);
}
let lookalike: [(std::ffi::OsString, std::ffi::OsString); 1] = [(
std::ffi::OsString::from(WRAPPER_ENVIRONMENT_PREFIX.trim_end_matches('_').to_owned() + "X"),
std::ffi::OsString::from("tag"),
)];
assert!(read_layout_with(repo.as_path(), &lookalike, &scope).is_none(), "a name outside the namespace can be Git's");
remove(root.as_path());
}
#[test]
fn scope_files_that_mention_discovery_keys_are_declined() {
let root = fixture("repo-read-scopes");
let repo = repository(root.as_path(), "repo");
let home = root.join("home");
std::fs::create_dir(&home).expect("home");
let mut scope = hermetic_scope(root.as_path());
scope.home = Some(home.clone());
scope.global = None;
assert!(read_layout_with(repo.as_path(), &[], &scope).is_some(), "an absent global file is an empty file");
std::fs::write(home.join(".gitconfig"), "[user]\n\tname = x\n[includeIf \"gitdir:/nowhere/\"]\n\tpath = extra\n").expect("global");
std::fs::write(home.join("extra"), "[core]\n\tbare = false\n").expect("included");
assert!(read_layout_with(repo.as_path(), &[], &scope).is_none(), "a conditional include that mentions core.bare");
std::fs::write(home.join("extra"), "[core]\n\teditor = x\n").expect("included");
let layout = read_layout_with(repo.as_path(), &[], &scope).expect("an include without the keys is plain");
assert_eq!(Some(spelled(&layout)), git_lines(repo.as_path()));
std::fs::write(home.join(".gitconfig"), "[core\n").expect("broken");
assert!(read_layout_with(repo.as_path(), &[], &scope).is_none(), "a file the reader cannot parse");
remove(root.as_path());
}
#[test]
fn git_paths_equal_rev_parse() {
let root = fixture("repo-read-gitpath");
let repo = repository(root.as_path(), "repo");
let layout = answered_equal(root.as_path(), repo.as_path(), "main");
for name in ["index", "objects", "HEAD", "MERGE_HEAD", "CHERRY_PICK_HEAD", "REVERT_HEAD", "cli-git-transactions", "packed-refs", "config"] {
let output = git_output(repo.as_path(), &["rev-parse", "--path-format=absolute", "--git-path", name]);
let expected: String = String::from_utf8(output.stdout).expect("UTF-8").trim_end().to_string();
assert_eq!(git_path(&layout, name).map(|path| { return path.display().to_string(); }), Some(expected), "{name}");
}
assert!(git_path(&layout, "hooks").is_none(), "hooks follows core.hooksPath and is left to Git");
std::os::unix::fs::symlink("nowhere", repo.join(".git/MERGE_HEAD")).expect("link");
assert!(git_path(&layout, "MERGE_HEAD").is_none());
remove(root.as_path());
}