@ 261027.5

261023.5 · 7155f6408d370e699795dc75e954635abfaf08a6

ignore: treat dotfiles ending in a dot as hidden (#3564)

`file_name()` returned `None` for any path ending in `.` on Unix, which was meant to cover the `.` and `..` components but also matched real names like `.foo.`. The hidden check then found no file name and treated those files as visible. Only a final component that is exactly `.` or `..` has no file name now.

PR #3564

reading declarations

crates/ignore/src/pathutil.rs

@@ -151,15 +151,15 @@ pub(crate) fn file_name<'a, P: AsRef<Path> + ?Sized>(         let path = path.as_os_str().as_bytes();         if path.is_empty() {             return None;-        } else if path.len() == 1 && path[0] == b'.' {-            return None;-        } else if path.last() == Some(&b'.') {-            return None;-        } else if path.len() >= 2 && &path[path.len() - 2..] == &b".."[..] {-            return None;         }         let last_slash = memrchr(b'/', path).map(|i| i + 1).unwrap_or(0);-        Some(OsStr::from_bytes(&path[last_slash..]))+        let name = &path[last_slash..];+        // Only a final component that is exactly `.` or `..` has no file+        // name. A real name that merely ends in a `.`, like `.foo.`, does.+        if name == b"." || name == b".." {+            return None;+        }+        Some(OsStr::from_bytes(name))     }      #[cfg(not(unix))]@@ -169,3 +169,50 @@ pub(crate) fn file_name<'a, P: AsRef<Path> + ?Sized>(      imp(path.as_ref()) }++#[cfg(test)]+mod tests {+    use std::ffi::OsStr;+    use std::path::Path;++    use super::{file_name, is_hidden_path};++    #[test]+    fn file_name_basic() {+        assert_eq!(file_name("a"), Some(OsStr::new("a")));+        assert_eq!(file_name("a/b"), Some(OsStr::new("b")));+        assert_eq!(file_name("/a/b.txt"), Some(OsStr::new("b.txt")));+    }++    // On Windows, `file_name` defers to `std`, which normalizes a trailing+    // `.` component away, so these only apply to the Unix implementation.+    #[cfg(unix)]+    #[test]+    fn file_name_dot_components() {+        assert_eq!(file_name(""), None);+        assert_eq!(file_name("."), None);+        assert_eq!(file_name(".."), None);+        assert_eq!(file_name("a/."), None);+        assert_eq!(file_name("a/.."), None);+        assert_eq!(file_name("/a/b/.."), None);+    }++    #[test]+    fn file_name_trailing_dot() {+        assert_eq!(file_name("a."), Some(OsStr::new("a.")));+        assert_eq!(file_name("a/.b."), Some(OsStr::new(".b.")));+        assert_eq!(file_name("a/..."), Some(OsStr::new("...")));+        assert_eq!(file_name("a/..b.."), Some(OsStr::new("..b..")));+    }++    #[test]+    fn hidden_trailing_dot() {+        assert!(is_hidden_path(Path::new(".foo")));+        assert!(is_hidden_path(Path::new("a/.foo.")));+        assert!(is_hidden_path(Path::new("a/.foo..")));+        assert!(is_hidden_path(Path::new("a/...")));+        assert!(!is_hidden_path(Path::new("a/foo.")));+        assert!(!is_hidden_path(Path::new("a/.")));+        assert!(!is_hidden_path(Path::new("a/..")));+    }+}

crates/ignore/src/walk.rs

@@ -2397,6 +2397,41 @@ mod tests {         );     } +    // Names that start with a `.` are hidden, even when they also end in a+    // `.`. A trailing `.` must not be confused with a `.` or `..` component.+    //+    // Windows doesn't allow file names that end in a `.`.+    #[cfg(unix)]+    #[test]+    fn hidden_with_trailing_dot() {+        let td = tmpdir();+        wfile(td.path().join("visible."), "");+        wfile(td.path().join(".hidden"), "");+        wfile(td.path().join(".hidden."), "");+        wfile(td.path().join(".hidden.."), "");+        wfile(td.path().join("..."), "");+        mkdirp(td.path().join(".dir."));+        wfile(td.path().join(".dir./inner"), "");++        assert_paths(td.path(), &WalkBuilder::new(td.path()), &["visible."]);++        let mut builder = WalkBuilder::new(td.path());+        builder.hidden(false);+        assert_paths(+            td.path(),+            &builder,+            &[+                ".dir.",+                ".dir./inner",+                ".hidden",+                ".hidden.",+                ".hidden..",+                "...",+                "visible.",+            ],+        );+    }+     #[test]     fn gitignore_parent() {         let td = tmpdir();