Skip to main content

objects/worktree/
worktree_reserved.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Reserved worktree paths that user ignore rules cannot un-ignore.
3//!
4//! Root `.heddle/` holds identity material (`identity.toml`), credentials,
5//! and repository-engine state. Gitignore last-match-wins would otherwise
6//! let `!.heddle/` pull that tree into capture. Nested `.heddle/`
7//! directories (fixtures) stay ordinary content.
8
9use std::path::{Component, Path};
10
11/// Whether `path` is the worktree-root `.heddle` entry or a path under it.
12///
13/// Root-anchored only: `examples/calculator/.heddle/` is not reserved.
14/// Leading `./` is skipped so `./.heddle/identity.toml` matches.
15#[must_use]
16pub fn is_reserved_worktree_path(path: &Path) -> bool {
17    first_normal_component(path).is_some_and(|name| name == ".heddle")
18}
19
20/// Whether a directory child is reserved without allocating a joined path.
21///
22/// Used by the walker prune so root `.heddle` and anything already under
23/// that tree are skipped even if a matcher is later refactored.
24#[must_use]
25pub fn is_reserved_directory_child(parent: &Path, name: &str) -> bool {
26    if is_reserved_worktree_path(parent) {
27        return true;
28    }
29    name == ".heddle" && is_worktree_root(parent)
30}
31
32fn is_worktree_root(path: &Path) -> bool {
33    path.as_os_str().is_empty() || path == Path::new(".")
34}
35
36fn first_normal_component(path: &Path) -> Option<&std::ffi::OsStr> {
37    for component in path.components() {
38        match component {
39            Component::CurDir => continue,
40            Component::Normal(name) => return Some(name),
41            Component::ParentDir | Component::Prefix(_) | Component::RootDir => return None,
42        }
43    }
44    None
45}
46
47#[cfg(test)]
48mod tests {
49    use std::path::Path;
50
51    use super::{is_reserved_directory_child, is_reserved_worktree_path};
52
53    #[test]
54    fn reserves_root_heddle_tree_and_identity() {
55        for path in [
56            ".heddle",
57            ".heddle/identity.toml",
58            ".heddle/objects/pack",
59            ".heddle/info/exclude",
60            "./.heddle/identity.toml",
61        ] {
62            assert!(
63                is_reserved_worktree_path(Path::new(path)),
64                "expected reserved: {path}"
65            );
66        }
67    }
68
69    #[test]
70    fn does_not_reserve_nested_or_unrelated_paths() {
71        for path in [
72            "",
73            ".",
74            "src/main.rs",
75            "heddle",
76            ".heddleignore",
77            "examples/calculator/.heddle/identity.toml",
78            "examples/calculator/.heddle",
79            "../.heddle/identity.toml",
80        ] {
81            assert!(
82                !is_reserved_worktree_path(Path::new(path)),
83                "expected not reserved: {path}"
84            );
85        }
86    }
87
88    #[test]
89    fn directory_child_reserves_root_heddle_and_descendants() {
90        assert!(is_reserved_directory_child(Path::new(""), ".heddle"));
91        assert!(is_reserved_directory_child(Path::new("."), ".heddle"));
92        assert!(is_reserved_directory_child(
93            Path::new(".heddle"),
94            "identity.toml"
95        ));
96        assert!(!is_reserved_directory_child(Path::new(""), "src"));
97        assert!(!is_reserved_directory_child(
98            Path::new("examples/calculator"),
99            ".heddle"
100        ));
101    }
102}