Skip to main content

git_policy_cli/worktree_copying/
worktree_copy_prune.rs

1//! What:
2//!  What a copy from a main checkout leaves out of the ignored state it hands over:
3//!  caches, optionally the records of earlier runs, and every directory that is another checkout.
4//! Why:
5//!  A worktree added from the main checkout should work at once, so builds and installed dependencies are copied,
6//!  but a cache is rebuilt by its tool, a log of an earlier run is no input, and another checkout's files would make the
7//!  new worktree a second claimant of that checkout's administrative state.
8//!  The rule is a handful of directory names rather than a list of paths, so a reader can predict it
9//!  (`doc/planning/cli-git-main-worktree-ignored-copy.md` measures the set it leaves out).
10//!
11//! In TS you'd write (pseudocode):
12//! ```ts
13//! const prune = { kind: 'main-checkout', runRecords: COPY_RUN_RECORDS };
14//! if (skipsPath(prune, 'package/x/.cache/typescript')) continue;
15//! ```
16
17/// What:
18///  Whether the records of earlier runs (logs, mutation and test reports, verification output) are copied.
19/// Why:
20///   The owner answered on 2026-11-02 "Skip run records (Recommended)", so they are left out;
21///  setting it to `true` is the one-line change that copies them.
22///
23/// In TS you'd write (pseudocode):
24/// ```ts
25/// export const COPY_RUN_RECORDS = false;
26/// ```
27pub const COPY_RUN_RECORDS: bool = false;
28
29/// What:
30///  Directory names that are caches wherever they sit below the source root.
31/// Why:
32///   A package-manager store or a build tool's project cache is rebuilt by the tool that made it.
33///
34/// In TS you'd write (pseudocode):
35/// ```ts
36/// const CACHE_NAMES = ['.pnpm-store', '.gradle', '.kotlin', '.zig-cache'] as const;
37/// ```
38const CACHE_NAMES: [&str; 4] = [".pnpm-store", ".gradle", ".kotlin", ".zig-cache"];
39
40/// What:
41///  Directory names that are caches only inside a Cargo `target` directory.
42/// Why:
43///   `incremental` holds rustc's incremental-compilation state and `zig-global-cache` the Zig compiler's cache;
44///  the compiled dependencies beside them are the build the new worktree needs.
45///
46/// In TS you'd write (pseudocode):
47/// ```ts
48/// const TARGET_CACHE_NAMES = ['incremental', 'zig-global-cache'] as const;
49/// ```
50const TARGET_CACHE_NAMES: [&str; 2] = ["incremental", "zig-global-cache"];
51
52/// What:
53///  Directory names that hold the output of earlier test, mutation and lint runs wherever they sit.
54/// Why:
55///   They are reports, not inputs; they are copied only while `COPY_RUN_RECORDS` is set.
56///
57/// In TS you'd write (pseudocode):
58/// ```ts
59/// const RUN_RECORD_NAMES = ['mutants.out', 'mutants.out.old', 'playwright-report', 'test-results'] as const;
60/// ```
61const RUN_RECORD_NAMES: [&str; 4] = ["mutants.out", "mutants.out.old", "playwright-report", "test-results"];
62
63/// What:
64///  The name Git gives the entry that makes a directory a checkout of its own.
65/// Why:
66///   A directory that holds it, file or directory, is another checkout, never ignored state of this one.
67///
68/// In TS you'd write (pseudocode):
69/// ```ts
70/// const CHECKOUT_MARKER = '.git';
71/// ```
72const CHECKOUT_MARKER: &str = ".git";
73
74/// What:
75///  The leave-out rule one copy applies while it walks a source.
76/// Why:
77///   A linked or bare source hands over everything it ignores, so only the main checkout has a rule.
78///
79/// In TS you'd write (pseudocode):
80/// ```ts
81/// type Prune = { kind: 'never' } | { kind: 'main-checkout'; runRecords: boolean };
82/// ```
83#[derive(Clone, Copy, Debug, Eq, PartialEq)]
84pub enum Prune {
85    /// Nothing is left out beyond the registered worktrees and the stage.
86    Never,
87    /// The main checkout's rule; `run_records` says whether the records of earlier runs are copied.
88    MainCheckout {
89        /// Whether run records (reports and logs of earlier runs) are copied.
90        run_records: bool,
91    },
92}
93
94impl Prune {
95    /// What:
96    ///  The rule the main checkout uses, with the run-record setting the crate ships.
97    /// Why:
98    ///   One place reads the shipped constant, so changing the owner's answer is a one-line change.
99    ///
100    /// In TS you'd write (pseudocode):
101    /// ```ts
102    /// Prune.mainCheckout() // { kind: 'main-checkout', runRecords: COPY_RUN_RECORDS }
103    /// ```
104    pub fn main_checkout() -> Prune {
105        return Prune::MainCheckout { run_records: COPY_RUN_RECORDS };
106    }
107
108    /// What:
109    ///  Whether the path, relative to the source root, and everything below it is left out.
110    /// Why:
111    ///   The decision uses directory names only, so it is the same for a root Git listed and a child the walk found.
112    ///
113    /// In TS you'd write (pseudocode):
114    /// ```ts
115    /// skipsPath(prune, 'package/x/target/debug/incremental') // true
116    /// ```
117    pub fn skips_path(self, relative_path: &str) -> bool {
118        let Prune::MainCheckout { run_records } = self else {
119            return false;
120        };
121        let components: Vec<&str> = relative_path.split('/').collect();
122        for (index, component) in components.iter().enumerate() {
123            let parent: Option<&str> = index.checked_sub(1).map(|before| return components[before]);
124            let below_target: bool = components[..index].contains(&"target");
125            // The repository root's own `.cache` is where the repository keeps tools and generated inputs that tasks read.
126            let nested_cache: bool = *component == ".cache" && index > 0;
127            let cache: bool = CACHE_NAMES.contains(component)
128                || nested_cache
129                || (below_target && TARGET_CACHE_NAMES.contains(component));
130            let record: bool = RUN_RECORD_NAMES.contains(component)
131                || (*component == ".monochromatic" && components[..index].contains(&"node_modules"))
132                || (*component == "verification" && parent == Some("target"))
133                || (*component == "cache" && parent == Some(".serena"));
134            if cache || (record && !run_records) {
135                return true;
136            }
137        }
138        return false;
139    }
140
141    /// What:
142    ///  Whether a directory with these child names is another checkout and is left out whole.
143    /// Why:
144    ///   The registered worktrees are excluded by path; this also catches a clone or a stale worktree directory
145    ///  that Git no longer registers.
146    ///
147    /// In TS you'd write (pseudocode):
148    /// ```ts
149    /// skipsCheckout(prune, ['.git', 'src']) // true
150    /// ```
151    pub fn skips_checkout(self, child_names: &[String]) -> bool {
152        return match self {
153            Prune::Never => false,
154            Prune::MainCheckout { .. } => child_names.iter().any(|name| return name == CHECKOUT_MARKER),
155        };
156    }
157}
158
159/// Prune rule controls stay out of the release executable.
160#[cfg(test)]
161#[path = "worktree_copy_prune_tests.rs"]
162mod tests;