diff --git a/crates/forge_app/src/infra.rs b/crates/forge_app/src/infra.rs index bc57aa38dc..3d8d438af4 100644 --- a/crates/forge_app/src/infra.rs +++ b/crates/forge_app/src/infra.rs @@ -102,6 +102,10 @@ pub trait FileWriterInfra: Send + Sync { /// Writes the content of a file at the specified path. async fn write(&self, path: &Path, contents: Bytes) -> anyhow::Result<()>; + /// Appends content to a file at the specified path, creating it if it does + /// not exist. + async fn append(&self, path: &Path, contents: Bytes) -> anyhow::Result<()>; + /// Writes content to a temporary file with the given prefix and extension, /// and returns its path. The file will be kept (not deleted) after /// creation. diff --git a/crates/forge_fs/src/write.rs b/crates/forge_fs/src/write.rs index e5019041bc..7972e6e94c 100644 --- a/crates/forge_fs/src/write.rs +++ b/crates/forge_fs/src/write.rs @@ -1,6 +1,7 @@ use std::path::Path; use anyhow::{Context, Result}; +use tokio::io::AsyncWriteExt as _; impl crate::ForgeFS { pub async fn create_dir_all>(path: T) -> Result<()> { @@ -15,6 +16,24 @@ impl crate::ForgeFS { .with_context(|| format!("Failed to write file {}", path.as_ref().display())) } + /// Appends content to an existing file, or creates it if it does not exist. + pub async fn append, U: AsRef<[u8]>>(path: T, contents: U) -> Result<()> { + let mut file = tokio::fs::OpenOptions::new() + .create(true) + .append(true) + .open(path.as_ref()) + .await + .with_context(|| { + format!( + "Failed to open file for appending {}", + path.as_ref().display() + ) + })?; + file.write_all(contents.as_ref()) + .await + .with_context(|| format!("Failed to append to file {}", path.as_ref().display())) + } + pub async fn remove_file>(path: T) -> Result<()> { tokio::fs::remove_file(path.as_ref()) .await diff --git a/crates/forge_infra/src/forge_infra.rs b/crates/forge_infra/src/forge_infra.rs index e5188c99b1..399584fe04 100644 --- a/crates/forge_infra/src/forge_infra.rs +++ b/crates/forge_infra/src/forge_infra.rs @@ -175,6 +175,10 @@ impl FileWriterInfra for ForgeInfra { self.file_write_service.write(path, contents).await } + async fn append(&self, path: &Path, contents: Bytes) -> anyhow::Result<()> { + self.file_write_service.append(path, contents).await + } + async fn write_temp(&self, prefix: &str, ext: &str, content: &str) -> anyhow::Result { self.file_write_service .write_temp(prefix, ext, content) diff --git a/crates/forge_infra/src/fs_write.rs b/crates/forge_infra/src/fs_write.rs index b1836fe3b1..442a9a2529 100644 --- a/crates/forge_infra/src/fs_write.rs +++ b/crates/forge_infra/src/fs_write.rs @@ -38,6 +38,11 @@ impl FileWriterInfra for ForgeFileWriteService { Ok(forge_fs::ForgeFS::write(path, contents.to_vec()).await?) } + async fn append(&self, path: &Path, contents: Bytes) -> anyhow::Result<()> { + self.create_parent_dirs(path).await?; + Ok(forge_fs::ForgeFS::append(path, contents.to_vec()).await?) + } + async fn write_temp(&self, prefix: &str, ext: &str, content: &str) -> anyhow::Result { let path = tempfile::Builder::new() .disable_cleanup(true) diff --git a/crates/forge_infra/src/http.rs b/crates/forge_infra/src/http.rs index 7bb0ca8cfb..a900eb1cad 100644 --- a/crates/forge_infra/src/http.rs +++ b/crates/forge_infra/src/http.rs @@ -231,7 +231,7 @@ impl ForgeHttpInfra { let body_clone = body.clone(); let debug_path = debug_path.clone(); tokio::spawn(async move { - let _ = file_writer.write(&debug_path, body_clone).await; + let _ = file_writer.append(&debug_path, body_clone).await; }); } } @@ -332,6 +332,14 @@ mod tests { Ok(()) } + async fn append(&self, path: &std::path::Path, contents: Bytes) -> anyhow::Result<()> { + self.writes + .lock() + .await + .push((path.to_path_buf(), contents)); + Ok(()) + } + async fn write_temp( &self, _prefix: &str, diff --git a/crates/forge_repo/src/forge_repo.rs b/crates/forge_repo/src/forge_repo.rs index 478aa26979..4fa8ebedf0 100644 --- a/crates/forge_repo/src/forge_repo.rs +++ b/crates/forge_repo/src/forge_repo.rs @@ -320,6 +320,9 @@ where async fn write(&self, path: &Path, contents: Bytes) -> anyhow::Result<()> { self.infra.write(path, contents).await } + async fn append(&self, path: &Path, contents: Bytes) -> anyhow::Result<()> { + self.infra.append(path, contents).await + } async fn write_temp(&self, prefix: &str, ext: &str, content: &str) -> anyhow::Result { self.infra.write_temp(prefix, ext, content).await } diff --git a/crates/forge_repo/src/provider/provider_repo.rs b/crates/forge_repo/src/provider/provider_repo.rs index 243aac260b..2806ede74d 100644 --- a/crates/forge_repo/src/provider/provider_repo.rs +++ b/crates/forge_repo/src/provider/provider_repo.rs @@ -865,6 +865,10 @@ mod env_tests { Ok(()) } + async fn append(&self, _path: &std::path::Path, _content: Bytes) -> anyhow::Result<()> { + Ok(()) + } + async fn write_temp( &self, _prefix: &str, @@ -1343,6 +1347,10 @@ mod env_tests { Ok(()) } + async fn append(&self, _path: &std::path::Path, _content: Bytes) -> anyhow::Result<()> { + Ok(()) + } + async fn write_temp( &self, _prefix: &str, diff --git a/crates/forge_services/src/attachment.rs b/crates/forge_services/src/attachment.rs index ef6e515430..1ec1db7e7b 100644 --- a/crates/forge_services/src/attachment.rs +++ b/crates/forge_services/src/attachment.rs @@ -380,6 +380,21 @@ pub mod tests { Ok(()) } + async fn append(&self, path: &Path, contents: Bytes) -> anyhow::Result<()> { + let mut existing = bytes::Bytes::new(); + let index = self.files.lock().unwrap().iter().position(|v| v.0 == path); + if let Some(index) = index { + existing = self.files.lock().unwrap().remove(index).1; + } + let mut combined = existing.to_vec(); + combined.extend_from_slice(&contents); + self.files + .lock() + .unwrap() + .push((path.to_path_buf(), combined.into())); + Ok(()) + } + async fn write_temp(&self, _: &str, _: &str, content: &str) -> anyhow::Result { let temp_dir = crate::utils::TempDir::new().unwrap(); let path = temp_dir.path();