cargo-reclaim 0.2.2

Safe Cargo cleanup for target directories, stale artifacts, and Cargo home caches
Documentation
use std::collections::HashSet;
use std::path::{Path, PathBuf};

use crate::planner::{ActiveObservation, TargetContext};

pub trait ActiveObservationProvider {
    fn observe(&self, scope: &ActiveObservationScope) -> ActiveObservation;
}

#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ActiveObservationScope {
    target_contexts: Vec<TargetContext>,
}

impl ActiveObservationScope {
    pub fn from_target_contexts(contexts: impl IntoIterator<Item = TargetContext>) -> Self {
        let mut seen = HashSet::new();
        let mut target_contexts = Vec::new();

        for context in contexts {
            let key = ContextKey::from(&context);
            if seen.insert(key) {
                target_contexts.push(context);
            }
        }

        Self { target_contexts }
    }

    pub fn target_contexts(&self) -> &[TargetContext] {
        &self.target_contexts
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct ContextKey {
    project_root: Option<PathBuf>,
    target_root: PathBuf,
    build_root: Option<PathBuf>,
}

impl From<&TargetContext> for ContextKey {
    fn from(context: &TargetContext) -> Self {
        Self {
            project_root: normalize_optional_path(context.project_root.as_deref()),
            target_root: normalize_path(&context.target_root),
            build_root: normalize_optional_path(context.build_root.as_deref()),
        }
    }
}

fn normalize_optional_path(path: Option<&Path>) -> Option<PathBuf> {
    path.map(normalize_path)
}

fn normalize_path(path: &Path) -> PathBuf {
    let mut normalized = PathBuf::new();

    for component in path.components() {
        match component {
            std::path::Component::CurDir => {}
            std::path::Component::ParentDir => {
                if !normalized.pop() {
                    normalized.push(component.as_os_str());
                }
            }
            _ => normalized.push(component.as_os_str()),
        }
    }

    normalized
}

#[cfg(test)]
mod tests {
    use crate::planner::TargetContext;

    use super::ActiveObservationScope;

    #[test]
    fn scope_dedupes_equivalent_target_contexts() {
        let scope = ActiveObservationScope::from_target_contexts([
            TargetContext::new("/work/project/target")
                .with_project_root("/work/project")
                .with_build_root("/work/project/build"),
            TargetContext::new("/work/project/./target")
                .with_project_root("/work/project")
                .with_build_root("/work/project/build"),
        ]);

        assert_eq!(scope.target_contexts().len(), 1);
    }
}