dx_forge/core/
tracking.rs1use 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#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct GeneratedFileInfo {
17 pub file_path: PathBuf,
19
20 pub tool_name: String,
22
23 pub generated_at: DateTime<Utc>,
25
26 pub source_hash: String,
28
29 pub metadata: HashMap<String, String>,
31
32 pub modified: bool,
34}
35
36impl GeneratedFileInfo {
37 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 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
71pub struct GeneratedCodeTracker {
73 _db: Database,
74 tracked_files: HashMap<PathBuf, GeneratedFileInfo>,
75 index_path: PathBuf,
76}
77
78impl GeneratedCodeTracker {
79 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 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 pub fn track_file(
104 &mut self,
105 file_path: PathBuf,
106 tool_name: &str,
107 metadata: HashMap<String, String>,
108 ) -> Result<()> {
109 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 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 pub fn get_file_info(&self, path: &Path) -> Option<&GeneratedFileInfo> {
144 self.tracked_files.get(path)
145 }
146
147 pub fn is_tracked(&self, path: &Path) -> bool {
149 self.tracked_files.contains_key(path)
150 }
151
152 pub fn get_all_files(&self) -> Vec<PathBuf> {
154 self.tracked_files.keys().cloned().collect()
155 }
156
157 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 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 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 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 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#[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 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}