nargo_changes/
file_change.rs1#![warn(missing_docs)]
2
3use filetime::FileTime;
4use nargo_types::{Error, Result, Span};
5use serde::{Deserialize, Serialize};
6use sha2::{Digest, Sha256};
7use std::{collections::HashMap, fs::read, path::{Path, PathBuf}};
8use walkdir::WalkDir;
9
10use crate::types::FileChangeType;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct FileChange {
15 pub path: PathBuf,
17 pub r#type: FileChangeType,
19 pub old_hash: Option<String>,
21 pub new_hash: Option<String>,
23 pub modified_time: Option<u64>,
25}
26
27pub struct FileChangeDetector {
29 pub base_dir: PathBuf,
31 pub include_patterns: Vec<String>,
33 pub exclude_patterns: Vec<String>,
35}
36
37impl FileChangeDetector {
38 pub fn new(base_dir: &Path) -> Self {
40 Self { base_dir: base_dir.to_path_buf(), include_patterns: vec!["**/*".to_string()], exclude_patterns: vec!["target/**".to_string(), ".git/**".to_string(), "node_modules/**".to_string()] }
41 }
42
43 pub fn with_include_patterns(mut self, patterns: Vec<String>) -> Self {
45 self.include_patterns = patterns;
46 self
47 }
48
49 pub fn with_exclude_patterns(mut self, patterns: Vec<String>) -> Self {
51 self.exclude_patterns = patterns;
52 self
53 }
54
55 pub fn compute_file_hash(&self, path: &Path) -> Result<String> {
57 let content = read(path)?;
58 let mut hasher = Sha256::new();
59 hasher.update(&content);
60 let hash = hasher.finalize();
61 Ok(format!("{:x}", hash))
62 }
63
64 pub fn scan_changes(&self, previous_state: Option<&HashMap<PathBuf, String>>) -> Result<Vec<FileChange>> {
66 let mut current_state = HashMap::new();
67 let mut changes = Vec::new();
68
69 for entry in WalkDir::new(&self.base_dir).into_iter().filter_map(|e| e.ok()).filter(|e| e.file_type().is_file()) {
71 let path = entry.path();
72 let relative_path = path.strip_prefix(&self.base_dir).unwrap_or(path);
73 let relative_path_str = relative_path.to_str().unwrap_or("");
74
75 let should_include = self.include_patterns.iter().any(|pattern| glob::Pattern::new(pattern).unwrap().matches(relative_path_str));
77
78 if !should_include {
79 continue;
80 }
81
82 let should_exclude = self.exclude_patterns.iter().any(|pattern| glob::Pattern::new(pattern).unwrap().matches(relative_path_str));
84
85 if should_exclude {
86 continue;
87 }
88
89 let current_hash = self.compute_file_hash(path)?;
91 current_state.insert(relative_path.to_path_buf(), current_hash.clone());
92
93 if let Some(prev_state) = previous_state {
95 if let Some(old_hash) = prev_state.get(relative_path) {
96 if old_hash != ¤t_hash {
97 changes.push(FileChange { path: relative_path.to_path_buf(), r#type: FileChangeType::Modified, old_hash: Some(old_hash.clone()), new_hash: Some(current_hash), modified_time: Some(FileTime::from_last_modification_time(&entry.metadata().map_err(|e| Error::external_error("fs".to_string(), e.to_string(), Span::unknown()))?).unix_seconds() as u64) });
98 }
99 }
100 else {
101 changes.push(FileChange { path: relative_path.to_path_buf(), r#type: FileChangeType::Added, old_hash: None, new_hash: Some(current_hash), modified_time: Some(FileTime::from_last_modification_time(&entry.metadata().map_err(|e| Error::external_error("fs".to_string(), e.to_string(), Span::unknown()))?).unix_seconds() as u64) });
102 }
103 }
104 }
105
106 if let Some(prev_state) = previous_state {
108 for (path, old_hash) in prev_state {
109 if !current_state.contains_key(path) {
110 changes.push(FileChange { path: path.clone(), r#type: FileChangeType::Deleted, old_hash: Some(old_hash.clone()), new_hash: None, modified_time: None });
111 }
112 }
113 }
114
115 Ok(changes)
116 }
117
118 pub fn generate_change_summary(&self, changes: &[FileChange]) -> String {
120 let mut summary = String::new();
121 let mut added = 0;
122 let mut modified = 0;
123 let mut deleted = 0;
124
125 for change in changes {
126 match change.r#type {
127 FileChangeType::Added => added += 1,
128 FileChangeType::Modified => modified += 1,
129 FileChangeType::Deleted => deleted += 1,
130 }
131 }
132
133 summary.push_str(&format!("File changes summary:\n"));
134 summary.push_str(&format!("- Added: {}\n", added));
135 summary.push_str(&format!("- Modified: {}\n", modified));
136 summary.push_str(&format!("- Deleted: {}\n", deleted));
137
138 if !changes.is_empty() {
139 summary.push_str("\nDetailed changes:\n");
140 for change in changes {
141 let change_type_str = match change.r#type {
142 FileChangeType::Added => "Added",
143 FileChangeType::Modified => "Modified",
144 FileChangeType::Deleted => "Deleted",
145 };
146 summary.push_str(&format!("- {}: {}\n", change_type_str, change.path.display()));
147 }
148 }
149
150 summary
151 }
152}