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
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
//! What:
//! The linked-worktree ignored-state copy around one forwarded Git invocation: the public entry point the
//! integrator calls, the settlement and summary it writes, and the exit status it returns.
//! Why:
//! A worktree that Git creates (or a bare repository's) needs its ignored files from the source worktree.
//! No lock stands around the command: the destination comes from the command line, the copy is a transaction of its own
//! (a journal and a stage claim that name their owner process), and a copy a dead process left is recovered first by the
//! next command. Concurrent adds in one repository never wait for one another (the owner's decision of 2026-11-03).
//!
//! Integrator contract:
//! The caller decides the bypass facts (`--no-worktree-copy`, short-circuit global help or version, whether the
//! command creates a worktree after alias resolution, and the destination its command line names) with the native
//! modules that already own them, passes the repository identity and the effective working directory, and supplies the
//! runner that starts real Git. This module never runs the commit refusal; it is not wired yet.
//! A main-checkout source is observed only when the command creates a worktree; it then hands over its ignored
//! state under the main checkout's leave-out rule (`worktree_copy_prune.rs`), with its registered worktrees excluded.
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! await runGitWithWorktreeCopy({ args, gitPath, identity, command, environment });
//! ```
use super::add_timing::{begin as begin_timing, finish as finish_timing, step as timed};
use super::json_record::quote;
use super::transaction_git::GitContext;
use super::worktree_copy_error::{WorktreeCopyError, lifecycle_failure, raw_failure};
use super::worktree_copy_recovery::recover_pending_worktree_copies;
use super::worktree_copy_selection::CopySelection;
use super::worktree_copy_registry::{
CreatedWorktrees, WorktreeCopyObservation, destination_is_registered, find_created_worktree, observe_copy_source,
};
use super::worktree_copy_transaction::synchronize_created_worktree_noting;
use super::worktree_identity::WorktreeIdentity;
use super::worktree_add_plan::AddPlan;
use super::worktree_ladder::{LadderResult, begin_ladder};
#[cfg(target_os = "linux")]
use super::package_repair::{RepairEnvironment, RepairRequest, RepairSwitch, repair_worktree};
use std::io::Write;
use std::path::{Path, PathBuf};
/// What:
/// How a real Git run ended.
/// Why:
/// Its numeric status is the exit status the wrapper keeps; a signal has none.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type ForwardedGitExecution = { failure?: SubprocessError };
/// ```
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct GitStatus {
/// The exit code, absent when Git ended by a signal.
pub code: Option<i32>,
}
impl GitStatus {
/// Whether Git exited with status zero.
pub fn succeeded(&self) -> bool {
return self.code == Some(0);
}
}
/// What:
/// Everything the lifecycle needs to decide how one invocation is settled.
/// Why:
/// The decisions that depend on argv and global options are made by their owners and passed in as facts.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// runGitWithWorktreeCopy({ args, gitPath, identity, command, environment })
/// ```
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Invocation {
/// The effective repository identity captured before real Git.
pub identity: WorktreeIdentity,
/// The working directory the repository queries run in.
pub effective_cwd: PathBuf,
/// Whether the command creates a worktree after alias resolution.
pub creates: bool,
/// The destination the creating command's line names, in the form Git records it; absent when the line has a form that is
/// not read, in which case no copy is made and the command says so.
pub destination: Option<PathBuf>,
/// Whether `--no-worktree-copy` appeared in flag position.
pub opted_out: bool,
/// Whether real Git only prints help or its version.
pub short_circuit: bool,
/// The real Git executable and the variables every Git child receives.
pub context: GitContext,
/// The destination of a `git worktree add` in a form the cheap-worktree ladder serves, when it is one.
pub add_plan: Option<AddPlan>,
/// The person's `--quiet` and `--no-checkout`, read from the arguments, which decide what the package-manager repair of
/// a new worktree may print and whether it runs.
#[cfg(target_os = "linux")]
pub repair: RepairSwitch,
}
/// What:
/// The result of one settled invocation: Git's status and the copy failure, if any.
/// Why:
/// Both facts decide the exit status, so they travel together.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// // a resolved run, or a ForwardedGitWorktreeCopyError carrying both facts
/// ```
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Settlement {
/// How real Git ended.
pub git: GitStatus,
/// The ignored-state copy failure, when copying failed.
pub copy_failure: Option<WorktreeCopyError>,
}
/// What:
/// Which Git run the settlement asks its runner to start.
/// Why:
/// The person's command is one run; the cheap-worktree ladder adds `--no-checkout` to it and starts the commands that
/// complete the worktree itself, each with the caller's streams.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type GitStep = { kind: 'person' } | { kind: 'without-checkout'; insertAt: number } | { kind: 'other'; args: string[]; env: [string, string][] };
/// ```
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum GitStep<'a> {
/// The person's arguments, unchanged.
Person,
/// The person's arguments with `--no-checkout` inserted at this index.
WithoutCheckout(usize),
/// Another Git command with these arguments and these variables added.
Other {
/// The arguments, global options and subcommand included.
arguments: &'a [std::ffi::OsString],
/// The variables added to the run's environment.
environment: &'a [(std::ffi::OsString, std::ffi::OsString)],
},
}
/// Runs one real Git step.
pub type GitRunner<'a> = dyn FnMut(&GitStep<'_>) -> Result<GitStatus, WorktreeCopyError> + 'a;
/// What:
/// The one summary line a successful copy writes.
/// Why:
/// Users and agents read the count, the source, and the destination count from it.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// renderSummary({ copiedEntries, destinationCount, sourceRoot })
/// ```
pub fn summary_line(copied: usize, destinations: usize, source: Option<&Path>) -> String {
let source_text: String = match source {
None => String::from("bare repository with an empty source set"),
Some(path) => quote(path.to_string_lossy().as_ref()),
};
let worktree_noun: &str = if destinations == 1 {
"worktree"
} else {
"worktrees"
};
let entry_noun: &str = if copied == 1 { "entry" } else { "entries" };
return format!(
"cli-git: copied {copied} ignored filesystem {entry_noun} from {source_text} into {destinations} new {worktree_noun}.\n"
);
}
/// What:
/// The exit status of a settled invocation.
/// Why:
/// A copy-only failure is 2; Git's own status is kept when Git failed; a signal without a status is 1.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// // src/bin.ts: copy failure 2, Git status kept, signal 1
/// ```
pub fn exit_code(settlement: &Settlement) -> i32 {
let git_code: i32 = settlement.git.code.unwrap_or(1);
// `match` on whether Git and the copy each succeeded.
match (
settlement.git.succeeded(),
settlement.copy_failure.is_some(),
) {
(true, false) => return 0,
(false, false) => return git_code,
(true, true) => return 2,
(false, true) => return git_code,
}
}
/// What:
/// Runs one forwarded invocation with no synchronization, and settles its status.
/// Why:
/// Every bypass forwards exactly the same way, so the runner is called once.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// await forwardOnly({ args, gitPath, environment });
/// ```
fn forward_only(run_git: &mut GitRunner) -> Result<Settlement, WorktreeCopyError> {
let git: GitStatus = run_git(&GitStep::Person)?;
return Ok(Settlement {
git,
copy_failure: None,
});
}
/// The line a successful `git worktree add` prints when its command line has a form whose destination is not read.
const UNKNOWN_DESTINATION_NOTICE: &str = "cli-git: this `git worktree add` names its worktree in a form cli-git does not read, so the worktree received no ignored-state copy. Name the path as a plain operand (`git worktree add [-b <branch>] <path> [<commit-ish>]`, with `-C <directory>` or `-c <name>=<value>` before `worktree`) to have it copied.\n";
/// What:
/// The line for a destination Git made whose registration could not be confirmed.
/// Why:
/// A worktree that gets no copy must say so; here the pointer in its `.git` file and the registration it names do not
/// name each other, so cli-git cannot tell the worktree is the command's own and copies nothing.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// `cli-git: ${quote(destination)} was made by git worktree add, but ...`
/// ```
fn unconfirmed_destination_notice(destination: &Path) -> String {
return format!(
"cli-git: {} was made, but its registration could not be confirmed as this command's, so it received no ignored-state copy. `git worktree list` shows whether Git registered it.\n",
quote(destination.to_string_lossy().as_ref())
);
}
/// What:
/// Synchronizes the worktree this command registered at its destination and writes the summary line for it; the root of
/// the created worktree, a snapshot's included, comes back for the package-manager repair.
/// Why:
/// The destination is the one the command line named, so a worktree another add registered at the same time is never
/// copied into here; a failure stops the copy with no summary written.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// const found = await findCreatedWorktree(...); const copied = await synchronizeCreatedWorktree(...); process.stderr.write(renderSummary(copied));
/// ```
fn synchronize_after(
invocation: &Invocation,
observation: &WorktreeCopyObservation,
registered_before: bool,
git_succeeded: bool,
carried: Option<&Path>,
stderr: &mut dyn Write,
) -> Result<Vec<PathBuf>, WorktreeCopyError> {
// A main checkout that names no path hands over nothing: no stage, no journal and no summary line.
if matches!(&observation.selection, CopySelection::Paths(paths) if paths.is_empty()) {
return Ok(carried.map(Path::to_path_buf).into_iter().collect());
}
let Some(destination) = invocation.destination.as_deref() else {
stderr
.write_all(UNKNOWN_DESTINATION_NOTICE.as_bytes())
.map_err(|error| return raw_failure(&error))?;
return Ok(Vec::new());
};
let mut found: CreatedWorktrees =
find_created_worktree(&invocation.context, observation, destination, registered_before)?;
// A destination Git made but whose registration cannot be confirmed is told, not skipped without a word.
if found.created.is_empty()
&& git_succeeded
&& !registered_before
&& std::fs::symlink_metadata(destination.join(".git")).is_ok()
{
let line: String = unconfirmed_destination_notice(destination);
stderr
.write_all(line.as_bytes())
.map_err(|error| return raw_failure(&error))?;
return Ok(Vec::new());
}
let created_roots: Vec<PathBuf> = found.created.iter().map(|worktree| { return worktree.root.clone(); }).collect();
// A destination that received the source's ignored state with a snapshot needs no copy.
found.created.retain(|worktree| { return carried != Some(worktree.root.as_path()); });
if found.created.is_empty() {
return Ok(created_roots);
}
let mut copied: usize = 0;
// `match` on the source: a bare repository has nothing to copy, so only a worktree source is copied.
match observation.source_root.as_ref() {
None => {}
Some(source) => {
for created in &found.created {
copied += synchronize_created_worktree_noting(
&invocation.context,
observation.common_dir.as_path(),
source.as_path(),
created.root.as_path(),
found.registered_roots.as_slice(),
&observation.selection,
stderr,
)?;
}
}
}
let line: String = summary_line(
copied,
found.created.len(),
observation.source_root.as_deref(),
);
stderr
.write_all(line.as_bytes())
.map_err(|error| return raw_failure(&error))?;
return Ok(created_roots);
}
/// What:
/// What is left to do once the ignored state is in place: the source the ignored files came from and the worktrees the
/// command created.
/// Why:
/// The package-manager repairs touch only the new worktrees and read the source, so no other add has to wait for them; they
/// run last, with the facts the copy found.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type AfterLock = { sourceRoot?: string; created: string[] };
/// ```
struct AfterLock {
/// The checkout the ignored files came from; none for a bare repository, which copies nothing.
source_root: Option<PathBuf>,
/// The roots of every worktree the command created, a snapshot's included; empty after a failed copy.
created: Vec<PathBuf>,
}
/// What:
/// Repair the package-manager state of every worktree the command created, once its ignored state is in place.
/// Why:
/// A copy or a snapshot keeps links and build state exactly as the source had them; each tool's own offline repair makes
/// them work at the new location (`package_repair.rs`). It never fails the add: a repair that cannot finish says so in one
/// line that names the command to run. A bare repository has no source worktree, so nothing was copied and nothing is repaired.
/// It runs on the worktree the registration binding confirmed, and no lock stands around it, so a slow tool never holds
/// another add.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// for (const root of roots) repairWorktree({ git, source, destination: root, switch, environment }, stderr);
/// ```
#[cfg(target_os = "linux")]
fn repair_created(invocation: &Invocation, after: &AfterLock, stderr: &mut dyn Write) {
let Some(source) = after.source_root.as_deref() else { return };
let environment: RepairEnvironment = RepairEnvironment::from_process();
for root in &after.created {
let request: RepairRequest<'_> = RepairRequest { git: &invocation.context, source, destination: root.as_path(), switch: invocation.repair, environment: &environment };
repair_worktree(&request, stderr);
}
}
/// What:
/// Runs one creating command: whether its destination was registered already, the ladder and real Git, and the copy.
/// Why:
/// Nothing here waits for another command: the destination, the stage claim and the journal belong to this command alone,
/// so any number of adds run side by side in one repository.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// const before = isRegistered(destination); const status = await runLadder(); await synchronize(destination, before);
/// ```
fn settle_creation(
invocation: &Invocation,
observation: &WorktreeCopyObservation,
run_git: &mut GitRunner,
stderr: &mut dyn Write,
) -> Result<(Settlement, AfterLock), WorktreeCopyError> {
let registered_before: bool = invocation
.destination
.as_deref()
.is_some_and(|destination| { return destination_is_registered(observation.common_dir.as_path(), destination); });
let ladder = timed("chooseRung", || { return begin_ladder(invocation.add_plan.as_ref(), &invocation.context, &invocation.identity); });
let ran: Result<GitStatus, WorktreeCopyError> = timed("gitAdd", || { return run_git(&ladder.first_step()); });
let settled: Result<LadderResult, WorktreeCopyError> = match ran {
Ok(status) => Ok(timed("finishRung", || { return ladder.finish(status, run_git, &invocation.context, &mut std::io::stdout()); })),
Err(error) => {
// Git did not start: the ladder only has its empty subvolume to remove, whatever status it is given.
ladder.finish(GitStatus { code: None }, run_git, &invocation.context, &mut std::io::sink());
Err(error)
}
};
let LadderResult { git, carried } = settled?;
let (copy_failure, created): (Option<WorktreeCopyError>, Vec<PathBuf>) =
if git.succeeded() || invocation.destination.is_some() {
match timed("copyIgnored", || { return synchronize_after(invocation, observation, registered_before, git.succeeded(), carried.as_deref(), stderr); }) {
Ok(roots) => (None, roots),
// A copy that failed leaves a worktree that is not whole; repairing it would hide that.
Err(failure) => (Some(failure), Vec::new()),
}
} else {
(None, Vec::new())
};
return Ok((Settlement { git, copy_failure }, AfterLock { source_root: observation.source_root.clone(), created }));
}
/// What:
/// Settles one forwarded invocation: bypasses forward untouched; every other command first recovers what dead owners left,
/// and a creating command then copies the ignored state into the worktree it created.
/// Why:
/// The integrator calls this once per forwarded command and turns the settlement into the exit status with [`exit_code`].
///
/// In TS you'd write (pseudocode):
/// ```ts
/// await runGitWithWorktreeCopy({ args, gitPath, identity, command, environment });
/// ```
pub fn run_with_worktree_copy(
invocation: &Invocation,
run_git: &mut GitRunner,
stderr: &mut dyn Write,
) -> Result<Settlement, WorktreeCopyError> {
return settle_invocation(invocation, run_git, stderr).map_err(lifecycle_failure);
}
/// What:
/// The settlement of one invocation, before the lifecycle words its raw failures.
/// Why:
/// Only the top of the lifecycle words raw failures, as the incumbent's `asWorktreeCopyError` does.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// // body of runGitWithWorktreeCopy
/// ```
fn settle_invocation(
invocation: &Invocation,
run_git: &mut GitRunner,
stderr: &mut dyn Write,
) -> Result<Settlement, WorktreeCopyError> {
// `if` chain in the incumbent's order: each bypass forwards without touching the repository.
if invocation.opted_out || invocation.short_circuit {
return forward_only(run_git);
}
let Some(observation) = observe_copy_source(
&invocation.identity,
invocation.effective_cwd.as_path(),
)?
else {
return forward_only(run_git);
};
// The record of an add's steps covers a creating command the ladder serves; every other command is forwarded unrecorded.
let recorded: bool = invocation.creates && invocation.add_plan.is_some();
if recorded {
begin_timing(observation.common_dir.clone());
}
let settled: Result<Settlement, WorktreeCopyError> = settle_observed(invocation, &observation, run_git, stderr);
if recorded {
finish_timing(settled.as_ref().map_or(1, exit_code));
}
return settled;
}
/// What:
/// The part of a forwarded command's settlement that follows the observation of its source: recovery, then, for a creating
/// command, the ladder, the copy of the ignored state and the repairs.
/// Why:
/// The caller records the steps of a creating command around this whole part, whichever way it ends.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// await settleObserved({ invocation, observation, runGit, stderr });
/// ```
fn settle_observed(
invocation: &Invocation,
observation: &WorktreeCopyObservation,
run_git: &mut GitRunner,
stderr: &mut dyn Write,
) -> Result<Settlement, WorktreeCopyError> {
// Recovery never waits: it takes over only journals and claims whose owner is dead, and one rename picks its single winner.
timed("recover", || { return recover_pending_worktree_copies(observation.common_dir.as_path(), stderr); })?;
if !invocation.creates {
return forward_only(run_git);
}
let (settlement, after) = settle_creation(invocation, observation, run_git, stderr)?;
// No lock stands around the add: the repairs touch only the new worktree and read the source, and another add never waits
// for them.
#[cfg(target_os = "linux")]
timed("repair", || { repair_created(invocation, &after, stderr); });
#[cfg(not(target_os = "linux"))]
let _after = after;
return Ok(settlement);
}
/// Lifecycle controls stay out of the release executable.
#[cfg(test)]
#[path = "worktree_copy_lifecycle_tests.rs"]
mod tests;