monochromatic-git-policy-cli 0.6.0

Repository-owned Git policy wrapper: forwards Git commands and checks commits against configured policies.
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
//! What:
//!  The Cargo repair: for each Cargo workspace of a new worktree, clone the source workspace's build state when Cargo keeps
//!  it outside the worktree, then `cargo clean --package <member>` for every member so only the worktree's own crates
//!  rebuild.
//! Why:
//!  Cargo bakes absolute paths into what it builds (`CARGO_MANIFEST_DIR`, build script output), so build state copied from
//!  another location is wrong for the worktree's own crates, but registry and git dependencies hold nothing path-specific
//!  and can be reused. The owner chose this on 2026-11-03: "Reuse deps, rebuild own", done with "Run cargo clean -p".
//!  Where `build-dir` is outside the worktree (the person's `~/.cargo/config.toml` keys it by workspace path) a copy of
//!  ignored files does not carry it, so it is cloned; under Cargo's default layout it is inside `target/`, which is already
//!  copied. `cargo metadata` names both directories, so no configuration is read by the wrapper.
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! const findings = await repairCargo(request); // one per workspace that failed, one for the rest that worked
//! ```

use crate::add_timing::{begin_work, end_work};
use crate::diagnostic_log::debug;
use crate::package_repair_request::RepairRequest;
use crate::package_repair_cargo_concurrent::{ConcurrentWork, ItemLabel, map_concurrent};
use crate::package_repair_cargo_lock::{read_lock, restore_lock};
use crate::package_repair_cargo_workspaces::{MANIFEST, Workspace, distinct_workspaces, manifest_directories, read_workspace};
use crate::package_repair_command::{ToolCommand, ToolEnd, ToolLimits, ToolRun, find_program, run_tool};
use crate::package_repair_report::{Finding, Outcome, Tool, missing_tool};
use crate::package_repair_tree_clone::clone_tree;
use crate::shell_quote::shell_quote;
use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::time::Instant;

/// Tag used for diagnostics from this module.
const TAG: &str = "packageRepairCargo";

/// The program the person's environment resolves.
const PROGRAM: &str = "cargo";

/// What:
///  What a workspace's build state needs in the new worktree.
/// Why:
///   The decision is pure so each layout is tested without Cargo.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type BuildStatePlan = { kind: 'nothing'; reason: string } | { kind: 'clean-only' } | { kind: 'clone-then-clean'; from: string; to: string };
/// ```
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum BuildStatePlan {
    /// Nothing is cloned or cleaned; the text says why, for the log.
    Nothing(String),
    /// The state is already in the worktree (copied with `target/`): only the members are cleaned.
    CleanOnly,
    /// The source's external build directory is cloned to the worktree's, then the members are cleaned.
    CloneThenClean {
        /// The source workspace's build directory.
        from: PathBuf,
        /// The worktree's build directory, which does not exist yet.
        to: PathBuf,
    },
}

/// What:
///  Facts about the disk that decide a plan.
/// Why:
///   They are read once, so the planner stays pure.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type StateFacts = { sourceBuild?: string; sourceBuildExists: boolean; destinationBuildExists: boolean; destinationTargetExists: boolean };
/// ```
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct StateFacts {
    /// The build directory of the source's counterpart workspace; none when the source has no such workspace.
    pub source_build: Option<PathBuf>,
    /// Whether that directory exists.
    pub source_build_exists: bool,
    /// Whether the worktree's build directory exists.
    pub destination_build_exists: bool,
    /// Whether the worktree's target directory exists.
    pub destination_target_exists: bool,
}

/// What:
///  Decide what the worktree's build state needs.
/// Why:
///   Reusing state that is shared with the source, or cleaning where there is none, would do harm or nothing.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// function planBuildState(destination: Workspace, destinationRoot: string, facts: StateFacts): BuildStatePlan;
/// ```
pub fn plan_build_state(destination: &Workspace, destination_root: &Path, facts: &StateFacts) -> BuildStatePlan {
    let Some(source_build) = facts.source_build.as_ref() else {
        return BuildStatePlan::Nothing(String::from("the source has no such workspace, so no state was carried"));
    };
    if *source_build == destination.build_directory {
        return BuildStatePlan::Nothing(String::from("the source and the worktree share one build directory, which a clean would damage"));
    }
    if destination.build_directory.starts_with(destination_root) {
        return if facts.destination_build_exists || facts.destination_target_exists {
            BuildStatePlan::CleanOnly
        } else {
            BuildStatePlan::Nothing(String::from("the build directory is inside the worktree and was not carried"))
        };
    }
    if facts.destination_build_exists {
        return BuildStatePlan::Nothing(String::from("the worktree's build directory already holds state of its own"));
    }
    if facts.source_build_exists {
        return BuildStatePlan::CloneThenClean { from: source_build.clone(), to: destination.build_directory.clone() };
    }
    return if facts.destination_target_exists { BuildStatePlan::CleanOnly } else { BuildStatePlan::Nothing(String::from("neither side holds build state")) };
}

/// What:
///  The arguments of the clean that removes exactly the members' own artifacts.
/// Why:
///   `--offline` keeps the clean from reaching the network; one `--package` per member keeps registry and git dependencies.
///  `--locked` is not used: a committed `Cargo.lock` that no longer matches its manifest (several do here) would make the clean
///  fail, and the person's own clean would not; the file is put back as checked out instead (`restore_lock`).
///
/// In TS you'd write (pseudocode):
/// ```ts
/// function cleanArguments(members: string[]): string[];
/// ```
pub fn clean_arguments(members: &[String]) -> Vec<OsString> {
    let mut arguments: Vec<OsString> = ["clean", "--offline"].iter().map(OsString::from).collect();
    for member in members {
        arguments.push(OsString::from("--package"));
        arguments.push(OsString::from(member));
    }
    return arguments;
}

/// What:
///  The command a person runs in a workspace to do what the repair could not.
/// Why:
///   A printed line must work as pasted, so the directory change is part of it.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// function manualCommand(root: string, members: string[]): string;
/// ```
fn manual_command(root: &Path, members: &[String]) -> String {
    let mut words: Vec<String> = Vec::new();
    for word in clean_arguments(members) {
        words.push(shell_quote(word.as_encoded_bytes()));
    }
    return format!("cd {} && {PROGRAM} {}", shell_quote(root.as_os_str().as_encoded_bytes()), words.join(" "));
}

/// What a repair of one workspace did.
enum Done {
    /// Nothing was needed; the text says why.
    Skipped,
    /// The members were cleaned, and the build directory was cloned first when `cloned` is set.
    Cleaned {
        /// Whether a build directory was cloned.
        cloned: bool,
        /// How many entries of the source's build directory could not be read and were left out of the clone.
        left_out: usize,
    },
    /// The workspace could not be repaired.
    Failed(Finding),
}

/// What:
///  Repair one workspace: plan, clone, clean.
/// Why:
///   Each workspace's state is in a directory of its own, so workspaces never touch each other's.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// async function repairWorkspace(cargo: string, workspace: Workspace, request: RepairRequest, cloneWorkers: number): Promise<Done>;
/// ```
fn repair_workspace(cargo: &Path, workspace: &Workspace, destination_root: &Path, request: &RepairRequest<'_>, clone_workers: usize) -> Done {
    let manual: String = manual_command(workspace.root.as_path(), workspace.members.as_slice());
    let Ok(relative) = workspace.root.strip_prefix(destination_root) else {
        debug(TAG, format!("{} lies outside {} and is left alone", workspace.root.display(), destination_root.display()).as_str());
        return Done::Skipped;
    };
    let source_workspace: PathBuf = request.source.join(relative);
    let source_build: Option<PathBuf> = if source_workspace.join(MANIFEST).is_file() {
        match timed_read_workspace("cargoMetadataSource", cargo, source_workspace.as_path(), request.environment.limits()) {
            Ok(found) => Some(found.build_directory),
            Err(reason) => {
                debug(TAG, format!("the source workspace {} cannot be read: {reason}", source_workspace.display()).as_str());
                None
            }
        }
    } else {
        None
    };
    let facts: StateFacts = StateFacts {
        source_build_exists: match source_build.as_ref() {
            Some(path) => path.exists(),
            None => false,
        },
        source_build,
        destination_build_exists: workspace.build_directory.exists(),
        destination_target_exists: workspace.target_directory.exists(),
    };
    let plan: BuildStatePlan = plan_build_state(workspace, destination_root, &facts);
    debug(TAG, format!("{}: {plan:?}", workspace.root.display()).as_str());
    let (cloned, left_out): (bool, usize) = match &plan {
        BuildStatePlan::Nothing(_) => return Done::Skipped,
        BuildStatePlan::CleanOnly => (false, 0),
        BuildStatePlan::CloneThenClean { from, to } => {
            match timed_clone_tree(from.as_path(), to.as_path(), clone_workers) {
                Ok(made) => {
                    debug(TAG, format!("cloned {} entries from {} to {}", made.made, from.display(), to.display()).as_str());
                    for path in &made.left_out {
                        debug(TAG, format!("{} could not be read and is not in the clone", from.join(path).display()).as_str());
                    }
                    (true, made.left_out.len())
                }
                Err(reason) => return workspace_failed(manual.as_str(), format!("the build state could not be cloned: {reason}")),
            }
        }
    };
    let command: ToolCommand = ToolCommand { arguments: clean_arguments(workspace.members.as_slice()), directory: workspace.root.clone(), set: Vec::new() };
    let lock_before: Option<Vec<u8>> = read_lock(workspace.root.as_path());
    let run: ToolRun = timed_clean(cargo, &command, request.environment.limits());
    restore_lock(workspace.root.as_path(), lock_before.as_deref());
    return match run.end {
        ToolEnd::Succeeded => Done::Cleaned { cloned, left_out },
        ToolEnd::Failed(reason) => workspace_failed(manual.as_str(), reason),
        ToolEnd::TimedOut => workspace_failed(manual.as_str(), String::from("it showed no sign of work for the quiet window and was stopped")),
        ToolEnd::CeilingReached(ceiling) => workspace_failed(manual.as_str(), crate::package_repair_report::ceiling_reason(crate::package_repair_report::Tool::Cargo, ceiling)),
    };
}

/// What:
///  The words that say how many entries of the cloned build state could not be read, or nothing when all could.
/// Why:
///   The person is told what the clone left out: the entries are the source's, unreadable to everyone but a chmod the repair
///  never performs, and Cargo rebuilds whatever of them it needs.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// leftOutWords(2) // '; 2 unreadable entries were left out'
/// ```
fn left_out_words(left_out: usize) -> String {
    return match left_out {
        0 => String::new(),
        1 => String::from("; 1 unreadable entry was left out"),
        many => format!("; {many} unreadable entries were left out"),
    };
}

/// What:
///  The finding of a workspace whose repair failed: the reason, and the command the person can run instead.
/// Why:
///   Every failing step of `repair_workspace` ends the same way, and a named function stands where a closure that
///  built this result stood.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// const workspaceFailed = (manual: string, reason: string) => ({ kind: 'Failed', reason, runInstead: manual });
/// ```
fn workspace_failed(manual: &str, reason: String) -> Done {
    return Done::Failed(Finding { tool: Tool::Cargo, outcome: Outcome::Failed { reason, run_instead: manual.to_owned() } });
}

/// What:
///  Read one workspace with `cargo metadata`, timed as the step `name`.
/// Why:
///   The add timing record sums the work of worker threads by name; the call runs in place between `begin_work` and `end_work`.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// const began = beginWork();
/// const workspace = await readWorkspace(cargo, directory, limits);
/// endWork(name, began);
/// ```
fn timed_read_workspace(name: &str, cargo: &Path, directory: &Path, limits: ToolLimits) -> Result<Workspace, String> {
    let began: Instant = begin_work();
    let read: Result<Workspace, String> = read_workspace(cargo, directory, limits);
    end_work(name, began);
    return read;
}

/// What:
///  Clone a build directory with the timing record's step `cargoCloneTree`.
/// Why:
///   Same as `timed_read_workspace`: the call runs in place between `begin_work` and `end_work`.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// const began = beginWork();
/// const made = await cloneTree(from, to, workers);
/// endWork('cargoCloneTree', began);
/// ```
fn timed_clone_tree(from: &Path, to: &Path, clone_workers: usize) -> Result<crate::package_repair_tree_clone::Cloned, String> {
    let began: Instant = begin_work();
    let made: Result<crate::package_repair_tree_clone::Cloned, String> = clone_tree(from, to, clone_workers);
    end_work("cargoCloneTree", began);
    return made;
}

/// What:
///  Run `cargo clean` with the timing record's step `cargoClean`.
/// Why:
///   Same as `timed_read_workspace`: the call runs in place between `begin_work` and `end_work`.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// const began = beginWork();
/// const run = await runTool(cargo, command, limits);
/// endWork('cargoClean', began);
/// ```
fn timed_clean(cargo: &Path, command: &ToolCommand, limits: ToolLimits) -> ToolRun {
    let began: Instant = begin_work();
    let run: ToolRun = run_tool(cargo, command, limits);
    end_work("cargoClean", began);
    return run;
}

/// The label of a metadata read whose worker panicked: the directory it was reading.
struct ReadingLabel;

impl ItemLabel<PathBuf> for ReadingLabel {
    fn label(&self, directory: &PathBuf) -> String {
        return format!("reading the Cargo workspace at {}", directory.display());
    }
}

/// The label of a workspace repair whose worker panicked: the workspace it was repairing.
struct RepairLabel;

impl ItemLabel<Workspace> for RepairLabel {
    fn label(&self, workspace: &Workspace) -> String {
        return format!("the repair of the Cargo workspace at {}", workspace.root.display());
    }
}

/// What:
///  The job of reading the Cargo workspace of one directory, timed as the step `cargoMetadataWorktree`.
/// Why:
///   The concurrent runner takes its job as a value with a `run` method, and the state the job needs (the Cargo program and
///  the request) is two borrowed fields, so a struct stands where a closure that captured them stood.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// class ReadWorkspaceJob implements ConcurrentWork<string, Workspace> { constructor(readonly cargo: string, readonly request: RepairRequest) {} }
/// ```
struct ReadWorkspaceJob<'a> {
    /// The Cargo program found on the person's search path.
    cargo: &'a Path,
    /// The request whose environment bounds the tool.
    request: &'a RepairRequest<'a>,
}

impl ConcurrentWork<PathBuf, Result<Workspace, String>> for ReadWorkspaceJob<'_> {
    fn run(&self, directory: &PathBuf) -> Result<Workspace, String> {
        return timed_read_workspace("cargoMetadataWorktree", self.cargo, directory.as_path(), self.request.environment.limits());
    }
}

/// What:
///  The job of repairing one Cargo workspace, timed as the step `cargoRepairWorkspace`.
/// Why:
///   Same as `ReadWorkspaceJob`: four borrowed fields and a count stand where a closure that captured them stood.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// class RepairWorkspaceJob implements ConcurrentWork<Workspace, Done> {}
/// ```
struct RepairWorkspaceJob<'a> {
    /// The Cargo program found on the person's search path.
    cargo: &'a Path,
    /// The real path of the new worktree.
    destination_root: &'a Path,
    /// The request whose source, Git and environment the repair reads.
    request: &'a RepairRequest<'a>,
    /// How many threads one build-state clone may use.
    clone_workers: usize,
}

impl ConcurrentWork<Workspace, Done> for RepairWorkspaceJob<'_> {
    fn run(&self, workspace: &Workspace) -> Done {
        return timed_repair_workspace(self, workspace);
    }
}

/// Repair one workspace with the timing record's step `cargoRepairWorkspace`; the call runs in place between `begin_work` and `end_work`.
fn timed_repair_workspace(job: &RepairWorkspaceJob<'_>, workspace: &Workspace) -> Done {
    let began: Instant = begin_work();
    let done: Done = repair_workspace(job.cargo, workspace, job.destination_root, job.request, job.clone_workers);
    end_work("cargoRepairWorkspace", began);
    return done;
}

/// What:
///  Repair every Cargo workspace of the new worktree.
/// Why:
///   Workspaces are independent (each has its own build directory), so they are repaired side by side.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// async function repairCargo(request: RepairRequest): Promise<Finding[]>;
/// ```
pub fn repair_cargo(request: &RepairRequest<'_>) -> Vec<Finding> {
    let started: Instant = Instant::now();
    let directories: Vec<PathBuf> = match manifest_directories(request) {
        Ok(found) => found,
        Err(reason) => {
            debug(TAG, format!("{}: {reason}", request.destination.display()).as_str());
            return Vec::new();
        }
    };
    if directories.is_empty() {
        return Vec::new();
    }
    let Some(cargo) = find_program(PROGRAM, request.environment.path()) else {
        return vec![missing_tool(Tool::Cargo, PROGRAM, "cargo clean --offline --package <member> in each Cargo workspace of the worktree")];
    };
    let workers: usize = request.environment.workers;
    let reader: ReadWorkspaceJob<'_> = ReadWorkspaceJob { cargo: cargo.as_path(), request };
    let mut read: Vec<Result<Workspace, String>> = Vec::new();
    for outcome in map_concurrent(directories.as_slice(), workers, &ReadingLabel, &reader) {
        // A panic in the read and a failed read are both a failed read.
        read.push(match outcome {
            Ok(inner) => inner,
            Err(reason) => Err(reason),
        });
    }
    let mut findings: Vec<Finding> = Vec::new();
    let mut found: Vec<Workspace> = Vec::new();
    for (directory, outcome) in directories.iter().zip(read) {
        match outcome {
            Ok(workspace) => found.push(workspace),
            Err(reason) => findings.push(Finding { tool: Tool::Cargo, outcome: Outcome::Failed { reason, run_instead: manual_command(directory.as_path(), &[String::from("<member>")]) } }),
        }
    }
    let workspaces: Vec<Workspace> = distinct_workspaces(found);
    // Real paths: Cargo names the workspace by its real path, and the worktree may be reached through a link.
    let destination_root: PathBuf = match std::fs::canonicalize(request.destination) {
        Ok(real) => real,
        Err(_error) => request.destination.to_path_buf(),
    };
    let clone_workers: usize = if workspaces.len() > 1 { 1 } else { workers };
    let repairer: RepairWorkspaceJob<'_> = RepairWorkspaceJob { cargo: cargo.as_path(), destination_root: destination_root.as_path(), request, clone_workers };
    let mut done: Vec<Done> = Vec::new();
    for (outcome, workspace) in map_concurrent(workspaces.as_slice(), workers, &RepairLabel, &repairer).into_iter().zip(workspaces.iter()) {
        done.push(match outcome {
            Ok(finished) => finished,
            Err(reason) => Done::Failed(Finding { tool: Tool::Cargo, outcome: Outcome::Failed { reason, run_instead: manual_command(workspace.root.as_path(), &[String::from("<member>")]) } }),
        });
    }
    let mut cleaned: usize = 0;
    let mut cloned: usize = 0;
    let mut left_out: usize = 0;
    for outcome in done {
        match outcome {
            Done::Skipped => {}
            Done::Cleaned { cloned: was_cloned, left_out: unreadable } => {
                cleaned += 1;
                cloned += usize::from(was_cloned);
                left_out += unreadable;
            }
            Done::Failed(finding) => findings.push(finding),
        }
    }
    if cleaned > 0 {
        let workspace_noun: &str = if cleaned == 1 { "workspace" } else { "workspaces" };
        let directory_noun: &str = if cloned == 1 { "build directory" } else { "build directories" };
        findings.push(Finding { tool: Tool::Cargo, outcome: Outcome::Repaired { detail: format!("Cargo kept dependency builds and cleaned the worktree's own crates in {cleaned} {workspace_noun} ({cloned} {directory_noun} cloned{})", left_out_words(left_out)), millis: started.elapsed().as_millis() } });
    }
    return findings;
}

/// Cargo repair controls stay out of the release executable.
#[cfg(test)]
#[path = "package_repair_cargo_tests.rs"]
mod tests;