Skip to content

Commit b6722c1

Browse files
committed
ignore: fix parent gitignore matching across multiple roots
Parent matchers are cached by directory, but they also stored the canonicalized path passed to `Ignore::add_parents`. Reusing cached matchers while walking another root could therefore rewrite paths relative to the wrong root and apply scoped parent gitignore rules incorrectly. Keep the cached matcher chain independent of the walk root. Carry the absolute base path in `Ignore` instead, and add a regression test that checks cached parent matchers can be reused across roots without sharing their path semantics. Fixes #3419
1 parent bf469ae commit b6722c1

1 file changed

Lines changed: 78 additions & 26 deletions

File tree

crates/ignore/src/dir.rs

Lines changed: 78 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,12 @@ struct IgnoreOptions {
9393
#[derive(Clone, Debug)]
9494
pub(crate) struct Ignore {
9595
inner: Arc<IgnoreInner>,
96+
// Parent matchers are cached independently of the path being walked, but
97+
// matching them still needs the canonicalized path originally passed to
98+
// `add_parents`. For example, when walking `/tmp/project/src`, parent
99+
// matchers use `/tmp/project/src` to rewrite `foo.py` before matching it
100+
// against ignore files from `/tmp/project` and its ancestors.
101+
absolute_base: Option<Arc<PathBuf>>,
96102
}
97103

98104
#[derive(Clone, Debug)]
@@ -114,12 +120,9 @@ struct IgnoreInner {
114120
///
115121
/// If this is the root directory or there are otherwise no more
116122
/// directories to match, then `parent` is `None`.
117-
parent: Option<Ignore>,
123+
parent: Option<Arc<IgnoreInner>>,
118124
/// Whether this is an absolute parent matcher, as added by add_parent.
119125
is_absolute_parent: bool,
120-
/// The absolute base path of this matcher. Populated only if parent
121-
/// directories are added.
122-
absolute_base: Option<Arc<PathBuf>>,
123126
/// The directory that gitignores should be interpreted relative to.
124127
///
125128
/// Usually this is the directory containing the gitignore file. But in
@@ -154,6 +157,7 @@ struct IgnoreInner {
154157

155158
impl Ignore {
156159
/// Return the directory path of this matcher.
160+
#[cfg(test)]
157161
pub(crate) fn path(&self) -> &Path {
158162
&self.inner.dir
159163
}
@@ -163,14 +167,12 @@ impl Ignore {
163167
self.inner.parent.is_none()
164168
}
165169

166-
/// Returns true if this matcher was added via the `add_parents` method.
167-
pub(crate) fn is_absolute_parent(&self) -> bool {
168-
self.inner.is_absolute_parent
169-
}
170-
171170
/// Return this matcher's parent, if one exists.
172171
pub(crate) fn parent(&self) -> Option<Ignore> {
173-
self.inner.parent.clone()
172+
self.inner.parent.as_ref().map(|parent| Ignore {
173+
inner: parent.clone(),
174+
absolute_base: self.absolute_base.clone(),
175+
})
174176
}
175177

176178
/// Create a new `Ignore` matcher with the parent directories of `dir`.
@@ -216,22 +218,27 @@ impl Ignore {
216218
let mut compiled = self.inner.compiled.write().unwrap();
217219
if let Some(weak) = compiled.get(parent.as_os_str()) {
218220
if let Some(prebuilt) = weak.upgrade() {
219-
ig = Ignore { inner: prebuilt };
221+
ig = Ignore {
222+
inner: prebuilt,
223+
absolute_base: Some(absolute_base.clone()),
224+
};
220225
continue;
221226
}
222227
}
223228
let (mut igtmp, err) = ig.add_child_path(parent);
224229
errs.maybe_push(err);
225230
igtmp.is_absolute_parent = true;
226-
igtmp.absolute_base = Some(absolute_base.clone());
227231
igtmp.has_git =
228232
if self.inner.opts.require_git && self.inner.opts.git_ignore {
229233
parent.join(".git").exists() || parent.join(".jj").exists()
230234
} else {
231235
false
232236
};
233237
let ig_arc = Arc::new(igtmp);
234-
ig = Ignore { inner: ig_arc.clone() };
238+
ig = Ignore {
239+
inner: ig_arc.clone(),
240+
absolute_base: Some(absolute_base.clone()),
241+
};
235242
compiled.insert(
236243
parent.as_os_str().to_os_string(),
237244
Arc::downgrade(&ig_arc),
@@ -253,7 +260,13 @@ impl Ignore {
253260
dir: P,
254261
) -> (Ignore, Option<Error>) {
255262
let (ig, err) = self.add_child_path(dir.as_ref());
256-
(Ignore { inner: Arc::new(ig) }, err)
263+
(
264+
Ignore {
265+
inner: Arc::new(ig),
266+
absolute_base: self.absolute_base.clone(),
267+
},
268+
err,
269+
)
257270
}
258271

259272
/// Like add_child, but takes a full path and returns an IgnoreInner.
@@ -332,9 +345,8 @@ impl Ignore {
332345
dir: dir.to_path_buf(),
333346
overrides: self.inner.overrides.clone(),
334347
types: self.inner.types.clone(),
335-
parent: Some(self.clone()),
348+
parent: Some(self.inner.clone()),
336349
is_absolute_parent: false,
337-
absolute_base: self.inner.absolute_base.clone(),
338350
global_gitignores_relative_to: self
339351
.inner
340352
.global_gitignores_relative_to
@@ -575,29 +587,46 @@ impl Ignore {
575587

576588
/// Returns an iterator over parent ignore matchers, including this one.
577589
pub(crate) fn parents(&self) -> Parents<'_> {
578-
Parents(Some(self))
590+
Parents(Some(IgnoreRef { inner: &self.inner }))
579591
}
580592

581593
/// Returns the first absolute path of the first absolute parent, if
582594
/// one exists.
583595
fn absolute_base(&self) -> Option<&Path> {
584-
self.inner.absolute_base.as_ref().map(|p| &***p)
596+
self.absolute_base.as_ref().map(|p| &***p)
597+
}
598+
}
599+
600+
#[derive(Clone, Copy)]
601+
pub(crate) struct IgnoreRef<'a> {
602+
inner: &'a IgnoreInner,
603+
}
604+
605+
impl IgnoreRef<'_> {
606+
pub(crate) fn path(&self) -> &Path {
607+
&self.inner.dir
608+
}
609+
610+
pub(crate) fn is_absolute_parent(&self) -> bool {
611+
self.inner.is_absolute_parent
585612
}
586613
}
587614

588615
/// An iterator over all parents of an ignore matcher, including itself.
589-
///
590-
/// The lifetime `'a` refers to the lifetime of the initial `Ignore` matcher.
591-
pub(crate) struct Parents<'a>(Option<&'a Ignore>);
616+
pub(crate) struct Parents<'a>(Option<IgnoreRef<'a>>);
592617

593618
impl<'a> Iterator for Parents<'a> {
594-
type Item = &'a Ignore;
619+
type Item = IgnoreRef<'a>;
595620

596-
fn next(&mut self) -> Option<&'a Ignore> {
621+
fn next(&mut self) -> Option<IgnoreRef<'a>> {
597622
match self.0.take() {
598623
None => None,
599624
Some(ig) => {
600-
self.0 = ig.inner.parent.as_ref();
625+
self.0 = ig
626+
.inner
627+
.parent
628+
.as_deref()
629+
.map(|inner| IgnoreRef { inner });
601630
Some(ig)
602631
}
603632
}
@@ -700,7 +729,6 @@ impl IgnoreBuilder {
700729
types: self.types.clone(),
701730
parent: None,
702731
is_absolute_parent: true,
703-
absolute_base: None,
704732
global_gitignores_relative_to,
705733
explicit_ignores: Arc::new(self.explicit_ignores.clone()),
706734
custom_ignore_filenames: Arc::new(
@@ -714,6 +742,7 @@ impl IgnoreBuilder {
714742
has_git: false,
715743
opts: self.opts,
716744
}),
745+
absolute_base: None,
717746
}
718747
}
719748

@@ -966,7 +995,7 @@ fn strip_if_is_prefix<'a, P: AsRef<Path> + ?Sized>(
966995

967996
#[cfg(test)]
968997
mod tests {
969-
use std::{io::Write, path::Path};
998+
use std::{io::Write, path::Path, sync::Arc};
970999

9711000
use crate::{
9721001
Error, dir::IgnoreBuilder, gitignore::Gitignore, tests::TempDir,
@@ -1278,6 +1307,29 @@ mod tests {
12781307
assert!(ig2.matched("src/foo", false).is_ignore());
12791308
}
12801309

1310+
#[test]
1311+
fn absolute_parent_matchers_are_cached_across_roots() {
1312+
let td = tmpdir();
1313+
mkdirp(td.path().join(".git"));
1314+
mkdirp(td.path().join("src/build"));
1315+
mkdirp(td.path().join("tests/build"));
1316+
wfile(td.path().join(".gitignore"), "tests/**/build/\n");
1317+
1318+
let ig0 = IgnoreBuilder::new().build();
1319+
let (src_parents, err) = ig0.add_parents(td.path().join("src"));
1320+
assert!(err.is_none());
1321+
let (src, err) = src_parents.add_child(td.path().join("src"));
1322+
assert!(err.is_none());
1323+
let (tests_parents, err) = ig0.add_parents(td.path().join("tests"));
1324+
assert!(err.is_none());
1325+
let (tests, err) = tests_parents.add_child(td.path().join("tests"));
1326+
assert!(err.is_none());
1327+
1328+
assert!(Arc::ptr_eq(&src_parents.inner, &tests_parents.inner));
1329+
assert!(src.matched("build", true).is_none());
1330+
assert!(tests.matched("build", true).is_ignore());
1331+
}
1332+
12811333
#[test]
12821334
fn git_info_exclude_in_linked_worktree() {
12831335
let td = tmpdir();

0 commit comments

Comments
 (0)