ruplacer/
directory_patcher.rs

1use anyhow::{Context, Result};
2use std::path::Path;
3
4use crate::console::Console;
5use crate::file_patcher::FilePatcher;
6use crate::query::Query;
7use crate::settings::Settings;
8use crate::stats::Stats;
9
10#[derive(Debug)]
11/// Used to run replacement query on every text file present in a given path
12/// ```rust
13/// use ruplacer::{Console, DirectoryPatcher, Query, Settings, Stats};
14/// use std::path::PathBuf;
15///
16/// let settings = Settings{
17///     dry_run: true,
18///     .. Default::default()
19/// };
20/// let path = PathBuf::from("tests/data");
21/// let console = Console::new();
22/// let mut directory_patcher = DirectoryPatcher::new(&console, &path, &settings);
23///
24/// let query = Query::simple("old", "new");
25/// directory_patcher.run(&query).unwrap();
26/// let stats = directory_patcher.stats();
27/// println!("Found {} matching lines", stats.matching_lines());
28/// ```
29// Note: keep the dry_run: true in the doc test above or the integration test
30// will fail ...
31pub struct DirectoryPatcher<'a> {
32    path: &'a Path,
33    settings: &'a Settings,
34    console: &'a Console,
35    stats: Stats,
36}
37
38impl<'a> DirectoryPatcher<'a> {
39    pub fn new(
40        console: &'a Console,
41        path: &'a Path,
42        settings: &'a Settings,
43    ) -> DirectoryPatcher<'a> {
44        let stats = Stats::default();
45        DirectoryPatcher {
46            console,
47            path,
48            settings,
49            stats,
50        }
51    }
52
53    /// Run the given query on the selected files in self.path
54    pub fn run(&mut self, query: &Query) -> Result<()> {
55        let walker = self.build_walker()?;
56        for entry in walker {
57            let entry = entry.with_context(|| "Could not read directory entry")?;
58            if let Some(file_type) = entry.file_type() {
59                if file_type.is_file() {
60                    self.patch_file(entry.path(), query)?;
61                }
62            }
63        }
64        Ok(())
65    }
66
67    pub fn stats(self) -> Stats {
68        self.stats
69    }
70
71    pub(crate) fn patch_file(&mut self, entry: &Path, query: &Query) -> Result<()> {
72        let file_patcher = FilePatcher::new(self.console, entry, query)?;
73        let file_patcher = match file_patcher {
74            None => return Ok(()),
75            Some(f) => f,
76        };
77        let num_replacements = file_patcher.num_replacements();
78        if num_replacements != 0 {
79            self.console.print_message("\n");
80        }
81        let num_lines = file_patcher.num_lines();
82        self.stats.update(num_lines, num_replacements);
83        if self.settings.dry_run {
84            return Ok(());
85        }
86        file_patcher.run()?;
87        Ok(())
88    }
89
90    fn build_walker(&self) -> Result<ignore::Walk> {
91        let mut types_builder = ignore::types::TypesBuilder::new();
92        types_builder.add_defaults();
93        let mut count: u32 = 0;
94        for t in &self.settings.selected_file_types {
95            // Check if filter is file type or glob pattern
96            if t.contains('*') {
97                let new_type = format!("type{}", count);
98                // Note: .add(name, glob) only returns error with wrong name, hence unwrap()
99                types_builder.add(&new_type, t).unwrap();
100                types_builder.select(&new_type);
101                count += 1;
102            } else {
103                types_builder.select(t);
104            }
105        }
106        for t in &self.settings.ignored_file_types {
107            // Check if filter is file type or glob pattern
108            if t.contains('*') {
109                let new_type = format!("type{}", count);
110                // Note: .add(name, glob) only returns error with wrong name, hence unwrap()
111                types_builder.add(&new_type, t).unwrap();
112                types_builder.negate(&new_type);
113                count += 1;
114            } else {
115                types_builder.negate(t);
116            }
117        }
118        let types_matcher = types_builder.build()?;
119        let mut walk_builder = ignore::WalkBuilder::new(self.path);
120        walk_builder.types(types_matcher);
121        // Note: the walk_builder configures the "ignore" settings of the Walker,
122        // hence the negations
123        if self.settings.ignored {
124            walk_builder.ignore(false);
125        }
126        if self.settings.hidden {
127            walk_builder.hidden(false);
128        }
129        Ok(walk_builder.build())
130    }
131}