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. Pointer checkout also writes cursor files
6//! beside the `.heddle` file (`.heddle.identity`, `.heddle.last-turn`,
7//! `.identity.lock`, `.identity.tmp.*`, `.last-turn.tmp.*`). Gitignore
8//! last-match-wins would otherwise let `!.heddle/` pull that tree into
9//! capture. Nested `.heddle/` directories (fixtures) stay ordinary content.
10
11use std::path::{Component, Path};
12
13/// Whether `path` is a reserved worktree-root Heddle artifact.
14///
15/// Root-anchored only: `examples/calculator/.heddle/` is not reserved.
16/// Leading `./` is skipped so `./.heddle/identity.toml` matches.
17#[must_use]
18pub fn is_reserved_worktree_path(path: &Path) -> bool {
19    first_normal_component(path).is_some_and(is_reserved_root_name)
20}
21
22/// Whether a directory child is reserved without allocating a joined path.
23///
24/// Used by the walker prune so root `.heddle` and pointer-cursor artifacts
25/// are skipped even if a matcher is later refactored.
26#[must_use]
27pub fn is_reserved_directory_child(parent: &Path, name: &str) -> bool {
28    if is_reserved_worktree_path(parent) {
29        return true;
30    }
31    is_worktree_root(parent) && is_reserved_root_name(std::ffi::OsStr::new(name))
32}
33
34fn is_reserved_root_name(name: &std::ffi::OsStr) -> bool {
35    let Some(name) = name.to_str() else {
36        return false;
37    };
38    name == ".heddle"
39        || name == ".heddle.identity"
40        || name == ".heddle.last-turn"
41        || name == ".identity.lock"
42        || name == ".identity.tmp"
43        || name.starts_with(".identity.tmp.")
44        || name.starts_with(".last-turn.tmp.")
45}
46
47fn is_worktree_root(path: &Path) -> bool {
48    path.as_os_str().is_empty() || path == Path::new(".")
49}
50
51fn first_normal_component(path: &Path) -> Option<&std::ffi::OsStr> {
52    for component in path.components() {
53        match component {
54            Component::CurDir => continue,
55            Component::Normal(name) => return Some(name),
56            Component::ParentDir | Component::Prefix(_) | Component::RootDir => return None,
57        }
58    }
59    None
60}
61
62#[cfg(test)]
63mod tests {
64    use std::path::Path;
65
66    use super::{is_reserved_directory_child, is_reserved_worktree_path};
67
68    #[test]
69    fn reserves_root_heddle_tree_and_identity() {
70        for path in [
71            ".heddle",
72            ".heddle/identity.toml",
73            ".heddle/objects/pack",
74            ".heddle/info/exclude",
75            "./.heddle/identity.toml",
76        ] {
77            assert!(
78                is_reserved_worktree_path(Path::new(path)),
79                "expected reserved: {path}"
80            );
81        }
82    }
83
84    #[test]
85    fn reserves_pointer_checkout_cursor_artifacts() {
86        for path in [
87            ".heddle.identity",
88            "./.heddle.identity",
89            ".heddle.last-turn",
90            ".identity.lock",
91            ".identity.tmp.123.0",
92            ".identity.tmp",
93            ".last-turn.tmp.123.0",
94        ] {
95            assert!(
96                is_reserved_worktree_path(Path::new(path)),
97                "expected reserved: {path}"
98            );
99        }
100    }
101
102    #[test]
103    fn does_not_reserve_nested_or_unrelated_paths() {
104        for path in [
105            "",
106            ".",
107            "src/main.rs",
108            "heddle",
109            ".heddleignore",
110            "examples/calculator/.heddle/identity.toml",
111            "examples/calculator/.heddle",
112            "examples/foo/.heddle.identity",
113            "examples/foo/.heddle.last-turn",
114            "examples/foo/.identity.lock",
115            "src/.identity.tmp.1.2",
116            "../.heddle/identity.toml",
117        ] {
118            assert!(
119                !is_reserved_worktree_path(Path::new(path)),
120                "expected not reserved: {path}"
121            );
122        }
123    }
124
125    #[test]
126    fn directory_child_reserves_root_heddle_and_descendants() {
127        assert!(is_reserved_directory_child(Path::new(""), ".heddle"));
128        assert!(is_reserved_directory_child(Path::new("."), ".heddle"));
129        assert!(is_reserved_directory_child(
130            Path::new(".heddle"),
131            "identity.toml"
132        ));
133        assert!(is_reserved_directory_child(
134            Path::new(""),
135            ".heddle.identity"
136        ));
137        assert!(is_reserved_directory_child(
138            Path::new("."),
139            ".identity.lock"
140        ));
141        assert!(is_reserved_directory_child(
142            Path::new(""),
143            ".identity.tmp.9.1"
144        ));
145        assert!(!is_reserved_directory_child(Path::new(""), "src"));
146        assert!(!is_reserved_directory_child(
147            Path::new("examples/calculator"),
148            ".heddle"
149        ));
150        assert!(!is_reserved_directory_child(
151            Path::new("examples/foo"),
152            ".heddle.identity"
153        ));
154    }
155}