cargo-port 0.1.0

A TUI for inspecting and managing Rust projects
use crate::project;
use crate::project::AbsolutePath;
use crate::project::ManifestFingerprint;
use crate::project::WorkspaceMetadata;
use crate::scan;
use crate::scan::CargoMetadataError;
use crate::tui::app::App;
use crate::tui::app::target_index::TargetDirMember;
impl App {
    /// Merge a `cargo metadata` arrival back into the process-wide store and
    /// advance the startup metadata phase. The startup path drives the
    /// "Cargo metadata" row of the consolidated Startup panel; post-startup
    /// per-workspace refreshes land with Step 1b (watcher-triggered refresh)
    /// — until then only the startup path can arrive here.
    pub(super) fn handle_cargo_metadata_msg(
        &mut self,
        workspace_root: AbsolutePath,
        generation: u64,
        fingerprint: &ManifestFingerprint,
        result: Result<WorkspaceMetadata, CargoMetadataError>,
    ) {
        let Some(is_current) = self
            .scan
            .metadata_store()
            .lock()
            .ok()
            .map(|store| store.is_current_generation(&workspace_root, generation))
        else {
            tracing::warn!(
                workspace_root = %workspace_root.as_path().display(),
                generation,
                "cargo_metadata_store_lock_poisoned"
            );
            return;
        };
        if !is_current {
            tracing::debug!(
                workspace_root = %workspace_root.as_path().display(),
                generation,
                "cargo_metadata_msg_stale_generation"
            );
            return;
        }
        match result {
            Ok(workspace_metadata) => {
                if !self.accept_cargo_metadata(
                    &workspace_root,
                    generation,
                    fingerprint,
                    workspace_metadata,
                ) {
                    return;
                }
            },
            Err(err) => match err.user_facing_message() {
                Some(message) => {
                    let label = project::home_relative_path(workspace_root.as_path());
                    self.show_timed_toast(
                        format!("cargo metadata failed ({label})"),
                        message.to_string(),
                    );
                    tracing::warn!(
                        workspace_root = %workspace_root.as_path().display(),
                        generation,
                        error = %message,
                        "cargo_metadata_failed"
                    );
                },
                None => {
                    // `WorkspaceMissing`: the workspace root vanished
                    // between dispatch and run (typically the user just
                    // deleted a worktree). Stale-refresh race, not a real
                    // failure — suppress the toast.
                    tracing::debug!(
                        workspace_root = %workspace_root.as_path().display(),
                        generation,
                        "cargo_metadata_workspace_missing"
                    );
                },
            },
        }
        // If the user had a confirm popup waiting on this workspace's
        // re-fingerprint, clear the Verifying flag so the next render
        // shows Ready and 'y' starts working again.
        self.scan.clear_confirm_verifying_for(&workspace_root);
        self.startup.metadata.seen.insert(workspace_root);
        self.maybe_log_startup_phase_completions();
    }
    /// Merge a successful `cargo metadata` arrival. Returns `false` when the
    /// arrival was dropped because the captured fingerprint no longer
    /// matches what's on disk — caller should skip startup-phase bookkeeping
    /// so a later dispatch can still tick it off.
    pub(super) fn accept_cargo_metadata(
        &mut self,
        workspace_root: &AbsolutePath,
        generation: u64,
        fingerprint: &ManifestFingerprint,
        workspace_metadata: WorkspaceMetadata,
    ) -> bool {
        let current_fp = ManifestFingerprint::capture(workspace_root.as_path()).ok();
        let fingerprint_drift = current_fp
            .as_ref()
            .is_some_and(|current| current != fingerprint);
        if fingerprint_drift {
            tracing::debug!(
                workspace_root = %workspace_root.as_path().display(),
                generation,
                "cargo_metadata_msg_fingerprint_drift"
            );
            return false;
        }
        let target_directory = workspace_metadata.target_directory.clone();
        let member_roots = workspace_member_roots(&workspace_metadata);
        let needs_out_of_tree_walk = !target_directory
            .as_path()
            .starts_with(workspace_root.as_path());
        // Stamp Cargo fields (types / examples / benches /
        // publishable) from each PackageRecord onto the matching
        // Package / Workspace / VendoredPackage in the project list.
        // The workspace metadata is authoritative.
        self.project_list
            .apply_cargo_fields_from_workspace_metadata(&workspace_metadata);
        if let Ok(mut store) = self.scan.metadata_store().lock() {
            store.upsert(workspace_metadata);
        }
        if needs_out_of_tree_walk {
            scan::spawn_out_of_tree_target_walk(
                &self.net.http_client.handle,
                self.background.background_sender(),
                workspace_root.clone(),
                target_directory.clone(),
            );
        }
        // Refresh the target-dir index so build_clean_plan / siblings
        // lookups see the fresh membership. Every package under this
        // workspace shares `target_directory`; upsert each so a
        // subsequent clean on any member resolves to the correct dir.
        // (Members that were in *previous* metadata but not this one
        // will linger until a full scan restart — minor staleness,
        // acceptable for Step 6c.)
        for project_root in member_roots {
            self.scan
                .target_dir_index
                .upsert(TargetDirMember { project_root }, target_directory.clone());
        }
        tracing::info!(
            workspace_root = %workspace_root.as_path().display(),
            generation,
            "cargo_metadata_applied"
        );
        true
    }
}
/// Project root for each package covered by a [`WorkspaceMetadata`] —
/// derived from each package's `manifest_path.parent()`. Feeds the
/// `TargetDirIndex` membership update after a successful
/// `BackgroundMsg::CargoMetadata` arrival; every package under a given
/// workspace shares the metadata's `target_directory`.
fn workspace_member_roots(workspace_metadata: &WorkspaceMetadata) -> Vec<AbsolutePath> {
    workspace_metadata
        .packages
        .values()
        .filter_map(|pkg| {
            pkg.manifest_path
                .as_path()
                .parent()
                .map(|parent| AbsolutePath::from(parent.to_path_buf()))
        })
        .collect()
}