xvc_file/common/
gitignore.rs

1//! File and directory ignore handler
2use chrono::Utc;
3use crossbeam_channel::Sender;
4use relative_path::RelativePathBuf;
5use std::collections::HashMap;
6use std::fs::OpenOptions;
7use std::io::Write;
8
9use std::thread::JoinHandle;
10use xvc_core::util::git::build_gitignore;
11
12use crate::{Result, CHANNEL_CAPACITY};
13use xvc_core::{debug, error, info, uwr, XvcOutputSender};
14use xvc_core::{AbsolutePath, IgnoreRules, MatchResult};
15use xvc_core::{XvcPath, XvcRoot};
16
17/// Used to signal ignored files and directories to the ignore handler
18pub enum IgnoreOperation {
19    /// Ignore a directory
20    IgnoreDir {
21        /// The directory to ignore
22        dir: XvcPath,
23    },
24    /// Ignore a file
25    IgnoreFile {
26        /// The file to ignore
27        file: XvcPath,
28    },
29}
30
31/// Used to signal ignored files and directories to the ignore handler
32/// If None is sent, the ignore handler quits
33pub type IgnoreOp = Option<IgnoreOperation>;
34
35/// Spawn a thread that writes ignored files and directories to .gitignore
36///
37/// Control the thread by sending [IgnoreOperation]s to the ignore handler.
38pub fn make_ignore_handler(
39    output_snd: &XvcOutputSender,
40    xvc_root: &XvcRoot,
41) -> Result<(Sender<IgnoreOp>, JoinHandle<()>)> {
42    let (sender, receiver) = crossbeam_channel::bounded(CHANNEL_CAPACITY);
43    let output_snd = output_snd.clone();
44    let xvc_root = xvc_root.absolute_path().clone();
45
46    let handle = std::thread::spawn(move || {
47        let mut ignore_dirs = Vec::<XvcPath>::new();
48        let mut ignore_files = Vec::<XvcPath>::new();
49
50        let gitignore = build_gitignore(&xvc_root).unwrap();
51        for op in receiver {
52            if let Some(op) = op {
53                match op {
54                    IgnoreOperation::IgnoreDir { dir } => {
55                        let path = dir.to_absolute_path(&xvc_root).to_path_buf();
56
57                        if !ignore_dirs.contains(&dir)
58                            && matches!(gitignore.check(&path), MatchResult::NoMatch)
59                        {
60                            ignore_dirs.push(dir);
61                        }
62                    }
63                    IgnoreOperation::IgnoreFile { file } => {
64                        let path = file.to_absolute_path(&xvc_root).to_path_buf();
65                        if !ignore_files.contains(&file)
66                            && matches!(gitignore.check(&path), MatchResult::NoMatch)
67                        {
68                            ignore_files.push(file);
69                        }
70                    }
71                }
72            } else {
73                // We quit the loop when we get None
74                break;
75            }
76        }
77        debug!(output_snd, "Writing directories to .gitignore");
78
79        uwr!(
80            update_dir_gitignores(&xvc_root, &gitignore, &ignore_dirs),
81            output_snd
82        );
83
84        // Load again to get ignored directories
85        let gitignore = build_gitignore(&xvc_root).unwrap();
86        debug!(output_snd, "Writing files to .gitignore");
87        uwr!(
88            update_file_gitignores(&xvc_root, &gitignore, &ignore_files),
89            output_snd
90        );
91    });
92
93    Ok((sender, handle))
94}
95
96/// Write file and directory names to .gitignore found in the same dir
97///
98/// If `current_ignore` already ignores a file, it's not added separately.
99/// If the user chooses to ignore a files manually by general rules, files are not added here.
100///
101pub fn update_dir_gitignores(
102    xvc_root: &AbsolutePath,
103    current_gitignore: &IgnoreRules,
104    dirs: &[XvcPath],
105) -> Result<()> {
106    // Check if dirs are already ignored
107    let dirs: Vec<XvcPath> = dirs
108        .iter()
109        .filter_map(|dir| {
110            let abs_path = if dir.ends_with("/") {
111                xvc_root.join(dir.to_string())
112            } else {
113                xvc_root.join(format!("{}/", dir))
114            };
115
116            let ignore_res = current_gitignore.check(&abs_path);
117
118            match ignore_res {
119                MatchResult::Ignore => {
120                    info!("Path is already gitignored: {}", abs_path.to_string_lossy());
121                    None
122                }
123                MatchResult::NoMatch => {
124                    Some(dir.clone())
125                }
126                MatchResult::Whitelist => {
127                    error!("Path is whitelisted in Git. Please remove/modify the whitelisting rule: {}",
128                        abs_path.to_string_lossy());
129                    None
130                }
131            }}).collect();
132
133    // Check if files are already ignored
134    let mut changes = HashMap::<RelativePathBuf, Vec<String>>::new();
135
136    for dir in dirs {
137        let gi = dir
138            .parent()
139            .map(|p| p.join(".gitignore"))
140            .unwrap_or_else(|| RelativePathBuf::from(".gitignore"));
141
142        if !changes.contains_key(&gi) {
143            changes.insert(gi.clone(), Vec::<String>::new());
144        }
145
146        let path_v = changes.get_mut(&gi).unwrap();
147        path_v.push(
148            dir.file_name()
149                .map(|d| format!("/{}/", d))
150                .unwrap_or_else(|| "## Path Contains final ..".to_string()),
151        );
152    }
153
154    for (gitignore_file, values) in changes {
155        let append_str = format!(
156            "### Following {} lines are added by xvc on {}\n{}",
157            values.len(),
158            Utc::now().to_rfc2822(),
159            values.join("\n")
160        );
161        let gitignore_path = gitignore_file.to_path(xvc_root);
162
163        let mut file_o = OpenOptions::new()
164            .create(true)
165            .append(true)
166            .open(gitignore_path)?;
167
168        writeln!(file_o, "{}", append_str)?;
169    }
170
171    Ok(())
172}
173
174/// Write file and directory names to .gitignore found in the same dir
175pub fn update_file_gitignores(
176    xvc_root: &AbsolutePath,
177    current_gitignore: &IgnoreRules,
178    files: &[XvcPath],
179) -> Result<()> {
180    // Filter already ignored files
181    let files: Vec<XvcPath> = files.iter().filter_map(|f| match current_gitignore.check(&f.to_absolute_path(xvc_root)) {
182                MatchResult::NoMatch => {
183                    Some(f.clone())
184                }
185                MatchResult::Ignore => {
186                    info!("Already gitignored: {}", f.to_string());
187                    None
188                }
189                MatchResult::Whitelist => {
190                    error!("Path is whitelisted in Gitignore, please modify/remove the whitelisting rule: {}", f.to_string());
191                None
192            }}).collect();
193
194    let mut changes = HashMap::<RelativePathBuf, Vec<String>>::new();
195
196    for f in files {
197        let gi = f
198            .parent()
199            .map(|p| p.join(".gitignore"))
200            .unwrap_or_else(|| RelativePathBuf::from(".gitignore"));
201
202        if !changes.contains_key(&gi) {
203            changes.insert(gi.clone(), Vec::<String>::new());
204        }
205
206        let path_v = changes.get_mut(&gi).unwrap();
207        path_v.push(
208            f.file_name()
209                .map(|f| format!("/{}", f))
210                .unwrap_or_else(|| "## Path Contains final ..".to_string()),
211        );
212    }
213
214    for (gitignore_file, values) in changes {
215        let append_str = format!(
216            "### Following {} lines are added by xvc on {}\n{}",
217            values.len(),
218            Utc::now().to_rfc2822(),
219            values.join("\n")
220        );
221        let gitignore_path = gitignore_file.to_path(xvc_root);
222
223        let mut file_o = OpenOptions::new()
224            .create(true)
225            .append(true)
226            .open(gitignore_path)?;
227
228        writeln!(file_o, "{}", append_str)?;
229    }
230
231    Ok(())
232}