xvc_file/common/
gitignore.rs1use 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
17pub enum IgnoreOperation {
19 IgnoreDir {
21 dir: XvcPath,
23 },
24 IgnoreFile {
26 file: XvcPath,
28 },
29}
30
31pub type IgnoreOp = Option<IgnoreOperation>;
34
35pub 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 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 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
96pub fn update_dir_gitignores(
102 xvc_root: &AbsolutePath,
103 current_gitignore: &IgnoreRules,
104 dirs: &[XvcPath],
105) -> Result<()> {
106 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 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
174pub fn update_file_gitignores(
176 xvc_root: &AbsolutePath,
177 current_gitignore: &IgnoreRules,
178 files: &[XvcPath],
179) -> Result<()> {
180 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}