fix(git): authorize primary config sources

This commit is contained in:
Chris Bookholt
2026-07-03 18:32:25 -07:00
parent 21ca43303b
commit 3e1688f2cb
14 changed files with 1190 additions and 26 deletions
+10 -4
View File
@@ -13,6 +13,7 @@ use std::path::PathBuf;
use crate::FsmonitorOverride;
use crate::apply_output::parse_git_apply_output;
use crate::git_command::GitRunner;
use crate::git_config_sources::ensure_no_worktree_primary_config_sources;
use crate::patch_paths::extract_effective_paths_from_patch;
use crate::patch_paths::stage_effective_paths;
use crate::safe_git::DISABLED_HOOKS_PATH;
@@ -47,17 +48,24 @@ pub struct ApplyGitResult {
pub fn apply_git_patch(req: &ApplyGitRequest) -> io::Result<ApplyGitResult> {
let git = GitRunner::for_cwd_io(&req.cwd)?;
let mut cfg_parts = configured_git_config_parts();
ensure_no_worktree_primary_config_sources(&git, &req.cwd)?;
let requested_cwd = std::fs::canonicalize(&req.cwd)?;
let git_root = resolve_git_root(&git, &req.cwd, &cfg_parts)?;
if git_root != requested_cwd {
ensure_no_worktree_primary_config_sources(&git, &git_root)?;
}
// Write unified diff into a temporary file
let (tmpdir, patch_path) = write_temp_patch(&req.diff)?;
// Keep tmpdir alive until function end to ensure the file exists
let _guard = tmpdir;
let patch_paths = extract_effective_paths_from_patch(&git, &patch_path, req.revert)?;
let patch_paths =
extract_effective_paths_from_patch(&git, &git_root, &patch_path, req.revert, &cfg_parts)?;
cfg_parts.extend(safe_git_config_parts());
if req.revert && !req.preflight {
// Stage WT paths first to avoid index mismatch on revert.
stage_effective_paths(&git, &git_root, &patch_paths)?;
stage_effective_paths(&git, &git_root, &patch_paths, &cfg_parts)?;
}
// Build git args
@@ -66,8 +74,6 @@ pub fn apply_git_patch(req: &ApplyGitRequest) -> io::Result<ApplyGitResult> {
args.push("-R".into());
}
cfg_parts.extend(safe_git_config_parts());
args.push(patch_path.to_string_lossy().to_string());
// Optional preflight: dry-run only; do not modify working tree
+18
View File
@@ -59,6 +59,11 @@ impl GitCommand {
self.inner.env(key, value);
self
}
pub(crate) fn env_remove(&mut self, key: impl AsRef<OsStr>) -> &mut Self {
self.inner.env_remove(key);
self
}
}
impl GitRunner {
@@ -102,6 +107,19 @@ impl GitRunner {
Ok(command)
}
pub(crate) fn ensure_config_source_is_not_worktree_controlled(
&self,
path: &Path,
description: &str,
) -> io::Result<()> {
self.authority
.ensure_config_source_is_not_worktree_controlled(path, description)
}
pub(crate) fn config_environment_value(&self, name: &str) -> Option<&OsStr> {
self.config_environment.value(name)
}
pub(crate) fn output(&self, mut command: GitCommand) -> io::Result<std::process::Output> {
self.revalidate_active_repository_metadata()?;
isolate_git_command_environment(&mut command.inner);
+109
View File
@@ -242,6 +242,16 @@ fn locations_for_root(root: &Path) -> RepositoryAuthority {
.expect("test repository authority")
}
fn config_authority_for_root(root: &Path) -> RepositoryAuthority {
let mut roots = vec![root.to_path_buf()];
let canonical = std::fs::canonicalize(root).expect("canonical root");
if !roots.contains(&canonical) {
roots.push(canonical);
}
RepositoryAuthority::from_test_locations(roots.clone(), roots, vec![root.join(".git")])
.expect("config repository authority")
}
fn raw_parent_traversal(root: &Path, sibling: &str) -> PathBuf {
let separator = std::path::MAIN_SEPARATOR.to_string();
let mut path = root.as_os_str().to_os_string();
@@ -381,6 +391,105 @@ fn git_metadata_marker_parser_preserves_leading_path_space() {
assert!(parse_git_marker_path(b"gitdir:/missing-space\n", b"gitdir: ").is_err());
}
#[test]
fn config_source_authority_requires_an_absolute_raw_path() {
let fixture = tempdir_for_native_git();
let root = fixture.path().join("repo");
std::fs::create_dir_all(&root).expect("create repository");
run_git(&root, &["init", "-q"]);
let authority = config_authority_for_root(&root);
let path = Path::new("relative/config");
let error = authority
.ensure_config_source_is_not_worktree_controlled(path, "test config")
.expect_err("relative config path");
assert_eq!(error.kind(), io::ErrorKind::PermissionDenied, "{error}");
assert!(error.to_string().contains("test config"), "{error}");
assert!(error.to_string().contains("relative/config"), "{error}");
}
#[test]
fn config_source_authority_preserves_and_rejects_a_worktree_crossing_spelling() {
let fixture = tempdir_for_native_git();
let root = fixture.path().join("repo");
let safe = fixture.path().join("safe");
std::fs::create_dir_all(root.join("nested")).expect("create repository descendant");
std::fs::create_dir_all(&safe).expect("create safe directory");
run_git(&root, &["init", "-q"]);
let authority = config_authority_for_root(&root);
let raw = root
.join("nested")
.join("..")
.join("..")
.join("safe/config");
let error = authority
.ensure_config_source_is_not_worktree_controlled(&raw, "crossing config")
.expect_err("worktree-crossing config path");
assert_eq!(error.kind(), io::ErrorKind::PermissionDenied, "{error}");
assert!(error.to_string().contains("crossing config"), "{error}");
assert!(
error.to_string().contains(&raw.display().to_string()),
"{error}"
);
}
#[test]
fn config_source_authority_allows_protected_metadata_and_unrelated_external_paths() {
let fixture = tempdir_for_native_git();
let root = fixture.path().join("repo");
let external = fixture.path().join("external/config");
std::fs::create_dir_all(&root).expect("create repository");
std::fs::create_dir_all(external.parent().expect("external parent"))
.expect("create external directory");
run_git(&root, &["init", "-q"]);
let authority = config_authority_for_root(&root);
authority
.ensure_config_source_is_not_worktree_controlled(
&root.join(".git/config"),
"protected metadata config",
)
.expect("protected metadata config");
let canonical_config =
std::fs::canonicalize(root.join(".git/config")).expect("canonical metadata config");
authority
.ensure_config_source_is_not_worktree_controlled(
&canonical_config,
"canonical protected metadata config",
)
.expect("canonical protected metadata config");
authority
.ensure_config_source_is_not_worktree_controlled(&external, "external config")
.expect("unrelated external config");
}
#[test]
fn config_source_authority_rejects_an_unregistered_related_repository_root() {
let fixture = tempdir_for_native_git();
let root = fixture.path().join("repo");
let alias = fixture.path().join("unregistered-related-root");
std::fs::create_dir_all(&root).expect("create repository");
std::fs::create_dir_all(&alias).expect("create related root");
run_git(&root, &["init", "-q"]);
std::fs::write(
alias.join(".git"),
format!("gitdir: {}\n", root.join(".git").display()),
)
.expect("write related metadata marker");
let authority = config_authority_for_root(&root);
let path = alias.join("config");
let error = authority
.ensure_config_source_is_not_worktree_controlled(&path, "related config")
.expect_err("unregistered related repository config");
assert_eq!(error.kind(), io::ErrorKind::PermissionDenied, "{error}");
assert!(error.to_string().contains("related config"), "{error}");
}
fn selected_git(locations: &RepositoryAuthority, directories: &[&Path]) -> PathBuf {
let search_path = std::env::join_paths(directories).expect("PATH");
select_git_executable(locations, &search_path)
+14 -1
View File
@@ -2,6 +2,19 @@ use std::path::Component;
use std::path::Path;
pub(crate) fn parse_git_boolean(value: &[u8]) -> Option<bool> {
parse_git_boolean_with_minimum(value, i128::from(i32::MIN))
}
/// Parse the Git boolean grammar while excluding numeric `INT_MIN`.
///
/// Older supported Git releases reject that one signed endpoint after unit
/// expansion, so environment gates that must succeed across versions use this
/// conservative variant. The accepted spellings otherwise share one parser.
pub(crate) fn parse_git_boolean_symmetric_i32(value: &[u8]) -> Option<bool> {
parse_git_boolean_with_minimum(value, -i128::from(i32::MAX))
}
fn parse_git_boolean_with_minimum(value: &[u8], minimum: i128) -> Option<bool> {
if value.eq_ignore_ascii_case(b"true")
|| value.eq_ignore_ascii_case(b"yes")
|| value.eq_ignore_ascii_case(b"on")
@@ -61,7 +74,7 @@ pub(crate) fn parse_git_boolean(value: &[u8]) -> Option<bool> {
let magnitude = i128::from_str_radix(digits, base).ok()?;
let signed = if negative { -magnitude } else { magnitude };
let value = signed.checked_mul(factor)?;
(i128::from(i32::MIN)..=i128::from(i32::MAX))
(minimum..=i128::from(i32::MAX))
.contains(&value)
.then_some(value != 0)
}
@@ -0,0 +1,59 @@
use std::io;
use std::path::Path;
use crate::git_command::GitRunner;
mod path_safety;
mod primary_sources;
use path_safety::normalize_absolute_path;
use path_safety::resolve_literal_path;
use primary_sources::is_disabled_primary_config_path;
use primary_sources::legacy_primary_config_source_candidates;
use primary_sources::selected_git_home_config_candidates;
use primary_sources::selected_git_prefix_system_candidate;
/// Reject primary Git configuration paths selected from worktree-controlled
/// routes before any ordinary Git command can open them.
///
/// This layer intentionally does not authorize includes. The complete include
/// graph is added by the next stack layer before this is promoted to a full
/// config-source capability.
pub(crate) fn ensure_no_worktree_primary_config_sources(
git: &GitRunner,
git_root: &Path,
) -> io::Result<()> {
let git_root = normalize_absolute_path(std::fs::canonicalize(git_root)?)?;
for (description, candidate) in legacy_primary_config_source_candidates(git)? {
if !is_disabled_primary_config_path(&candidate) {
reject_source(git, &git_root, &candidate, description)?;
}
}
for candidate in selected_git_home_config_candidates(git, &git_root)? {
reject_source(git, &git_root, &candidate, "selected Git HOME config")?;
}
if let Some(candidate) = selected_git_prefix_system_candidate(git, &git_root)? {
reject_source(
git,
&git_root,
&candidate,
"selected Git prefix system config",
)?;
}
Ok(())
}
fn reject_source(
git: &GitRunner,
cwd: &Path,
candidate: &Path,
description: &str,
) -> io::Result<()> {
let absolute = resolve_literal_path(candidate, cwd);
git.ensure_config_source_is_not_worktree_controlled(&absolute, description)
}
#[cfg(test)]
#[path = "git_config_sources_tests.rs"]
mod tests;
@@ -0,0 +1,38 @@
use std::io;
use std::path::Path;
use std::path::PathBuf;
use codex_utils_absolute_path::AbsolutePathBuf;
pub(super) const CONFIG_PATH_KEY: &str = "codex.config-source.path";
#[cfg(unix)]
pub(super) fn git_var_path_from_bytes(path: &[u8]) -> io::Result<PathBuf> {
use std::os::unix::ffi::OsStringExt;
Ok(PathBuf::from(std::ffi::OsString::from_vec(path.to_vec())))
}
#[cfg(not(unix))]
pub(super) fn git_var_path_from_bytes(path: &[u8]) -> io::Result<PathBuf> {
Ok(PathBuf::from(std::str::from_utf8(path).map_err(|_| {
invalid_config_source("non-UTF-8 Git config source path")
})?))
}
pub(super) fn resolve_literal_path(path: impl AsRef<Path>, base: &Path) -> PathBuf {
let path = path.as_ref();
if path.is_absolute() {
path.to_path_buf()
} else {
base.join(path)
}
}
pub(super) fn normalize_absolute_path(path: impl AsRef<Path>) -> io::Result<PathBuf> {
Ok(AbsolutePathBuf::from_absolute_path(path)?.into_path_buf())
}
pub(super) fn invalid_config_source(message: &str) -> io::Error {
io::Error::new(io::ErrorKind::InvalidData, message)
}
@@ -0,0 +1,205 @@
use std::ffi::OsStr;
use std::io;
use std::path::Path;
use std::path::PathBuf;
use super::path_safety::CONFIG_PATH_KEY;
use super::path_safety::git_var_path_from_bytes;
use super::path_safety::invalid_config_source;
use crate::git_command::GitRunner;
use crate::git_config::parse_git_boolean_symmetric_i32;
pub(super) fn selected_git_home_config_candidates(
git: &GitRunner,
cwd: &Path,
) -> io::Result<Vec<PathBuf>> {
if git.config_environment_value("GIT_CONFIG_GLOBAL").is_some() {
return Ok(Vec::new());
}
#[cfg(not(windows))]
if git.config_environment_value("HOME").is_none() {
return Ok(Vec::new());
}
let dot_gitconfig = selected_git_path_candidate(git, cwd, "~/.gitconfig")?;
let mut candidates = vec![dot_gitconfig.clone()];
if git
.config_environment_value("XDG_CONFIG_HOME")
.is_none_or(OsStr::is_empty)
{
let home = dot_gitconfig
.parent()
.ok_or_else(|| invalid_config_source("selected Git HOME has no parent"))?;
candidates.push(home.join(".config/git/config"));
}
Ok(candidates)
}
pub(super) fn selected_git_prefix_system_candidate(
git: &GitRunner,
cwd: &Path,
) -> io::Result<Option<PathBuf>> {
if git_env_bool(git, "GIT_CONFIG_NOSYSTEM")?
|| git.config_environment_value("GIT_CONFIG_SYSTEM").is_some()
{
return Ok(None);
}
selected_git_path_candidate(git, cwd, "%(prefix)/etc/gitconfig").map(Some)
}
fn selected_git_path_candidate(git: &GitRunner, cwd: &Path, raw: &str) -> io::Result<PathBuf> {
let tempdir = tempfile::tempdir()?;
let nonexistent_git_dir = tempdir.path().join("nonexistent-git-dir");
let disabled_config = if cfg!(windows) { "NUL" } else { "/dev/null" };
let mut command = git.command_for_cwd(cwd)?;
command
.env("GIT_CONFIG_GLOBAL", disabled_config)
.env("GIT_CONFIG_NOSYSTEM", "1")
.env("GIT_CONFIG_COUNT", "0")
.env_remove("GIT_CONFIG_PARAMETERS")
.arg("--git-dir")
.arg(&nonexistent_git_dir)
.arg("-c")
.arg(format!("{CONFIG_PATH_KEY}={raw}"))
.args([
"config",
"--null",
"--no-includes",
"--path",
"--get",
CONFIG_PATH_KEY,
]);
let output = git.output(command)?;
if !output.status.success() {
return Err(io::Error::other(format!(
"isolated selected Git path expansion failed with status {}: {}",
output.status,
String::from_utf8_lossy(&output.stderr).trim()
)));
}
let value = output
.stdout
.strip_suffix(&[0])
.ok_or_else(|| invalid_config_source("unterminated selected Git path"))?;
if value.is_empty() || value.contains(&0) {
return Err(invalid_config_source("ambiguous selected Git path"));
}
git_var_path_from_bytes(value)
}
pub(super) fn legacy_primary_config_source_candidates(
git: &GitRunner,
) -> io::Result<Vec<(&'static str, PathBuf)>> {
let mut candidates = Vec::new();
match git.config_environment_value("GIT_CONFIG_GLOBAL") {
Some(path) if !path.is_empty() => {
candidates.push(("GIT_CONFIG_GLOBAL", PathBuf::from(path)));
}
Some(_) => {}
None => {
let homes = git_home_directories(git);
match git.config_environment_value("XDG_CONFIG_HOME") {
Some(xdg) if !xdg.is_empty() => candidates.push((
"XDG_CONFIG_HOME Git config",
PathBuf::from(xdg).join("git/config"),
)),
_ => {
#[cfg(windows)]
if let Some(app_data) = git.config_environment_value("APPDATA")
&& !app_data.is_empty()
{
candidates.push((
"APPDATA Git config",
PathBuf::from(app_data).join("Git/config"),
));
}
for home in &homes {
candidates.push((
"HOME XDG Git config",
home_config_path(home, ".config/git/config"),
));
}
}
}
for home in &homes {
candidates.push(("HOME Git config", home_config_path(home, ".gitconfig")));
}
}
}
if !git_env_bool(git, "GIT_CONFIG_NOSYSTEM")? {
match git.config_environment_value("GIT_CONFIG_SYSTEM") {
Some(path) if !path.is_empty() => {
candidates.push(("GIT_CONFIG_SYSTEM", PathBuf::from(path)));
}
Some(_) => {}
None => {
#[cfg(windows)]
if let Some(program_data) = git.config_environment_value("PROGRAMDATA")
&& !program_data.is_empty()
{
candidates.push((
"PROGRAMDATA Git config",
PathBuf::from(program_data).join("Git/config"),
));
}
}
}
}
Ok(candidates)
}
fn git_home_directories(git: &GitRunner) -> Vec<std::ffi::OsString> {
if let Some(home) = git.config_environment_value("HOME") {
return vec![home.to_owned()];
}
#[cfg(windows)]
{
let mut homes = Vec::new();
if let (Some(drive), Some(path)) = (
git.config_environment_value("HOMEDRIVE"),
git.config_environment_value("HOMEPATH"),
) {
let mut home = drive.to_owned();
home.push(path);
homes.push(home);
}
if let Some(profile) = git.config_environment_value("USERPROFILE")
&& !homes.iter().any(|home| home == profile)
{
homes.push(profile.to_owned());
}
homes
}
#[cfg(not(windows))]
Vec::new()
}
fn home_config_path(home: &std::ffi::OsStr, suffix: &str) -> PathBuf {
if home.is_empty() {
PathBuf::from(std::path::MAIN_SEPARATOR.to_string()).join(suffix)
} else {
PathBuf::from(home).join(suffix)
}
}
fn git_env_bool(git: &GitRunner, name: &str) -> io::Result<bool> {
let Some(value) = git.config_environment_value(name) else {
return Ok(false);
};
let value = value
.to_str()
.ok_or_else(|| invalid_config_source("non-UTF-8 Git boolean environment value"))?;
parse_git_boolean_symmetric_i32(value.as_bytes())
.ok_or_else(|| invalid_config_source("invalid Git boolean environment value"))
}
#[cfg(windows)]
pub(super) fn is_disabled_primary_config_path(path: &Path) -> bool {
path.as_os_str()
.to_str()
.is_some_and(|path| path.eq_ignore_ascii_case("NUL"))
}
#[cfg(not(windows))]
pub(super) fn is_disabled_primary_config_path(_path: &Path) -> bool {
false
}
@@ -0,0 +1,611 @@
use super::*;
use pretty_assertions::assert_eq;
use std::ffi::OsStr;
use std::path::PathBuf;
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_primary_config_sources(&git, root)
}
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 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");
}
#[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(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_uses_the_shared_parser_with_a_cross_version_numeric_subset() {
const TEST_NAME: &str = "git_config_sources::tests::git_config_nosystem_uses_the_shared_parser_with_a_cross_version_numeric_subset";
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"),
("0x1", "ignored"),
("010", "ignored"),
("1k", "ignored"),
("-1g", "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"),
("08", "invalid"),
("2147483648", "invalid"),
("-2147483648", "invalid"),
("-0x80000000", "invalid"),
("-020000000000", "invalid"),
("-2097152k", "invalid"),
("-2048m", "invalid"),
("-2g", "invalid"),
(" -2G", "invalid"),
("-2147483649", "invalid"),
("2g", "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}"),
}
}
@@ -64,6 +64,41 @@ fn git_boolean_rejects_values_native_git_rejects() {
}
}
#[test]
fn symmetric_git_boolean_parser_excludes_only_int_min_spellings() {
for value in [
b"-2147483648".as_slice(),
b"-0x80000000".as_slice(),
b"-020000000000".as_slice(),
b"-2097152k".as_slice(),
b"-2048m".as_slice(),
b"-2g".as_slice(),
b" -2G".as_slice(),
] {
assert_eq!(parse_git_boolean(value), Some(true), "value {value:?}");
assert_eq!(
parse_git_boolean_symmetric_i32(value),
None,
"value {value:?}"
);
}
for value in [
b"0x1".as_slice(),
b"010".as_slice(),
b"1k".as_slice(),
b"-1g".as_slice(),
b" 1".as_slice(),
b"-2147483647".as_slice(),
] {
assert_eq!(
parse_git_boolean_symmetric_i32(value),
parse_git_boolean(value),
"value {value:?}"
);
}
}
#[test]
fn path_containment_uses_component_boundaries() {
let root = Path::new("/repo/root");
+1
View File
@@ -7,6 +7,7 @@ mod fsmonitor;
mod git_command;
mod git_config;
mod git_config_environment;
mod git_config_sources;
mod git_executable;
mod info;
mod local_only;
+41 -15
View File
@@ -9,16 +9,23 @@ use crate::apply::safe_git_config_parts;
use crate::apply::write_temp_patch;
use crate::git_command::GitRunner;
use crate::git_config::path_is_within;
use crate::git_config_sources::ensure_no_worktree_primary_config_sources;
/// Extract paths with Git from a cwd whose config sources have already been
/// authorized for `git_config_args`.
pub(crate) fn extract_effective_paths_from_patch(
git: &GitRunner,
authorized_cwd: &Path,
patch_path: &Path,
revert: bool,
git_config_args: &[String],
) -> io::Result<Vec<String>> {
let forward_paths = git_apply_numstat_paths(git, patch_path, revert)?;
let forward_paths =
git_apply_numstat_paths(git, authorized_cwd, patch_path, revert, git_config_args)?;
// `git apply --numstat` reports only the destination of a rename. Parse the
// opposite orientation too so both endpoints are included in the result.
let reverse_paths = git_apply_numstat_paths(git, patch_path, !revert)?;
let reverse_paths =
git_apply_numstat_paths(git, authorized_cwd, patch_path, !revert, git_config_args)?;
if forward_paths.len() != reverse_paths.len() {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
@@ -44,27 +51,38 @@ pub(crate) fn extract_effective_paths_from_patch(
/// Security-sensitive callers must use the fallible internal extractor so an
/// invalid or ambiguous patch is rejected instead of becoming an empty list.
pub fn extract_paths_from_patch(diff_text: &str) -> Vec<String> {
let Ok(cwd) = std::env::current_dir() else {
return Vec::new();
};
extract_paths_from_patch_from_cwd(diff_text, &cwd)
}
fn extract_paths_from_patch_from_cwd(diff_text: &str, cwd: &Path) -> Vec<String> {
let Ok((tmpdir, patch_path)) = write_temp_patch(diff_text) else {
return Vec::new();
};
let paths = std::env::current_dir()
.ok()
.and_then(|cwd| GitRunner::for_cwd(&cwd).ok())
.and_then(|git| {
extract_effective_paths_from_patch(&git, &patch_path, /*revert*/ false).ok()
})
.unwrap_or_default();
let paths = (|| -> io::Result<Vec<String>> {
let git = GitRunner::for_cwd_io(cwd)?;
let git_root = crate::get_git_repo_root(cwd)
.ok_or_else(|| io::Error::other("not a Git repository"))?;
let git_root = std::fs::canonicalize(git_root)?;
ensure_no_worktree_primary_config_sources(&git, &git_root)?;
extract_effective_paths_from_patch(&git, &git_root, &patch_path, /*revert*/ false, &[])
})()
.unwrap_or_default();
drop(tmpdir);
paths
}
fn git_apply_numstat_paths(
git: &GitRunner,
authorized_cwd: &Path,
patch_path: &Path,
revert: bool,
git_config_args: &[String],
) -> io::Result<Vec<String>> {
let command_cwd = patch_path.parent().unwrap_or_else(|| Path::new("."));
let mut cmd = git.command_for_cwd(command_cwd)?;
let mut cmd = git.command_for_cwd(authorized_cwd)?;
cmd.args(git_config_args);
cmd.args(["apply", "--numstat", "-z"]);
if revert {
cmd.arg("-R");
@@ -226,16 +244,25 @@ fn invalid_windows_patch_component(component: &str) -> bool {
/// Stage only the files that actually exist on disk for the given diff.
pub fn stage_paths(git_root: &Path, diff: &str) -> io::Result<()> {
let git = GitRunner::for_cwd_io(git_root)?;
let git_config_args = safe_git_config_parts();
ensure_no_worktree_primary_config_sources(&git, git_root)?;
let (tmpdir, patch_path) = write_temp_patch(diff)?;
let paths = extract_effective_paths_from_patch(&git, &patch_path, /*revert*/ true)?;
let paths = extract_effective_paths_from_patch(
&git,
git_root,
&patch_path,
/*revert*/ true,
&git_config_args,
)?;
let _guard = tmpdir;
stage_effective_paths(&git, git_root, &paths)
stage_effective_paths(&git, git_root, &paths, &git_config_args)
}
pub(crate) fn stage_effective_paths(
git: &GitRunner,
git_root: &Path,
paths: &[String],
git_config_args: &[String],
) -> io::Result<()> {
let confined = confine_patch_paths(git, git_root, paths)?;
let mut existing = Vec::new();
@@ -259,8 +286,7 @@ pub(crate) fn stage_effective_paths(
"--".to_string(),
];
args.extend(existing);
let config_parts = safe_git_config_parts();
let (_code, _, _) = run_git(git, git_root, &config_parts, &args)?;
let (_code, _, _) = run_git(git, git_root, git_config_args, &args)?;
// We do not hard fail staging; best-effort is OK. Return Ok even on non-zero.
Ok(())
}
+15 -5
View File
@@ -62,13 +62,23 @@ fn read_file_normalized(path: &Path) -> String {
fn effective_paths(diff: &str, revert: bool) -> io::Result<Vec<String>> {
let (tmpdir, patch_path) = write_temp_patch(diff)?;
let cwd = std::env::current_dir()?;
let git = GitRunner::for_cwd_io(&cwd)?;
let paths = extract_effective_paths_from_patch(&git, &patch_path, revert)?;
let repo = init_repo();
let cwd = repo.path();
let git = GitRunner::for_cwd_io(cwd)?;
let git_root =
crate::get_git_repo_root(cwd).ok_or_else(|| io::Error::other("not a Git repository"))?;
let git_root = std::fs::canonicalize(git_root)?;
ensure_no_worktree_primary_config_sources(&git, &git_root)?;
let paths = extract_effective_paths_from_patch(&git, &git_root, &patch_path, revert, &[])?;
drop(tmpdir);
Ok(paths)
}
fn best_effort_paths(diff: &str) -> Vec<String> {
let repo = init_repo();
extract_paths_from_patch_from_cwd(diff, repo.path())
}
#[test]
fn effective_paths_cover_supported_patch_headers() {
let cases = [
@@ -127,7 +137,7 @@ fn effective_paths_cover_supported_patch_headers() {
"{name}, revert={revert}"
);
}
assert_eq!(extract_paths_from_patch(diff), expected, "{name}");
assert_eq!(best_effort_paths(diff), expected, "{name}");
}
let nul_rename_paths = parse_numstat_paths(b"0\t0\t\0old name.txt\0new name.txt\0")
@@ -150,7 +160,7 @@ fn effective_paths_follow_git_for_mismatched_headers() {
effective_paths(mismatch, /*revert*/ true).unwrap(),
expected
);
assert_eq!(extract_paths_from_patch(mismatch), expected);
assert_eq!(best_effort_paths(mismatch), expected);
}
#[test]
+1 -1
View File
@@ -88,7 +88,7 @@ pub(crate) fn repository_route_boundaries(
let canonical_root = std::fs::canonicalize(root)?;
let canonical_marker = std::fs::canonicalize(&marker)?;
if canonical_marker == canonical_root.join(".git") {
push_unique_location(&mut metadata_dirs, canonical_marker)?;
push_unique_location(&mut metadata_dirs, marker)?;
}
}
for common in common_dirs {
@@ -33,6 +33,29 @@ impl RepositoryAuthority {
Ok(canonical)
}
pub(crate) fn ensure_config_source_is_not_worktree_controlled(
&self,
path: &Path,
description: &str,
) -> io::Result<()> {
if !path.is_absolute() {
return Err(worktree_controlled_config_source(path, description));
}
let inspection = self.route_boundaries.inspect_route(path)?;
if inspection.crosses_worktree {
return Err(worktree_controlled_config_source(path, description));
}
for observed in inspection.observed_paths {
if self.route_boundaries.contains_known_boundary(&observed)? {
continue;
}
if self.has_related_repository_ancestor(&observed)? {
return Err(worktree_controlled_config_source(path, description));
}
}
Ok(())
}
pub(super) fn path_is_untrusted_for_executable_result(&self, path: &Path) -> io::Result<bool> {
let inspection = self.route_boundaries.inspect_route(path)?;
if inspection.touches_worktree || inspection.crosses_metadata {
@@ -145,3 +168,13 @@ impl RepositoryAuthority {
Ok(())
}
}
fn worktree_controlled_config_source(path: &Path, description: &str) -> io::Error {
io::Error::new(
io::ErrorKind::PermissionDenied,
format!(
"refusing to use worktree-controlled {description}: {}",
path.display()
),
)
}