Files
codex/codex-rs/git-utils/src/git_command.rs
2026-07-03 18:29:53 -07:00

163 lines
5.1 KiB
Rust

use std::ffi::OsStr;
use std::io;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
use crate::errors::GitReadError;
use crate::git_config_environment::GitConfigEnvironmentSnapshot;
#[cfg(test)]
use crate::git_executable::git_executable_name;
use crate::git_executable::harden_git_launch_environment;
#[cfg(test)]
use crate::git_executable::path_is_untrusted;
#[cfg(test)]
use crate::git_executable::search_directory_is_untrusted;
use crate::git_executable::select_git_executable;
#[cfg(all(test, windows))]
use crate::git_executable::windows_path_requires_fail_closed;
use crate::repository_authority::RepositoryAuthority;
#[cfg(test)]
use crate::repository_authority::parse_marker_path as parse_git_marker_path;
use crate::safe_git::isolate_git_command_environment;
/// A Git executable outside the repository-controlled roots for one operation.
#[derive(Debug)]
pub(crate) struct GitRunner {
/// Canonical executable target pinned at selection time. Never execute the
/// mutable PATH spelling after validation.
executable: PathBuf,
#[cfg(any(unix, test))]
argv0: PathBuf,
safe_path: std::ffi::OsString,
authority: RepositoryAuthority,
config_environment: GitConfigEnvironmentSnapshot,
}
/// A Git command that can only be spawned through [`GitRunner::output`],
/// keeping metadata revalidation and launch hardening at one choke point.
pub(crate) struct GitCommand {
inner: Command,
}
impl GitCommand {
pub(crate) fn arg(&mut self, arg: impl AsRef<OsStr>) -> &mut Self {
self.inner.arg(arg);
self
}
pub(crate) fn args<I, S>(&mut self, args: I) -> &mut Self
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
self.inner.args(args);
self
}
pub(crate) fn env(&mut self, key: impl AsRef<OsStr>, value: impl AsRef<OsStr>) -> &mut Self {
self.inner.env(key, value);
self
}
}
impl GitRunner {
pub(crate) fn for_cwd(cwd: &Path) -> Result<Self, GitReadError> {
#[cfg(test)]
GIT_RUNNER_CONSTRUCTION_COUNT.with(|count| count.set(count.get() + 1));
let authority = repository_authority_for_cwd(cwd)?;
let search_path = std::env::var_os("PATH").ok_or(GitReadError::NoTrustedGit)?;
Self::from_search_path(authority, &search_path)
}
pub(crate) fn for_cwd_io(cwd: &Path) -> io::Result<Self> {
Self::for_cwd(cwd).map_err(GitReadError::into_io_error)
}
pub(crate) fn command(&self) -> GitCommand {
let mut command = Command::new(&self.executable);
#[cfg(unix)]
{
use std::os::unix::process::CommandExt;
command.arg0(&self.argv0);
}
if let Some(parent) = self.executable.parent() {
command.current_dir(parent);
}
harden_git_launch_environment(&mut command, &self.safe_path);
self.config_environment.apply_to(&mut command);
GitCommand { inner: command }
}
pub(crate) fn command_for_cwd(&self, cwd: &Path) -> io::Result<GitCommand> {
let cwd = if cwd.is_absolute() {
cwd.to_path_buf()
} else {
std::env::current_dir()?.join(cwd)
};
let cwd = self.authority.canonical_command_cwd(&cwd)?;
let mut command = self.command();
command.arg("-C").arg(cwd);
Ok(command)
}
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);
command.inner.envs(crate::local_only_git_env());
harden_git_launch_environment(&mut command.inner, &self.safe_path);
command.inner.output()
}
fn revalidate_active_repository_metadata(&self) -> io::Result<()> {
self.authority.revalidate_active_repository_metadata()
}
fn from_search_path(
authority: RepositoryAuthority,
search_path: &OsStr,
) -> Result<Self, GitReadError> {
authority.ensure_primary_authority()?;
let selected = select_git_executable(&authority, search_path)?;
let config_environment = GitConfigEnvironmentSnapshot::capture().map_err(|error| {
GitReadError::InvalidConfigEnvironment {
reason: error.to_string(),
}
})?;
Ok(Self {
executable: selected.executable,
#[cfg(any(unix, test))]
argv0: selected.argv0,
safe_path: selected.safe_path,
authority,
config_environment,
})
}
}
#[cfg(test)]
thread_local! {
static GIT_RUNNER_CONSTRUCTION_COUNT: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}
#[cfg(test)]
pub(crate) fn reset_git_runner_construction_count() {
GIT_RUNNER_CONSTRUCTION_COUNT.with(|count| count.set(0));
}
#[cfg(test)]
pub(crate) fn git_runner_construction_count() -> usize {
GIT_RUNNER_CONSTRUCTION_COUNT.with(std::cell::Cell::get)
}
pub(crate) fn repository_authority_for_cwd(
cwd: &Path,
) -> Result<RepositoryAuthority, GitReadError> {
RepositoryAuthority::discover(cwd)
}
#[cfg(test)]
#[path = "git_command_tests.rs"]
mod tests;