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