mirror of
https://github.com/openai/codex.git
synced 2026-09-08 15:50:34 +00:00
1589 lines
58 KiB
Rust
1589 lines
58 KiB
Rust
use super::*;
|
|
use pretty_assertions::assert_eq;
|
|
use std::ffi::OsStr;
|
|
|
|
fn run(cwd: &Path, args: &[&str]) -> (i32, String, String) {
|
|
let mut command = std::process::Command::new("git");
|
|
crate::safe_git::isolate_git_command_environment(&mut command);
|
|
let output = command
|
|
.args(args)
|
|
.current_dir(cwd)
|
|
.output()
|
|
.expect("run Git");
|
|
(
|
|
output.status.code().unwrap_or(-1),
|
|
String::from_utf8_lossy(&output.stdout).into_owned(),
|
|
String::from_utf8_lossy(&output.stderr).into_owned(),
|
|
)
|
|
}
|
|
|
|
fn run_success(cwd: &Path, args: &[&str]) -> String {
|
|
let (code, stdout, stderr) = run(cwd, args);
|
|
assert_eq!(code, 0, "git {args:?}: {stderr}");
|
|
stdout.trim().to_string()
|
|
}
|
|
|
|
fn init_repo() -> tempfile::TempDir {
|
|
let repo = tempfile::tempdir().expect("tempdir");
|
|
init_repo_at(repo.path());
|
|
repo
|
|
}
|
|
|
|
fn init_repo_at(root: &Path) {
|
|
std::fs::create_dir_all(root).expect("create repository directory");
|
|
run_success(root, &["init"]);
|
|
run_success(root, &["config", "user.email", "codex@example.com"]);
|
|
run_success(root, &["config", "user.name", "Codex"]);
|
|
std::fs::write(root.join("file.txt"), "orig\n").expect("write file");
|
|
run_success(root, &["add", "file.txt"]);
|
|
run_success(root, &["commit", "-m", "seed"]);
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
fn create_junction(path: &Path, target: &Path) {
|
|
// Bazel's GNU Windows runner can surface temporary paths with `/`
|
|
// separators. `mklink` treats those separators as option prefixes, so
|
|
// pass native separators to the cmd.exe built-in.
|
|
let path = path.as_os_str().to_string_lossy().replace('/', "\\");
|
|
let target = target.as_os_str().to_string_lossy().replace('/', "\\");
|
|
let output = std::process::Command::new("cmd.exe")
|
|
.args(["/D", "/C", "mklink", "/J"])
|
|
.arg(path)
|
|
.arg(target)
|
|
.output()
|
|
.expect("create junction");
|
|
assert!(
|
|
output.status.success(),
|
|
"mklink failed: stdout={} stderr={}",
|
|
String::from_utf8_lossy(&output.stdout),
|
|
String::from_utf8_lossy(&output.stderr)
|
|
);
|
|
}
|
|
|
|
fn guard(root: &Path) -> io::Result<()> {
|
|
let git = GitRunner::for_cwd_io(root)?;
|
|
ensure_no_worktree_config_sources(&git, root, &[])
|
|
}
|
|
|
|
fn add_include(root: &Path, key: &str, value: &str) {
|
|
run_success(root, &["config", "--add", key, value]);
|
|
}
|
|
|
|
fn assert_worktree_rejection(error: io::Error) {
|
|
assert_eq!(error.kind(), io::ErrorKind::PermissionDenied, "{error}");
|
|
assert!(error.to_string().contains("worktree-controlled"), "{error}");
|
|
}
|
|
|
|
fn run_isolated_source_test(test_name: &str, env: &[(&str, &OsStr)], removed: &[&str]) {
|
|
let mut command = std::process::Command::new(std::env::current_exe().expect("test binary"));
|
|
crate::safe_git::isolate_git_command_environment(&mut command);
|
|
command
|
|
.arg(test_name)
|
|
.arg("--exact")
|
|
.arg("--nocapture")
|
|
.env("CODEX_GIT_CONFIG_SOURCE_CHILD", "1")
|
|
.env("RUST_TEST_THREADS", "1");
|
|
for (name, value) in env {
|
|
command.env(name, value);
|
|
}
|
|
for name in removed {
|
|
command.env_remove(name);
|
|
}
|
|
let output = command.output().expect("run isolated source test");
|
|
assert!(
|
|
output.status.success(),
|
|
"isolated test {test_name} failed:\nstdout:\n{}\nstderr:\n{}",
|
|
String::from_utf8_lossy(&output.stdout),
|
|
String::from_utf8_lossy(&output.stderr)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn allows_protected_metadata_and_external_config_sources() {
|
|
let repo = init_repo();
|
|
let root = repo.path();
|
|
guard(root).expect("ordinary metadata config");
|
|
|
|
std::fs::write(
|
|
root.join(".git/protected.gitconfig"),
|
|
"[user]\nname = protected\n",
|
|
)
|
|
.expect("write protected config");
|
|
add_include(root, "include.path", "protected.gitconfig");
|
|
guard(root).expect("protected metadata include");
|
|
std::fs::create_dir(root.join(".git/subdir")).expect("create metadata subdirectory");
|
|
add_include(root, "include.path", "subdir/../protected.gitconfig");
|
|
guard(root).expect("metadata-local parent traversal");
|
|
|
|
let external = tempfile::tempdir().expect("external config directory");
|
|
let external_config = external.path().join("safe.gitconfig");
|
|
std::fs::write(&external_config, "[user]\nemail = safe@example.com\n")
|
|
.expect("write external config");
|
|
add_include(
|
|
root,
|
|
"include.path",
|
|
external_config.to_str().expect("UTF-8 external path"),
|
|
);
|
|
guard(root).expect("external include");
|
|
add_include(
|
|
root,
|
|
"include.path",
|
|
external
|
|
.path()
|
|
.join("missing.gitconfig")
|
|
.to_str()
|
|
.expect("UTF-8 missing external path"),
|
|
);
|
|
guard(root).expect("missing external include");
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_empty_primary_global_and_system_sources_in_the_worktree() {
|
|
const TEST_NAME: &str = "git_config_sources::tests::rejects_empty_primary_global_and_system_sources_in_the_worktree";
|
|
if std::env::var_os("CODEX_GIT_CONFIG_SOURCE_CHILD").is_none() {
|
|
let repo = init_repo();
|
|
let root = repo.path();
|
|
let unsafe_config = root.join("empty-primary.gitconfig");
|
|
std::fs::write(&unsafe_config, "").expect("write empty primary config");
|
|
let safe = tempfile::NamedTempFile::new().expect("safe external config");
|
|
run_isolated_source_test(
|
|
TEST_NAME,
|
|
&[
|
|
("CODEX_GIT_CONFIG_SOURCE_ROOT", root.as_os_str()),
|
|
("GIT_CONFIG_GLOBAL", unsafe_config.as_os_str()),
|
|
("GIT_CONFIG_SYSTEM", safe.path().as_os_str()),
|
|
],
|
|
&[],
|
|
);
|
|
run_isolated_source_test(
|
|
TEST_NAME,
|
|
&[
|
|
("CODEX_GIT_CONFIG_SOURCE_ROOT", root.as_os_str()),
|
|
("GIT_CONFIG_GLOBAL", safe.path().as_os_str()),
|
|
("GIT_CONFIG_SYSTEM", unsafe_config.as_os_str()),
|
|
],
|
|
&[],
|
|
);
|
|
return;
|
|
}
|
|
|
|
let root = PathBuf::from(
|
|
std::env::var_os("CODEX_GIT_CONFIG_SOURCE_ROOT").expect("fixture repository root"),
|
|
);
|
|
assert_worktree_rejection(guard(&root).expect_err("unsafe primary config source"));
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_missing_home_and_xdg_sources_in_the_worktree() {
|
|
const TEST_NAME: &str =
|
|
"git_config_sources::tests::rejects_missing_home_and_xdg_sources_in_the_worktree";
|
|
if std::env::var_os("CODEX_GIT_CONFIG_SOURCE_CHILD").is_none() {
|
|
let repo = init_repo();
|
|
let root = repo.path();
|
|
let xdg = root.join("xdg");
|
|
std::fs::create_dir(&xdg).expect("create XDG root");
|
|
let safe_system = tempfile::NamedTempFile::new().expect("safe system config");
|
|
run_isolated_source_test(
|
|
TEST_NAME,
|
|
&[
|
|
("CODEX_GIT_CONFIG_SOURCE_ROOT", root.as_os_str()),
|
|
("HOME", root.as_os_str()),
|
|
("XDG_CONFIG_HOME", xdg.as_os_str()),
|
|
("GIT_CONFIG_SYSTEM", safe_system.path().as_os_str()),
|
|
],
|
|
&["GIT_CONFIG_GLOBAL"],
|
|
);
|
|
return;
|
|
}
|
|
let root = PathBuf::from(
|
|
std::env::var_os("CODEX_GIT_CONFIG_SOURCE_ROOT").expect("fixture repository root"),
|
|
);
|
|
assert_worktree_rejection(guard(&root).expect_err("unsafe HOME/XDG source"));
|
|
}
|
|
|
|
#[test]
|
|
fn allows_explicitly_disabled_global_and_system_sources() {
|
|
const TEST_NAME: &str =
|
|
"git_config_sources::tests::allows_explicitly_disabled_global_and_system_sources";
|
|
if std::env::var_os("CODEX_GIT_CONFIG_SOURCE_CHILD").is_none() {
|
|
let repo = init_repo();
|
|
run_isolated_source_test(
|
|
TEST_NAME,
|
|
&[
|
|
("CODEX_GIT_CONFIG_SOURCE_ROOT", repo.path().as_os_str()),
|
|
("GIT_CONFIG_GLOBAL", OsStr::new("")),
|
|
("GIT_CONFIG_SYSTEM", OsStr::new("")),
|
|
],
|
|
&[],
|
|
);
|
|
return;
|
|
}
|
|
let root = PathBuf::from(
|
|
std::env::var_os("CODEX_GIT_CONFIG_SOURCE_ROOT").expect("fixture repository root"),
|
|
);
|
|
guard(&root).expect("disabled primary config sources");
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn allows_unset_home_when_git_has_no_global_source() {
|
|
const TEST_NAME: &str =
|
|
"git_config_sources::tests::allows_unset_home_when_git_has_no_global_source";
|
|
if std::env::var_os("CODEX_GIT_CONFIG_SOURCE_CHILD").is_none() {
|
|
let repo = init_repo();
|
|
run_isolated_source_test(
|
|
TEST_NAME,
|
|
&[
|
|
("CODEX_GIT_CONFIG_SOURCE_ROOT", repo.path().as_os_str()),
|
|
("GIT_CONFIG_NOSYSTEM", OsStr::new("1")),
|
|
],
|
|
&[
|
|
"HOME",
|
|
"XDG_CONFIG_HOME",
|
|
"GIT_CONFIG_GLOBAL",
|
|
"GIT_CONFIG_SYSTEM",
|
|
],
|
|
);
|
|
return;
|
|
}
|
|
let root = PathBuf::from(
|
|
std::env::var_os("CODEX_GIT_CONFIG_SOURCE_ROOT").expect("fixture repository root"),
|
|
);
|
|
guard(&root).expect("Git without HOME or global config");
|
|
}
|
|
|
|
#[test]
|
|
fn supports_pre_242_git_without_system_var_enumeration() {
|
|
assert_eq!(
|
|
primary_sources::parse_git_var_config_paths_result(
|
|
Some(129),
|
|
b"",
|
|
b"usage: git var (-l | <variable>)\n",
|
|
"GIT_CONFIG_SYSTEM",
|
|
)
|
|
.expect("pre-2.42 fallback"),
|
|
None
|
|
);
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn rejects_newline_bearing_raw_global_source_before_git_normalizes_it() {
|
|
const TEST_NAME: &str = "git_config_sources::tests::rejects_newline_bearing_raw_global_source_before_git_normalizes_it";
|
|
if std::env::var_os("CODEX_GIT_CONFIG_SOURCE_CHILD").is_none() {
|
|
let parent = tempfile::tempdir().expect("fixture parent");
|
|
let root = parent.path().join("repo");
|
|
init_repo_at(&root);
|
|
let newline_dir = parent.path().join("external\n");
|
|
std::fs::create_dir(&newline_dir).expect("create newline path component");
|
|
let unsafe_config = root.join("global.gitconfig");
|
|
std::fs::write(&unsafe_config, "").expect("write unsafe global config");
|
|
let raw = newline_dir.join("../repo/global.gitconfig");
|
|
run_isolated_source_test(
|
|
TEST_NAME,
|
|
&[
|
|
("CODEX_GIT_CONFIG_SOURCE_ROOT", root.as_os_str()),
|
|
("GIT_CONFIG_GLOBAL", raw.as_os_str()),
|
|
("GIT_CONFIG_NOSYSTEM", OsStr::new("1")),
|
|
],
|
|
&["GIT_CONFIG_SYSTEM"],
|
|
);
|
|
return;
|
|
}
|
|
|
|
let root = PathBuf::from(
|
|
std::env::var_os("CODEX_GIT_CONFIG_SOURCE_ROOT").expect("fixture repository root"),
|
|
);
|
|
assert_worktree_rejection(guard(&root).expect_err("newline raw global source"));
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn ignores_non_utf8_values_for_unrelated_global_keys() {
|
|
const TEST_NAME: &str =
|
|
"git_config_sources::tests::ignores_non_utf8_values_for_unrelated_global_keys";
|
|
if std::env::var_os("CODEX_GIT_CONFIG_SOURCE_CHILD").is_none() {
|
|
let repo = init_repo();
|
|
let global = tempfile::NamedTempFile::new().expect("external global config");
|
|
std::fs::write(global.path(), b"[user]\nname = \xff\n")
|
|
.expect("write non-UTF-8 unrelated config value");
|
|
run_isolated_source_test(
|
|
TEST_NAME,
|
|
&[
|
|
("CODEX_GIT_CONFIG_SOURCE_ROOT", repo.path().as_os_str()),
|
|
("GIT_CONFIG_GLOBAL", global.path().as_os_str()),
|
|
("GIT_CONFIG_NOSYSTEM", OsStr::new("1")),
|
|
],
|
|
&["GIT_CONFIG_SYSTEM"],
|
|
);
|
|
return;
|
|
}
|
|
|
|
let root = PathBuf::from(
|
|
std::env::var_os("CODEX_GIT_CONFIG_SOURCE_ROOT").expect("fixture repository root"),
|
|
);
|
|
guard(&root).expect("unrelated non-UTF-8 global value");
|
|
}
|
|
|
|
#[cfg(all(unix, not(target_os = "macos")))]
|
|
#[test]
|
|
fn preserves_non_utf8_unix_repository_and_config_origin_paths() {
|
|
use std::os::unix::ffi::OsStringExt;
|
|
|
|
const TEST_NAME: &str =
|
|
"git_config_sources::tests::preserves_non_utf8_unix_repository_and_config_origin_paths";
|
|
if std::env::var_os("CODEX_GIT_CONFIG_SOURCE_CHILD").is_none() {
|
|
let parent = tempfile::tempdir().expect("non-UTF-8 fixture parent");
|
|
let root = parent
|
|
.path()
|
|
.join(std::ffi::OsString::from_vec(b"repo-\xff".to_vec()));
|
|
init_repo_at(&root);
|
|
let safe = tempfile::NamedTempFile::new().expect("safe included config");
|
|
add_include(
|
|
&root,
|
|
"include.path",
|
|
safe.path().to_str().expect("UTF-8 safe include"),
|
|
);
|
|
let global = parent
|
|
.path()
|
|
.join(std::ffi::OsString::from_vec(b"global-\xff".to_vec()));
|
|
std::fs::write(
|
|
&global,
|
|
format!("[include]\npath = {}\n", safe.path().display()),
|
|
)
|
|
.expect("write non-UTF-8 global path");
|
|
run_isolated_source_test(
|
|
TEST_NAME,
|
|
&[
|
|
("CODEX_GIT_CONFIG_SOURCE_ROOT", root.as_os_str()),
|
|
("GIT_CONFIG_GLOBAL", global.as_os_str()),
|
|
("GIT_CONFIG_NOSYSTEM", OsStr::new("1")),
|
|
],
|
|
&["GIT_CONFIG_SYSTEM"],
|
|
);
|
|
return;
|
|
}
|
|
|
|
let root = PathBuf::from(
|
|
std::env::var_os("CODEX_GIT_CONFIG_SOURCE_ROOT").expect("fixture repository root"),
|
|
);
|
|
guard(&root).expect("non-UTF-8 repository and config origins");
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn rejects_worktree_fifo_primary_source_without_opening_it() {
|
|
const TEST_NAME: &str =
|
|
"git_config_sources::tests::rejects_worktree_fifo_primary_source_without_opening_it";
|
|
if std::env::var_os("CODEX_GIT_CONFIG_SOURCE_CHILD").is_none() {
|
|
let repo = init_repo();
|
|
let fifo = repo.path().join("global.fifo");
|
|
let status = std::process::Command::new("mkfifo")
|
|
.arg(&fifo)
|
|
.status()
|
|
.expect("run mkfifo");
|
|
assert!(status.success(), "mkfifo failed: {status}");
|
|
run_isolated_source_test(
|
|
TEST_NAME,
|
|
&[
|
|
("CODEX_GIT_CONFIG_SOURCE_ROOT", repo.path().as_os_str()),
|
|
("GIT_CONFIG_GLOBAL", fifo.as_os_str()),
|
|
("GIT_CONFIG_NOSYSTEM", OsStr::new("1")),
|
|
],
|
|
&["GIT_CONFIG_SYSTEM"],
|
|
);
|
|
return;
|
|
}
|
|
|
|
let root = PathBuf::from(
|
|
std::env::var_os("CODEX_GIT_CONFIG_SOURCE_ROOT").expect("fixture repository root"),
|
|
);
|
|
assert_worktree_rejection(guard(&root).expect_err("worktree FIFO config source"));
|
|
}
|
|
|
|
#[test]
|
|
fn git_config_nosystem_accepts_cross_version_boolean_values() {
|
|
const TEST_NAME: &str =
|
|
"git_config_sources::tests::git_config_nosystem_accepts_cross_version_boolean_values";
|
|
if std::env::var_os("CODEX_GIT_CONFIG_SOURCE_CHILD").is_none() {
|
|
let repo = init_repo();
|
|
let unsafe_system = repo.path().join("system.gitconfig");
|
|
std::fs::write(&unsafe_system, "").expect("write unsafe system config");
|
|
for (value, expected) in [
|
|
("1", "ignored"),
|
|
("2", "ignored"),
|
|
("-1", "ignored"),
|
|
("01", "ignored"),
|
|
("+1", "ignored"),
|
|
("2147483647", "ignored"),
|
|
("-2147483647", "ignored"),
|
|
("true", "ignored"),
|
|
("yes", "ignored"),
|
|
("on", "ignored"),
|
|
("", "rejected"),
|
|
("0", "rejected"),
|
|
("-0", "rejected"),
|
|
("false", "rejected"),
|
|
("no", "rejected"),
|
|
("off", "rejected"),
|
|
("not-a-bool", "invalid"),
|
|
("2147483648", "invalid"),
|
|
("-2147483648", "invalid"),
|
|
("-2147483649", "invalid"),
|
|
] {
|
|
run_isolated_source_test(
|
|
TEST_NAME,
|
|
&[
|
|
("CODEX_GIT_CONFIG_SOURCE_ROOT", repo.path().as_os_str()),
|
|
("CODEX_GIT_CONFIG_SOURCE_EXPECTED", OsStr::new(expected)),
|
|
("GIT_CONFIG_GLOBAL", OsStr::new("")),
|
|
("GIT_CONFIG_SYSTEM", unsafe_system.as_os_str()),
|
|
("GIT_CONFIG_NOSYSTEM", OsStr::new(value)),
|
|
],
|
|
&[],
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
let root = PathBuf::from(
|
|
std::env::var_os("CODEX_GIT_CONFIG_SOURCE_ROOT").expect("fixture repository root"),
|
|
);
|
|
match std::env::var("CODEX_GIT_CONFIG_SOURCE_EXPECTED")
|
|
.expect("expected child outcome")
|
|
.as_str()
|
|
{
|
|
"ignored" => guard(&root).expect("system source disabled"),
|
|
"rejected" => assert_worktree_rejection(guard(&root).expect_err("system source enabled")),
|
|
"invalid" => assert_eq!(
|
|
guard(&root).expect_err("invalid NOSYSTEM value").kind(),
|
|
io::ErrorKind::InvalidData
|
|
),
|
|
expected => panic!("unexpected child outcome {expected}"),
|
|
}
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
#[test]
|
|
fn rejects_both_windows_home_candidates_when_home_is_absent() {
|
|
const TEST_NAME: &str =
|
|
"git_config_sources::tests::rejects_both_windows_home_candidates_when_home_is_absent";
|
|
if std::env::var_os("CODEX_GIT_CONFIG_SOURCE_CHILD").is_none() {
|
|
let repo = init_repo();
|
|
let external_profile = tempfile::tempdir().expect("external USERPROFILE");
|
|
for home in [repo.path().to_path_buf(), repo.path().join("future-home")] {
|
|
let home = home.to_str().expect("UTF-8 Windows test path");
|
|
assert!(home.len() >= 3 && home.as_bytes()[1] == b':', "{home}");
|
|
run_isolated_source_test(
|
|
TEST_NAME,
|
|
&[
|
|
("CODEX_GIT_CONFIG_SOURCE_ROOT", repo.path().as_os_str()),
|
|
("GIT_CONFIG_NOSYSTEM", OsStr::new("1")),
|
|
("HOMEDRIVE", OsStr::new(&home[..2])),
|
|
("HOMEPATH", OsStr::new(&home[2..])),
|
|
("USERPROFILE", external_profile.path().as_os_str()),
|
|
],
|
|
&[
|
|
"HOME",
|
|
"XDG_CONFIG_HOME",
|
|
"GIT_CONFIG_GLOBAL",
|
|
"GIT_CONFIG_SYSTEM",
|
|
],
|
|
);
|
|
}
|
|
let external_home = external_profile
|
|
.path()
|
|
.to_str()
|
|
.expect("UTF-8 external Windows home");
|
|
assert!(
|
|
external_home.len() >= 3 && external_home.as_bytes()[1] == b':',
|
|
"{external_home}"
|
|
);
|
|
for profile in [
|
|
repo.path().to_path_buf(),
|
|
repo.path().join("future-profile"),
|
|
] {
|
|
run_isolated_source_test(
|
|
TEST_NAME,
|
|
&[
|
|
("CODEX_GIT_CONFIG_SOURCE_ROOT", repo.path().as_os_str()),
|
|
("GIT_CONFIG_NOSYSTEM", OsStr::new("1")),
|
|
("HOMEDRIVE", OsStr::new(&external_home[..2])),
|
|
("HOMEPATH", OsStr::new(&external_home[2..])),
|
|
("USERPROFILE", profile.as_os_str()),
|
|
],
|
|
&[
|
|
"HOME",
|
|
"XDG_CONFIG_HOME",
|
|
"GIT_CONFIG_GLOBAL",
|
|
"GIT_CONFIG_SYSTEM",
|
|
],
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
let root = PathBuf::from(
|
|
std::env::var_os("CODEX_GIT_CONFIG_SOURCE_ROOT").expect("fixture repository root"),
|
|
);
|
|
assert_worktree_rejection(guard(&root).expect_err("Windows synthesized HOME source"));
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
#[test]
|
|
fn allows_exact_windows_nul_for_both_primary_config_channels() {
|
|
const TEST_NAME: &str =
|
|
"git_config_sources::tests::allows_exact_windows_nul_for_both_primary_config_channels";
|
|
if std::env::var_os("CODEX_GIT_CONFIG_SOURCE_CHILD").is_none() {
|
|
let repo = init_repo();
|
|
run_isolated_source_test(
|
|
TEST_NAME,
|
|
&[
|
|
("CODEX_GIT_CONFIG_SOURCE_ROOT", repo.path().as_os_str()),
|
|
("GIT_CONFIG_GLOBAL", OsStr::new("NUL")),
|
|
("GIT_CONFIG_SYSTEM", OsStr::new("NUL")),
|
|
],
|
|
&["GIT_CONFIG_NOSYSTEM"],
|
|
);
|
|
return;
|
|
}
|
|
let root = PathBuf::from(
|
|
std::env::var_os("CODEX_GIT_CONFIG_SOURCE_ROOT").expect("fixture repository root"),
|
|
);
|
|
guard(&root).expect("exact NUL disables primary config channels");
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
#[test]
|
|
fn windows_appdata_global_config_matches_native_git_selection_and_precedence() {
|
|
const TEST_NAME: &str = "git_config_sources::tests::windows_appdata_global_config_matches_native_git_selection_and_precedence";
|
|
if std::env::var_os("CODEX_GIT_CONFIG_SOURCE_CHILD").is_none() {
|
|
for xdg in [None, Some(OsStr::new(""))] {
|
|
for existing in [true, false] {
|
|
let repo = init_repo();
|
|
let appdata = repo.path().join("appdata");
|
|
if existing {
|
|
let config = appdata.join("Git/config");
|
|
std::fs::create_dir_all(config.parent().expect("APPDATA config parent"))
|
|
.expect("create APPDATA config parent");
|
|
std::fs::write(&config, "[codex]\nappdata-marker = selected\n")
|
|
.expect("write APPDATA config");
|
|
}
|
|
let external_home = tempfile::tempdir().expect("external HOME");
|
|
let mut env = vec![
|
|
("CODEX_GIT_CONFIG_SOURCE_ROOT", repo.path().as_os_str()),
|
|
(
|
|
"CODEX_GIT_CONFIG_SOURCE_EXPECTED",
|
|
OsStr::new(if existing {
|
|
"reject-existing-appdata"
|
|
} else {
|
|
"reject-missing-appdata"
|
|
}),
|
|
),
|
|
("GIT_CONFIG_NOSYSTEM", OsStr::new("1")),
|
|
("HOME", external_home.path().as_os_str()),
|
|
("APPDATA", appdata.as_os_str()),
|
|
];
|
|
if let Some(xdg) = xdg {
|
|
env.push(("XDG_CONFIG_HOME", xdg));
|
|
}
|
|
let mut removed = vec![
|
|
"GIT_CONFIG_GLOBAL",
|
|
"GIT_CONFIG_SYSTEM",
|
|
"HOMEDRIVE",
|
|
"HOMEPATH",
|
|
"USERPROFILE",
|
|
];
|
|
if xdg.is_none() {
|
|
removed.push("XDG_CONFIG_HOME");
|
|
}
|
|
run_isolated_source_test(TEST_NAME, &env, &removed);
|
|
}
|
|
}
|
|
|
|
let repo = init_repo();
|
|
let appdata = repo.path().join("appdata");
|
|
let appdata_config = appdata.join("Git/config");
|
|
std::fs::create_dir_all(appdata_config.parent().expect("APPDATA config parent"))
|
|
.expect("create APPDATA config parent");
|
|
std::fs::write(&appdata_config, "[codex]\nappdata-marker = appdata\n")
|
|
.expect("write APPDATA config");
|
|
let external_home = tempfile::tempdir().expect("external HOME");
|
|
let external_xdg = tempfile::tempdir().expect("external XDG_CONFIG_HOME");
|
|
let xdg_config = external_xdg.path().join("git/config");
|
|
std::fs::create_dir_all(xdg_config.parent().expect("XDG config parent"))
|
|
.expect("create XDG config parent");
|
|
std::fs::write(&xdg_config, "[codex]\nappdata-marker = xdg\n").expect("write XDG config");
|
|
run_isolated_source_test(
|
|
TEST_NAME,
|
|
&[
|
|
("CODEX_GIT_CONFIG_SOURCE_ROOT", repo.path().as_os_str()),
|
|
("CODEX_GIT_CONFIG_SOURCE_EXPECTED", OsStr::new("allow-xdg")),
|
|
("GIT_CONFIG_NOSYSTEM", OsStr::new("1")),
|
|
("HOME", external_home.path().as_os_str()),
|
|
("APPDATA", appdata.as_os_str()),
|
|
("XDG_CONFIG_HOME", external_xdg.path().as_os_str()),
|
|
],
|
|
&[
|
|
"GIT_CONFIG_GLOBAL",
|
|
"GIT_CONFIG_SYSTEM",
|
|
"HOMEDRIVE",
|
|
"HOMEPATH",
|
|
"USERPROFILE",
|
|
],
|
|
);
|
|
|
|
let external_global = tempfile::NamedTempFile::new().expect("external global config");
|
|
std::fs::write(
|
|
external_global.path(),
|
|
"[codex]\nappdata-marker = explicit\n",
|
|
)
|
|
.expect("write explicit global config");
|
|
run_isolated_source_test(
|
|
TEST_NAME,
|
|
&[
|
|
("CODEX_GIT_CONFIG_SOURCE_ROOT", repo.path().as_os_str()),
|
|
(
|
|
"CODEX_GIT_CONFIG_SOURCE_EXPECTED",
|
|
OsStr::new("allow-explicit-global"),
|
|
),
|
|
("GIT_CONFIG_NOSYSTEM", OsStr::new("1")),
|
|
("HOME", repo.path().as_os_str()),
|
|
("APPDATA", appdata.as_os_str()),
|
|
("XDG_CONFIG_HOME", repo.path().as_os_str()),
|
|
("GIT_CONFIG_GLOBAL", external_global.path().as_os_str()),
|
|
],
|
|
&["GIT_CONFIG_SYSTEM", "HOMEDRIVE", "HOMEPATH", "USERPROFILE"],
|
|
);
|
|
return;
|
|
}
|
|
|
|
let root = PathBuf::from(
|
|
std::env::var_os("CODEX_GIT_CONFIG_SOURCE_ROOT").expect("fixture repository root"),
|
|
);
|
|
let expected =
|
|
std::env::var("CODEX_GIT_CONFIG_SOURCE_EXPECTED").expect("expected child outcome");
|
|
if expected != "reject-missing-appdata" {
|
|
let output = std::process::Command::new("git")
|
|
.args(["config", "--global", "--get", "codex.appdata-marker"])
|
|
.current_dir(&root)
|
|
.output()
|
|
.expect("run native Git global config query");
|
|
assert!(
|
|
output.status.success(),
|
|
"native Git global query failed: {}",
|
|
String::from_utf8_lossy(&output.stderr)
|
|
);
|
|
let actual = String::from_utf8_lossy(&output.stdout);
|
|
let native_expected = match expected.as_str() {
|
|
"reject-existing-appdata" => "selected",
|
|
"allow-xdg" => "xdg",
|
|
"allow-explicit-global" => "explicit",
|
|
expected => panic!("unexpected child outcome {expected}"),
|
|
};
|
|
assert_eq!(
|
|
actual.trim(),
|
|
native_expected,
|
|
"native Git global selection"
|
|
);
|
|
}
|
|
match expected.as_str() {
|
|
"reject-existing-appdata" | "reject-missing-appdata" => {
|
|
assert_worktree_rejection(guard(&root).expect_err("worktree APPDATA config"));
|
|
}
|
|
"allow-xdg" => guard(&root).expect("nonempty XDG suppresses APPDATA"),
|
|
"allow-explicit-global" => {
|
|
guard(&root).expect("explicit global config suppresses HOME, XDG, and APPDATA")
|
|
}
|
|
expected => panic!("unexpected child outcome {expected}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_empty_nonempty_absolute_missing_and_conditional_worktree_includes() {
|
|
for (name, body, include_key, include_value) in [
|
|
(
|
|
"empty",
|
|
"",
|
|
"include.path".to_string(),
|
|
"../driver-config".to_string(),
|
|
),
|
|
(
|
|
"nonempty",
|
|
"[user]\nname = worktree\n",
|
|
"include.path".to_string(),
|
|
"../driver-config".to_string(),
|
|
),
|
|
(
|
|
"missing",
|
|
"",
|
|
"include.path".to_string(),
|
|
"../future.gitconfig".to_string(),
|
|
),
|
|
(
|
|
"inactive includeIf",
|
|
"",
|
|
"includeIf.gitdir:/definitely/not/this/repository/**.path".to_string(),
|
|
"../driver-config".to_string(),
|
|
),
|
|
(
|
|
"case-insensitive gitdir includeIf",
|
|
"",
|
|
"includeIf.gitdir/i:/definitely/not/this/repository/**.path".to_string(),
|
|
"../driver-config".to_string(),
|
|
),
|
|
(
|
|
"onbranch includeIf",
|
|
"",
|
|
"includeIf.onbranch:definitely-never/**.path".to_string(),
|
|
"../driver-config".to_string(),
|
|
),
|
|
(
|
|
"hasconfig includeIf",
|
|
"",
|
|
"includeIf.hasconfig:remote.*.url:https://never.example/**.path".to_string(),
|
|
"../driver-config".to_string(),
|
|
),
|
|
] {
|
|
let repo = init_repo();
|
|
let root = repo.path();
|
|
if name != "missing" {
|
|
std::fs::write(root.join("driver-config"), body).expect("write config fixture");
|
|
}
|
|
add_include(root, &include_key, &include_value);
|
|
assert_worktree_rejection(guard(root).expect_err(name));
|
|
}
|
|
|
|
let repo = init_repo();
|
|
let root = repo.path();
|
|
let config = root.join("absolute.gitconfig");
|
|
std::fs::write(&config, "").expect("write absolute config");
|
|
add_include(
|
|
root,
|
|
"include.path",
|
|
config.to_str().expect("UTF-8 config path"),
|
|
);
|
|
assert_worktree_rejection(guard(root).expect_err("absolute include"));
|
|
|
|
let repo = init_repo();
|
|
let root = repo.path();
|
|
std::fs::write(root.join("driver-config"), "").expect("write conditional config");
|
|
let git_dir = run_success(root, &["rev-parse", "--absolute-git-dir"]).replace('\\', "/");
|
|
add_include(
|
|
root,
|
|
&format!("includeIf.gitdir:{git_dir}/.path"),
|
|
"../driver-config",
|
|
);
|
|
assert_worktree_rejection(guard(root).expect_err("active includeIf"));
|
|
}
|
|
|
|
#[test]
|
|
fn command_scoped_include_paths_follow_the_same_boundary() {
|
|
let repo = init_repo();
|
|
let root = repo.path();
|
|
let git = GitRunner::for_cwd_io(root).expect("Git runner");
|
|
let unsafe_config = root.join("command.gitconfig");
|
|
std::fs::write(&unsafe_config, "").expect("write worktree config");
|
|
let unsafe_args = vec![
|
|
"-c".to_string(),
|
|
format!("include.path={}", unsafe_config.display()),
|
|
];
|
|
assert_worktree_rejection(
|
|
ensure_no_worktree_config_sources(&git, root, &unsafe_args)
|
|
.expect_err("command-scoped worktree include"),
|
|
);
|
|
|
|
let external = tempfile::NamedTempFile::new().expect("safe external config");
|
|
let safe_args = vec![
|
|
"-c".to_string(),
|
|
format!("include.path={}", external.path().display()),
|
|
];
|
|
ensure_no_worktree_config_sources(&git, root, &safe_args)
|
|
.expect("command-scoped external include");
|
|
|
|
let relative_args = vec![
|
|
"-c".to_string(),
|
|
"include.path=relative.gitconfig".to_string(),
|
|
];
|
|
assert!(
|
|
ensure_no_worktree_config_sources(&git, root, &relative_args).is_err(),
|
|
"relative command includes must fail closed"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_every_duplicate_and_nested_include_target() {
|
|
for unsafe_first in [true, false] {
|
|
let repo = init_repo();
|
|
let root = repo.path();
|
|
std::fs::write(root.join("driver-config"), "").expect("write unsafe config");
|
|
let external = tempfile::NamedTempFile::new().expect("safe external config");
|
|
let safe = external.path().to_str().expect("UTF-8 safe path");
|
|
let values = if unsafe_first {
|
|
["../driver-config", safe]
|
|
} else {
|
|
[safe, "../driver-config"]
|
|
};
|
|
for value in values {
|
|
add_include(root, "include.path", value);
|
|
}
|
|
assert_worktree_rejection(guard(root).expect_err("duplicate include"));
|
|
}
|
|
|
|
let repo = init_repo();
|
|
let root = repo.path();
|
|
std::fs::write(root.join("driver-config"), "").expect("write nested target");
|
|
let external = tempfile::tempdir().expect("external config directory");
|
|
let outer = external.path().join("outer.gitconfig");
|
|
let nested = root.join("driver-config");
|
|
run_success(
|
|
root,
|
|
&[
|
|
"config",
|
|
"--file",
|
|
outer.to_str().expect("UTF-8 outer path"),
|
|
"include.path",
|
|
nested.to_str().expect("UTF-8 nested path"),
|
|
],
|
|
);
|
|
add_include(
|
|
root,
|
|
"include.path",
|
|
outer.to_str().expect("UTF-8 outer path"),
|
|
);
|
|
assert_worktree_rejection(guard(root).expect_err("nested include"));
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn rejects_symlink_aliases_across_the_worktree_and_metadata_boundaries() {
|
|
use std::os::unix::fs::symlink;
|
|
|
|
let repo = init_repo();
|
|
let root = repo.path();
|
|
let external = tempfile::tempdir().expect("external directory");
|
|
std::fs::write(external.path().join("safe.gitconfig"), "").expect("write safe config");
|
|
symlink(external.path(), root.join("inside-link")).expect("inside to outside link");
|
|
add_include(root, "include.path", "../inside-link/safe.gitconfig");
|
|
assert_worktree_rejection(guard(root).expect_err("worktree symlink alias"));
|
|
|
|
let repo = init_repo();
|
|
let root = repo.path();
|
|
std::fs::write(root.join("driver-config"), "").expect("write worktree target");
|
|
let external = tempfile::tempdir().expect("external directory");
|
|
symlink(root, external.path().join("outside-link")).expect("outside to inside link");
|
|
add_include(
|
|
root,
|
|
"include.path",
|
|
external
|
|
.path()
|
|
.join("outside-link/driver-config")
|
|
.to_str()
|
|
.expect("UTF-8 alias path"),
|
|
);
|
|
assert_worktree_rejection(guard(root).expect_err("external symlink alias"));
|
|
|
|
let repo = init_repo();
|
|
let root = repo.path();
|
|
std::fs::write(root.join("driver-config"), "").expect("write escaped target");
|
|
symlink(root, root.join(".git/config-link")).expect("metadata escape link");
|
|
add_include(root, "include.path", "config-link/driver-config");
|
|
assert_worktree_rejection(guard(root).expect_err("metadata symlink escape"));
|
|
|
|
let repo = init_repo();
|
|
let root = repo.path();
|
|
let external = tempfile::tempdir().expect("external directory");
|
|
symlink(
|
|
root.join("future-config"),
|
|
external.path().join("dangling-link"),
|
|
)
|
|
.expect("dangling link into worktree");
|
|
add_include(
|
|
root,
|
|
"include.path",
|
|
external
|
|
.path()
|
|
.join("dangling-link/config")
|
|
.to_str()
|
|
.expect("UTF-8 dangling alias"),
|
|
);
|
|
assert_worktree_rejection(guard(root).expect_err("dangling symlink alias"));
|
|
|
|
let repo = init_repo();
|
|
let root = repo.path();
|
|
let entry = tempfile::tempdir().expect("external entry directory");
|
|
let exit = tempfile::tempdir().expect("external exit directory");
|
|
std::fs::write(exit.path().join("safe.gitconfig"), "").expect("write exit config");
|
|
symlink(root, entry.path().join("through-worktree")).expect("link into worktree");
|
|
symlink(exit.path(), root.join("back-out")).expect("worktree-controlled exit link");
|
|
add_include(
|
|
root,
|
|
"include.path",
|
|
entry
|
|
.path()
|
|
.join("through-worktree/back-out/safe.gitconfig")
|
|
.to_str()
|
|
.expect("UTF-8 nested alias"),
|
|
);
|
|
assert_worktree_rejection(guard(root).expect_err("nested symlink traversal"));
|
|
|
|
let repo = init_repo();
|
|
let root = repo.path();
|
|
let entry = tempfile::tempdir().expect("external entry directory");
|
|
let exit = tempfile::tempdir().expect("external exit directory");
|
|
std::fs::write(exit.path().join("safe.gitconfig"), "").expect("write chained config");
|
|
symlink(exit.path(), root.join("workspace-link")).expect("workspace link out");
|
|
symlink(
|
|
root.join("workspace-link"),
|
|
entry.path().join("direct-chain"),
|
|
)
|
|
.expect("external link to workspace link");
|
|
add_include(
|
|
root,
|
|
"include.path",
|
|
entry
|
|
.path()
|
|
.join("direct-chain/safe.gitconfig")
|
|
.to_str()
|
|
.expect("UTF-8 direct chain"),
|
|
);
|
|
assert_worktree_rejection(guard(root).expect_err("direct symlink chain"));
|
|
|
|
let repo = init_repo();
|
|
let root = repo.path();
|
|
std::fs::create_dir(root.join("subdir")).expect("create symlink target");
|
|
std::fs::write(root.join("evil.gitconfig"), "[user]\nname = worktree\n")
|
|
.expect("write worktree config");
|
|
std::fs::write(
|
|
root.join(".git/evil.gitconfig"),
|
|
"[user]\nname = metadata\n",
|
|
)
|
|
.expect("write metadata decoy");
|
|
symlink(root.join("subdir"), root.join(".git/link")).expect("metadata link to worktree");
|
|
add_include(root, "include.path", "link/../evil.gitconfig");
|
|
assert_worktree_rejection(guard(root).expect_err("symlink parent traversal"));
|
|
|
|
let repo = init_repo();
|
|
let root = repo.path();
|
|
std::fs::create_dir(root.join("pivot")).expect("create worktree pivot");
|
|
std::fs::write(root.join(".git/protected.gitconfig"), "")
|
|
.expect("write protected metadata config");
|
|
add_include(
|
|
root,
|
|
"include.path",
|
|
root.join("pivot/../.git/protected.gitconfig")
|
|
.to_str()
|
|
.expect("UTF-8 pivot path"),
|
|
);
|
|
assert_worktree_rejection(guard(root).expect_err("raw path pivots through worktree"));
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn rejects_external_symlink_target_with_hidden_worktree_pivot_to_protected_config() {
|
|
use std::os::unix::fs::symlink;
|
|
|
|
let repo = init_repo();
|
|
let root = std::fs::canonicalize(repo.path()).expect("canonical repository");
|
|
let external = tempfile::tempdir().expect("external alias directory");
|
|
let protected = root.join(".git/protected.gitconfig");
|
|
std::fs::write(&protected, "[codex]\n\tpivot = protected\n").expect("write protected config");
|
|
std::fs::create_dir(root.join("pivot")).expect("create ordinary worktree pivot");
|
|
let entry = external.path().join("entry");
|
|
symlink(root.join("pivot/../.git/protected.gitconfig"), &entry)
|
|
.expect("external symlink target with raw worktree pivot");
|
|
add_include(
|
|
&root,
|
|
"include.path",
|
|
entry.to_str().expect("UTF-8 entry path"),
|
|
);
|
|
assert_eq!(
|
|
run_success(&root, &["config", "--includes", "--get", "codex.pivot"]),
|
|
"protected"
|
|
);
|
|
assert_worktree_rejection(
|
|
guard(&root).expect_err("hidden pivot inside external symlink target"),
|
|
);
|
|
}
|
|
|
|
#[cfg(target_os = "macos")]
|
|
#[test]
|
|
fn rejects_apfs_alias_symlink_target_with_hidden_worktree_pivot() {
|
|
use std::os::unix::fs::symlink;
|
|
|
|
let temp = std::fs::canonicalize(std::env::temp_dir()).expect("canonical temp directory");
|
|
let parent = tempfile::tempdir_in(temp).expect("Data-volume fixture");
|
|
let root = parent.path().join("repo");
|
|
init_repo_at(&root);
|
|
let root = std::fs::canonicalize(root).expect("canonical repository");
|
|
let data_alias = PathBuf::from("/System/Volumes/Data")
|
|
.join(root.strip_prefix("/").expect("absolute repository path"));
|
|
if std::fs::metadata(&data_alias).is_err() {
|
|
eprintln!("APFS Data alias unavailable; skipping native pivot assertion");
|
|
return;
|
|
}
|
|
let protected = root.join(".git/protected.gitconfig");
|
|
std::fs::write(&protected, "[codex]\n\tpivot = protected\n").expect("write protected config");
|
|
std::fs::create_dir(root.join("pivot")).expect("create ordinary worktree pivot");
|
|
let external = tempfile::tempdir().expect("external alias directory");
|
|
let entry = external.path().join("entry");
|
|
symlink(data_alias.join("pivot/../.git/protected.gitconfig"), &entry)
|
|
.expect("APFS alias target with raw worktree pivot");
|
|
add_include(
|
|
&root,
|
|
"include.path",
|
|
entry.to_str().expect("UTF-8 entry path"),
|
|
);
|
|
assert_eq!(
|
|
run_success(&root, &["config", "--includes", "--get", "codex.pivot"]),
|
|
"protected"
|
|
);
|
|
assert_worktree_rejection(guard(&root).expect_err("APFS hidden pivot inside symlink target"));
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn rejects_unregistered_same_common_worktree_used_only_as_alias_intermediate() {
|
|
use std::os::unix::fs::symlink;
|
|
|
|
let parent = tempfile::tempdir().expect("fixture");
|
|
let main = parent.path().join("main");
|
|
let linked = parent.path().join("linked");
|
|
let stale = parent.path().join("stale-unregistered");
|
|
let external = parent.path().join("external");
|
|
let safe = external.join("safe.gitconfig");
|
|
let entry = external.join("entry");
|
|
init_repo_at(&main);
|
|
run_success(
|
|
&main,
|
|
&[
|
|
"worktree",
|
|
"add",
|
|
"-b",
|
|
"same-common-active",
|
|
linked.to_str().expect("UTF-8 linked path"),
|
|
],
|
|
);
|
|
std::fs::create_dir_all(&stale).expect("create stale worktree");
|
|
std::fs::write(
|
|
stale.join(".git"),
|
|
format!("gitdir: {}\n", main.join(".git").display()),
|
|
)
|
|
.expect("write stale same-common marker");
|
|
std::fs::create_dir_all(&external).expect("create external directory");
|
|
std::fs::write(&safe, "[codex]\n\tsameCommon = protected\n")
|
|
.expect("write external safe config");
|
|
symlink(&safe, stale.join("switch")).expect("stale worktree controlled switch");
|
|
symlink(stale.join("switch"), &entry).expect("external entry through stale worktree");
|
|
add_include(
|
|
&linked,
|
|
"include.path",
|
|
entry.to_str().expect("UTF-8 entry path"),
|
|
);
|
|
assert_eq!(
|
|
run_success(
|
|
&linked,
|
|
&["config", "--includes", "--get", "codex.sameCommon"]
|
|
),
|
|
"protected"
|
|
);
|
|
assert!(
|
|
guard(&linked).is_err(),
|
|
"unregistered same-common route intermediate must fail closed"
|
|
);
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
#[test]
|
|
fn rejects_windows_junction_aliases_across_worktree_and_metadata_boundaries() {
|
|
let repo = init_repo();
|
|
let root = repo.path();
|
|
std::fs::write(root.join("driver-config"), "").expect("write worktree config");
|
|
let external = tempfile::tempdir().expect("external directory");
|
|
create_junction(&external.path().join("into-worktree"), root);
|
|
add_include(
|
|
root,
|
|
"include.path",
|
|
external
|
|
.path()
|
|
.join("into-worktree/driver-config")
|
|
.to_str()
|
|
.expect("UTF-8 junction path"),
|
|
);
|
|
assert_worktree_rejection(guard(root).expect_err("external junction into worktree"));
|
|
|
|
let repo = init_repo();
|
|
let root = repo.path();
|
|
let external = tempfile::tempdir().expect("external directory");
|
|
std::fs::write(external.path().join("safe.gitconfig"), "").expect("write external config");
|
|
create_junction(&root.join("out-of-worktree"), external.path());
|
|
add_include(root, "include.path", "../out-of-worktree/safe.gitconfig");
|
|
assert_worktree_rejection(guard(root).expect_err("worktree junction to external"));
|
|
|
|
let repo = init_repo();
|
|
let root = repo.path();
|
|
std::fs::write(root.join("driver-config"), "").expect("write worktree config");
|
|
create_junction(&root.join(".git/metadata-escape"), root);
|
|
add_include(root, "include.path", "metadata-escape/driver-config");
|
|
assert_worktree_rejection(guard(root).expect_err("metadata junction escape"));
|
|
|
|
let repo = init_repo();
|
|
let root = repo.path();
|
|
let entry = tempfile::tempdir().expect("external entry directory");
|
|
let exit = tempfile::tempdir().expect("external exit directory");
|
|
std::fs::write(exit.path().join("safe.gitconfig"), "").expect("write external config");
|
|
create_junction(&entry.path().join("through-worktree"), root);
|
|
create_junction(&root.join("back-out"), exit.path());
|
|
add_include(
|
|
root,
|
|
"include.path",
|
|
entry
|
|
.path()
|
|
.join("through-worktree/back-out/safe.gitconfig")
|
|
.to_str()
|
|
.expect("UTF-8 multi-hop junction path"),
|
|
);
|
|
assert_worktree_rejection(guard(root).expect_err("multi-hop junction traversal"));
|
|
|
|
let repo = init_repo();
|
|
let root = repo.path();
|
|
let external = tempfile::tempdir().expect("external directory");
|
|
create_junction(&external.path().join("into-worktree"), root);
|
|
add_include(
|
|
root,
|
|
"include.path",
|
|
external
|
|
.path()
|
|
.join("into-worktree/future.gitconfig")
|
|
.to_str()
|
|
.expect("UTF-8 missing junction target"),
|
|
);
|
|
assert_worktree_rejection(guard(root).expect_err("junction missing suffix"));
|
|
}
|
|
|
|
#[test]
|
|
fn linked_worktree_metadata_is_allowed_but_linked_worktree_config_is_rejected() {
|
|
let parent = tempfile::tempdir().expect("worktree parent");
|
|
let main = parent.path().join("main");
|
|
let linked = parent.path().join("linked");
|
|
init_repo_at(&main);
|
|
run_success(
|
|
&main,
|
|
&[
|
|
"worktree",
|
|
"add",
|
|
"-b",
|
|
"config-guard-linked",
|
|
linked.to_str().expect("UTF-8 linked path"),
|
|
],
|
|
);
|
|
|
|
let git_dir = PathBuf::from(run_success(&linked, &["rev-parse", "--absolute-git-dir"]));
|
|
let protected = git_dir.join("protected.gitconfig");
|
|
std::fs::write(&protected, "[user]\nname = linked\n").expect("write linked metadata config");
|
|
add_include(
|
|
&linked,
|
|
"include.path",
|
|
protected.to_str().expect("UTF-8 protected path"),
|
|
);
|
|
guard(&linked).expect("linked metadata include");
|
|
run_success(&linked, &["config", "--unset-all", "include.path"]);
|
|
|
|
std::fs::write(main.join("driver-config"), "").expect("write main-worktree config");
|
|
add_include(
|
|
&linked,
|
|
"include.path",
|
|
main.join("driver-config")
|
|
.to_str()
|
|
.expect("UTF-8 main-worktree config"),
|
|
);
|
|
assert_worktree_rejection(guard(&linked).expect_err("main worktree include"));
|
|
run_success(&linked, &["config", "--unset-all", "include.path"]);
|
|
add_include(
|
|
&linked,
|
|
"include.path",
|
|
main.join("future.gitconfig")
|
|
.to_str()
|
|
.expect("UTF-8 missing main-worktree config"),
|
|
);
|
|
assert_worktree_rejection(guard(&linked).expect_err("missing main worktree include"));
|
|
run_success(&linked, &["config", "--unset-all", "include.path"]);
|
|
|
|
let unsafe_config = linked.join("driver-config");
|
|
std::fs::write(&unsafe_config, "").expect("write linked worktree config");
|
|
add_include(
|
|
&linked,
|
|
"include.path",
|
|
unsafe_config.to_str().expect("UTF-8 unsafe path"),
|
|
);
|
|
assert_worktree_rejection(guard(&linked).expect_err("linked worktree include"));
|
|
}
|
|
|
|
#[test]
|
|
fn enclosing_repository_sibling_worktrees_are_always_untrusted() {
|
|
let parent = tempfile::tempdir().expect("fixture parent");
|
|
let outer = parent.path().join("outer");
|
|
let sibling = parent.path().join("sibling");
|
|
init_repo_at(&outer);
|
|
run_success(
|
|
&outer,
|
|
&[
|
|
"worktree",
|
|
"add",
|
|
"-b",
|
|
"config-guard-sibling",
|
|
sibling.to_str().expect("UTF-8 sibling worktree"),
|
|
],
|
|
);
|
|
let nested = outer.join("nested");
|
|
init_repo_at(&nested);
|
|
|
|
std::fs::write(sibling.join("driver-config"), "").expect("write sibling config");
|
|
add_include(
|
|
&nested,
|
|
"include.path",
|
|
sibling
|
|
.join("driver-config")
|
|
.to_str()
|
|
.expect("UTF-8 sibling config"),
|
|
);
|
|
assert_worktree_rejection(guard(&nested).expect_err("enclosing repo sibling config"));
|
|
|
|
run_success(&nested, &["config", "--unset-all", "include.path"]);
|
|
std::fs::remove_dir_all(&sibling).expect("remove sibling but retain registry entry");
|
|
add_include(
|
|
&nested,
|
|
"include.path",
|
|
sibling
|
|
.join("future.gitconfig")
|
|
.to_str()
|
|
.expect("UTF-8 missing sibling config"),
|
|
);
|
|
assert_worktree_rejection(guard(&nested).expect_err("missing enclosing repo sibling config"));
|
|
|
|
let nested_primary = outer.join("inner-primary");
|
|
let inner_linked = parent.path().join("inner-linked");
|
|
init_repo_at(&nested_primary);
|
|
run_success(
|
|
&nested_primary,
|
|
&[
|
|
"worktree",
|
|
"add",
|
|
"-b",
|
|
"inner-linked-from-outer",
|
|
inner_linked.to_str().expect("UTF-8 inner linked worktree"),
|
|
],
|
|
);
|
|
for candidate in [
|
|
outer.join("outer.txt"),
|
|
outer.join("future-outer.gitconfig"),
|
|
] {
|
|
if candidate.file_name() == Some(OsStr::new("outer.txt")) {
|
|
std::fs::write(&candidate, "").expect("write outer config source");
|
|
}
|
|
add_include(
|
|
&inner_linked,
|
|
"include.path",
|
|
candidate.to_str().expect("UTF-8 outer config source"),
|
|
);
|
|
assert_worktree_rejection(
|
|
guard(&inner_linked).expect_err("enclosing primary config source"),
|
|
);
|
|
run_success(&inner_linked, &["config", "--unset-all", "include.path"]);
|
|
}
|
|
add_include(
|
|
&inner_linked,
|
|
"include.path",
|
|
sibling
|
|
.join("future-from-inner.gitconfig")
|
|
.to_str()
|
|
.expect("UTF-8 enclosing sibling source"),
|
|
);
|
|
assert_worktree_rejection(
|
|
guard(&inner_linked).expect_err("enclosing sibling from linked inner worktree"),
|
|
);
|
|
run_success(&inner_linked, &["config", "--unset-all", "include.path"]);
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn preserves_symlinked_caller_ancestry_and_each_include_spelling() {
|
|
use std::os::unix::fs::symlink;
|
|
|
|
let parent = tempfile::tempdir().expect("fixture parent");
|
|
let outer = parent.path().join("outer");
|
|
let nested = parent.path().join("external-nested");
|
|
init_repo_at(&outer);
|
|
init_repo_at(&nested);
|
|
std::fs::write(outer.join("driver-config"), "").expect("write outer config");
|
|
symlink(&nested, outer.join("nested-link")).expect("link nested cwd through outer repo");
|
|
add_include(
|
|
&nested,
|
|
"include.path",
|
|
outer
|
|
.join("driver-config")
|
|
.to_str()
|
|
.expect("UTF-8 outer config"),
|
|
);
|
|
let git = GitRunner::for_cwd_io(&outer.join("nested-link"))
|
|
.expect("Git runner from symlinked nested cwd");
|
|
assert_worktree_rejection(
|
|
ensure_no_worktree_config_sources(&git, &nested, &[])
|
|
.expect_err("logical outer ancestry config"),
|
|
);
|
|
|
|
let repo = init_repo();
|
|
let root = repo.path();
|
|
std::fs::write(root.join("driver-config"), "").expect("write unsafe child config");
|
|
let external = tempfile::tempdir().expect("external include tree");
|
|
let shared = external.path().join("shared.gitconfig");
|
|
std::fs::write(&shared, "[include]\npath = child.gitconfig\n")
|
|
.expect("write shared parent config");
|
|
let unsafe_dir = external.path().join("unsafe-spelling");
|
|
let safe_dir = external.path().join("safe-spelling");
|
|
std::fs::create_dir(&unsafe_dir).expect("create unsafe spelling directory");
|
|
std::fs::create_dir(&safe_dir).expect("create safe spelling directory");
|
|
symlink(&shared, unsafe_dir.join("parent.gitconfig"))
|
|
.expect("unsafe spelling of shared parent");
|
|
symlink(&shared, safe_dir.join("parent.gitconfig")).expect("safe spelling of shared parent");
|
|
symlink(
|
|
root.join("driver-config"),
|
|
unsafe_dir.join("child.gitconfig"),
|
|
)
|
|
.expect("unsafe relative child");
|
|
std::fs::write(safe_dir.join("child.gitconfig"), "").expect("safe relative child");
|
|
add_include(
|
|
root,
|
|
"include.path",
|
|
unsafe_dir
|
|
.join("parent.gitconfig")
|
|
.to_str()
|
|
.expect("UTF-8 unsafe spelling"),
|
|
);
|
|
add_include(
|
|
root,
|
|
"include.path",
|
|
safe_dir
|
|
.join("parent.gitconfig")
|
|
.to_str()
|
|
.expect("UTF-8 safe spelling"),
|
|
);
|
|
assert_worktree_rejection(guard(root).expect_err("unsafe relative alias child"));
|
|
|
|
let parent = tempfile::tempdir().expect("linked-main dangling fixture");
|
|
let main = parent.path().join("main");
|
|
let linked = parent.path().join("linked");
|
|
init_repo_at(&main);
|
|
run_success(
|
|
&main,
|
|
&[
|
|
"worktree",
|
|
"add",
|
|
"-b",
|
|
"config-guard-dangling-main",
|
|
linked.to_str().expect("UTF-8 linked worktree"),
|
|
],
|
|
);
|
|
let external = tempfile::tempdir().expect("external dangling alias directory");
|
|
symlink(
|
|
main.join("future.gitconfig"),
|
|
external.path().join("dangling-main-config"),
|
|
)
|
|
.expect("dangling alias into main worktree");
|
|
add_include(
|
|
&linked,
|
|
"include.path",
|
|
external
|
|
.path()
|
|
.join("dangling-main-config")
|
|
.to_str()
|
|
.expect("UTF-8 dangling main alias"),
|
|
);
|
|
assert_worktree_rejection(guard(&linked).expect_err("dangling alias into main worktree"));
|
|
}
|
|
|
|
#[cfg(target_os = "macos")]
|
|
#[test]
|
|
fn rejects_apfs_data_firmlink_aliases_for_existing_and_missing_worktree_configs() {
|
|
let data_root = Path::new("/System/Volumes/Data");
|
|
if !data_root.is_dir() {
|
|
return;
|
|
}
|
|
let home = PathBuf::from(std::env::var_os("HOME").expect("HOME"));
|
|
let temp_parent = home.join(".cache/codex-git-utils-tests");
|
|
std::fs::create_dir_all(&temp_parent).expect("create test temp parent");
|
|
|
|
for exists in [true, false] {
|
|
let repo = tempfile::Builder::new()
|
|
.prefix("firmlink-config-")
|
|
.tempdir_in(&temp_parent)
|
|
.expect("home tempdir");
|
|
let root = repo.path();
|
|
init_repo_at(root);
|
|
if exists {
|
|
std::fs::write(root.join("driver-config"), "").expect("write aliased config");
|
|
}
|
|
let alias = data_root
|
|
.join(root.strip_prefix("/").expect("absolute repository path"))
|
|
.join("driver-config");
|
|
add_include(
|
|
root,
|
|
"include.path",
|
|
alias.to_str().expect("UTF-8 firmlink alias"),
|
|
);
|
|
assert_worktree_rejection(guard(root).expect_err("APFS firmlink alias"));
|
|
}
|
|
|
|
let family = tempfile::Builder::new()
|
|
.prefix("firmlink-linked-family-")
|
|
.tempdir_in(&temp_parent)
|
|
.expect("home linked-family tempdir");
|
|
let main = family.path().join("main");
|
|
let linked = family.path().join("linked");
|
|
init_repo_at(&main);
|
|
run_success(
|
|
&main,
|
|
&[
|
|
"worktree",
|
|
"add",
|
|
"-b",
|
|
"firmlink-linked-main",
|
|
linked.to_str().expect("UTF-8 linked path"),
|
|
],
|
|
);
|
|
for exists in [true, false] {
|
|
let name = if exists {
|
|
"driver-config"
|
|
} else {
|
|
"future.gitconfig"
|
|
};
|
|
if exists {
|
|
std::fs::write(main.join(name), "").expect("write main config");
|
|
}
|
|
let alias = data_root
|
|
.join(main.strip_prefix("/").expect("absolute main path"))
|
|
.join(name);
|
|
add_include(
|
|
&linked,
|
|
"include.path",
|
|
alias.to_str().expect("UTF-8 main firmlink alias"),
|
|
);
|
|
assert_worktree_rejection(guard(&linked).expect_err("main firmlink alias"));
|
|
run_success(&linked, &["config", "--unset-all", "include.path"]);
|
|
}
|
|
std::fs::write(main.join(".git/protected.gitconfig"), "")
|
|
.expect("write protected main metadata config");
|
|
let protected_alias = data_root
|
|
.join(main.strip_prefix("/").expect("absolute main path"))
|
|
.join(".git/protected.gitconfig");
|
|
add_include(
|
|
&linked,
|
|
"include.path",
|
|
protected_alias
|
|
.to_str()
|
|
.expect("UTF-8 protected metadata alias"),
|
|
);
|
|
guard(&linked).expect("protected metadata firmlink alias");
|
|
}
|
|
|
|
#[test]
|
|
fn include_path_expansion_supports_git_prefix() {
|
|
let repo = init_repo();
|
|
let root = repo.path();
|
|
let git = GitRunner::for_cwd_io(root).expect("Git runner");
|
|
let prefix =
|
|
expand_git_config_path(&git, root, "%(prefix)/etc/gitconfig").expect("expand Git prefix");
|
|
assert!(prefix.is_absolute());
|
|
}
|
|
|
|
#[cfg(not(windows))]
|
|
#[test]
|
|
fn include_path_expansion_preserves_colon_parentheses_as_literal_text() {
|
|
let repo = init_repo();
|
|
let root = repo.path();
|
|
let git = GitRunner::for_cwd_io(root).expect("Git runner");
|
|
let optional = GitConfigEntry {
|
|
scope: crate::git_config::GitConfigScope::Local,
|
|
origin: crate::git_config::GitConfigOrigin::File(".git/config".into()),
|
|
key: "include.path".to_string(),
|
|
value: ":(optional)../future.gitconfig".to_string(),
|
|
};
|
|
let optional_path = resolve_include_path(&git, root, &optional).expect("resolve optional path");
|
|
assert_eq!(
|
|
AbsolutePathBuf::resolve_path_against_base(optional_path, root).as_path(),
|
|
root.join(".git/:(optional)../future.gitconfig")
|
|
);
|
|
|
|
let unknown = GitConfigEntry {
|
|
value: ":(unknown)../future.gitconfig".to_string(),
|
|
..optional
|
|
};
|
|
let unknown_path =
|
|
resolve_include_path(&git, root, &unknown).expect("resolve literal unknown path");
|
|
assert_eq!(
|
|
AbsolutePathBuf::resolve_path_against_base(unknown_path, root).as_path(),
|
|
root.join(".git/:(unknown)../future.gitconfig")
|
|
);
|
|
|
|
for spelling in [":(optional)", ":(unknown)"] {
|
|
let external = tempfile::tempdir().expect("external literal config directory");
|
|
let literal_dir = external.path().join(spelling);
|
|
std::fs::create_dir(&literal_dir).expect("create literal include directory");
|
|
std::fs::write(literal_dir.join("child"), "[probe]\nvalue = literal\n")
|
|
.expect("write literal include child");
|
|
let parent = external.path().join("parent.gitconfig");
|
|
std::fs::write(&parent, format!("[include]\npath = {spelling}/child\n"))
|
|
.expect("write literal parent config");
|
|
let value = run_success(
|
|
root,
|
|
&[
|
|
"config",
|
|
"--file",
|
|
parent.to_str().expect("UTF-8 parent path"),
|
|
"--includes",
|
|
"--get",
|
|
"probe.value",
|
|
],
|
|
);
|
|
assert_eq!(value, "literal");
|
|
add_include(
|
|
root,
|
|
"include.path",
|
|
parent.to_str().expect("UTF-8 parent path"),
|
|
);
|
|
guard(root).expect("literal include path is external");
|
|
run_success(root, &["config", "--unset-all", "include.path"]);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn windows_path_validator_rejects_namespaces_streams_and_alias_components() {
|
|
for path in [
|
|
r"\??\C:\repo\config",
|
|
r"\\?\GLOBALROOT\Device\config",
|
|
r"\\.\pipe\config",
|
|
r"C:\repo\config:stream",
|
|
r"C:relative\config",
|
|
r":(optional)..\future.gitconfig",
|
|
r":(unknown)..\future.gitconfig",
|
|
r"C:\repo\NUL.gitconfig",
|
|
r"C:\repo\COM¹.gitconfig",
|
|
r"C:\repo\LPT³.gitconfig",
|
|
r"C:\repo\trailing.\config",
|
|
] {
|
|
assert!(windows_config_path_is_ambiguous(path), "{path:?}");
|
|
}
|
|
for path in [
|
|
r"C:\external\config",
|
|
r"\\server\share\config",
|
|
r"\\?\C:\repo\.git\config",
|
|
r"\\.\C:\repo\.git\config",
|
|
r"\\?\UNC\server\share\config",
|
|
r"\\.\UNC\server\share\config",
|
|
r"..\driver-config",
|
|
r".\driver-config",
|
|
] {
|
|
assert!(!windows_config_path_is_ambiguous(path), "{path:?}");
|
|
}
|
|
}
|