dx_forge/core/
tracking.rs

1//! Generated code tracking system
2//!
3//! Tracks files generated by DX tools for cleanup and dependency management.
4
5use anyhow::{Context, Result};
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use sha2::{Digest, Sha256};
9use std::collections::HashMap;
10use std::path::{Path, PathBuf};
11
12use crate::storage::Database;
13
14/// Information about a generated file
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct GeneratedFileInfo {
17    /// Path to the generated file
18    pub file_path: PathBuf,
19    
20    /// Name of the tool that generated this file
21    pub tool_name: String,
22    
23    /// When the file was generated
24    pub generated_at: DateTime<Utc>,
25    
26    /// SHA-256 hash of the file content when generated
27    pub source_hash: String,
28    
29    /// Additional metadata about the generation
30    pub metadata: HashMap<String, String>,
31    
32    /// Whether the file has been modified since generation
33    pub modified: bool,
34}
35
36impl GeneratedFileInfo {
37    /// Create new generated file info
38    pub fn new(
39        file_path: PathBuf,
40        tool_name: String,
41        source_hash: String,
42        metadata: HashMap<String, String>,
43    ) -> Self {
44        Self {
45            file_path,
46            tool_name,
47            generated_at: Utc::now(),
48            source_hash,
49            metadata,
50            modified: false,
51        }
52    }
53    
54    /// Check if file has been modified since generation
55    pub async fn check_modified(&mut self) -> Result<bool> {
56        if !self.file_path.exists() {
57            self.modified = true;
58            return Ok(true);
59        }
60        
61        let content = tokio::fs::read(&self.file_path).await?;
62        let mut hasher = Sha256::new();
63        hasher.update(&content);
64        let current_hash = format!("{:x}", hasher.finalize());
65        
66        self.modified = current_hash != self.source_hash;
67        Ok(self.modified)
68    }
69}
70
71/// Tracks code generated by DX tools
72pub struct GeneratedCodeTracker {
73    _db: Database,
74    tracked_files: HashMap<PathBuf, GeneratedFileInfo>,
75    index_path: PathBuf,
76}
77
78impl GeneratedCodeTracker {
79    /// Create a new code tracker
80    pub fn new(forge_dir: &Path) -> Result<Self> {
81        let db_path = forge_dir.join("forge.db");
82        let db = Database::new(&db_path)
83            .context("Failed to open tracking database")?;
84        
85        let index_path = forge_dir.join("generated_files.json");
86        
87        // Load existing tracked files
88        let tracked_files = if index_path.exists() {
89            let content = std::fs::read_to_string(&index_path)?;
90            serde_json::from_str(&content).unwrap_or_default()
91        } else {
92            HashMap::new()
93        };
94        
95        Ok(Self {
96            _db: db,
97            tracked_files,
98            index_path,
99        })
100    }
101    
102    /// Track a new generated file
103    pub fn track_file(
104        &mut self,
105        file_path: PathBuf,
106        tool_name: &str,
107        metadata: HashMap<String, String>,
108    ) -> Result<()> {
109        // Compute file hash
110        let source_hash = if file_path.exists() {
111            let content = std::fs::read(&file_path)?;
112            let mut hasher = Sha256::new();
113            hasher.update(&content);
114            format!("{:x}", hasher.finalize())
115        } else {
116            String::new()
117        };
118        
119        let info = GeneratedFileInfo::new(
120            file_path.clone(),
121            tool_name.to_string(),
122            source_hash,
123            metadata,
124        );
125        
126        self.tracked_files.insert(file_path.clone(), info);
127        self.save_index()?;
128        
129        tracing::debug!("Tracked generated file: {:?}", file_path);
130        Ok(())
131    }
132    
133    /// Get all files generated by a specific tool
134    pub fn get_files_by_tool(&self, tool_name: &str) -> Vec<PathBuf> {
135        self.tracked_files
136            .values()
137            .filter(|info| info.tool_name == tool_name)
138            .map(|info| info.file_path.clone())
139            .collect()
140    }
141    
142    /// Get file info
143    pub fn get_file_info(&self, path: &Path) -> Option<&GeneratedFileInfo> {
144        self.tracked_files.get(path)
145    }
146    
147    /// Check if a file is tracked as generated
148    pub fn is_tracked(&self, path: &Path) -> bool {
149        self.tracked_files.contains_key(path)
150    }
151    
152    /// Get all tracked files
153    pub fn get_all_files(&self) -> Vec<PathBuf> {
154        self.tracked_files.keys().cloned().collect()
155    }
156    
157    /// Remove a file from tracking (without deleting the actual file)
158    pub fn untrack_file(&mut self, path: &Path) -> Result<()> {
159        if self.tracked_files.remove(path).is_some() {
160            self.save_index()?;
161            tracing::debug!("Untracked file: {:?}", path);
162        }
163        Ok(())
164    }
165    
166    /// Cleanup all files generated by a tool
167    pub async fn cleanup_tool_files(&mut self, tool_name: &str) -> Result<Vec<PathBuf>> {
168        let files_to_remove: Vec<PathBuf> = self.get_files_by_tool(tool_name);
169        let mut removed = Vec::new();
170        
171        for file in &files_to_remove {
172            if file.exists() {
173                tokio::fs::remove_file(file).await
174                    .context(format!("Failed to remove file: {:?}", file))?;
175                removed.push(file.clone());
176                tracing::info!("Removed generated file: {:?}", file);
177            }
178            
179            self.tracked_files.remove(file);
180        }
181        
182        if !removed.is_empty() {
183            self.save_index()?;
184        }
185        
186        Ok(removed)
187    }
188    
189    /// Check all tracked files for modifications
190    pub async fn check_modifications(&mut self) -> Result<Vec<PathBuf>> {
191        let mut modified = Vec::new();
192        
193        for (path, info) in &mut self.tracked_files {
194            if info.check_modified().await? {
195                modified.push(path.clone());
196            }
197        }
198        
199        if !modified.is_empty() {
200            self.save_index()?;
201        }
202        
203        Ok(modified)
204    }
205    
206    /// Get statistics about tracked files
207    pub fn stats(&self) -> TrackingStats {
208        let mut by_tool: HashMap<String, usize> = HashMap::new();
209        
210        for info in self.tracked_files.values() {
211            *by_tool.entry(info.tool_name.clone()).or_insert(0) += 1;
212        }
213        
214        let modified_count = self.tracked_files
215            .values()
216            .filter(|info| info.modified)
217            .count();
218        
219        TrackingStats {
220            total_files: self.tracked_files.len(),
221            files_by_tool: by_tool,
222            modified_files: modified_count,
223        }
224    }
225    
226    /// Save the tracking index to disk
227    fn save_index(&self) -> Result<()> {
228        let content = serde_json::to_string_pretty(&self.tracked_files)?;
229        std::fs::write(&self.index_path, content)?;
230        Ok(())
231    }
232}
233
234/// Statistics about tracked files
235#[derive(Debug, Serialize, Deserialize)]
236pub struct TrackingStats {
237    pub total_files: usize,
238    pub files_by_tool: HashMap<String, usize>,
239    pub modified_files: usize,
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245    use tempfile::TempDir;
246    
247    #[tokio::test]
248    async fn test_track_file() {
249        let temp_dir = TempDir::new().unwrap();
250        let mut tracker = GeneratedCodeTracker::new(temp_dir.path()).unwrap();
251        
252        let test_file = temp_dir.path().join("test.txt");
253        std::fs::write(&test_file, "test content").unwrap();
254        
255        let mut metadata = HashMap::new();
256        metadata.insert("version".to_string(), "1.0.0".to_string());
257        
258        tracker.track_file(test_file.clone(), "test-tool", metadata).unwrap();
259        
260        assert!(tracker.is_tracked(&test_file));
261        let files = tracker.get_files_by_tool("test-tool");
262        assert_eq!(files.len(), 1);
263    }
264    
265    #[tokio::test]
266    async fn test_cleanup_tool_files() {
267        let temp_dir = TempDir::new().unwrap();
268        let mut tracker = GeneratedCodeTracker::new(temp_dir.path()).unwrap();
269        
270        let test_file = temp_dir.path().join("test.txt");
271        std::fs::write(&test_file, "test content").unwrap();
272        
273        tracker.track_file(test_file.clone(), "test-tool", HashMap::new()).unwrap();
274        
275        let removed = tracker.cleanup_tool_files("test-tool").await.unwrap();
276        
277        assert_eq!(removed.len(), 1);
278        assert!(!test_file.exists());
279        assert!(!tracker.is_tracked(&test_file));
280    }
281    
282    #[tokio::test]
283    async fn test_modification_detection() {
284        let temp_dir = TempDir::new().unwrap();
285        let mut tracker = GeneratedCodeTracker::new(temp_dir.path()).unwrap();
286        
287        let test_file = temp_dir.path().join("test.txt");
288        std::fs::write(&test_file, "original content").unwrap();
289        
290        tracker.track_file(test_file.clone(), "test-tool", HashMap::new()).unwrap();
291        
292        // Modify the file
293        std::fs::write(&test_file, "modified content").unwrap();
294        
295        let modified = tracker.check_modifications().await.unwrap();
296        assert_eq!(modified.len(), 1);
297        assert_eq!(modified[0], test_file);
298    }
299}