@ 261104.1

260924.3.4 · b3a8ff687b0d0bc4095fb23ec8dc60ff3be1308e

Take the broken symlink depth from the walker

The depth of a broken symlink was derived from its path by stripping the matching search root, because the walker did not report a depth on the errors that carry broken symlinks. ignore 0.4.28 fills that depth in, so read it from the error instead and drop the path arithmetic.

The path based version could not express one case. When two search roots overlap, the walker visits the same broken symlink once per root at a different depth each time, and both visits carry the same path, so a single path derived answer had to serve both. Searching roots a and a/b with --min-depth 2 dropped a/b/blink even though a real file in its place was kept.

reading declarations

Cargo.toml

@@ -34,7 +34,7 @@ path = "src/main.rs" aho-corasick = "1.1" nu-ansi-term = "0.50" argmax = "0.4.0"-ignore = "0.4.25"+ignore = "0.4.28" regex = "1.12.2" regex-syntax = "0.8" ctrlc = "3.5"

src/dir_entry.rs

@@ -11,9 +11,9 @@ use crate::filesystem::strip_current_dir; #[derive(Debug)] enum DirEntryInner {     Normal(ignore::DirEntry),-    // Broken symlinks are surfaced by the walker as errors that carry no depth,-    // so we record the depth (relative to the search root) at creation time.-    BrokenSymlink { path: PathBuf, depth: usize },+    // Broken symlinks reach us as walk errors rather than entries, so we carry+    // over the depth the walker recorded on the error.+    BrokenSymlink { path: PathBuf, depth: Option<usize> }, }  #[derive(Debug)]@@ -33,7 +33,7 @@ impl DirEntry {         }     } -    pub fn broken_symlink(path: PathBuf, depth: usize) -> Self {+    pub fn broken_symlink(path: PathBuf, depth: Option<usize>) -> Self {         Self {             inner: DirEntryInner::BrokenSymlink { path, depth },             metadata: OnceCell::new(),@@ -100,7 +100,7 @@ impl DirEntry {     pub fn depth(&self) -> Option<usize> {         match &self.inner {             DirEntryInner::Normal(e) => Some(e.depth()),-            DirEntryInner::BrokenSymlink { depth, .. } => Some(*depth),+            DirEntryInner::BrokenSymlink { depth, .. } => *depth,         }     } 

src/walk.rs

@@ -440,7 +440,7 @@ impl WorkerState {     }      /// Spawn the sender threads.-    fn spawn_senders(&self, walker: WalkParallel, roots: &[PathBuf], tx: Sender<Batch>) {+    fn spawn_senders(&self, walker: WalkParallel, tx: Sender<Batch>) {         walker.run(|| {             let patterns = &self.patterns;             let config = &self.config;@@ -484,25 +484,24 @@ impl WorkerState {                 }                 let entry = match entry {                     Ok(e) => DirEntry::normal(e),-                    Err(ignore::Error::WithPath {-                        path,-                        err: inner_err,-                    }) if inner_err-                        .io_error()-                        .is_some_and(|io_error| io_error.kind() == io::ErrorKind::NotFound)-                        && path-                            .symlink_metadata()-                            .ok()-                            .is_some_and(|m| m.file_type().is_symlink()) =>-                    {-                        let depth = depth_relative_to_roots(&path, roots);-                        DirEntry::broken_symlink(path, depth)-                    }                     Err(err) => {-                        return match tx.send(WorkerResult::Error(err)) {-                            Ok(_) => WalkState::Continue,-                            Err(_) => WalkState::Quit,-                        };+                        // The depth has to be read off the error before it is+                        // taken apart, since it is recorded on an inner variant.+                        let depth = err.depth();+                        match err {+                            ignore::Error::WithPath {+                                path,+                                err: inner_err,+                            } if is_broken_symlink(&path, &inner_err) => {+                                DirEntry::broken_symlink(path, depth)+                            }+                            err => {+                                return match tx.send(WorkerResult::Error(err)) {+                                    Ok(_) => WalkState::Continue,+                                    Err(_) => WalkState::Quit,+                                };+                            }+                        }                     }                 }; @@ -641,7 +640,7 @@ impl WorkerState {             let receiver = scope.spawn(|| self.receive(rx));              // Spawn the sender threads.-            self.spawn_senders(walker, paths, tx);+            self.spawn_senders(walker, tx);              receiver.join().unwrap()         });@@ -654,37 +653,19 @@ impl WorkerState {     } } -/// Compute the depth of `path` relative to the search root it was found under.+/// Whether a walk error is really a broken symlink rather than a failure worth+/// reporting. ///-/// `ignore::DirEntry::depth()` provides this for normal entries, but broken-/// symlinks are surfaced as errors that carry no depth, so we derive it from the-/// path instead (see issue #1017). The walker reports every entry's path-/// prefixed by the search root it was found under, so we strip the matching root-/// and count the remaining components. Both the path and the roots are put into-/// absolute form first, so the comparison is correct regardless of whether-/// `--absolute-path` has already made the roots absolute (otherwise an absolute-/// path would fail to match a relative root and the depth would be wrong). When-/// more than one root is a prefix, the deepest (most specific) one wins. If none-/// matches, which should not happen, we fall back to the full component count,-/// keeping the entry visible rather than silently dropping it.-fn depth_relative_to_roots(path: &Path, roots: &[PathBuf]) -> usize {-    // `Path::join` ignores the base when its argument is already absolute, so-    // this leaves absolute inputs untouched and anchors relative ones at the cwd.-    let cwd = std::env::current_dir().ok();-    let absolute = |p: &Path| -> PathBuf {-        match &cwd {-            Some(cwd) => cwd.join(p),-            None => p.to_path_buf(),-        }-    };--    let absolute_path = absolute(path);-    roots-        .iter()-        .filter_map(|root| absolute_path.strip_prefix(absolute(root)).ok())-        .map(|relative| relative.components().count())-        .min()-        .unwrap_or_else(|| path.components().count())+/// A symlink whose target is missing is surfaced by the walker as a NotFound+/// error against the link's own path, so it never arrives as an entry. fd still+/// wants to match and print it (see issue #1017), which means recovering it here.+fn is_broken_symlink(path: &Path, err: &ignore::Error) -> bool {+    err.io_error()+        .is_some_and(|io_error| io_error.kind() == io::ErrorKind::NotFound)+        && path+            .symlink_metadata()+            .ok()+            .is_some_and(|m| m.file_type().is_symlink()) }  fn search_str_for_entry<'a>(

tests/tests.rs

@@ -1285,6 +1285,52 @@ fn test_min_depth_broken_symlink_absolute_path() {     ); } +/// Minimum depth with a broken symlink under overlapping search roots (#1017)+///+/// When two search roots overlap, the walker visits the same broken symlink once+/// per root, at a different depth each time. Here `one/two/broken_symlink` is at+/// depth 2 under root `one` and at depth 1 under root `one/two`, so --min-depth 2+/// must keep the entry the walker reached via `one` and drop the one it reached+/// via `one/two`. A depth derived from the entry's path cannot distinguish the+/// two visits, since both carry the same path.+#[test]+fn test_min_depth_broken_symlink_overlapping_roots() {+    let mut te = TestEnv::new(DEFAULT_DIRS, DEFAULT_FILES);+    te.create_broken_symlink("one/two/broken_symlink")+        .expect("Failed to create broken symlink.");++    // Only the route through `one` reaches depth 2.+    te.assert_output(+        &[+            "--follow",+            "--type",+            "symlink",+            "--min-depth",+            "2",+            "broken_symlink",+            "one",+            "one/two",+        ],+        "one/two/broken_symlink",+    );++    // Both routes clear --min-depth 1, so it is reported once per root.+    te.assert_output(+        &[+            "--follow",+            "--type",+            "symlink",+            "--min-depth",+            "1",+            "broken_symlink",+            "one",+            "one/two",+        ],+        "one/two/broken_symlink+        one/two/broken_symlink",+    );+}+ /// Maximum depth with a broken symlink (#1017) /// /// A broken symlink must be filtered by --max-depth like any other entry. The