use allow_core::{CargoAllowError, CargoAllowResult};
use allow_match::{CheckMode, evaluate};
use allow_policy::{render_policy, validate_policy};
use allow_report::MutationReceipt;
use crate::{
EvidenceValidationMode, MutationLock, SourceTreeReportContext, config_path, emit_text,
evidence_inventory::{
current_evidence_source_tree_files, validate_evidence_references_for_source_tree,
},
load_world_with_evidence_mode, resolve_source_tree_root, write_file,
};
#[path = "prune_args.rs"]
mod prune_args;
#[path = "prune_render.rs"]
mod prune_render;
#[path = "prune_stale.rs"]
mod prune_stale;
#[path = "prune_types.rs"]
mod prune_types;
pub(crate) use prune_args::PruneArgs;
use prune_args::PruneFormat;
use prune_render::{render_prune_stale_json, render_prune_stale_result};
use prune_stale::{
config_without_prune_candidates, prune_stale_candidates,
removed_toml_blocks as stale_removed_toml_blocks,
};
use prune_types::{PruneCandidate, PruneContext, PruneRenderMode};
#[cfg(test)]
use crate::RootArgs;
#[cfg(test)]
use allow_core::{AllowConfig, FindingKind, MatchOutcome, MatchStatus};
#[cfg(test)]
use std::path::PathBuf;
pub(crate) fn cmd_prune(args: &PruneArgs) -> CargoAllowResult<()> {
if !args.stale {
return Err(CargoAllowError::new(
"prune currently supports only --stale",
));
}
if args.dry_run && args.write {
return Err(CargoAllowError::new(
"pass either --dry-run or --write, not both",
));
}
let mutation_lock = if args.write {
let cwd = std::env::current_dir()
.map_err(|error| CargoAllowError::new(format!("failed to read cwd: {error}")))?;
let root = resolve_source_tree_root(args.root.root.as_deref(), cwd)?;
let path = config_path(&root, args.config.as_deref()).ok_or_else(|| {
CargoAllowError::new("no policy config found; run `cargo-allow init` or pass --config")
})?;
Some(MutationLock::acquire(path)?)
} else {
None
};
let (root, cfg, findings, inventory_facts, _federation) = load_world_with_evidence_mode(
args.root.root.as_deref(),
args.config.as_deref(),
true,
None,
args.include_untracked,
EvidenceValidationMode::ReportOnly,
)?;
let _mutation_lock = mutation_lock;
let outcomes = evaluate(&cfg, &findings, CheckMode::NoNew);
let candidates = prune_stale_candidates(&cfg, &outcomes);
let policy_path = config_path(&root, args.config.as_deref()).ok_or_else(|| {
CargoAllowError::new("no policy config found; run `cargo-allow init` or pass --config")
})?;
let mut receipt_candidates = candidates.iter().collect::<Vec<_>>();
receipt_candidates.sort_by(|left, right| left.id.cmp(&right.id));
let before_fingerprints = receipt_candidates
.iter()
.map(|candidate| {
cfg.allow
.iter()
.find(|entry| entry.id == candidate.id)
.ok_or_else(|| {
CargoAllowError::new(format!(
"internal error: stale candidate {} is missing from policy",
candidate.id
))
})
.map(|entry| Some(allow_core::allow_entry_content_fingerprint(entry)))
})
.collect::<CargoAllowResult<Vec<_>>>()?;
let repo_root = root.display().to_string();
let config_source = crate::policy_config::git_relative_config_path(&root, Some(&policy_path))?
.to_string_lossy()
.replace('\\', "/");
let recovery_command = format!("git diff -- {config_source}");
let mutation_receipt = MutationReceipt {
operation: "prune",
tool_version: env!("CARGO_PKG_VERSION"),
repo_root: Some(&repo_root),
config_source: Some(&config_source),
ledger_ids: Vec::new(),
changed_allow_ids: receipt_candidates
.iter()
.map(|candidate| candidate.id.as_str())
.collect(),
before_fingerprints,
after_fingerprints: vec![None; receipt_candidates.len()],
result: if args.write && !candidates.is_empty() {
"written"
} else {
"stdout"
},
next_commands: vec![
recovery_command,
"cargo-allow check --mode no-new".to_string(),
],
};
let rendered_policy =
(args.format == PruneFormat::Human && !candidates.is_empty()).then(|| render_policy(&cfg));
let removed_toml_blocks = rendered_policy
.as_deref()
.map(|rendered| stale_removed_toml_blocks(rendered, &candidates))
.unwrap_or_default();
let written_path = if args.write && !candidates.is_empty() {
let pruned = config_without_prune_candidates(&cfg, &candidates);
validate_policy(&pruned)?;
let evidence_source_tree_files =
current_evidence_source_tree_files(&root, args.include_untracked);
validate_evidence_references_for_source_tree(
&root,
&pruned,
evidence_source_tree_files.as_ref(),
)?;
write_file(&policy_path, &render_policy(&pruned))?;
Some(policy_path.clone())
} else {
None
};
let source_context = SourceTreeReportContext::new(&root, inventory_facts);
let context = PruneContext {
inventory: source_context.inventory(),
mutation_receipt,
};
let text = match args.format {
PruneFormat::Human => render_prune_stale_result(
&candidates,
&removed_toml_blocks,
args.dry_run,
args.write,
written_path.as_deref(),
context,
),
PruneFormat::Json => render_prune_stale_json(
&candidates,
args.dry_run,
args.write,
written_path.as_deref(),
context,
),
};
emit_text(args.output.as_deref(), &text)?;
Ok(())
}
#[cfg(test)]
pub(crate) fn sample_prune_json_for_contract_test() -> String {
let candidates = Vec::new();
render_prune_stale_json(
&candidates,
true,
false,
None,
PruneContext {
inventory: allow_report::InventoryContext::source_syntax(
"git_tracked",
Some("H:/Code/Rust/cargo-allow"),
Some(49),
),
mutation_receipt: allow_report::MutationReceipt {
operation: "prune",
tool_version: env!("CARGO_PKG_VERSION"),
repo_root: Some("H:/Code/Rust/cargo-allow"),
config_source: Some("policy/allow.toml"),
ledger_ids: Vec::new(),
changed_allow_ids: Vec::new(),
before_fingerprints: Vec::new(),
after_fingerprints: Vec::new(),
result: "stdout",
next_commands: vec!["cargo-allow check --mode no-new".to_string()],
},
},
)
}
#[cfg(test)]
#[path = "prune_render_tests.rs"]
mod render_tests;
#[cfg(test)]
#[path = "prune_tests.rs"]
mod tests;