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`, `.identity.lock`,
7//! `.identity.tmp.*`). Gitignore last-match-wins would otherwise let
8//! `!.heddle/` pull that tree into capture. Nested `.heddle/` directories
9//! (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 == ".identity.lock"
41        || name == ".identity.tmp"
42        || name.starts_with(".identity.tmp.")
43}
44
45fn is_worktree_root(path: &Path) -> bool {
46    path.as_os_str().is_empty() || path == Path::new(".")
47}
48
49fn first_normal_component(path: &Path) -> Option<&std::ffi::OsStr> {
50    for component in path.components() {
51        match component {
52            Component::CurDir => continue,
53            Component::Normal(name) => return Some(name),
54            Component::ParentDir | Component::Prefix(_) | Component::RootDir => return None,
55        }
56    }
57    None
58}
59
60#[cfg(test)]
61mod tests {
62    use std::path::Path;
63
64    use super::{is_reserved_directory_child, is_reserved_worktree_path};
65
66    #[test]
67    fn reserves_root_heddle_tree_and_identity() {
68        for path in [
69            ".heddle",
70            ".heddle/identity.toml",
71            ".heddle/objects/pack",
72            ".heddle/info/exclude",
73            "./.heddle/identity.toml",
74        ] {
75            assert!(
76                is_reserved_worktree_path(Path::new(path)),
77                "expected reserved: {path}"
78            );
79        }
80    }
81
82    #[test]
83    fn reserves_pointer_checkout_cursor_artifacts() {
84        for path in [
85            ".heddle.identity",
86            "./.heddle.identity",
87            ".identity.lock",
88            ".identity.tmp.123.0",
89            ".identity.tmp",
90        ] {
91            assert!(
92                is_reserved_worktree_path(Path::new(path)),
93                "expected reserved: {path}"
94            );
95        }
96    }
97
98    #[test]
99    fn does_not_reserve_nested_or_unrelated_paths() {
100        for path in [
101            "",
102            ".",
103            "src/main.rs",
104            "heddle",
105            ".heddleignore",
106            "examples/calculator/.heddle/identity.toml",
107            "examples/calculator/.heddle",
108            "examples/foo/.heddle.identity",
109            "examples/foo/.identity.lock",
110            "src/.identity.tmp.1.2",
111            "../.heddle/identity.toml",
112        ] {
113            assert!(
114                !is_reserved_worktree_path(Path::new(path)),
115                "expected not reserved: {path}"
116            );
117        }
118    }
119
120    #[test]
121    fn directory_child_reserves_root_heddle_and_descendants() {
122        assert!(is_reserved_directory_child(Path::new(""), ".heddle"));
123        assert!(is_reserved_directory_child(Path::new("."), ".heddle"));
124        assert!(is_reserved_directory_child(
125            Path::new(".heddle"),
126            "identity.toml"
127        ));
128        assert!(is_reserved_directory_child(
129            Path::new(""),
130            ".heddle.identity"
131        ));
132        assert!(is_reserved_directory_child(
133            Path::new("."),
134            ".identity.lock"
135        ));
136        assert!(is_reserved_directory_child(
137            Path::new(""),
138            ".identity.tmp.9.1"
139        ));
140        assert!(!is_reserved_directory_child(Path::new(""), "src"));
141        assert!(!is_reserved_directory_child(
142            Path::new("examples/calculator"),
143            ".heddle"
144        ));
145        assert!(!is_reserved_directory_child(
146            Path::new("examples/foo"),
147            ".heddle.identity"
148        ));
149    }
150}