Skip to main content

nargo_changes/
file_change.rs

1#![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/// File change information.
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct FileChange {
15    /// The path to the file.
16    pub path: PathBuf,
17    /// The type of change.
18    pub r#type: FileChangeType,
19    /// The previous hash (if applicable).
20    pub old_hash: Option<String>,
21    /// The new hash (if applicable).
22    pub new_hash: Option<String>,
23    /// The modification time (seconds since epoch).
24    pub modified_time: Option<u64>,
25}
26
27/// File change detector.
28pub struct FileChangeDetector {
29    /// The base directory to scan for changes.
30    pub base_dir: PathBuf,
31    /// The list of file patterns to include.
32    pub include_patterns: Vec<String>,
33    /// The list of file patterns to exclude.
34    pub exclude_patterns: Vec<String>,
35}
36
37impl FileChangeDetector {
38    /// Creates a new file change detector.
39    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    /// Sets the include patterns.
44    pub fn with_include_patterns(mut self, patterns: Vec<String>) -> Self {
45        self.include_patterns = patterns;
46        self
47    }
48
49    /// Sets the exclude patterns.
50    pub fn with_exclude_patterns(mut self, patterns: Vec<String>) -> Self {
51        self.exclude_patterns = patterns;
52        self
53    }
54
55    /// Computes the hash of a file.
56    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    /// Scans for file changes compared to a previous state.
65    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        // Scan all files in the base directory
70        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            // Check if the file should be included
76            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            // Check if the file should be excluded
83            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            // Compute the current hash
90            let current_hash = self.compute_file_hash(path)?;
91            current_state.insert(relative_path.to_path_buf(), current_hash.clone());
92
93            // Check if this is a new file or a modified file
94            if let Some(prev_state) = previous_state {
95                if let Some(old_hash) = prev_state.get(relative_path) {
96                    if old_hash != &current_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        // Check for deleted files
107        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    /// Generates a summary of the changes.
119    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}