use super::tests::{answered_equal, declined, git_lines, read_here, spelled};
use crate::test_support::{fixture, git, remove, repository};
use std::path::{Path, PathBuf};
struct Pair {
main: PathBuf,
main_git: PathBuf,
linked: PathBuf,
linked_git: PathBuf,
}
impl Pair {
fn side(&self, linked: bool) -> (PathBuf, &Path) {
if linked {
return (self.linked.join("sub"), self.linked_git.as_path());
}
return (self.main.join("sub"), self.main_git.as_path());
}
}
fn pair(root: &Path, name: &str, version_one: bool, common_extra: &str) -> Pair {
let main = repository(root, name);
let linked = root.join(format!("{name}-linked"));
git(main.as_path(), &["worktree", "add", "--quiet", "-b", format!("side-{name}").as_str(), linked.to_str().expect("UTF-8"), "HEAD"]);
if version_one {
git(main.as_path(), &["config", "--local", "core.repositoryformatversion", "1"]);
}
let config = main.join(".git/config");
let mut text = std::fs::read_to_string(&config).expect("config");
text.push_str(common_extra);
std::fs::write(&config, text).expect("append to config");
std::fs::create_dir(main.join("sub")).expect("sub");
std::fs::create_dir(linked.join("sub")).expect("sub");
let linked_git = main.join(".git/worktrees").join(format!("{name}-linked"));
assert!(linked_git.join("commondir").exists(), "the linked worktree's Git directory");
return Pair { main_git: main.join(".git"), main, linked, linked_git };
}
const ENABLED: &str = "[extensions]\n\tworktreeConfig = true\n";
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum Expect {
Answer,
Decline,
}
fn check(root: &Path, start: &Path, label: &str, expect: Expect) {
match expect {
Expect::Answer => {
answered_equal(root, start, label);
}
Expect::Decline => declined(root, start, label),
}
}
fn check_file(root: &Path, pairs: &[&Pair], label: &str, text: &str, files: &[(&str, &str)], expect: Expect) {
for (number, pair) in pairs.iter().enumerate() {
for linked in [false, true] {
let (start, git_dir) = pair.side(linked);
std::fs::write(git_dir.join("config.worktree"), text).expect("config.worktree");
for (name, content) in files {
std::fs::write(git_dir.join(name), content).expect("included file");
}
check(root, start.as_path(), format!("{label} (pair {number}, linked {linked})").as_str(), expect);
std::fs::remove_file(git_dir.join("config.worktree")).expect("remove config.worktree");
}
}
}
#[test]
fn an_enabled_extension_without_a_file_is_read() {
let root = fixture("repo-read-wt-enabled");
for (number, version_one) in [false, true].into_iter().enumerate() {
for spelling in ["true", "yes", "on", "1", "TRUE"] {
let name = format!("e{number}-{spelling}");
let both = pair(root.as_path(), name.as_str(), version_one, format!("[extensions]\n\tworktreeConfig = {spelling}\n").as_str());
for linked in [false, true] {
let (start, _) = both.side(linked);
let layout = answered_equal(root.as_path(), start.as_path(), format!("{name} linked={linked}").as_str());
assert_eq!(layout.prefix, b"sub/".to_vec());
}
}
}
let bare_name = pair(root.as_path(), "bare-name", false, "[extensions]\n\tworktreeConfig\n");
answered_equal(root.as_path(), bare_name.side(false).0.as_path(), "bare name, main");
answered_equal(root.as_path(), bare_name.side(true).0.as_path(), "bare name, linked");
remove(root.as_path());
}
#[test]
fn a_file_is_ignored_when_the_extension_is_off() {
let root = fixture("repo-read-wt-off");
let ignored = "[core]\n\tbare = true\n\tworktree = /elsewhere\n\tfilemode = maybe\n";
for (number, extension) in ["", "[extensions]\n\tworktreeConfig = false\n", "[extensions]\n\tworktreeConfig = no\n"].into_iter().enumerate() {
let both = pair(root.as_path(), format!("off-{number}").as_str(), number > 0, extension);
check_file(root.as_path(), &[&both], format!("extension variant {number}").as_str(), ignored, &[], Expect::Answer);
}
remove(root.as_path());
}
#[test]
fn a_non_boolean_extension_value_is_declined() {
let root = fixture("repo-read-wt-nonbool");
for (number, value) in ["maybe", "2", ""].into_iter().enumerate() {
let both = pair(root.as_path(), format!("nb-{number}").as_str(), true, format!("[extensions]\n\tworktreeConfig = {value}\n").as_str());
for linked in [false, true] {
declined(root.as_path(), both.side(linked).0.as_path(), format!("value {value:?}, linked {linked}").as_str());
}
}
remove(root.as_path());
}
#[test]
fn values_only_the_worktree_file_sets_are_read() {
let root = fixture("repo-read-wt-values");
let v0 = pair(root.as_path(), "values-v0", false, ENABLED);
let v1 = pair(root.as_path(), "values-v1", true, ENABLED);
let text = "[user]\n\tname = Only Here\n[core]\n\tfilemode = true\n\thooksPath = /nowhere\n[alias]\n\tx = status\n";
check_file(root.as_path(), &[&v0, &v1], "worktree-only values", text, &[], Expect::Answer);
for linked in [false, true] {
let (start, git_dir) = v1.side(linked);
std::fs::write(git_dir.join("config.worktree"), text).expect("config.worktree");
let output = git(start.as_path(), &["config", "--get", "user.name"]);
assert_eq!(String::from_utf8(output.stdout).expect("UTF-8").trim(), "Only Here", "linked {linked}");
}
remove(root.as_path());
}
#[test]
fn typed_values_in_both_files_are_checked() {
let root = fixture("repo-read-wt-typed");
let both = pair(root.as_path(), "typed", true, ENABLED);
check_file(root.as_path(), &[&both], "good typed value", "[core]\n\tfilemode = false\n", &[], Expect::Answer);
check_file(root.as_path(), &[&both], "bad typed value", "[core]\n\tfilemode = maybe\n", &[], Expect::Decline);
for linked in [false, true] {
let (start, _) = both.side(linked);
assert!(git_lines(start.as_path()).is_some(), "the fixture is healthy without the bad file");
}
let conflict = pair(root.as_path(), "typed-conflict", true, format!("{ENABLED}[core]\n\tfilemode = maybe\n").as_str());
check_file(root.as_path(), &[&conflict], "bad common value overridden", "[core]\n\tfilemode = true\n", &[], Expect::Decline);
remove(root.as_path());
}
#[test]
fn bare_and_worktree_settings_follow_the_precedence_git_applies() {
let root = fixture("repo-read-wt-precedence");
let bare_common = pair(root.as_path(), "common-bare", true, format!("{ENABLED}[core]\n\tbare = true\n").as_str());
let false_common = pair(root.as_path(), "common-false", true, format!("{ENABLED}[core]\n\tbare = false\n").as_str());
let moved_common = pair(root.as_path(), "common-moved", true, format!("{ENABLED}[core]\n\tworktree = /elsewhere\n").as_str());
let plain = pair(root.as_path(), "plain", true, ENABLED);
check_file(root.as_path(), &[&bare_common], "common true, worktree false", "[core]\n\tbare = false\n", &[], Expect::Answer);
check_file(root.as_path(), &[&bare_common], "common true, worktree absent", "", &[], Expect::Decline);
check_file(root.as_path(), &[&bare_common], "common true, worktree true", "[core]\n\tbare = true\n", &[], Expect::Decline);
check_file(root.as_path(), &[&false_common], "common false, worktree true", "[core]\n\tbare = true\n", &[], Expect::Decline);
check_file(root.as_path(), &[&false_common], "common false, worktree false", "[core]\n\tbare = false\n", &[], Expect::Answer);
check_file(root.as_path(), &[&plain], "true then false", "[core]\n\tbare = true\n\tbare = false\n", &[], Expect::Answer);
check_file(root.as_path(), &[&plain], "false then true", "[core]\n\tbare = false\n\tbare = true\n", &[], Expect::Decline);
check_file(root.as_path(), &[&plain], "bad boolean overridden", "[core]\n\tbare = maybe\n\tbare = false\n", &[], Expect::Decline);
check_file(root.as_path(), &[&plain], "worktree file moves the worktree", "[core]\n\tworktree = /elsewhere\n", &[], Expect::Decline);
check_file(root.as_path(), &[&moved_common], "common moves the worktree", "[core]\n\tbare = false\n", &[], Expect::Decline);
check_file(root.as_path(), &[&plain], "bare and worktree together", "[core]\n\tbare = true\n\tworktree = /elsewhere\n", &[], Expect::Decline);
remove(root.as_path());
}
#[test]
fn the_last_core_bare_wins_without_the_extension_too() {
let root = fixture("repo-read-wt-last");
let both = pair(root.as_path(), "last", false, "[core]\n\tbare = true\n\tbare = false\n");
for linked in [false, true] {
let (start, _) = both.side(linked);
if let Some(layout) = read_here(root.as_path(), start.as_path()) {
assert_eq!(Some(spelled(&layout)), git_lines(start.as_path()), "linked {linked}");
}
}
answered_equal(root.as_path(), both.side(false).0.as_path(), "main, last entry false");
remove(root.as_path());
}
#[test]
fn adversarial_spellings_are_read_as_git_reads_them() {
let root = fixture("repo-read-wt-spelling");
let v0 = pair(root.as_path(), "spell-v0", false, ENABLED);
let v1 = pair(root.as_path(), "spell-v1", true, ENABLED);
let pairs: [&Pair; 2] = [&v0, &v1];
let cases: [(&str, &str, Expect); 18] = [
("key text inside a quoted value", "[user]\n\tname = \"core.bare = true\"\n", Expect::Answer),
("escaped quote then comment", "[user]\n\tname = a\\\"b # core.bare = true\n", Expect::Answer),
("keys only in comments", "# core.bare = true\n; core.worktree = x\n[user]\n\tname = x\n", Expect::Answer),
("false", "[core]\n\tbare = false\n", Expect::Answer),
("false with trailing comment", "[core]\n\tbare = false ; was true\n", Expect::Answer),
("false in split quotes", "[core]\n\tbare = \"fa\"\"lse\"\n", Expect::Answer),
("false in a quoted word", "[core]\n\tbare = \"false\"\n", Expect::Answer),
("true split by a continuation line", "[core]\n\tbare = tr\\\nue\n", Expect::Decline),
("upper-case section and key", "[CORE]\n\tBare = TRUE\n", Expect::Decline),
("bare name is true", "[core]\n\tbare\n", Expect::Decline),
("not a boolean", "[core]\n\tbare = maybe\n", Expect::Decline),
("a subsection is another key", "[core \"sub\"]\n\tbare = true\n", Expect::Answer),
("worktree path", "[core]\n\tworktree = /elsewhere\n", Expect::Decline),
("worktree path in quotes", "[core]\n\tworktree = \"/elsewhere\"\n", Expect::Decline),
("open quote", "[core]\n\tbare = \"false\n", Expect::Decline),
("section header broken", "[core\n\tbare = false\n", Expect::Decline),
("byte order mark", "\u{feff}[user]\n\tname = x\n", Expect::Answer),
("carriage returns", "[user]\r\n\tname = x\r\n", Expect::Answer),
];
for (label, text, expect) in cases {
check_file(root.as_path(), &pairs, label, text, &[], expect);
}
remove(root.as_path());
}
#[test]
fn includes_in_the_worktree_file_are_scanned_and_never_interpreted() {
let root = fixture("repo-read-wt-include");
let v1 = pair(root.as_path(), "include", true, ENABLED);
let pairs: [&Pair; 1] = [&v1];
let plain = ("inc-plain", "[user]\n\tname = Included\n");
let bare = ("inc-bare", "[core]\n\tbare = true\n");
let false_bare = ("inc-false", "[core]\n\tbare = false\n");
let moved = ("inc-moved", "[core]\n\tworktree = /elsewhere\n");
let nested = ("inc-nested", "[include]\n\tpath = inc-bare\n");
let bad_type = ("inc-typed", "[core]\n\tfilemode = maybe\n");
let cases: [(&str, &str, Vec<(&str, &str)>, Expect); 14] = [
("include without the keys", "[include]\n\tpath = inc-plain\n", vec![plain], Expect::Answer),
("include with core.bare", "[include]\n\tpath = inc-bare\n", vec![bare], Expect::Decline),
("include with core.bare = false", "[include]\n\tpath = inc-false\n", vec![false_bare], Expect::Decline),
("include with core.worktree", "[include]\n\tpath = inc-moved\n", vec![moved], Expect::Decline),
("nested include with core.bare", "[include]\n\tpath = inc-nested\n", vec![nested, bare], Expect::Decline),
("include with a bad typed value", "[include]\n\tpath = inc-typed\n", vec![bad_type], Expect::Decline),
("includeIf that cannot match, without keys", "[includeIf \"gitdir:/nowhere/\"]\n\tpath = inc-plain\n", vec![plain], Expect::Answer),
("includeIf that cannot match, with core.bare", "[includeIf \"gitdir:/nowhere/\"]\n\tpath = inc-bare\n", vec![bare], Expect::Decline),
("includeIf onbranch with core.bare", "[includeIf \"onbranch:nothing\"]\n\tpath = inc-bare\n", vec![bare], Expect::Decline),
("include of a missing file", "[include]\n\tpath = inc-missing\n", vec![], Expect::Answer),
("include in quotes", "[include]\n\tpath = \"inc-bare\"\n", vec![bare], Expect::Decline),
("include of itself", "[include]\n\tpath = config.worktree\n", vec![], Expect::Decline),
("include with a user path", "[include]\n\tpath = ~nobody/x\n", vec![], Expect::Decline),
("include without a value", "[include]\n\tpath\n", vec![], Expect::Decline),
];
for (label, text, files, expect) in cases {
check_file(root.as_path(), &pairs, label, text, files.as_slice(), expect);
}
std::fs::write(v1.main_git.join("only-common"), "[core]\n\tbare = true\n").expect("only common");
let (linked_start, linked_git) = v1.side(true);
std::fs::write(linked_git.join("config.worktree"), "[include]\n\tpath = only-common\n").expect("config.worktree");
answered_equal(root.as_path(), linked_start.as_path(), "relative include resolves beside config.worktree");
remove(root.as_path());
}
#[test]
fn an_unreadable_worktree_file_is_declined_or_equal_git() {
let root = fixture("repo-read-wt-unreadable");
let both = pair(root.as_path(), "unreadable", true, ENABLED);
for linked in [false, true] {
let (start, git_dir) = both.side(linked);
let path = git_dir.join("config.worktree");
std::fs::create_dir(&path).expect("directory");
assert!(read_here(root.as_path(), start.as_path()).is_none(), "a directory, linked {linked}");
std::fs::remove_dir(&path).expect("remove directory");
std::os::unix::fs::symlink("nowhere", &path).expect("dangling link");
if let Some(layout) = read_here(root.as_path(), start.as_path()) {
assert_eq!(Some(spelled(&layout)), git_lines(start.as_path()), "dangling link, linked {linked}");
}
std::fs::remove_file(&path).expect("remove link");
}
remove(root.as_path());
}