Skip to main content
Glama
by 8b-is
smart_edit_diff.rs11.2 kB
// smart_edit_diff.rs - Local diff storage for Smart Edit operations // Stores diffs in .st_bumpers folder with timestamps for audit trail use anyhow::{Context, Result}; use similar::TextDiff; use std::fs::{self, File}; use std::io::Write; use std::path::{Path, PathBuf}; use std::time::{SystemTime, UNIX_EPOCH}; pub struct DiffStorage { project_root: PathBuf, pub st_folder: PathBuf, } impl DiffStorage { /// Initialize diff storage for a project pub fn new(project_root: impl AsRef<Path>) -> Result<Self> { let project_root = project_root.as_ref().to_path_buf(); let st_folder = project_root.join(".st_bumpers"); // Create .st_bumpers folder if it doesn't exist if !st_folder.exists() { fs::create_dir(&st_folder).context("Failed to create .st_bumpers folder")?; } // Ensure .st_bumpers is in .gitignore Self::ensure_gitignore(&project_root)?; Ok(DiffStorage { project_root, st_folder, }) } /// Ensure .st_bumpers/ is in .gitignore fn ensure_gitignore(project_root: &Path) -> Result<()> { let gitignore_path = project_root.join(".gitignore"); // Check if .gitignore exists and contains .st_bumpers/ let needs_update = if gitignore_path.exists() { let content = fs::read_to_string(&gitignore_path)?; !content .lines() .any(|line| line.trim() == ".st_bumpers/" || line.trim() == ".st_bumpers") } else { true }; if needs_update { // Append .st_bumpers/ to .gitignore let mut file = fs::OpenOptions::new() .create(true) .append(true) .open(&gitignore_path)?; // Add newline if file exists and doesn't end with one if gitignore_path.exists() { let content = fs::read_to_string(&gitignore_path)?; if !content.is_empty() && !content.ends_with('\n') { writeln!(file)?; } } writeln!(file, ".st_bumpers/")?; } Ok(()) } /// Store a diff for a file before Smart Edit operation pub fn store_diff( &self, file_path: &Path, original_content: &str, new_content: &str, ) -> Result<PathBuf> { // Get relative path from project root let relative_path = file_path .strip_prefix(&self.project_root) .unwrap_or(file_path); // Create diff let diff = TextDiff::from_lines(original_content, new_content); // Generate filename with timestamp let timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); let filename = format!( "{}-{}", relative_path.to_string_lossy().replace('/', "-"), timestamp ); let diff_path = self.st_folder.join(&filename); // Write unified diff format let mut file = File::create(&diff_path)?; // Use the simple unified diff format let mut unified_diff = diff.unified_diff(); let unified = unified_diff.context_radius(3).header( &format!("a/{}", relative_path.display()), &format!("b/{}", relative_path.display()), ); write!(file, "{}", unified)?; Ok(diff_path) } /// Store the original file before any edits (for first edit) pub fn store_original(&self, file_path: &Path, content: &str) -> Result<()> { let relative_path = file_path .strip_prefix(&self.project_root) .unwrap_or(file_path); let original_path = self .st_folder .join(relative_path.to_string_lossy().replace('/', "-")); // Only store if it doesn't exist if !original_path.exists() { fs::write(&original_path, content)?; } Ok(()) } /// Get the latest stored version of a file pub fn get_latest_version(&self, file_path: &Path) -> Result<Option<String>> { let relative_path = file_path .strip_prefix(&self.project_root) .unwrap_or(file_path); let base_name = relative_path.to_string_lossy().replace('/', "-"); // Find all diffs for this file let mut diffs: Vec<_> = fs::read_dir(&self.st_folder)? .filter_map(|entry| entry.ok()) .filter(|entry| { let name = entry.file_name().to_string_lossy().to_string(); name.starts_with(&base_name) && name.contains('-') }) .collect(); // Sort by timestamp (newest first) diffs.sort_by_key(|entry| { let name = entry.file_name().to_string_lossy().to_string(); name.split('-') .next_back() .and_then(|ts| ts.parse::<u64>().ok()) .unwrap_or(0) }); diffs.reverse(); // If we have diffs, reconstruct the latest version if !diffs.is_empty() { // Start with original if it exists let original_path = self.st_folder.join(&base_name); let content = if original_path.exists() { fs::read_to_string(&original_path)? } else { // Try to get from actual file fs::read_to_string(file_path)? }; // Apply diffs in order (oldest to newest) for _diff_entry in diffs.iter().rev() { // This is simplified - in production you'd parse and apply the diff // For now, we'll just return that we have history } return Ok(Some(content)); } Ok(None) } /// List all diffs for all files in the .st_bumpers folder pub fn list_all_diffs(&self) -> Result<Vec<(String, u64)>> { let mut all_diffs = Vec::new(); if !self.st_folder.exists() { return Ok(all_diffs); } for entry in fs::read_dir(&self.st_folder)? { let entry = entry?; let file_name = entry.file_name(); let file_name_str = file_name.to_string_lossy(); // Skip the original files (those without timestamps) if !file_name_str.contains('-') { continue; } // Extract timestamp from filename if let Some(dash_pos) = file_name_str.rfind('-') { if let Ok(timestamp) = file_name_str[dash_pos + 1..].parse::<u64>() { let file_path = file_name_str[..dash_pos].replace('-', "/"); all_diffs.push((file_path, timestamp)); } } } Ok(all_diffs) } /// List all stored diffs for a file pub fn list_diffs(&self, file_path: &Path) -> Result<Vec<DiffInfo>> { let relative_path = file_path .strip_prefix(&self.project_root) .unwrap_or(file_path); let base_name = relative_path.to_string_lossy().replace('/', "-"); let mut diffs = Vec::new(); for entry in fs::read_dir(&self.st_folder)? { let entry = entry?; let name = entry.file_name().to_string_lossy().to_string(); if name.starts_with(&base_name) && name.contains('-') { if let Some(timestamp_str) = name.split('-').next_back() { if let Ok(timestamp) = timestamp_str.parse::<u64>() { diffs.push(DiffInfo { path: entry.path(), timestamp, file_path: file_path.to_path_buf(), }); } } } } // Sort by timestamp (newest first) diffs.sort_by_key(|d| d.timestamp); diffs.reverse(); Ok(diffs) } /// Clean up old diffs (keep last N diffs per file) pub fn cleanup_old_diffs(&self, keep_count: usize) -> Result<usize> { let mut removed_count = 0; // Group diffs by file let mut file_diffs: std::collections::HashMap<String, Vec<PathBuf>> = std::collections::HashMap::new(); for entry in fs::read_dir(&self.st_folder)? { let entry = entry?; let name = entry.file_name().to_string_lossy().to_string(); // Skip non-diff files (like originals) if !name.contains('-') { continue; } // Extract base filename if let Some(pos) = name.rfind('-') { let base = &name[..pos]; file_diffs .entry(base.to_string()) .or_default() .push(entry.path()); } } // Remove old diffs for each file for (_, mut diffs) in file_diffs { if diffs.len() > keep_count { // Sort by timestamp (embedded in filename) diffs.sort(); // Remove oldest diffs let to_remove = diffs.len() - keep_count; for diff_path in diffs.into_iter().take(to_remove) { fs::remove_file(diff_path)?; removed_count += 1; } } } Ok(removed_count) } } #[derive(Debug)] pub struct DiffInfo { pub path: PathBuf, pub timestamp: u64, pub file_path: PathBuf, } impl DiffInfo { /// Get human-readable timestamp pub fn timestamp_str(&self) -> String { use chrono::{DateTime, Utc}; let datetime = DateTime::<Utc>::from_timestamp(self.timestamp as i64, 0).unwrap_or_else(Utc::now); datetime.format("%Y-%m-%d %H:%M:%S").to_string() } } #[cfg(test)] mod tests { use super::*; use tempfile::TempDir; #[test] fn test_diff_storage_creation() { let temp_dir = TempDir::new().unwrap(); let _storage = DiffStorage::new(temp_dir.path()).unwrap(); // Check .st_bumpers folder was created assert!(temp_dir.path().join(".st_bumpers").exists()); // Check .gitignore was updated let gitignore = fs::read_to_string(temp_dir.path().join(".gitignore")).unwrap(); assert!(gitignore.contains(".st_bumpers/")); } #[test] fn test_store_diff() { let temp_dir = TempDir::new().unwrap(); let storage = DiffStorage::new(temp_dir.path()).unwrap(); let file_path = temp_dir.path().join("test.rs"); let original = "fn main() {\n println!(\"Hello\");\n}"; let modified = "fn main() {\n println!(\"Hello, World!\");\n}"; let diff_path = storage.store_diff(&file_path, original, modified).unwrap(); assert!(diff_path.exists()); let diff_content = fs::read_to_string(&diff_path).unwrap(); assert!(diff_content.contains("--- a/test.rs")); assert!(diff_content.contains("+++ b/test.rs")); assert!(diff_content.contains("- println!(\"Hello\");")); assert!(diff_content.contains("+ println!(\"Hello, World!\");")); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/8b-is/smart-tree'

If you have feedback or need assistance with the MCP directory API, please join our Discord server