Recompute ignore status when .gitignore changes or for new entries

This commit is contained in:
Antonio Scandurra 2021-04-22 15:14:23 +02:00
parent 499e55e950
commit af3bc236b7
2 changed files with 189 additions and 21 deletions

View file

@ -17,11 +17,12 @@ use postage::{
};
use smol::{channel::Sender, Timer};
use std::{
collections::{HashMap, HashSet},
collections::{BTreeMap, HashMap, HashSet},
ffi::OsStr,
fmt, fs,
future::Future,
io::{self, Read, Write},
mem,
ops::{AddAssign, Deref},
os::unix::fs::MetadataExt,
path::{Path, PathBuf},
@ -30,6 +31,7 @@ use std::{
};
const GITIGNORE: &'static str = ".gitignore";
const EDIT_BATCH_LEN: usize = 2000;
#[derive(Clone, Debug)]
enum ScanState {
@ -58,6 +60,7 @@ impl Worktree {
let id = ctx.model_id();
let snapshot = Snapshot {
id,
scan_id: 0,
path: path.into(),
root_inode: None,
ignores: Default::default(),
@ -230,9 +233,10 @@ impl fmt::Debug for Worktree {
#[derive(Clone)]
pub struct Snapshot {
id: usize,
scan_id: usize,
path: Arc<Path>,
root_inode: Option<u64>,
ignores: HashMap<u64, Gitignore>,
ignores: HashMap<u64, (Arc<Gitignore>, usize)>,
entries: SumTree<Entry>,
}
@ -272,15 +276,7 @@ impl Snapshot {
})
}
pub fn is_path_ignored(&self, path: impl AsRef<Path>) -> Result<bool> {
if let Some(inode) = self.inode_for_path(path.as_ref()) {
self.is_inode_ignored(inode)
} else {
Ok(false)
}
}
pub fn is_inode_ignored(&self, mut inode: u64) -> Result<bool> {
fn is_inode_ignored(&self, mut inode: u64) -> Result<bool> {
let mut components = Vec::new();
let mut relative_path = PathBuf::new();
let mut entry = self
@ -297,7 +293,7 @@ impl Snapshot {
components.push(child_name.as_ref());
inode = parent;
if let Some(ignore) = self.ignores.get(&inode) {
if let Some((ignore, _)) = self.ignores.get(&inode) {
relative_path.clear();
relative_path.extend(components.iter().rev());
match ignore.matched_path_or_any_parents(&relative_path, entry.is_dir()) {
@ -506,7 +502,8 @@ impl Snapshot {
log::info!("error in ignore file {:?} - {:?}", path, err);
}
self.ignores.insert(dir_inode, ignore);
self.ignores
.insert(dir_inode, (Arc::new(ignore), self.scan_id));
}
fn remove_ignore_file(&mut self, dir_inode: u64) {
@ -585,16 +582,37 @@ pub enum Entry {
is_symlink: bool,
children: Arc<[(u64, Arc<OsStr>)]>,
pending: bool,
is_ignored: Option<bool>,
},
File {
inode: u64,
parent: Option<u64>,
is_symlink: bool,
path: PathEntry,
is_ignored: Option<bool>,
},
}
impl Entry {
fn is_ignored(&self) -> Option<bool> {
match self {
Entry::Dir { is_ignored, .. } => *is_ignored,
Entry::File { is_ignored, .. } => *is_ignored,
}
}
fn set_ignored(&mut self, ignored: bool) {
match self {
Entry::Dir { is_ignored, .. } => *is_ignored = Some(ignored),
Entry::File {
is_ignored, path, ..
} => {
*is_ignored = Some(ignored);
path.is_ignored = Some(ignored);
}
}
}
fn inode(&self) -> u64 {
match self {
Entry::Dir { inode, .. } => *inode,
@ -625,6 +643,7 @@ impl sum_tree::Item for Entry {
} else {
0
},
recompute_is_ignored: self.is_ignored().is_none(),
}
}
}
@ -641,12 +660,14 @@ impl sum_tree::KeyedItem for Entry {
pub struct EntrySummary {
max_ino: u64,
file_count: usize,
recompute_is_ignored: bool,
}
impl<'a> AddAssign<&'a EntrySummary> for EntrySummary {
fn add_assign(&mut self, rhs: &'a EntrySummary) {
self.max_ino = rhs.max_ino;
self.file_count += rhs.file_count;
self.recompute_is_ignored |= rhs.recompute_is_ignored;
}
}
@ -721,6 +742,8 @@ impl BackgroundScanner {
}
fn scan_dirs(&self) -> io::Result<()> {
self.snapshot.lock().scan_id += 1;
let path = self.path();
let metadata = fs::metadata(&path)?;
let inode = metadata.ino();
@ -735,6 +758,7 @@ impl BackgroundScanner {
is_symlink,
children: Arc::from([]),
pending: true,
is_ignored: None,
};
{
@ -776,14 +800,17 @@ impl BackgroundScanner {
None,
Entry::File {
parent: None,
path: PathEntry::new(inode, &relative_path),
path: PathEntry::new(inode, &relative_path, None),
inode,
is_symlink,
is_ignored: None,
},
);
snapshot.root_inode = Some(inode);
}
self.recompute_ignore_statuses();
Ok(())
}
@ -811,6 +838,7 @@ impl BackgroundScanner {
is_symlink: child_is_symlink,
children: Arc::from([]),
pending: true,
is_ignored: None,
},
));
new_jobs.push(ScanJob {
@ -824,9 +852,10 @@ impl BackgroundScanner {
child_name,
Entry::File {
parent: Some(job.inode),
path: PathEntry::new(child_inode, &child_relative_path),
path: PathEntry::new(child_inode, &child_relative_path, None),
inode: child_inode,
is_symlink: child_is_symlink,
is_ignored: None,
},
));
};
@ -842,6 +871,8 @@ impl BackgroundScanner {
fn process_events(&self, mut events: Vec<fsevent::Event>) -> bool {
let mut snapshot = self.snapshot();
snapshot.scan_id += 1;
let root_path = if let Ok(path) = snapshot.path.canonicalize() {
path
} else {
@ -910,9 +941,133 @@ impl BackgroundScanner {
}
});
self.recompute_ignore_statuses();
true
}
fn recompute_ignore_statuses(&self) {
self.compute_ignore_status_for_new_ignores();
self.compute_ignore_status_for_new_entries();
}
fn compute_ignore_status_for_new_ignores(&self) {
struct IgnoreJob {
inode: u64,
ignore_queue_tx: crossbeam_channel::Sender<IgnoreJob>,
}
let snapshot = self.snapshot.lock().clone();
let mut new_ignore_parents = BTreeMap::new();
for (parent_inode, (_, scan_id)) in &snapshot.ignores {
if *scan_id == snapshot.scan_id {
let parent_path = snapshot.path_for_inode(*parent_inode, false).unwrap();
new_ignore_parents.insert(parent_path, *parent_inode);
}
}
let mut new_ignores = new_ignore_parents.into_iter().peekable();
let (ignore_queue_tx, ignore_queue_rx) = crossbeam_channel::unbounded();
while let Some((parent_path, parent_inode)) = new_ignores.next() {
while new_ignores
.peek()
.map_or(false, |(p, _)| p.starts_with(&parent_path))
{
new_ignores.next().unwrap();
}
ignore_queue_tx
.send(IgnoreJob {
inode: parent_inode,
ignore_queue_tx: ignore_queue_tx.clone(),
})
.unwrap();
}
drop(ignore_queue_tx);
self.thread_pool.scoped(|scope| {
let (edits_tx, edits_rx) = crossbeam_channel::unbounded();
scope.execute(move || {
let mut edits = Vec::new();
while let Ok(edit) = edits_rx.recv() {
edits.push(edit);
while let Ok(edit) = edits_rx.try_recv() {
edits.push(edit);
if edits.len() == EDIT_BATCH_LEN {
break;
}
}
self.snapshot.lock().entries.edit(mem::take(&mut edits));
}
});
for _ in 0..self.thread_pool.thread_count() - 1 {
let edits_tx = edits_tx.clone();
scope.execute(|| {
let edits_tx = edits_tx;
while let Ok(job) = ignore_queue_rx.recv() {
let mut entry = snapshot.entries.get(&job.inode).unwrap().clone();
entry.set_ignored(snapshot.is_inode_ignored(job.inode).unwrap());
if let Entry::Dir { children, .. } = &entry {
for (child_inode, _) in children.as_ref() {
job.ignore_queue_tx
.send(IgnoreJob {
inode: *child_inode,
ignore_queue_tx: job.ignore_queue_tx.clone(),
})
.unwrap();
}
}
edits_tx.send(Edit::Insert(entry)).unwrap();
}
});
}
});
}
fn compute_ignore_status_for_new_entries(&self) {
let snapshot = self.snapshot.lock().clone();
let (entries_tx, entries_rx) = crossbeam_channel::unbounded();
self.thread_pool.scoped(|scope| {
let (edits_tx, edits_rx) = crossbeam_channel::unbounded();
scope.execute(move || {
let mut edits = Vec::new();
while let Ok(edit) = edits_rx.recv() {
edits.push(edit);
while let Ok(edit) = edits_rx.try_recv() {
edits.push(edit);
if edits.len() == EDIT_BATCH_LEN {
break;
}
}
self.snapshot.lock().entries.edit(mem::take(&mut edits));
}
});
scope.execute(|| {
let entries_tx = entries_tx;
for entry in snapshot.entries.filter::<_, ()>(|e| e.recompute_is_ignored) {
entries_tx.send(entry.clone()).unwrap();
}
});
for _ in 0..self.thread_pool.thread_count() - 2 {
let edits_tx = edits_tx.clone();
scope.execute(|| {
let edits_tx = edits_tx;
while let Ok(mut entry) = entries_rx.recv() {
entry.set_ignored(snapshot.is_inode_ignored(entry.inode()).unwrap());
edits_tx.send(Edit::Insert(entry)).unwrap();
}
});
}
});
}
fn fs_entry_for_path(&self, root_path: &Path, path: &Path) -> Result<Option<Entry>> {
let metadata = match fs::metadata(&path) {
Err(err) => {
@ -947,6 +1102,7 @@ impl BackgroundScanner {
is_symlink,
pending: true,
children: Arc::from([]),
is_ignored: None,
}
} else {
Entry::File {
@ -958,7 +1114,9 @@ impl BackgroundScanner {
root_path
.parent()
.map_or(path, |parent| path.strip_prefix(parent).unwrap()),
None,
),
is_ignored: None,
}
};
@ -1143,8 +1301,10 @@ mod tests {
app.read(|ctx| tree.read(ctx).scan_complete()).await;
app.read(|ctx| {
let tree = tree.read(ctx);
assert!(!tree.is_path_ignored("tracked-dir/tracked-file1").unwrap());
assert!(tree.is_path_ignored("ignored-dir/ignored-file1").unwrap());
let tracked = tree.entry_for_path("tracked-dir/tracked-file1").unwrap();
let ignored = tree.entry_for_path("ignored-dir/ignored-file1").unwrap();
assert_eq!(tracked.is_ignored(), Some(false));
assert_eq!(ignored.is_ignored(), Some(true));
});
fs::write(dir.path().join("tracked-dir/tracked-file2"), "").unwrap();
@ -1152,8 +1312,10 @@ mod tests {
app.read(|ctx| tree.read(ctx).next_scan_complete()).await;
app.read(|ctx| {
let tree = tree.read(ctx);
assert!(!tree.is_path_ignored("tracked-dir/tracked-file2").unwrap());
assert!(tree.is_path_ignored("ignored-dir/ignored-file2").unwrap());
let tracked = tree.entry_for_path("tracked-dir/tracked-file2").unwrap();
let ignored = tree.entry_for_path("ignored-dir/ignored-file2").unwrap();
assert_eq!(tracked.is_ignored(), Some(false));
assert_eq!(ignored.is_ignored(), Some(true));
});
});
}
@ -1189,6 +1351,7 @@ mod tests {
let scanner = BackgroundScanner::new(
Arc::new(Mutex::new(Snapshot {
id: 0,
scan_id: 0,
path: root_dir.path().into(),
root_inode: None,
entries: Default::default(),
@ -1219,6 +1382,7 @@ mod tests {
let new_scanner = BackgroundScanner::new(
Arc::new(Mutex::new(Snapshot {
id: 0,
scan_id: 0,
path: root_dir.path().into(),
root_inode: None,
entries: Default::default(),

View file

@ -21,10 +21,11 @@ pub struct PathEntry {
pub path_chars: CharBag,
pub path: Arc<[char]>,
pub lowercase_path: Arc<[char]>,
pub is_ignored: Option<bool>,
}
impl PathEntry {
pub fn new(ino: u64, path: &Path) -> Self {
pub fn new(ino: u64, path: &Path, is_ignored: Option<bool>) -> Self {
let path = path.to_string_lossy();
let lowercase_path = path.to_lowercase().chars().collect::<Vec<_>>().into();
let path: Arc<[char]> = path.chars().collect::<Vec<_>>().into();
@ -35,6 +36,7 @@ impl PathEntry {
path_chars,
path,
lowercase_path,
is_ignored,
}
}
}
@ -196,7 +198,7 @@ fn match_single_tree_paths<'a>(
continue;
}
if !include_ignored && snapshot.is_inode_ignored(path_entry.ino).unwrap_or(true) {
if !include_ignored && path_entry.is_ignored.unwrap_or(false) {
continue;
}
@ -500,6 +502,7 @@ mod tests {
path_chars,
path,
lowercase_path,
is_ignored: Some(false),
});
}
@ -512,6 +515,7 @@ mod tests {
match_single_tree_paths(
&Snapshot {
id: 0,
scan_id: 0,
path: PathBuf::new().into(),
root_inode: None,
ignores: Default::default(),