cargo-mend 0.19.0

Opinionated visibility auditing for Rust crates and workspaces
use std::collections::BTreeSet;
use std::ffi::OsStr;
use std::fs;
use std::path::Path;
use std::path::PathBuf;

use anyhow::Context;
use anyhow::Result;
use walkdir::WalkDir;

use crate::selection::Selection;

pub(super) struct CompilerFixTransaction {
    snapshots: Vec<(PathBuf, String)>,
}

impl CompilerFixTransaction {
    pub(super) fn capture(selection: &Selection) -> Result<Self> {
        let mut source_files = BTreeSet::new();
        for package_root in &selection.package_roots {
            for entry in WalkDir::new(package_root)
                .into_iter()
                .filter_entry(|entry| source_entry_is_in_scope(entry.path(), selection))
            {
                let entry = entry.with_context(|| {
                    format!(
                        "failed to inspect Rust sources under {}",
                        package_root.display()
                    )
                })?;
                let path = entry.path();
                if entry.file_type().is_file()
                    && path.extension().and_then(OsStr::to_str) == Some("rs")
                {
                    source_files.insert(path.to_path_buf());
                }
            }
        }

        let snapshots = source_files
            .into_iter()
            .map(|path| {
                let text = fs::read_to_string(&path)
                    .with_context(|| format!("failed to snapshot {}", path.display()))?;
                Ok((path, text))
            })
            .collect::<Result<Vec<_>>>()?;
        Ok(Self { snapshots })
    }

    pub(super) fn restore(&self) -> Result<()> {
        for (path, original) in &self.snapshots {
            if fs::read_to_string(path).is_ok_and(|current| current == *original) {
                continue;
            }
            fs::write(path, original)
                .with_context(|| format!("failed to restore {}", path.display()))?;
        }
        Ok(())
    }
}

fn source_entry_is_in_scope(path: &Path, selection: &Selection) -> bool {
    !path.starts_with(&selection.target_directory)
        && path.file_name().and_then(OsStr::to_str) != Some(".git")
}