1use crate::errors::{DnxError, Result};
2use std::collections::HashMap;
3use std::path::{Path, PathBuf};
4
5pub struct PatchManager {
8 project_root: PathBuf,
9 patches: HashMap<String, String>,
11}
12
13impl PatchManager {
14 pub fn new(project_root: PathBuf, patches: HashMap<String, String>) -> Self {
15 Self {
16 project_root,
17 patches,
18 }
19 }
20
21 pub fn prepare_patch(&self, package_name: &str, version: &str) -> Result<PathBuf> {
24 let pkg_key = format!("{}@{}", package_name, version);
25 let node_modules = self.project_root.join("node_modules");
26 let dnx_dir = node_modules.join(".dnx");
27 let pkg_dir = dnx_dir
28 .join(&pkg_key)
29 .join("node_modules")
30 .join(package_name);
31
32 if !pkg_dir.exists() {
33 return Err(DnxError::Linker(format!(
34 "Package {} is not installed. Run `dnx install` first.",
35 pkg_key
36 )));
37 }
38
39 let patch_dir = self.project_root.join(".dnx-patches");
41 std::fs::create_dir_all(&patch_dir)
42 .map_err(|e| DnxError::Io(format!("Failed to create patch directory: {}", e)))?;
43
44 let target = patch_dir.join(&pkg_key);
45 if target.exists() {
46 std::fs::remove_dir_all(&target)
47 .map_err(|e| DnxError::Io(format!("Failed to clean patch directory: {}", e)))?;
48 }
49
50 copy_dir_recursive(&pkg_dir, &target)?;
51
52 Ok(target)
53 }
54
55 pub fn commit_patch(&self, patch_dir: &Path) -> Result<PathBuf> {
59 let dir_name = patch_dir
61 .file_name()
62 .and_then(|n| n.to_str())
63 .ok_or_else(|| DnxError::Io("Invalid patch directory name".to_string()))?;
64
65 let (package_name, version) = parse_pkg_key(dir_name)?;
67
68 let pkg_key = format!("{}@{}", package_name, version);
69 let node_modules = self.project_root.join("node_modules");
70 let dnx_dir = node_modules.join(".dnx");
71 let original_dir = dnx_dir
72 .join(&pkg_key)
73 .join("node_modules")
74 .join(&package_name);
75
76 if !original_dir.exists() {
77 return Err(DnxError::Linker(format!(
78 "Original package {} not found",
79 pkg_key
80 )));
81 }
82
83 let diff = generate_diff(&original_dir, patch_dir, &package_name)?;
85
86 if diff.is_empty() {
87 return Err(DnxError::Linker(
88 "No changes detected. Make changes to the package before committing.".to_string(),
89 ));
90 }
91
92 let patches_dir = self.project_root.join("patches");
94 std::fs::create_dir_all(&patches_dir)
95 .map_err(|e| DnxError::Io(format!("Failed to create patches directory: {}", e)))?;
96
97 let patch_filename = format!("{}@{}.patch", package_name.replace('/', "+"), version);
98 let patch_path = patches_dir.join(&patch_filename);
99 std::fs::write(&patch_path, &diff)
100 .map_err(|e| DnxError::Io(format!("Failed to write patch file: {}", e)))?;
101
102 let _ = std::fs::remove_dir_all(patch_dir);
104
105 Ok(patch_path)
106 }
107
108 pub fn apply_patches(&self) -> Result<PatchStats> {
111 let mut stats = PatchStats::default();
112
113 for (pkg_key, patch_file) in &self.patches {
114 let patch_path = self.project_root.join(patch_file);
115 if !patch_path.exists() {
116 eprintln!(
117 "\x1b[33m⚠ Patch file not found: {}\x1b[0m",
118 patch_path.display()
119 );
120 stats.failed += 1;
121 continue;
122 }
123
124 let (package_name, _version) = match parse_pkg_key(pkg_key) {
125 Ok(v) => v,
126 Err(_) => {
127 stats.failed += 1;
128 continue;
129 }
130 };
131
132 let node_modules = self.project_root.join("node_modules");
133 let dnx_dir = node_modules.join(".dnx");
134 let pkg_dir = dnx_dir
135 .join(pkg_key)
136 .join("node_modules")
137 .join(&package_name);
138
139 if !pkg_dir.exists() {
140 stats.failed += 1;
141 continue;
142 }
143
144 match apply_patch_file(&patch_path, &pkg_dir) {
145 Ok(()) => stats.applied += 1,
146 Err(e) => {
147 eprintln!(
148 "\x1b[33m⚠ Failed to apply patch for {}: {}\x1b[0m",
149 pkg_key, e
150 );
151 stats.failed += 1;
152 }
153 }
154 }
155
156 Ok(stats)
157 }
158}
159
160#[derive(Debug, Default)]
161pub struct PatchStats {
162 pub applied: usize,
163 pub failed: usize,
164}
165
166fn parse_pkg_key(key: &str) -> Result<(String, String)> {
169 if let Some(stripped) = key.strip_prefix('@') {
170 if let Some(idx) = stripped.find('@') {
172 return Ok((key[..idx + 1].to_string(), stripped[idx + 1..].to_string()));
173 }
174 } else if let Some(idx) = key.find('@') {
175 return Ok((key[..idx].to_string(), key[idx + 1..].to_string()));
176 }
177 Err(DnxError::Linker(format!("Invalid package key: {}", key)))
178}
179
180fn generate_diff(original: &Path, modified: &Path, _pkg_name: &str) -> Result<String> {
182 let mut diff = String::new();
183
184 for entry in walkdir::WalkDir::new(modified)
186 .into_iter()
187 .filter_map(|e| e.ok())
188 {
189 if !entry.file_type().is_file() {
190 continue;
191 }
192
193 let relative = entry
194 .path()
195 .strip_prefix(modified)
196 .map_err(|e| DnxError::Io(format!("Failed to get relative path: {}", e)))?;
197
198 let original_file = original.join(relative);
199 let modified_file = entry.path();
200
201 let original_content = if original_file.exists() {
202 std::fs::read_to_string(&original_file).unwrap_or_default()
203 } else {
204 String::new()
205 };
206
207 let modified_content = std::fs::read_to_string(modified_file).unwrap_or_default();
208
209 if original_content != modified_content {
210 let relative_str = relative.to_string_lossy().replace('\\', "/");
211 diff.push_str(&format!("--- a/{}\n", relative_str));
212 diff.push_str(&format!("+++ b/{}\n", relative_str));
213
214 let orig_lines: Vec<&str> = original_content.lines().collect();
216 let mod_lines: Vec<&str> = modified_content.lines().collect();
217
218 diff.push_str(&format!(
219 "@@ -1,{} +1,{} @@\n",
220 orig_lines.len(),
221 mod_lines.len()
222 ));
223
224 for line in &orig_lines {
225 diff.push_str(&format!("-{}\n", line));
226 }
227 for line in &mod_lines {
228 diff.push_str(&format!("+{}\n", line));
229 }
230 }
231 }
232
233 for entry in walkdir::WalkDir::new(original)
235 .into_iter()
236 .filter_map(|e| e.ok())
237 {
238 if !entry.file_type().is_file() {
239 continue;
240 }
241 let relative = entry
242 .path()
243 .strip_prefix(original)
244 .map_err(|e| DnxError::Io(format!("Failed to get relative path: {}", e)))?;
245 let modified_file = modified.join(relative);
246 if !modified_file.exists() {
247 let relative_str = relative.to_string_lossy().replace('\\', "/");
248 diff.push_str(&format!("--- a/{}\n", relative_str));
249 diff.push_str("+++ /dev/null\n");
250 let content = std::fs::read_to_string(entry.path()).unwrap_or_default();
251 let lines: Vec<&str> = content.lines().collect();
252 diff.push_str(&format!("@@ -1,{} +0,0 @@\n", lines.len()));
253 for line in &lines {
254 diff.push_str(&format!("-{}\n", line));
255 }
256 }
257 }
258
259 Ok(diff)
260}
261
262fn apply_patch_file(patch_path: &Path, pkg_dir: &Path) -> Result<()> {
265 let patch_content = std::fs::read_to_string(patch_path)
266 .map_err(|e| DnxError::Io(format!("Failed to read patch file: {}", e)))?;
267
268 let mut current_file: Option<PathBuf> = None;
269 let mut new_content = String::new();
270 let mut in_hunk = false;
271
272 for line in patch_content.lines() {
273 if let Some(relative) = line.strip_prefix("+++ b/") {
274 if let Some(ref file_path) = current_file {
276 if let Some(parent) = file_path.parent() {
277 std::fs::create_dir_all(parent)
278 .map_err(|e| DnxError::Io(format!("Failed to create directory: {}", e)))?;
279 }
280 std::fs::write(file_path, &new_content)
281 .map_err(|e| DnxError::Io(format!("Failed to write patched file: {}", e)))?;
282 }
283
284 if relative == "/dev/null" {
285 current_file = None;
286 } else {
287 current_file = Some(pkg_dir.join(relative));
288 }
289 new_content.clear();
290 in_hunk = false;
291 } else if line.starts_with("--- a/") {
292 } else if line.starts_with("@@") {
294 in_hunk = true;
295 } else if in_hunk {
296 if let Some(stripped) = line.strip_prefix('+') {
297 new_content.push_str(stripped);
298 new_content.push('\n');
299 } else if line.starts_with('-') {
300 } else if let Some(stripped) = line.strip_prefix(' ') {
302 new_content.push_str(stripped);
303 new_content.push('\n');
304 }
305 }
306 }
307
308 if let Some(ref file_path) = current_file {
310 if let Some(parent) = file_path.parent() {
311 std::fs::create_dir_all(parent)
312 .map_err(|e| DnxError::Io(format!("Failed to create directory: {}", e)))?;
313 }
314 std::fs::write(file_path, &new_content)
315 .map_err(|e| DnxError::Io(format!("Failed to write patched file: {}", e)))?;
316 }
317
318 let patch_content2 = std::fs::read_to_string(patch_path).unwrap_or_default();
320 let mut delete_next = false;
321 for line in patch_content2.lines() {
322 if line.starts_with("+++ /dev/null") {
323 delete_next = true;
324 } else if delete_next && line.starts_with("--- a/") {
325 let relative = &line[6..];
326 let file_path = pkg_dir.join(relative);
327 let _ = std::fs::remove_file(&file_path);
328 delete_next = false;
329 } else {
330 delete_next = false;
331 }
332 }
333
334 Ok(())
335}
336
337fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<()> {
339 std::fs::create_dir_all(dst).map_err(|e| {
340 DnxError::Io(format!(
341 "Failed to create directory {}: {}",
342 dst.display(),
343 e
344 ))
345 })?;
346
347 for entry in std::fs::read_dir(src)
348 .map_err(|e| DnxError::Io(format!("Failed to read directory {}: {}", src.display(), e)))?
349 {
350 let entry = entry.map_err(|e| DnxError::Io(format!("Failed to read entry: {}", e)))?;
351 let src_path = entry.path();
352 let dst_path = dst.join(entry.file_name());
353
354 if entry
355 .file_type()
356 .map_err(|e| DnxError::Io(format!("Failed to get file type: {}", e)))?
357 .is_dir()
358 {
359 copy_dir_recursive(&src_path, &dst_path)?;
360 } else {
361 std::fs::copy(&src_path, &dst_path).map_err(|e| {
362 DnxError::Io(format!(
363 "Failed to copy {} to {}: {}",
364 src_path.display(),
365 dst_path.display(),
366 e
367 ))
368 })?;
369 }
370 }
371
372 Ok(())
373}