| 1 | |
|
| 2 | |
|
| 3 | |
|
| 4 | |
|
| 5 | |
|
| 6 | |
|
| 7 | |
|
| 8 | |
|
| 9 | |
|
| 10 | |
|
| 11 | |
|
| 12 | |
|
| 13 | |
|
| 14 | | use super::*; |
| 15 | | use ignore::gitignore::{Gitignore, GitignoreBuilder}; |
| 16 | | use ignore::Match; |
| 17 | | use std::sync::{Arc, Mutex}; |
| 18 | |
|
| 19 | |
|
| 20 | |
|
| 21 | | #[derive(Clone, Copy, Debug, PartialEq, Eq)] |
| 22 | | pub(crate) struct Stat { |
| 23 | | pub size: u64, |
| 24 | | pub mtime_ns: u128, |
| 25 | | pub exec: bool, |
| 26 | | } |
| 27 | |
|
| 28 | | impl Stat { |
| 29 | 17187× | pub(crate) fn of(m: &fs::Metadata) -> Stat { |
| 30 | 17187× | Stat { |
| 31 | 17187× | size: m.len(), |
| 32 | 17187× | mtime_ns: super::prompt::mtime_ns(m), |
| 33 | 17187× | exec: super::is_executable(m), |
| 34 | 17187× | } |
| 35 | 17187× | } |
| 36 | | } |
| 37 | |
|
| 38 | |
|
| 39 | | #[derive(Default)] |
| 40 | | pub(crate) struct Listing { |
| 41 | | |
| 42 | | pub files: Vec<(String, PathBuf, Stat)>, |
| 43 | | |
| 44 | | pub symlinks: Vec<(String, Vec<u8>)>, |
| 45 | | |
| 46 | | pub nested: Vec<PathBuf>, |
| 47 | | |
| 48 | | |
| 49 | | |
| 50 | | |
| 51 | | pub dirs: Vec<(String, u128)>, |
| 52 | | } |
| 53 | |
|
| 54 | |
|
| 55 | | #[derive(Clone, Default)] |
| 56 | | struct Rules(Option<Arc<(Gitignore, Rules)>>); |
| 57 | |
|
| 58 | | impl Rules { |
| 59 | | |
| 60 | 3030× | fn enter(&self, dir: &Path, names: &[String]) -> Rules { |
| 61 | | |
| 62 | 3030× | let files: Vec<&str> = [".gitignore", ".ignore", ".oxyignore"] |
| 63 | 3030× | .into_iter() |
| 64 | 61013× | .filter(|f| names.iter().any(|n| n == f)) |
| 65 | 3030× | .collect(); |
| 66 | 3030× | if files.is_empty() { |
| 67 | 2991× | return self.clone(); |
| 68 | 39× | } |
| 69 | 39× | let mut b = GitignoreBuilder::new(dir); |
| 70 | 41× | for f in files { |
| 71 | 41× | |
| 72 | 41× | let _ = b.add(dir.join(f)); |
| 73 | 41× | } |
| 74 | 39× | match b.build() { |
| 75 | 39× | Ok(g) if !g.is_empty() => Rules(Some(Arc::new((g, self.clone())))), |
| 76 | 0× | _ => self.clone(), |
| 77 | | } |
| 78 | 3030× | } |
| 79 | |
|
| 80 | 4277× | fn ignored(&self, path: &Path, is_dir: bool) -> bool { |
| 81 | 4277× | let mut at = &self.0; |
| 82 | 4375× | while let Some(node) = at { |
| 83 | 164× | match node.0.matched(path, is_dir) { |
| 84 | 60× | Match::Ignore(_) => return true, |
| 85 | 6× | Match::Whitelist(_) => return false, |
| 86 | 98× | Match::None => {} |
| 87 | | } |
| 88 | 98× | at = &(node.1).0; |
| 89 | | } |
| 90 | 4211× | false |
| 91 | 4277× | } |
| 92 | | } |
| 93 | |
|
| 94 | | struct Ctx<'a> { |
| 95 | | in_scope: &'a (dyn Fn(&str) -> bool + Sync), |
| 96 | | known: &'a (dyn Fn(&str) -> bool + Sync), |
| 97 | | out: Mutex<Listing>, |
| 98 | | failed: Mutex<Option<String>>, |
| 99 | | } |
| 100 | |
|
| 101 | |
|
| 102 | |
|
| 103 | 4× | pub(crate) fn list(root: &Path, in_scope: &(dyn Fn(&str) -> bool + Sync)) -> Result<Listing> { |
| 104 | 4× | list_knowing(root, in_scope, &|_| false) |
| 105 | 4× | } |
| 106 | |
|
| 107 | |
|
| 108 | |
|
| 109 | |
|
| 110 | |
|
| 111 | 2161× | pub(crate) fn list_knowing( |
| 112 | 2161× | root: &Path, |
| 113 | 2161× | in_scope: &(dyn Fn(&str) -> bool + Sync), |
| 114 | 2161× | known: &(dyn Fn(&str) -> bool + Sync), |
| 115 | 2161× | ) -> Result<Listing> { |
| 116 | 2161× | let ctx = Ctx { |
| 117 | 2161× | in_scope, |
| 118 | 2161× | known, |
| 119 | 2161× | out: Mutex::new(Listing::default()), |
| 120 | 2161× | failed: Mutex::new(None), |
| 121 | 2161× | }; |
| 122 | 2161× | let root_time = fs::metadata(root) |
| 123 | 2161× | .map(|m| super::prompt::mtime_ns(&m)) |
| 124 | 2161× | .unwrap_or(0); |
| 125 | 2161× | rayon::scope(|s| { |
| 126 | 2161× | visit( |
| 127 | 2161× | s, |
| 128 | 2161× | root.to_path_buf(), |
| 129 | 2161× | String::new(), |
| 130 | 2161× | root_time, |
| 131 | | true, |
| 132 | 2161× | Rules::default(), |
| 133 | 2161× | &ctx, |
| 134 | | ) |
| 135 | 2161× | }); |
| 136 | 2161× | if let Some(e) = ctx.failed.into_inner().unwrap_or_else(|e| e.into_inner()) { |
| 137 | 0× | return fail(e); |
| 138 | 2161× | } |
| 139 | 2161× | let mut out = ctx.out.into_inner().unwrap_or_else(|e| e.into_inner()); |
| 140 | | |
| 141 | | { |
| 142 | | use rayon::prelude::*; |
| 143 | 64438× | out.files.par_sort_unstable_by(|a, b| a.0.cmp(&b.0)); |
| 144 | 2161× | out.symlinks.sort_unstable_by(|a, b| a.0.cmp(&b.0)); |
| 145 | 2161× | out.dirs.sort_unstable_by(|a, b| a.0.cmp(&b.0)); |
| 146 | 2161× | out.nested.sort_unstable(); |
| 147 | | } |
| 148 | 2161× | Ok(out) |
| 149 | 2161× | } |
| 150 | |
|
| 151 | | #[allow(clippy::too_many_arguments)] |
| 152 | 3097× | fn visit<'s>( |
| 153 | 3097× | s: &rayon::Scope<'s>, |
| 154 | 3097× | dir: PathBuf, |
| 155 | 3097× | rel: String, |
| 156 | 3097× | mtime: u128, |
| 157 | 3097× | top: bool, |
| 158 | 3097× | rules: Rules, |
| 159 | 3097× | ctx: &'s Ctx<'s>, |
| 160 | 3097× | ) { |
| 161 | 3097× | let read = || -> std::io::Result<Vec<fs::DirEntry>> { fs::read_dir(&dir)?.collect() }; |
| 162 | 3097× | let entries = match read() { |
| 163 | 3097× | Ok(e) => e, |
| 164 | 0× | Err(e) => { |
| 165 | 0× | ctx.failed |
| 166 | 0× | .lock() |
| 167 | 0× | .unwrap_or_else(|e| e.into_inner()) |
| 168 | 0× | .get_or_insert_with(|| format!("{}: {e}", dir.display())); |
| 169 | 0× | return; |
| 170 | | } |
| 171 | | }; |
| 172 | 3097× | let names: Vec<String> = entries |
| 173 | 3097× | .iter() |
| 174 | 20507× | .map(|e| e.file_name().to_string_lossy().into_owned()) |
| 175 | 3097× | .collect(); |
| 176 | 13570× | if !top && names.iter().any(|n| n == OXY_DIR || n == ".git") { |
| 177 | 67× | ctx.out |
| 178 | 67× | .lock() |
| 179 | 67× | .unwrap_or_else(|e| e.into_inner()) |
| 180 | | .nested |
| 181 | 67× | .push(dir); |
| 182 | 67× | return; |
| 183 | 3030× | } |
| 184 | 3030× | let rules = rules.enter(&dir, &names); |
| 185 | 3030× | let mut mine = Listing::default(); |
| 186 | 3030× | mine.dirs.push((rel.clone(), mtime)); |
| 187 | 20369× | for (e, name) in entries.into_iter().zip(names) { |
| 188 | | |
| 189 | | |
| 190 | 20369× | if name == OXY_DIR || name.eq_ignore_ascii_case(".git") { |
| 191 | 2164× | continue; |
| 192 | 18205× | } |
| 193 | 18205× | let Ok(ft) = e.file_type() else { continue }; |
| 194 | 18205× | let path = e.path(); |
| 195 | 18205× | let child = if rel.is_empty() { |
| 196 | 4770× | name |
| 197 | | } else { |
| 198 | 13435× | format!("{rel}/{name}") |
| 199 | | }; |
| 200 | 18205× | if (ft.is_dir() || !(ctx.known)(&child)) && rules.ignored(&path, ft.is_dir()) { |
| 201 | 60× | continue; |
| 202 | 18145× | } |
| 203 | 18145× | if ft.is_dir() { |
| 204 | 936× | let rules = rules.clone(); |
| 205 | | |
| 206 | 936× | let time = e |
| 207 | 936× | .metadata() |
| 208 | 936× | .map(|m| super::prompt::mtime_ns(&m)) |
| 209 | 936× | .unwrap_or(0); |
| 210 | 936× | s.spawn(move |s| visit(s, path, child, time, false, rules, ctx)); |
| 211 | 17209× | } else if !(ctx.in_scope)(&child) { |
| 212 | 17190× | } else if ft.is_symlink() { |
| 213 | 3× | match fs::read_link(&path) { |
| 214 | 3× | Ok(t) => mine |
| 215 | 3× | .symlinks |
| 216 | 3× | .push((child, t.to_string_lossy().as_bytes().to_vec())), |
| 217 | 0× | Err(e) => { |
| 218 | 0× | ctx.failed |
| 219 | 0× | .lock() |
| 220 | 0× | .unwrap_or_else(|e| e.into_inner()) |
| 221 | 0× | .get_or_insert_with(|| format!("{}: {e}", path.display())); |
| 222 | | } |
| 223 | | } |
| 224 | 17187× | } else if ft.is_file() { |
| 225 | 17187× | match e.metadata() { |
| 226 | 17187× | Ok(m) => mine.files.push((child, path, Stat::of(&m))), |
| 227 | 0× | Err(e) => { |
| 228 | 0× | ctx.failed |
| 229 | 0× | .lock() |
| 230 | 0× | .unwrap_or_else(|e| e.into_inner()) |
| 231 | 0× | .get_or_insert_with(|| format!("{}: {e}", path.display())); |
| 232 | | } |
| 233 | | } |
| 234 | 0× | } |
| 235 | | } |
| 236 | 3030× | let mut out = ctx.out.lock().unwrap_or_else(|e| e.into_inner()); |
| 237 | 3030× | out.files.append(&mut mine.files); |
| 238 | 3030× | out.symlinks.append(&mut mine.symlinks); |
| 239 | 3030× | out.dirs.append(&mut mine.dirs); |
| 240 | 3097× | } |
| 241 | |
|
| 242 | | #[cfg(test)] |
| 243 | | mod tests { |
| 244 | | use super::*; |
| 245 | |
|
| 246 | 22× | fn write(root: &Path, rel: &str, text: &str) { |
| 247 | 22× | let p = root.join(rel); |
| 248 | 22× | fs::create_dir_all(p.parent().unwrap()).unwrap(); |
| 249 | 22× | fs::write(p, text).unwrap(); |
| 250 | 22× | } |
| 251 | |
|
| 252 | 2× | fn listed(root: &Path, in_scope: &(dyn Fn(&str) -> bool + Sync)) -> (Vec<String>, Vec<String>) { |
| 253 | 2× | let l = list(root, in_scope).unwrap(); |
| 254 | 2× | let mut files: Vec<String> = l.files.into_iter().map(|(r, _, _)| r).collect(); |
| 255 | 2× | files.extend(l.symlinks.into_iter().map(|(r, _)| r)); |
| 256 | 2× | files.sort(); |
| 257 | 2× | let mut nested: Vec<String> = l |
| 258 | 2× | .nested |
| 259 | 2× | .iter() |
| 260 | 4× | .map(|d| { |
| 261 | 4× | d.strip_prefix(root) |
| 262 | 4× | .unwrap() |
| 263 | 4× | .to_string_lossy() |
| 264 | 4× | .replace('\\', "/") |
| 265 | 4× | }) |
| 266 | 2× | .collect(); |
| 267 | 2× | nested.sort(); |
| 268 | 2× | (files, nested) |
| 269 | 2× | } |
| 270 | |
|
| 271 | | #[test] |
| 272 | 1× | fn ignore_files_nested_repositories_and_scope() { |
| 273 | 1× | let dir = tempfile::tempdir().unwrap(); |
| 274 | 1× | let root = dir.path(); |
| 275 | 1× | write(root, ".oxy/config", ""); |
| 276 | 1× | write(root, ".oxyignore", "*.log\nbuild/\n"); |
| 277 | 1× | write(root, ".hidden", ""); |
| 278 | 1× | write(root, "a.txt", ""); |
| 279 | 1× | write(root, "a.log", ""); |
| 280 | 1× | write(root, "build/out.o", ""); |
| 281 | 1× | write(root, "src/main.c", ""); |
| 282 | 1× | write(root, "src/deep/x.log", ""); |
| 283 | | |
| 284 | 1× | write(root, "keep/.oxyignore", "!*.log\n"); |
| 285 | 1× | write(root, "keep/k.log", ""); |
| 286 | 1× | write(root, "other/.ignore", "secret\n"); |
| 287 | 1× | write(root, "other/secret", ""); |
| 288 | 1× | write(root, "other/public", ""); |
| 289 | | |
| 290 | 1× | write(root, "gitproj/.gitignore", "build/\n*.tmp\nkeep.tmp\n"); |
| 291 | 1× | write(root, "gitproj/.oxyignore", "!keep.tmp\n"); |
| 292 | 1× | write(root, "gitproj/build/out.bin", ""); |
| 293 | 1× | write(root, "gitproj/x.tmp", ""); |
| 294 | 1× | write(root, "gitproj/keep.tmp", ""); |
| 295 | 1× | write(root, "gitproj/src.c", ""); |
| 296 | | |
| 297 | 1× | write(root, "sub/.oxy/config", ""); |
| 298 | 1× | write(root, "sub/inner.txt", ""); |
| 299 | 1× | write(root, "vendor/g/.git/HEAD", ""); |
| 300 | | #[cfg(unix)] |
| 301 | 1× | std::os::unix::fs::symlink("a.txt", root.join("link")).unwrap(); |
| 302 | |
|
| 303 | 1× | let (files, nested) = listed(root, &|_| true); |
| 304 | 1× | let mut want = vec![ |
| 305 | | ".hidden", |
| 306 | 1× | ".oxyignore", |
| 307 | 1× | "a.txt", |
| 308 | 1× | "gitproj/.gitignore", |
| 309 | 1× | "gitproj/.oxyignore", |
| 310 | 1× | "gitproj/keep.tmp", |
| 311 | 1× | "gitproj/src.c", |
| 312 | 1× | "keep/.oxyignore", |
| 313 | 1× | "keep/k.log", |
| 314 | 1× | "other/.ignore", |
| 315 | 1× | "other/public", |
| 316 | 1× | "src/main.c", |
| 317 | | ]; |
| 318 | 1× | if cfg!(unix) { |
| 319 | 1× | want.push("link"); |
| 320 | 1× | } |
| 321 | 1× | want.sort(); |
| 322 | 1× | assert_eq!(files, want); |
| 323 | 1× | assert_eq!(nested, vec!["sub", "vendor/g"]); |
| 324 | |
|
| 325 | | |
| 326 | 13× | let (files, _) = listed(root, &|p: &str| p.starts_with("src/")); |
| 327 | 1× | assert_eq!(files, vec!["src/main.c"]); |
| 328 | 1× | } |
| 329 | | } |