Skip to content

Commit 732a5f0

Browse files
committed
feat: FileWatcher now ignores changes from patterns in .gitignore
`FileWatcher` will build a gitignore ruleset from all `.gitignore` files in the repo and any global `.gitignore`, and use this to ignore file change events. This prevents a lot of `FileUpdate` events being generated when ignored items like build folders change, which may have been contributing to freezes. If a change is detected involving a `.gitignore` file, the ruleset is rebuilt. Changes are also now no longer watched from a thread, which caused problems with some watcher implementations such as `Fsevents` on macOS.
1 parent 740a86f commit 732a5f0

1 file changed

Lines changed: 63 additions & 21 deletions

File tree

src/file_watcher.rs

Lines changed: 63 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,28 @@
1-
use crate::{error::Error, Res};
2-
use ignore::gitignore::GitignoreBuilder;
3-
use notify::{Event, EventKind, RecursiveMode, Watcher};
1+
use crate::{error::Error, open_repo, Res};
2+
use ignore::gitignore::{gitconfig_excludes_path, GitignoreBuilder, Gitignore};
3+
use notify::{Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
44
use std::{
55
path::Path,
6+
path::PathBuf,
67
sync::{
78
atomic::{AtomicBool, Ordering},
89
Arc,
910
},
10-
thread,
1111
};
1212

1313
pub struct FileWatcher {
14+
_watcher: RecommendedWatcher,
1415
pending_updates: Arc<AtomicBool>,
1516
}
1617

1718
impl FileWatcher {
18-
pub fn new(path: &Path) -> Res<Self> {
19+
pub fn new(repo_dir: &Path) -> Res<Self> {
1920
let pending_updates = Arc::new(AtomicBool::new(false));
2021
let pending_updates_w = pending_updates.clone();
2122

22-
let gitignore = GitignoreBuilder::new(path)
23-
.add_line(None, super::LOG_FILE_NAME)
24-
.map_err(Error::FileWatcherGitignore)?
25-
.build()
26-
.unwrap();
23+
let path_buf = repo_dir.to_owned();
24+
let path_buf_copy = path_buf.clone();
25+
let mut gitignore = build_gitignore(repo_dir)?;
2726

2827
let mut watcher = notify::recommended_watcher(move |res: Result<Event, notify::Error>| {
2928
if let Ok(event) = res {
@@ -32,7 +31,20 @@ impl FileWatcher {
3231
}
3332

3433
for path in event.paths {
35-
if !gitignore.matched(&path, path.is_dir()).is_ignore() {
34+
if path
35+
.file_name()
36+
.map_or(false, |name| name == ".gitignore")
37+
{
38+
log::info!("Rebuilding gitignore ruleset");
39+
if let Ok(new_gitignore) = build_gitignore(&path_buf_copy) {
40+
gitignore = new_gitignore;
41+
}
42+
}
43+
44+
if !gitignore
45+
.matched_path_or_any_parents(&path, path.is_dir())
46+
.is_ignore()
47+
{
3648
log::info!("File changed: {:?}", path);
3749
pending_updates_w.store(true, Ordering::Relaxed);
3850
break;
@@ -42,17 +54,18 @@ impl FileWatcher {
4254
})
4355
.map_err(Error::FileWatcher)?;
4456

45-
let path_buf = path.to_owned();
46-
thread::spawn(move || {
47-
if let Err(err) = watcher
48-
.watch(path_buf.as_ref(), RecursiveMode::Recursive)
49-
.map_err(Error::FileWatcher)
50-
{
51-
log::error!("Couldn't start file-watcher due to: {}", err);
52-
}
53-
});
57+
watcher
58+
.watch(&path_buf, RecursiveMode::Recursive)
59+
.map_err(Error::FileWatcher)?;
60+
log::info!(
61+
"File watcher started (kind: {:?})",
62+
RecommendedWatcher::kind()
63+
);
5464

55-
Ok(Self { pending_updates })
65+
Ok(Self {
66+
_watcher: watcher,
67+
pending_updates,
68+
})
5669
}
5770

5871
pub fn pending_updates(&self) -> bool {
@@ -66,3 +79,32 @@ fn is_changed(event: &Event) -> bool {
6679
EventKind::Create(_) | EventKind::Modify(_) | EventKind::Remove(_)
6780
)
6881
}
82+
83+
fn build_gitignore(path: &Path) -> Res<Gitignore> {
84+
let mut gitignore_builder = GitignoreBuilder::new(path);
85+
for gitignore_path in repo_gitignore_paths(path)? {
86+
gitignore_builder.add(gitignore_path);
87+
}
88+
gitignore_builder.add_line(None, super::LOG_FILE_NAME).ok();
89+
gitignore_builder
90+
.build()
91+
.map_err(Error::FileWatcherGitignore)
92+
}
93+
94+
fn repo_gitignore_paths(repo_dir: &Path) -> Res<Vec<PathBuf>> {
95+
let mut gitignore_paths = gitconfig_excludes_path().map_or_else(|| vec![], |path| vec![path]);
96+
gitignore_paths
97+
.extend(
98+
open_repo(repo_dir)?
99+
.index()
100+
.map_err(Error::OpenRepo)?
101+
.iter()
102+
.filter_map(|entry| {
103+
match std::str::from_utf8(&entry.path).map(Path::new) {
104+
Ok(path) if path.file_name() == Some(std::ffi::OsStr::new(".gitignore")) => Some(path.to_path_buf()),
105+
_ => None
106+
}
107+
})
108+
);
109+
Ok(gitignore_paths)
110+
}

0 commit comments

Comments
 (0)