Skip to content

Commit fe24392

Browse files
tstackttiimm
authored andcommitted
[source_hier] pay attention to .gitignore
1 parent fd76ecf commit fe24392

3 files changed

Lines changed: 121 additions & 14 deletions

File tree

Cargo.lock

Lines changed: 30 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ tree-sitter-cpp = "0.23.4"
2424
tree-sitter-rust-orchard = "0.12.0"
2525
tree-sitter-java = "0.23.5"
2626
tree-sitter-python = "0.25.0"
27+
ignore = "0.4"
2728
rayon = "1.11.0"
2829
miette = { version = "7.6.0", features = ["fancy", "serde"] }
2930

src/source_hier.rs

Lines changed: 90 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::{LogError, SourceLanguage};
2+
use ignore::gitignore::{Gitignore, GitignoreBuilder};
23
use serde::{Deserialize, Serialize};
34
use std::cell::RefCell;
45
use std::collections::BTreeMap;
@@ -11,6 +12,16 @@ fn is_ignored_dir(name: &OsStr) -> bool {
1112
name == ".git" || name == ".hg" || name == ".svn" || name == ".vscode"
1213
}
1314

15+
fn build_gitignore(root: &Path) -> Gitignore {
16+
let mut builder = GitignoreBuilder::new(root);
17+
builder.add(root.join(".gitignore"));
18+
builder.build().unwrap_or_else(|_| Gitignore::empty())
19+
}
20+
21+
fn is_gitignored(gi: &Gitignore, path: &Path, is_dir: bool) -> bool {
22+
gi.matched_path_or_any_parents(path, is_dir).is_ignore()
23+
}
24+
1425
/// Result of a shallow check of a file system path. Mainly interested in getting a directory
1526
/// listing without descending into the child trees.
1627
enum ShallowCheckResult {
@@ -80,16 +91,26 @@ impl SourceHierContent {
8091
.collect())
8192
}
8293

83-
fn from_dir(path: &Path) -> Self {
94+
fn from_dir(path: &Path, gi: &Gitignore) -> Self {
8495
match Self::entries_of(path) {
8596
Ok(entries) => Self::Directory {
8697
entries: entries
8798
.into_iter()
88-
.filter(|entry| !is_ignored_dir(&entry.0))
99+
.filter(|entry| {
100+
!is_ignored_dir(&entry.0) && {
101+
let child_path = path.join(&entry.0);
102+
let is_dir = entry
103+
.1
104+
.as_ref()
105+
.map(|m| m.is_dir())
106+
.unwrap_or(false);
107+
!is_gitignored(gi, &child_path, is_dir)
108+
}
109+
})
89110
.map(|(entry_name, meta)| {
90111
(
91112
entry_name.to_os_string(),
92-
SourceHierNode::from_int(&path.join(entry_name), meta),
113+
SourceHierNode::from_int(&path.join(entry_name), meta, gi),
93114
)
94115
})
95116
.collect(),
@@ -103,11 +124,11 @@ impl SourceHierContent {
103124
}
104125
}
105126

106-
fn from(path: &Path, metadata: Result<fs::Metadata, io::Error>) -> Self {
127+
fn from(path: &Path, metadata: Result<fs::Metadata, io::Error>, gi: &Gitignore) -> Self {
107128
match metadata {
108129
Ok(meta) => {
109130
if meta.is_dir() {
110-
Self::from_dir(path)
131+
Self::from_dir(path, gi)
111132
} else if meta.is_file() {
112133
match SourceLanguage::from_path(&path) {
113134
Some(language) => match meta.modified() {
@@ -183,6 +204,7 @@ impl SourceHierContent {
183204
path: &Path,
184205
latest_meta: Result<fs::Metadata, io::Error>,
185206
deleted_events: &mut Vec<ScanEvent>,
207+
gi: &Gitignore,
186208
) -> bool {
187209
let latest_content = Self::shallow_check(path, &latest_meta);
188210
*self = match self {
@@ -199,7 +221,7 @@ impl SourceHierContent {
199221
}
200222
_ => {
201223
deleted_events.push(ScanEvent::DeletedFile(PathBuf::from(path), info.id));
202-
Self::from(path, latest_meta)
224+
Self::from(path, latest_meta, gi)
203225
}
204226
},
205227
SourceHierContent::Directory { ref mut entries } => match latest_content {
@@ -216,23 +238,27 @@ impl SourceHierContent {
216238
let mut new_entries: Vec<(OsString, Result<fs::Metadata, io::Error>)> =
217239
Vec::new();
218240
for (name, meta) in latest_entries {
219-
if is_ignored_dir(&name.as_os_str()) {
241+
let child_path = path.join(&name);
242+
let is_dir = meta.as_ref().map(|m| m.is_dir()).unwrap_or(false);
243+
if is_ignored_dir(&name.as_os_str())
244+
|| is_gitignored(gi, &child_path, is_dir)
245+
{
220246
} else if let Some(existing_entry) = entries.get_mut(&name) {
221-
existing_entry.sync(&path.join(&name), meta, deleted_events)
247+
existing_entry.sync(&child_path, meta, deleted_events, gi)
222248
} else {
223249
new_entries.push((name, meta));
224250
changed = true;
225251
}
226252
}
227253
new_entries.into_iter().for_each(|(name, meta)| {
228-
let node = SourceHierNode::from_int(&path.join(&name), meta);
254+
let node = SourceHierNode::from_int(&path.join(&name), meta, gi);
229255
entries.insert(name, node);
230256
});
231257
return changed;
232258
}
233-
_ => Self::from(path, latest_meta),
259+
_ => Self::from(path, latest_meta, gi),
234260
},
235-
_ => Self::from(path, latest_meta),
261+
_ => Self::from(path, latest_meta, gi),
236262
};
237263
true
238264
}
@@ -275,13 +301,13 @@ pub struct SourceHierNode {
275301
}
276302

277303
impl SourceHierNode {
278-
fn from_int(path: &Path, metadata: Result<fs::Metadata, io::Error>) -> Self {
304+
fn from_int(path: &Path, metadata: Result<fs::Metadata, io::Error>, gi: &Gitignore) -> Self {
279305
match metadata {
280306
Ok(meta) => {
281307
if meta.is_dir() {
282308
Self {
283309
last_scan_time: None,
284-
content: SourceHierContent::from_dir(path),
310+
content: SourceHierContent::from_dir(path, gi),
285311
}
286312
} else if meta.is_file() {
287313
match SourceLanguage::from_path(&path) {
@@ -357,8 +383,9 @@ impl SourceHierNode {
357383
path: &Path,
358384
meta: Result<fs::Metadata, io::Error>,
359385
deleted_events: &mut Vec<ScanEvent>,
386+
gi: &Gitignore,
360387
) {
361-
if self.content.sync_int(path, meta, deleted_events) {
388+
if self.content.sync_int(path, meta, deleted_events, gi) {
362389
self.last_scan_time = None;
363390
}
364391
}
@@ -446,13 +473,15 @@ impl SourceHierTree {
446473

447474
/// Synchronize the state of this tree with the file system.
448475
pub fn sync(&mut self) {
476+
let gi = build_gitignore(&self.root_path);
449477
SourceFileInfo::NEXT_ID.with(|id_opt| {
450478
*id_opt.borrow_mut() = self.next_id;
451479
});
452480
self.root_node.sync(
453481
&self.root_path,
454482
fs::metadata(&self.root_path),
455483
&mut self.deleted_events,
484+
&gi,
456485
);
457486
self.next_id = SourceFileInfo::NEXT_ID.with(|id_opt| *id_opt.borrow());
458487
self.stats = self.compute_stats();
@@ -617,4 +646,51 @@ mod test {
617646
let deleted_dir_events: Vec<ScanEvent> = tree.scan().map(redact_event).collect();
618647
assert_yaml_snapshot!(deleted_dir_events);
619648
}
649+
650+
#[test]
651+
fn test_gitignore_filtering() {
652+
let temp_dir = tempdir().expect("Failed to create temporary directory");
653+
let root = temp_dir.path();
654+
655+
// Create a .gitignore that ignores *.log files and the "build/" directory
656+
let mut gitignore = File::create(root.join(".gitignore")).unwrap();
657+
writeln!(gitignore, "*.log").unwrap();
658+
writeln!(gitignore, "build/").unwrap();
659+
drop(gitignore);
660+
661+
// Create files: one that should be found, ones that should be ignored
662+
fs::create_dir(root.join("src")).unwrap();
663+
File::create(root.join("src/main.rs"))
664+
.unwrap()
665+
.write(b"fn main() {}")
666+
.unwrap();
667+
File::create(root.join("debug.log"))
668+
.unwrap()
669+
.write(b"some log")
670+
.unwrap();
671+
fs::create_dir(root.join("build")).unwrap();
672+
File::create(root.join("build/output.rs"))
673+
.unwrap()
674+
.write(b"generated")
675+
.unwrap();
676+
677+
let mut tree = SourceHierTree::from(root);
678+
tree.sync();
679+
let events: Vec<ScanEvent> = tree.scan().map(redact_event).collect();
680+
681+
// Only main.rs should appear as a NewFile event
682+
assert!(events
683+
.iter()
684+
.any(|e| matches!(e, ScanEvent::NewFile(p, _) if p == Path::new("main.rs"))));
685+
assert!(!events
686+
.iter()
687+
.any(|e| matches!(e, ScanEvent::NewFile(p, _) if p == Path::new("debug.log"))));
688+
assert!(!events
689+
.iter()
690+
.any(|e| matches!(e, ScanEvent::NewFile(p, _) if p == Path::new("output.rs"))));
691+
692+
// Verify stats don't count ignored files
693+
let stats = tree.stats();
694+
assert_eq!(stats.files, 1);
695+
}
620696
}

0 commit comments

Comments
 (0)