diff --git a/codex-rs/git-utils/src/info.rs b/codex-rs/git-utils/src/info.rs index 2041d510d9..1a24d9f9a6 100644 --- a/codex-rs/git-utils/src/info.rs +++ b/codex-rs/git-utils/src/info.rs @@ -21,7 +21,7 @@ use crate::GitSha; use crate::git_command::GitRunner; use crate::safe_git::DISABLED_HOOKS_PATH; use crate::safe_git::GIT_COMMAND_TIMEOUT; -use crate::safe_git::selected_executable_filter_from; +use crate::status_guard::selected_executable_filter_from; /// Return `true` if the project folder specified by the `Config` is inside a /// Git repository. diff --git a/codex-rs/git-utils/src/lib.rs b/codex-rs/git-utils/src/lib.rs index db384712f3..ff8c432da8 100644 --- a/codex-rs/git-utils/src/lib.rs +++ b/codex-rs/git-utils/src/lib.rs @@ -17,6 +17,7 @@ mod patch_paths; mod platform; mod reverse_staging; mod safe_git; +mod status_guard; pub use apply::ApplyGitRequest; pub use apply::ApplyGitResult; diff --git a/codex-rs/git-utils/src/safe_git.rs b/codex-rs/git-utils/src/safe_git.rs index 11a4a034f5..2da9d5e1b7 100644 --- a/codex-rs/git-utils/src/safe_git.rs +++ b/codex-rs/git-utils/src/safe_git.rs @@ -7,11 +7,8 @@ use std::path::Path; use std::path::PathBuf; use std::process::Command; use std::process::Stdio; -use tokio::process::Command as TokioCommand; use tokio::time::Duration; -use tokio::time::timeout; -use crate::GitReadError; use crate::git_command::GitRunner; use crate::git_config::GitConfigEntry; use crate::git_config::parse_effective_config; @@ -80,7 +77,7 @@ impl GitConfigOverrideFile { } #[derive(Debug, Clone, PartialEq, Eq)] -enum FilterAttributeValue { +pub(crate) enum FilterAttributeValue { Driver(String), AmbiguousSentinel(String), } @@ -131,85 +128,6 @@ pub(crate) fn isolate_tokio_git_command_environment(command: &mut tokio::process } } -pub(crate) async fn selected_executable_filter_from( - git: &GitRunner, - cwd: &Path, -) -> Result)>, GitReadError> { - let git_root = resolve_git_root_async(git, cwd).await?; - let entries = read_filter_config_async(git, &git_root).await?; - let executable_drivers = - executable_filter_drivers(&entries).map_err(|_| invalid_output("filterConfig"))?; - if executable_drivers.is_empty() { - return Ok(None); - } - let paths = read_paths_async(git, &git_root, PathSelection::Tracked).await?; - let attributes = read_filter_attributes_async(git, &git_root, &paths).await?; - let attributes = - resolve_filter_attribute_sentinels_async(git, &git_root, attributes, &executable_drivers) - .await?; - Ok(selected_filter(&executable_drivers, &attributes)) -} - -async fn resolve_git_root_async(git: &GitRunner, cwd: &Path) -> Result { - let requested_cwd = std::fs::canonicalize(cwd).map_err(|_| GitReadError::NotRepository { - path: cwd.to_path_buf(), - })?; - let expected_root = crate::get_git_repo_root(&requested_cwd) - .and_then(|root| std::fs::canonicalize(root).ok()) - .ok_or_else(|| GitReadError::NotRepository { - path: requested_cwd.clone(), - })?; - let mut command = git.tokio_command(); - command - .env("GIT_OPTIONAL_LOCKS", "0") - .args([ - "-c", - &format!("core.hooksPath={DISABLED_HOOKS_PATH}"), - "-c", - "core.fsmonitor=false", - "rev-parse", - "--show-toplevel", - ]) - .current_dir(&requested_cwd) - .kill_on_drop(true); - let output = command_output(git, command, "resolveGitRoot").await?; - if !output.status.success() { - return Err(command_failed("resolveGitRoot", output.status.code())); - } - let reported_root = git_root_from_stdout(output.stdout).and_then(|path| { - std::fs::canonicalize(path).map_err(|_| invalid_output("resolveGitRoot")) - })?; - if reported_root != expected_root { - return Err(GitReadError::RepositoryRootMismatch { - expected_root, - reported_root, - }); - } - Ok(reported_root) -} - -fn git_root_from_stdout(output: Vec) -> Result { - let output = output.strip_suffix(b"\n").unwrap_or(&output); - #[cfg(windows)] - let output = output.strip_suffix(b"\r").unwrap_or(output); - if output.is_empty() { - return Err(invalid_output("resolveGitRoot")); - } - #[cfg(unix)] - { - use std::ffi::OsString; - use std::os::unix::ffi::OsStringExt; - - Ok(PathBuf::from(OsString::from_vec(output.to_vec()))) - } - #[cfg(not(unix))] - { - String::from_utf8(output.to_vec()) - .map(PathBuf::from) - .map_err(|_| invalid_output("resolveGitRoot")) - } -} - pub(crate) fn ensure_no_selected_executable_git_filters( git: &GitRunner, cwd: &Path, @@ -409,240 +327,6 @@ fn run_effective_config_query( git.output(command) } -async fn read_filter_config_async( - git: &GitRunner, - cwd: &Path, -) -> Result, GitReadError> { - let scoped = run_filter_config_query_async(git, cwd, /*show_scope*/ true).await?; - if scoped - .status - .code() - .is_some_and(|code| code == 0 || code == 1) - { - return parse_effective_config(&scoped.stdout).map_err(|_| invalid_output("filterConfig")); - } - - let legacy = run_filter_config_query_async(git, cwd, /*show_scope*/ false).await?; - if !legacy - .status - .code() - .is_some_and(|code| code == 0 || code == 1) - { - return Err(command_failed("filterConfig", legacy.status.code())); - } - parse_effective_config_with_origins(&legacy.stdout).map_err(|_| invalid_output("filterConfig")) -} - -async fn run_filter_config_query_async( - git: &GitRunner, - cwd: &Path, - show_scope: bool, -) -> Result { - let mut command = git.tokio_command(); - command.args(["config", "--null"]); - if show_scope { - command.arg("--show-scope"); - } - command - .args([ - "--show-origin", - "--includes", - "--get-regexp", - EXECUTABLE_FILTER_CONFIG_PATTERN, - ]) - .current_dir(cwd) - .kill_on_drop(true); - command_output(git, command, "filterConfig").await -} - -#[derive(Clone, Copy)] -enum PathSelection { - Tracked, -} - -async fn read_paths_async( - git: &GitRunner, - cwd: &Path, - selection: PathSelection, -) -> Result>, GitReadError> { - let mut command = git.tokio_command(); - let hooks_config = format!("core.hooksPath={DISABLED_HOOKS_PATH}"); - let mut args = vec![ - "-c", - hooks_config.as_str(), - "-c", - "core.fsmonitor=false", - "ls-files", - "-z", - ]; - match selection { - PathSelection::Tracked => args.push("--cached"), - } - command - .env("GIT_OPTIONAL_LOCKS", "0") - .args(args) - .current_dir(cwd) - .kill_on_drop(true); - let operation = match selection { - PathSelection::Tracked => "trackedPaths", - }; - let output = command_output(git, command, operation).await?; - if !output.status.success() { - return Err(command_failed(operation, output.status.code())); - } - parse_nul_paths(&output.stdout).map_err(|_| invalid_output(operation)) -} - -async fn read_filter_attributes_async( - git: &GitRunner, - cwd: &Path, - paths: &[Vec], -) -> Result, FilterAttributeValue>, GitReadError> { - if paths.is_empty() { - return Ok(BTreeMap::new()); - } - let mut input = tempfile::tempfile() - .map_err(|_| command_failed("filterAttributes", /*exit_code*/ None))?; - write_nul_paths(&mut input, paths).map_err(|_| invalid_output("filterAttributes"))?; - input - .rewind() - .map_err(|_| command_failed("filterAttributes", /*exit_code*/ None))?; - - let mut command = git.tokio_command(); - command - .env("GIT_OPTIONAL_LOCKS", "0") - .args([ - "-c", - &format!("core.hooksPath={DISABLED_HOOKS_PATH}"), - "-c", - "core.fsmonitor=false", - "check-attr", - "--stdin", - "-z", - "filter", - ]) - .current_dir(cwd) - .stdin(Stdio::from(input)) - .kill_on_drop(true); - let output = command_output(git, command, "filterAttributes").await?; - if !output.status.success() { - return Err(command_failed("filterAttributes", output.status.code())); - } - parse_filter_attributes(&output.stdout, paths).map_err(|_| invalid_output("filterAttributes")) -} - -async fn resolve_filter_attribute_sentinels_async( - git: &GitRunner, - cwd: &Path, - attributes: BTreeMap, FilterAttributeValue>, - executable_drivers: &BTreeSet, -) -> Result, String>, GitReadError> { - 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_async(git, cwd, &path, &driver) - .await? - { - resolved.insert(path, driver); - } - } - } - } - Ok(resolved) -} - -async fn sentinel_spelling_selects_filter_driver_async( - git: &GitRunner, - cwd: &Path, - path: &[u8], - driver: &str, -) -> Result { - let required = - run_sentinel_selection_probe_async(git, cwd, path, driver, /*required*/ true).await?; - if required.status.success() { - return Ok(false); - } - let optional = - run_sentinel_selection_probe_async(git, cwd, path, driver, /*required*/ false).await?; - if optional.status.success() { - return Ok(true); - } - Err(command_failed( - "filterAttributeSelection", - optional.status.code(), - )) -} - -async fn run_sentinel_selection_probe_async( - git: &GitRunner, - cwd: &Path, - path: &[u8], - driver: &str, - required: bool, -) -> Result { - let path = git_path_argument(path).map_err(|_| invalid_output("filterAttributeSelection"))?; - let mut command = git.tokio_command(); - command - .env("GIT_OPTIONAL_LOCKS", "0") - .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(path) - .current_dir(cwd) - .stdin(Stdio::null()) - .kill_on_drop(true); - command_output(git, command, "filterAttributeSelection").await -} - -async fn command_output( - git: &GitRunner, - command: TokioCommand, - operation: &str, -) -> Result { - match timeout(GIT_COMMAND_TIMEOUT, git.output_tokio(command)).await { - Err(_) => Err(GitReadError::CommandTimedOut { - operation: operation.to_string(), - }), - Ok(Err(error)) if error.kind() == io::ErrorKind::NotFound => { - Err(GitReadError::NoTrustedGit) - } - Ok(Err(_)) => Err(command_failed(operation, /*exit_code*/ None)), - Ok(Ok(output)) => Ok(output), - } -} - -fn command_failed(operation: &str, exit_code: Option) -> GitReadError { - GitReadError::CommandFailed { - operation: operation.to_string(), - exit_code, - } -} - -fn invalid_output(operation: &str) -> GitReadError { - GitReadError::InvalidOutput { - operation: operation.to_string(), - } -} - fn read_filter_attributes( git: &GitRunner, cwd: &Path, @@ -795,14 +479,14 @@ fn run_sentinel_selection_probe( } #[cfg(unix)] -fn git_path_argument(path: &[u8]) -> io::Result { +pub(crate) fn git_path_argument(path: &[u8]) -> io::Result { 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 { +pub(crate) fn git_path_argument(path: &[u8]) -> io::Result { let path = std::str::from_utf8(path) .map_err(|_| invalid_filter_output("non-UTF-8 Git filter attribute path"))?; Ok(path.into()) @@ -818,7 +502,7 @@ fn selected_executable_filter_for( Ok(selected_filter(&executable_drivers, attributes)) } -fn selected_filter( +pub(crate) fn selected_filter( drivers: &BTreeSet, attributes: &BTreeMap, String>, ) -> Option<(String, Vec)> { @@ -838,7 +522,7 @@ fn selected_executable_filter( selected_executable_filter_for(entries, attributes, FilterExecution::AnyWorktreeOperation) } -fn executable_filter_drivers( +pub(crate) fn executable_filter_drivers( entries: &BTreeMap, ) -> io::Result> { executable_filter_drivers_for(entries, FilterExecution::AnyWorktreeOperation) @@ -937,7 +621,7 @@ fn filter_driver_and_command(key: &str) -> io::Result<(String, FilterCommand)> { Ok((driver.to_string(), command)) } -fn parse_nul_paths(output: &[u8]) -> io::Result>> { +pub(crate) fn parse_nul_paths(output: &[u8]) -> io::Result>> { if output.is_empty() { return Ok(Vec::new()); } @@ -954,7 +638,7 @@ fn parse_nul_paths(output: &[u8]) -> io::Result>> { Ok(paths) } -fn write_nul_paths(input: &mut std::fs::File, paths: &[Vec]) -> io::Result<()> { +pub(crate) fn write_nul_paths(input: &mut std::fs::File, paths: &[Vec]) -> io::Result<()> { let mut unique = BTreeSet::new(); for path in paths { if path.is_empty() || path.contains(&0) { @@ -968,7 +652,7 @@ fn write_nul_paths(input: &mut std::fs::File, paths: &[Vec]) -> io::Result<( Ok(()) } -fn parse_filter_attributes( +pub(crate) fn parse_filter_attributes( output: &[u8], expected_paths: &[Vec], ) -> io::Result, FilterAttributeValue>> { diff --git a/codex-rs/git-utils/src/safe_git_tests.rs b/codex-rs/git-utils/src/safe_git_tests.rs index 77ceb82738..2fabd4efe0 100644 --- a/codex-rs/git-utils/src/safe_git_tests.rs +++ b/codex-rs/git-utils/src/safe_git_tests.rs @@ -6,6 +6,9 @@ use crate::get_has_changes; use crate::git_config::GitConfigScope; #[cfg(unix)] use crate::patch_paths::stage_paths; +use crate::status_guard::git_root_from_stdout; +use crate::status_guard::resolve_git_root_async; +use crate::status_guard::selected_executable_filter_from; use crate::try_get_has_changes; use pretty_assertions::assert_eq; use std::collections::BTreeMap; diff --git a/codex-rs/git-utils/src/status_guard.rs b/codex-rs/git-utils/src/status_guard.rs new file mode 100644 index 0000000000..fbcf5fb9af --- /dev/null +++ b/codex-rs/git-utils/src/status_guard.rs @@ -0,0 +1,342 @@ +use std::collections::BTreeMap; +use std::collections::BTreeSet; +use std::io; +use std::io::Seek; +use std::path::Path; +use std::path::PathBuf; +use std::process::Stdio; + +use tokio::process::Command as TokioCommand; +use tokio::time::timeout; + +use crate::GitReadError; +use crate::git_command::GitRunner; +use crate::git_config::GitConfigEntry; +use crate::git_config::parse_effective_config; +use crate::git_config::parse_effective_config_with_origins; +use crate::safe_git::DISABLED_HOOKS_PATH; +use crate::safe_git::EXECUTABLE_FILTER_CONFIG_PATTERN; +use crate::safe_git::FilterAttributeValue; +use crate::safe_git::GIT_COMMAND_TIMEOUT; +use crate::safe_git::executable_filter_drivers; +use crate::safe_git::git_path_argument; +use crate::safe_git::parse_filter_attributes; +use crate::safe_git::parse_nul_paths; +use crate::safe_git::selected_filter; +use crate::safe_git::write_nul_paths; + +pub(crate) async fn selected_executable_filter_from( + git: &GitRunner, + cwd: &Path, +) -> Result)>, GitReadError> { + let git_root = resolve_git_root_async(git, cwd).await?; + let entries = read_filter_config_async(git, &git_root).await?; + let executable_drivers = + executable_filter_drivers(&entries).map_err(|_| invalid_output("filterConfig"))?; + if executable_drivers.is_empty() { + return Ok(None); + } + let paths = read_paths_async(git, &git_root, PathSelection::Tracked).await?; + let attributes = read_filter_attributes_async(git, &git_root, &paths).await?; + let attributes = + resolve_filter_attribute_sentinels_async(git, &git_root, attributes, &executable_drivers) + .await?; + Ok(selected_filter(&executable_drivers, &attributes)) +} + +pub(crate) async fn resolve_git_root_async( + git: &GitRunner, + cwd: &Path, +) -> Result { + let requested_cwd = std::fs::canonicalize(cwd).map_err(|_| GitReadError::NotRepository { + path: cwd.to_path_buf(), + })?; + let expected_root = crate::get_git_repo_root(&requested_cwd) + .and_then(|root| std::fs::canonicalize(root).ok()) + .ok_or_else(|| GitReadError::NotRepository { + path: requested_cwd.clone(), + })?; + let mut command = git.tokio_command(); + command + .env("GIT_OPTIONAL_LOCKS", "0") + .args([ + "-c", + &format!("core.hooksPath={DISABLED_HOOKS_PATH}"), + "-c", + "core.fsmonitor=false", + "rev-parse", + "--show-toplevel", + ]) + .current_dir(&requested_cwd) + .kill_on_drop(true); + let output = command_output(git, command, "resolveGitRoot").await?; + if !output.status.success() { + return Err(command_failed("resolveGitRoot", output.status.code())); + } + let reported_root = git_root_from_stdout(output.stdout).and_then(|path| { + std::fs::canonicalize(path).map_err(|_| invalid_output("resolveGitRoot")) + })?; + if reported_root != expected_root { + return Err(GitReadError::RepositoryRootMismatch { + expected_root, + reported_root, + }); + } + Ok(reported_root) +} + +pub(crate) fn git_root_from_stdout(output: Vec) -> Result { + let output = output.strip_suffix(b"\n").unwrap_or(&output); + #[cfg(windows)] + let output = output.strip_suffix(b"\r").unwrap_or(output); + if output.is_empty() { + return Err(invalid_output("resolveGitRoot")); + } + #[cfg(unix)] + { + use std::ffi::OsString; + use std::os::unix::ffi::OsStringExt; + + Ok(PathBuf::from(OsString::from_vec(output.to_vec()))) + } + #[cfg(not(unix))] + { + String::from_utf8(output.to_vec()) + .map(PathBuf::from) + .map_err(|_| invalid_output("resolveGitRoot")) + } +} + +async fn read_filter_config_async( + git: &GitRunner, + cwd: &Path, +) -> Result, GitReadError> { + let scoped = run_filter_config_query_async(git, cwd, /*show_scope*/ true).await?; + if scoped + .status + .code() + .is_some_and(|code| code == 0 || code == 1) + { + return parse_effective_config(&scoped.stdout).map_err(|_| invalid_output("filterConfig")); + } + + let legacy = run_filter_config_query_async(git, cwd, /*show_scope*/ false).await?; + if !legacy + .status + .code() + .is_some_and(|code| code == 0 || code == 1) + { + return Err(command_failed("filterConfig", legacy.status.code())); + } + parse_effective_config_with_origins(&legacy.stdout).map_err(|_| invalid_output("filterConfig")) +} + +async fn run_filter_config_query_async( + git: &GitRunner, + cwd: &Path, + show_scope: bool, +) -> Result { + let mut command = git.tokio_command(); + command.args(["config", "--null"]); + if show_scope { + command.arg("--show-scope"); + } + command + .args([ + "--show-origin", + "--includes", + "--get-regexp", + EXECUTABLE_FILTER_CONFIG_PATTERN, + ]) + .current_dir(cwd) + .kill_on_drop(true); + command_output(git, command, "filterConfig").await +} + +#[derive(Clone, Copy)] +enum PathSelection { + Tracked, +} + +async fn read_paths_async( + git: &GitRunner, + cwd: &Path, + selection: PathSelection, +) -> Result>, GitReadError> { + let mut command = git.tokio_command(); + let hooks_config = format!("core.hooksPath={DISABLED_HOOKS_PATH}"); + let mut args = vec![ + "-c", + hooks_config.as_str(), + "-c", + "core.fsmonitor=false", + "ls-files", + "-z", + ]; + match selection { + PathSelection::Tracked => args.push("--cached"), + } + command + .env("GIT_OPTIONAL_LOCKS", "0") + .args(args) + .current_dir(cwd) + .kill_on_drop(true); + let operation = match selection { + PathSelection::Tracked => "trackedPaths", + }; + let output = command_output(git, command, operation).await?; + if !output.status.success() { + return Err(command_failed(operation, output.status.code())); + } + parse_nul_paths(&output.stdout).map_err(|_| invalid_output(operation)) +} + +async fn read_filter_attributes_async( + git: &GitRunner, + cwd: &Path, + paths: &[Vec], +) -> Result, FilterAttributeValue>, GitReadError> { + if paths.is_empty() { + return Ok(BTreeMap::new()); + } + let mut input = tempfile::tempfile() + .map_err(|_| command_failed("filterAttributes", /*exit_code*/ None))?; + write_nul_paths(&mut input, paths).map_err(|_| invalid_output("filterAttributes"))?; + input + .rewind() + .map_err(|_| command_failed("filterAttributes", /*exit_code*/ None))?; + + let mut command = git.tokio_command(); + command + .env("GIT_OPTIONAL_LOCKS", "0") + .args([ + "-c", + &format!("core.hooksPath={DISABLED_HOOKS_PATH}"), + "-c", + "core.fsmonitor=false", + "check-attr", + "--stdin", + "-z", + "filter", + ]) + .current_dir(cwd) + .stdin(Stdio::from(input)) + .kill_on_drop(true); + let output = command_output(git, command, "filterAttributes").await?; + if !output.status.success() { + return Err(command_failed("filterAttributes", output.status.code())); + } + parse_filter_attributes(&output.stdout, paths).map_err(|_| invalid_output("filterAttributes")) +} + +async fn resolve_filter_attribute_sentinels_async( + git: &GitRunner, + cwd: &Path, + attributes: BTreeMap, FilterAttributeValue>, + executable_drivers: &BTreeSet, +) -> Result, String>, GitReadError> { + 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_async(git, cwd, &path, &driver) + .await? + { + resolved.insert(path, driver); + } + } + } + } + Ok(resolved) +} + +async fn sentinel_spelling_selects_filter_driver_async( + git: &GitRunner, + cwd: &Path, + path: &[u8], + driver: &str, +) -> Result { + let required = + run_sentinel_selection_probe_async(git, cwd, path, driver, /*required*/ true).await?; + if required.status.success() { + return Ok(false); + } + let optional = + run_sentinel_selection_probe_async(git, cwd, path, driver, /*required*/ false).await?; + if optional.status.success() { + return Ok(true); + } + Err(command_failed( + "filterAttributeSelection", + optional.status.code(), + )) +} + +async fn run_sentinel_selection_probe_async( + git: &GitRunner, + cwd: &Path, + path: &[u8], + driver: &str, + required: bool, +) -> Result { + let path = git_path_argument(path).map_err(|_| invalid_output("filterAttributeSelection"))?; + let mut command = git.tokio_command(); + command + .env("GIT_OPTIONAL_LOCKS", "0") + .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(path) + .current_dir(cwd) + .stdin(Stdio::null()) + .kill_on_drop(true); + command_output(git, command, "filterAttributeSelection").await +} + +async fn command_output( + git: &GitRunner, + command: TokioCommand, + operation: &str, +) -> Result { + match timeout(GIT_COMMAND_TIMEOUT, git.output_tokio(command)).await { + Err(_) => Err(GitReadError::CommandTimedOut { + operation: operation.to_string(), + }), + Ok(Err(error)) if error.kind() == io::ErrorKind::NotFound => { + Err(GitReadError::NoTrustedGit) + } + Ok(Err(_)) => Err(command_failed(operation, /*exit_code*/ None)), + Ok(Ok(output)) => Ok(output), + } +} + +fn command_failed(operation: &str, exit_code: Option) -> GitReadError { + GitReadError::CommandFailed { + operation: operation.to_string(), + exit_code, + } +} + +fn invalid_output(operation: &str) -> GitReadError { + GitReadError::InvalidOutput { + operation: operation.to_string(), + } +}