cargo-allow 0.1.11

Source-tree exception ledger and policy scanner for Rust repositories.
use allow_core::{CargoAllowError, CargoAllowResult};
use allow_match::{CheckMode, evaluate};
use allow_policy::{render_policy, validate_policy};

#[path = "refresh_args.rs"]
mod refresh_args;
#[path = "refresh_render.rs"]
mod refresh_render;
#[path = "refresh_select.rs"]
mod refresh_select;
#[path = "refresh_types.rs"]
mod refresh_types;
use allow_report::MutationReceipt;
pub(crate) use refresh_args::{RefreshArgs, RefreshFormat};
use refresh_render::{render_refresh_json, render_refresh_result};
use refresh_select::{apply_last_seen_refresh, select_location_drift_refresh};
use refresh_types::{RefreshContext, RefreshEmitInput, RefreshRenderInput};

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,
};

pub(crate) fn cmd_refresh(args: &RefreshArgs) -> CargoAllowResult<()> {
    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, mut 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 (entry_index, finding_index, drift_message) =
        select_location_drift_refresh(&cfg, &outcomes, &findings, &args.allow_id)?;
    let finding = findings.get(finding_index).ok_or_else(|| {
        CargoAllowError::new("internal error: selected finding index out of range")
    })?;
    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 original_entry = cfg
        .allow
        .get(entry_index)
        .ok_or_else(|| {
            CargoAllowError::new("internal error: selected allow entry index out of range")
        })?
        .clone();
    let previous_last_seen = cfg
        .allow
        .get(entry_index)
        .ok_or_else(|| {
            CargoAllowError::new("internal error: selected allow entry index out of range")
        })?
        .last_seen
        .clone();
    let mut preview_entry = cfg
        .allow
        .get(entry_index)
        .ok_or_else(|| {
            CargoAllowError::new("internal error: selected allow entry index out of range")
        })?
        .clone();
    apply_last_seen_refresh(&mut preview_entry, finding);
    let written_path = if args.write {
        let entry = cfg.allow.get_mut(entry_index).ok_or_else(|| {
            CargoAllowError::new("internal error: selected allow entry index out of range")
        })?;
        apply_last_seen_refresh(entry, finding);
        validate_policy(&cfg)?;
        let evidence_source_tree_files =
            current_evidence_source_tree_files(&root, args.include_untracked);
        validate_evidence_references_for_source_tree(
            &root,
            &cfg,
            evidence_source_tree_files.as_ref(),
        )?;
        write_file(&policy_path, &render_policy(&cfg))?;
        Some(policy_path.clone())
    } else {
        None
    };
    let entry_for_render = if args.write {
        cfg.allow.get(entry_index).ok_or_else(|| {
            CargoAllowError::new("internal error: selected allow entry index out of range")
        })?
    } else {
        &preview_entry
    };
    let repo_root = root.display().to_string();
    let config_source = policy_path.display().to_string();
    let mutation_receipt = MutationReceipt {
        operation: "refresh",
        tool_version: env!("CARGO_PKG_VERSION"),
        repo_root: Some(&repo_root),
        config_source: Some(&config_source),
        ledger_ids: Vec::new(),
        changed_allow_ids: vec![original_entry.id.as_str()],
        before_fingerprints: vec![Some(allow_core::allow_entry_content_fingerprint(
            &original_entry,
        ))],
        after_fingerprints: vec![Some(allow_core::allow_entry_content_fingerprint(
            &preview_entry,
        ))],
        result: if args.write { "written" } else { "stdout" },
        next_commands: vec![
            format!("cargo-allow explain --id {}", original_entry.id),
            "cargo-allow check --mode no-new".to_string(),
        ],
    };
    render_and_emit(
        args,
        RefreshEmitInput {
            entry: entry_for_render,
            finding,
            previous_last_seen,
            drift_message: &drift_message,
            root: &root,
            inventory_facts,
            written_path: written_path.as_deref(),
            mutation_receipt,
        },
    )
}

fn render_and_emit(args: &RefreshArgs, input: RefreshEmitInput<'_>) -> CargoAllowResult<()> {
    let source_context = SourceTreeReportContext::new(input.root, input.inventory_facts);
    let context = RefreshContext {
        inventory: source_context.inventory(),
    };
    let written = input.written_path.map(|path| path.display().to_string());
    let written_ref = written.as_deref();
    let render_input = RefreshRenderInput {
        entry: input.entry,
        finding: input.finding,
        previous_last_seen: input.previous_last_seen,
        drift_message: input.drift_message,
        dry_run: args.dry_run,
        write_requested: args.write,
        written_path: written_ref,
        context,
        mutation_receipt: input.mutation_receipt,
    };
    let text = match args.format {
        RefreshFormat::Human => render_refresh_result(render_input),
        RefreshFormat::Json => render_refresh_json(render_input),
    };
    emit_text(args.output.as_deref(), &text)?;
    Ok(())
}

#[cfg(test)]
pub(crate) fn sample_refresh_json_for_contract_test() -> String {
    use allow_core::{Finding, FindingKind, LastSeen, Selector, Span, StructuralIdentity};
    let entry = allow_core::AllowEntry {
        id: "allow-0250".to_string(),
        kind: FindingKind::LintException,
        family: Some("expect".to_string()),
        path: Some("src/lib.rs".into()),
        glob: None,
        owner: "lint".to_string(),
        classification: "reviewed_lint_exception".to_string(),
        reason: "Fixture refresh receipt".to_string(),
        evidence: vec!["test:refresh-receipt-fixture".to_string()],
        links: Vec::new(),
        occurrence_limit: None,
        lifecycle: allow_core::Lifecycle {
            created: Some("2026-05-09".to_string()),
            review_after: Some("2026-09-09".to_string()),
            expires: None,
        },
        selector: Selector::default(),
        last_seen: Some(LastSeen {
            line: 22,
            column: 4,
        }),
    };
    let finding = Finding {
        kind: FindingKind::LintException,
        family: Some("expect".to_string()),
        path: "src/lib.rs".into(),
        identity: StructuralIdentity::new("rust", "attribute"),
        message: "fixture refresh drift".to_string(),
        ledger: None,
        span: Some(Span {
            line: 22,
            column: 4,
        }),
    };
    render_refresh_json(RefreshRenderInput {
        entry: &entry,
        finding: &finding,
        previous_last_seen: Some(LastSeen {
            line: 14,
            column: 8,
        }),
        drift_message: "allow-drift last_seen changed from 14:8 to 22:4",
        dry_run: true,
        write_requested: false,
        written_path: None,
        context: RefreshContext {
            inventory: allow_report::InventoryContext::source_syntax(
                "filesystem_include_untracked",
                Some("tests/fixtures/refresh/advisory-drift"),
                Some(2),
            ),
        },
        mutation_receipt: allow_report::MutationReceipt {
            operation: "refresh",
            tool_version: env!("CARGO_PKG_VERSION"),
            repo_root: Some("tests/fixtures/refresh/advisory-drift"),
            config_source: Some("policy/allow.toml"),
            ledger_ids: Vec::new(),
            changed_allow_ids: vec!["allow-0250"],
            before_fingerprints: vec![Some(
                "sha256:v1:0000000000000000000000000000000000000000000000000000000000000000"
                    .to_string(),
            )],
            after_fingerprints: vec![Some(
                "sha256:v1:1111111111111111111111111111111111111111111111111111111111111111"
                    .to_string(),
            )],
            result: "stdout",
            next_commands: vec!["cargo-allow check --mode no-new".to_string()],
        },
    })
}

#[cfg(test)]
#[path = "refresh_tests.rs"]
mod tests;