Merge apply filter neutralization into staging guard

This commit is contained in:
Chris Bookholt
2026-07-01 16:09:25 -07:00
10 changed files with 1633 additions and 143 deletions
+353 -39
View File
@@ -13,7 +13,30 @@ use crate::git_config::parse_effective_config;
use crate::git_config::parse_effective_config_with_origins;
pub(crate) const DISABLED_HOOKS_PATH: &str = if cfg!(windows) { "NUL" } else { "/dev/null" };
pub(crate) const EXECUTABLE_FILTER_CONFIG_PATTERN: &str = r"^filter\..*\.(clean|smudge|process)$";
pub(crate) const EXECUTABLE_FILTER_CONFIG_PATTERN: &str =
r"^filter\..*\.(clean|smudge|process|required)$";
#[derive(Debug, Clone, PartialEq, Eq)]
enum FilterAttributeValue {
Driver(String),
AmbiguousSentinel(String),
}
pub(crate) struct GitFilterNeutralization {
git_config_args: Vec<String>,
_config_dir: Option<tempfile::TempDir>,
filter_config: BTreeMap<String, GitConfigEntry>,
}
impl GitFilterNeutralization {
pub(crate) fn git_config_args(&self) -> &[String] {
&self.git_config_args
}
pub(crate) fn filter_value(&self, driver: &str, name: &str) -> Option<&str> {
effective_filter_value(&self.filter_config, driver, name)
}
}
const ISOLATED_GIT_ENVIRONMENT: [&str; 11] = [
"GIT_DIR",
@@ -47,7 +70,7 @@ pub(crate) fn ensure_no_selected_executable_git_filters(
cwd: &Path,
paths: &[String],
git_config_args: &[String],
) -> io::Result<()> {
) -> io::Result<GitFilterNeutralization> {
ensure_no_selected_executable_git_filters_for(
git,
cwd,
@@ -62,7 +85,7 @@ pub(crate) fn ensure_no_selected_git_add_filters(
cwd: &Path,
paths: &[String],
git_config_args: &[String],
) -> io::Result<()> {
) -> io::Result<GitFilterNeutralization> {
ensure_no_selected_executable_git_filters_for(
git,
cwd,
@@ -78,27 +101,114 @@ fn ensure_no_selected_executable_git_filters_for(
paths: &[String],
git_config_args: &[String],
execution: FilterExecution,
) -> io::Result<()> {
) -> io::Result<GitFilterNeutralization> {
let entries = read_filter_config(git, cwd, git_config_args)?;
if !entries.values().any(|entry| !entry.value.is_empty()) {
return Ok(());
let neutralized_drivers = executable_filter_drivers_for(&entries, execution)?;
let target_drivers = match execution {
FilterExecution::AnyWorktreeOperation => neutralized_drivers.clone(),
FilterExecution::GitAdd => executable_filter_drivers(&entries)?,
};
if target_drivers.is_empty() {
return Ok(GitFilterNeutralization {
git_config_args: Vec::new(),
_config_dir: None,
filter_config: entries,
});
}
let paths = paths
.iter()
.map(|path| path.as_bytes().to_vec())
.collect::<Vec<_>>();
let attributes = read_filter_attributes(git, cwd, &paths, git_config_args)?;
if let Some((driver, path)) = selected_executable_filter_for(&entries, &attributes, execution)?
{
return Err(io::Error::new(
io::ErrorKind::Unsupported,
format!(
"refusing to run an internal Git worktree operation with executable filter {driver:?} selected for {}",
String::from_utf8_lossy(&path)
),
));
let attributes = read_filter_attributes(git, cwd, &paths, git_config_args, &target_drivers)?;
for (path, driver) in &attributes {
if !target_drivers.contains(driver) {
continue;
}
let refused = neutralized_drivers.contains(driver)
|| matches!(execution, FilterExecution::GitAdd)
&& git_filter_required(git, cwd, &entries, driver)?;
if refused {
return Err(io::Error::new(
io::ErrorKind::Unsupported,
format!(
"refusing to run an internal Git worktree operation with executable filter {driver:?} selected for {}",
String::from_utf8_lossy(path)
),
));
}
}
if neutralized_drivers.is_empty() {
return Ok(GitFilterNeutralization {
git_config_args: Vec::new(),
_config_dir: None,
filter_config: entries,
});
}
executable_filter_guard(git, cwd, entries, &neutralized_drivers)
}
fn executable_filter_guard(
git: &GitRunner,
cwd: &Path,
filter_config: BTreeMap<String, GitConfigEntry>,
executable_drivers: &BTreeSet<String>,
) -> io::Result<GitFilterNeutralization> {
let config_dir = tempfile::tempdir()?;
let config_path = config_dir.path().join("filter-neutralization.gitconfig");
std::fs::write(&config_path, [])?;
let guard = GitFilterNeutralization {
git_config_args: vec![
"-c".to_string(),
format!(
"include.path={}",
config_path
.to_str()
.ok_or_else(|| invalid_filter_output("non-UTF-8 filter guard path"))?
),
],
_config_dir: Some(config_dir),
filter_config,
};
for driver in executable_drivers {
debug_assert!(["clean", "smudge", "process"].into_iter().any(|name| {
guard
.filter_value(driver, name)
.is_some_and(|value| !value.is_empty())
}));
for command in ["clean", "smudge", "process"] {
guard.write_config_value(git, cwd, &config_path, driver, command, "")?;
}
guard.write_config_value(git, cwd, &config_path, driver, "required", "false")?;
}
Ok(guard)
}
impl GitFilterNeutralization {
fn write_config_value(
&self,
git: &GitRunner,
cwd: &Path,
config_path: &Path,
driver: &str,
name: &str,
value: &str,
) -> io::Result<()> {
let mut command = git.command();
command
.args(["config", "--file"])
.arg(config_path)
.args(["--add", &format!("filter.{driver}.{name}"), value])
.current_dir(cwd);
let output = git.output(command)?;
if !output.status.success() {
return Err(io::Error::other(format!(
"failed to write Git filter neutralization for {driver:?} (status {}): {}",
output.status,
String::from_utf8_lossy(&output.stderr).trim()
)));
}
Ok(())
}
Ok(())
}
#[derive(Clone, Copy)]
@@ -192,6 +302,7 @@ fn read_filter_attributes(
cwd: &Path,
paths: &[Vec<u8>],
git_config_args: &[String],
executable_drivers: &BTreeSet<String>,
) -> io::Result<BTreeMap<Vec<u8>, String>> {
if paths.is_empty() {
return Ok(BTreeMap::new());
@@ -224,7 +335,131 @@ fn read_filter_attributes(
String::from_utf8_lossy(&output.stderr).trim()
)));
}
parse_filter_attributes(&output.stdout, paths)
let attributes = parse_filter_attributes(&output.stdout, paths)?;
resolve_filter_attribute_sentinels(git, cwd, attributes, git_config_args, executable_drivers)
}
fn resolve_filter_attribute_sentinels(
git: &GitRunner,
cwd: &Path,
attributes: BTreeMap<Vec<u8>, FilterAttributeValue>,
git_config_args: &[String],
executable_drivers: &BTreeSet<String>,
) -> io::Result<BTreeMap<Vec<u8>, String>> {
let mut resolved = BTreeMap::new();
for (path, attribute) in attributes {
match attribute {
FilterAttributeValue::Driver(driver) => {
resolved.insert(path, driver);
}
FilterAttributeValue::AmbiguousSentinel(driver) => {
if executable_drivers.contains(&driver)
&& sentinel_spelling_selects_filter_driver(
git,
cwd,
&path,
&driver,
git_config_args,
)?
{
resolved.insert(path, driver);
}
}
}
}
Ok(resolved)
}
/// `git check-attr` serializes both its three special states and literal
/// driver names with the same `set`, `unset`, and `unspecified` strings. Ask
/// Git to resolve the ambiguity with every command for that driver overridden
/// to empty. A required literal driver fails while a special state succeeds.
/// Retrying with the driver optional distinguishes that expected failure from
/// an unrelated probe error. No filter process or shell is started.
fn sentinel_spelling_selects_filter_driver(
git: &GitRunner,
cwd: &Path,
path: &[u8],
driver: &str,
git_config_args: &[String],
) -> io::Result<bool> {
let required = run_sentinel_selection_probe(
git,
cwd,
path,
driver,
git_config_args,
/*required*/ true,
)?;
if required.status.success() {
return Ok(false);
}
let optional = run_sentinel_selection_probe(
git,
cwd,
path,
driver,
git_config_args,
/*required*/ false,
)?;
if optional.status.success() {
return Ok(true);
}
Err(io::Error::other(format!(
"git filter attribute selection probe failed with required status {} and optional status {}: {}",
required.status,
optional.status,
String::from_utf8_lossy(&optional.stderr).trim()
)))
}
fn run_sentinel_selection_probe(
git: &GitRunner,
cwd: &Path,
path: &[u8],
driver: &str,
git_config_args: &[String],
required: bool,
) -> io::Result<std::process::Output> {
let mut command = git.command();
command
.env("GIT_OPTIONAL_LOCKS", "0")
.args(git_config_args)
.args([
"-c",
&format!("core.hooksPath={DISABLED_HOOKS_PATH}"),
"-c",
"core.fsmonitor=false",
"-c",
&format!("filter.{driver}.required={required}"),
"-c",
&format!("filter.{driver}.clean="),
"-c",
&format!("filter.{driver}.smudge="),
"-c",
&format!("filter.{driver}.process="),
"hash-object",
"--stdin",
])
.arg("--path")
.arg(git_path_argument(path)?)
.current_dir(cwd)
.stdin(Stdio::null());
git.output(command)
}
#[cfg(unix)]
fn git_path_argument(path: &[u8]) -> io::Result<std::ffi::OsString> {
use std::os::unix::ffi::OsStringExt;
Ok(std::ffi::OsString::from_vec(path.to_vec()))
}
#[cfg(not(unix))]
fn git_path_argument(path: &[u8]) -> io::Result<std::ffi::OsString> {
let path = std::str::from_utf8(path)
.map_err(|_| invalid_filter_output("non-UTF-8 Git filter attribute path"))?;
Ok(path.into())
}
fn selected_executable_filter_for(
@@ -232,23 +467,20 @@ fn selected_executable_filter_for(
attributes: &BTreeMap<Vec<u8>, String>,
execution: FilterExecution,
) -> io::Result<Option<(String, Vec<u8>)>> {
let mut executable_drivers = BTreeSet::new();
for entry in entries.values() {
let (driver, command) = filter_driver_and_command(&entry.key)?;
let relevant = match execution {
FilterExecution::AnyWorktreeOperation => true,
FilterExecution::GitAdd => command != FilterCommand::Smudge,
};
if relevant && !entry.value.is_empty() {
executable_drivers.insert(driver);
}
}
let executable_drivers = executable_filter_drivers_for(entries, execution)?;
Ok(selected_filter(&executable_drivers, attributes))
}
fn selected_filter(
drivers: &BTreeSet<String>,
attributes: &BTreeMap<Vec<u8>, String>,
) -> Option<(String, Vec<u8>)> {
for (path, driver) in attributes {
if executable_drivers.contains(driver) {
return Ok(Some((driver.clone(), path.clone())));
if drivers.contains(driver) {
return Some((driver.clone(), path.clone()));
}
}
Ok(None)
None
}
#[cfg(test)]
@@ -259,7 +491,87 @@ fn selected_executable_filter(
selected_executable_filter_for(entries, attributes, FilterExecution::AnyWorktreeOperation)
}
#[cfg(test)]
fn executable_filter_drivers(
entries: &BTreeMap<String, GitConfigEntry>,
) -> io::Result<BTreeSet<String>> {
executable_filter_drivers_for(entries, FilterExecution::AnyWorktreeOperation)
}
fn executable_filter_drivers_for(
entries: &BTreeMap<String, GitConfigEntry>,
execution: FilterExecution,
) -> io::Result<BTreeSet<String>> {
let mut executable_drivers = BTreeSet::new();
for entry in entries.values() {
if entry.key.ends_with(".required") {
continue;
}
let (driver, command) = filter_driver_and_command(&entry.key)?;
let relevant = match execution {
FilterExecution::AnyWorktreeOperation => true,
FilterExecution::GitAdd => command != FilterCommand::Smudge,
};
if relevant && !entry.value.is_empty() {
executable_drivers.insert(driver);
}
}
Ok(executable_drivers)
}
fn effective_filter_value<'a>(
entries: &'a BTreeMap<String, GitConfigEntry>,
driver: &str,
name: &str,
) -> Option<&'a str> {
entries
.get(&format!("filter.{driver}.{name}"))
.map(|entry| entry.value.as_str())
}
fn git_filter_required(
git: &GitRunner,
cwd: &Path,
entries: &BTreeMap<String, GitConfigEntry>,
driver: &str,
) -> io::Result<bool> {
let Some(value) = effective_filter_value(entries, driver, "required") else {
return Ok(false);
};
let config = format!("codex.required={value}");
let mut command = git.command();
command
.env("GIT_OPTIONAL_LOCKS", "0")
.args([
"-c",
&config,
"config",
"--type=bool",
"--get",
"codex.required",
])
.current_dir(cwd);
let output = git.output(command)?;
if !output.status.success() {
return Err(io::Error::new(
io::ErrorKind::Unsupported,
format!(
"refusing selected Git filter {driver:?} with malformed required value {value:?}: {}",
String::from_utf8_lossy(&output.stderr).trim()
),
));
}
match String::from_utf8_lossy(&output.stdout).trim() {
"true" => Ok(true),
"false" => Ok(false),
normalized => Err(io::Error::new(
io::ErrorKind::Unsupported,
format!(
"refusing selected Git filter {driver:?} with unexpected normalized required value {normalized:?}"
),
)),
}
}
fn filter_driver_name(key: &str) -> io::Result<String> {
filter_driver_and_command(key).map(|(driver, _command)| driver)
}
@@ -279,7 +591,6 @@ fn filter_driver_and_command(key: &str) -> io::Result<(String, FilterCommand)> {
.strip_suffix(suffix)
.map(|driver| (driver, command))
})
.filter(|(driver, _command)| !driver.is_empty())
.ok_or_else(|| invalid_filter_output("malformed filter config key"))?;
Ok((driver.to_string(), command))
}
@@ -301,7 +612,7 @@ fn write_nul_paths(input: &mut std::fs::File, paths: &[Vec<u8>]) -> io::Result<(
fn parse_filter_attributes(
output: &[u8],
expected_paths: &[Vec<u8>],
) -> io::Result<BTreeMap<Vec<u8>, String>> {
) -> io::Result<BTreeMap<Vec<u8>, FilterAttributeValue>> {
let expected = expected_paths
.iter()
.map(Vec::as_slice)
@@ -329,10 +640,13 @@ fn parse_filter_attributes(
}
let driver = std::str::from_utf8(record[2])
.map_err(|_| invalid_filter_output("non-UTF-8 Git filter attribute value"))?;
if attributes
.insert(record[0].to_vec(), driver.to_string())
.is_some()
{
let value = match driver {
"set" | "unset" | "unspecified" => {
FilterAttributeValue::AmbiguousSentinel(driver.to_string())
}
_ => FilterAttributeValue::Driver(driver.to_string()),
};
if attributes.insert(record[0].to_vec(), value).is_some() {
return Err(invalid_filter_output(
"duplicate Git filter attribute record",
));