Skip to main content

git_warp/
rewrite.rs

1use crate::error::Result;
2use ignore::{WalkBuilder, DirEntry};
3use rayon::prelude::*;
4use std::path::{Path, PathBuf};
5use std::fs;
6
7pub struct PathRewriter {
8    src_path: PathBuf,
9    dest_path: PathBuf,
10}
11
12impl PathRewriter {
13    pub fn new<P: AsRef<Path>, Q: AsRef<Path>>(src_path: P, dest_path: Q) -> Self {
14        Self {
15            src_path: src_path.as_ref().to_path_buf(),
16            dest_path: dest_path.as_ref().to_path_buf(),
17        }
18    }
19    
20    /// Rewrite absolute paths in gitignored files
21    pub fn rewrite_paths(&self) -> Result<()> {
22        let src_str = self.src_path.to_string_lossy();
23        let dest_str = self.dest_path.to_string_lossy();
24        
25        // Build a list of files to process
26        let files: Vec<PathBuf> = WalkBuilder::new(&self.dest_path)
27            .hidden(false) // Process hidden files
28            .git_ignore(true) // Respect gitignore
29            .build()
30            .filter_map(|entry| {
31                match entry {
32                    Ok(entry) => {
33                        if entry.file_type()?.is_file() {
34                            Some(entry.path().to_path_buf())
35                        } else {
36                            None
37                        }
38                    }
39                    Err(_) => None,
40                }
41            })
42            .collect();
43        
44        // Process files in parallel
45        files.par_iter().for_each(|file_path| {
46            if let Err(e) = self.rewrite_file(file_path, &src_str, &dest_str) {
47                log::warn!("Failed to rewrite paths in {}: {}", file_path.display(), e);
48            }
49        });
50        
51        Ok(())
52    }
53    
54    /// Rewrite paths in a single file
55    fn rewrite_file(&self, file_path: &Path, src_str: &str, dest_str: &str) -> Result<()> {
56        // Read file content
57        let content = match fs::read_to_string(file_path) {
58            Ok(content) => content,
59            Err(_) => {
60                // Skip binary files or files we can't read as UTF-8
61                return Ok(());
62            }
63        };
64        
65        // Check if file contains the source path
66        if !content.contains(src_str) {
67            return Ok(());
68        }
69        
70        // Skip files that are likely binary
71        if self.is_likely_binary(&content) {
72            return Ok(());
73        }
74        
75        // Replace paths
76        let new_content = content.replace(src_str, dest_str);
77        
78        // Write back if content changed
79        if new_content != content {
80            fs::write(file_path, new_content)?;
81            log::debug!("Rewrote paths in: {}", file_path.display());
82        }
83        
84        Ok(())
85    }
86    
87    /// Simple heuristic to detect binary files
88    fn is_likely_binary(&self, content: &str) -> bool {
89        // Check for null bytes (common in binary files)
90        content.contains('\0') ||
91        // Check for very high ratio of non-printable characters
92        {
93            let total = content.len();
94            if total == 0 {
95                return false;
96            }
97            
98            let printable = content.chars()
99                .filter(|c| c.is_ascii_graphic() || c.is_ascii_whitespace())
100                .count();
101            
102            let printable_ratio = printable as f64 / total as f64;
103            printable_ratio < 0.95
104        }
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111    use tempfile::tempdir;
112    use std::fs;
113    
114    #[test]
115    fn test_path_rewriting() {
116        let temp_dir = tempdir().unwrap();
117        let src_dir = temp_dir.path().join("src");
118        let dest_dir = temp_dir.path().join("dest");
119        
120        // Create source and destination directories
121        fs::create_dir_all(&src_dir).unwrap();
122        fs::create_dir_all(&dest_dir).unwrap();
123        
124        // Create a test file with absolute paths
125        let test_content = format!("export PATH=\"{}:$PATH\"", src_dir.display());
126        fs::write(dest_dir.join("activate.sh"), &test_content).unwrap();
127        
128        // Create .gitignore to ensure file is processed
129        fs::write(dest_dir.join(".gitignore"), "activate.sh").unwrap();
130        
131        // Run path rewriter
132        let rewriter = PathRewriter::new(&src_dir, &dest_dir);
133        rewriter.rewrite_paths().unwrap();
134        
135        // Check that paths were rewritten
136        let rewritten_content = fs::read_to_string(dest_dir.join("activate.sh")).unwrap();
137        assert!(rewritten_content.contains(&dest_dir.to_string_lossy().to_string()));
138        assert!(!rewritten_content.contains(&src_dir.to_string_lossy().to_string()));
139    }
140    
141    #[test]
142    fn test_binary_detection() {
143        let rewriter = PathRewriter::new("/tmp", "/tmp2");
144        
145        // Text content
146        assert!(!rewriter.is_likely_binary("Hello, world!\nThis is text."));
147        
148        // Binary-like content with null bytes
149        assert!(rewriter.is_likely_binary("Hello\0world"));
150        
151        // Content with many non-printable characters
152        let binary_like: String = (0..100).map(|_| '\x01').collect();
153        assert!(rewriter.is_likely_binary(&binary_like));
154    }
155}