use std::path::{Path, PathBuf};
use anyhow::Result;
use super::apply_patch_handler::parse_apply_patch_command;
use super::tool_handler::{ToolOutput, ToolSession, TurnContext};
use super::turn_diff_tracker::SharedTurnDiffTracker;
use crate::tools::editing::Patch;
pub const CODEX_APPLY_PATCH_ARG: &str = "--codex-run-as-apply-patch";
#[derive(Clone, Debug)]
pub struct ApplyPatchRequest {
pub patch: String,
pub cwd: PathBuf,
pub timeout_ms: Option<u64>,
pub user_explicitly_approved: bool,
pub codex_exe: Option<PathBuf>,
}
impl ApplyPatchRequest {
pub fn new(patch: String, cwd: PathBuf) -> Self {
Self {
patch,
cwd,
timeout_ms: Some(30000),
user_explicitly_approved: false,
codex_exe: None,
}
}
pub fn with_timeout(mut self, timeout_ms: u64) -> Self {
self.timeout_ms = Some(timeout_ms);
self
}
pub fn with_approval(mut self, approved: bool) -> Self {
self.user_explicitly_approved = approved;
self
}
pub fn with_codex_exe(mut self, exe: PathBuf) -> Self {
self.codex_exe = Some(exe);
self
}
}
pub fn maybe_parse_apply_patch_from_command(command: &[String]) -> Option<String> {
let (is_apply_patch, patch_content) = parse_apply_patch_command(command);
is_apply_patch.then_some(patch_content).flatten()
}
#[allow(clippy::too_many_arguments)]
pub async fn intercept_apply_patch(
command: &[String],
cwd: &Path,
timeout_ms: Option<u64>,
_session: &dyn ToolSession,
_turn: &TurnContext,
tracker: Option<&SharedTurnDiffTracker>,
_call_id: &str,
_tool_name: &str,
) -> Result<Option<ToolOutput>, ApplyPatchError> {
let Some(patch) = maybe_parse_apply_patch_from_command(command) else {
return Ok(None);
};
let req = ApplyPatchRequest::new(patch.clone(), cwd.to_path_buf())
.with_timeout(timeout_ms.unwrap_or(30000));
if let Some(tracker) = tracker {
let mut t = tracker.write().await;
t.on_patch_begin(Default::default());
}
let result = execute_patch(&req).await;
if let Some(tracker) = tracker {
let mut t = tracker.write().await;
t.on_patch_end(result.is_ok());
}
match result {
Ok(output) => Ok(Some(ToolOutput::simple(output))),
Err(e) => Ok(Some(ToolOutput::error(e.to_string()))),
}
}
async fn execute_patch(req: &ApplyPatchRequest) -> Result<String, ApplyPatchError> {
let patch = Patch::parse(&req.patch).map_err(|e| ApplyPatchError::ParseError(e.to_string()))?;
if patch.is_empty() {
return Ok("Patch is empty, no changes applied".to_string());
}
let results = patch
.apply(&req.cwd)
.await
.map_err(|e| ApplyPatchError::PatchFailed(e.to_string()))?;
Ok(results.join("\n"))
}
#[derive(Debug, thiserror::Error)]
pub enum ApplyPatchError {
#[error("Failed to parse patch: {0}")]
ParseError(String),
#[error("Patch application failed: {0}")]
PatchFailed(String),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn maybe_parse_apply_patch_detects_direct_invocation() {
assert_eq!(
maybe_parse_apply_patch_from_command(&[
"apply_patch".to_string(),
"*** Begin Patch\n*** End Patch".to_string(),
]),
Some("*** Begin Patch\n*** End Patch".to_string())
);
assert_eq!(
maybe_parse_apply_patch_from_command(&[
"applypatch".to_string(),
"*** Begin Patch\n*** End Patch".to_string(),
]),
Some("*** Begin Patch\n*** End Patch".to_string())
);
assert_eq!(
maybe_parse_apply_patch_from_command(&[
"codex".to_string(),
CODEX_APPLY_PATCH_ARG.to_string(),
]),
None
);
assert_eq!(
maybe_parse_apply_patch_from_command(&[
"git".to_string(),
"apply".to_string(),
"test.patch".to_string(),
]),
None
);
assert_eq!(
maybe_parse_apply_patch_from_command(&["patch".to_string(), "-p1".to_string(),]),
None
);
assert_eq!(
maybe_parse_apply_patch_from_command(&["echo".to_string(), "hello".to_string(),]),
None
);
}
#[test]
fn test_apply_patch_request_builder() {
let req = ApplyPatchRequest::new("patch content".to_string(), PathBuf::from("/tmp"))
.with_timeout(5000)
.with_approval(true)
.with_codex_exe(PathBuf::from("/usr/bin/codex"));
assert_eq!(req.patch, "patch content");
assert_eq!(req.timeout_ms, Some(5000));
assert!(req.user_explicitly_approved);
assert_eq!(req.codex_exe, Some(PathBuf::from("/usr/bin/codex")));
}
}