cargo-reclaim 0.2.1

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

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ActiveObservation {
    pub process_view: ProcessView,
}

impl ActiveObservation {
    pub fn not_attempted() -> Self {
        Self {
            process_view: ProcessView::NotAttempted,
        }
    }

    pub fn permission_limited(reason: impl Into<String>) -> Self {
        Self {
            process_view: ProcessView::PermissionLimited {
                reason: reason.into(),
            },
        }
    }

    pub fn failed(reason: impl Into<String>) -> Self {
        Self {
            process_view: ProcessView::Failed {
                reason: reason.into(),
            },
        }
    }

    pub fn complete(processes: impl IntoIterator<Item = ObservedCargoProcess>) -> Self {
        Self {
            process_view: ProcessView::Complete {
                processes: processes.into_iter().collect(),
            },
        }
    }
}

impl Default for ActiveObservation {
    fn default() -> Self {
        Self::not_attempted()
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ProcessView {
    NotAttempted,
    PermissionLimited {
        reason: String,
    },
    Failed {
        reason: String,
    },
    Complete {
        processes: Vec<ObservedCargoProcess>,
    },
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ObservedCargoProcess {
    pub tool: CargoTool,
    pub cwd: Option<PathBuf>,
    pub referenced_paths: Vec<PathBuf>,
}

impl ObservedCargoProcess {
    pub fn new(tool: CargoTool) -> Self {
        Self {
            tool,
            cwd: None,
            referenced_paths: Vec::new(),
        }
    }

    pub fn with_cwd(mut self, cwd: impl Into<PathBuf>) -> Self {
        self.cwd = Some(cwd.into());
        self
    }

    pub fn with_referenced_path(mut self, path: impl Into<PathBuf>) -> Self {
        self.referenced_paths.push(path.into());
        self
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CargoTool {
    Cargo,
    Rustc,
}

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

impl TargetContext {
    pub fn new(target_root: impl Into<PathBuf>) -> Self {
        Self {
            project_root: None,
            target_root: target_root.into(),
            build_root: None,
        }
    }

    pub fn with_project_root(mut self, project_root: impl Into<PathBuf>) -> Self {
        self.project_root = Some(project_root.into());
        self
    }

    pub fn with_build_root(mut self, build_root: impl Into<PathBuf>) -> Self {
        self.build_root = Some(build_root.into());
        self
    }

    pub(super) fn active_match<'a>(
        &'a self,
        process: &'a ObservedCargoProcess,
    ) -> Option<ActiveMatch<'a>> {
        if let (Some(project_root), Some(cwd)) = (&self.project_root, &process.cwd)
            && path_is_under(cwd, project_root)
        {
            return Some(ActiveMatch::CwdUnderProject { cwd, project_root });
        }

        for referenced_path in &process.referenced_paths {
            if paths_overlap(referenced_path, &self.target_root) {
                return Some(ActiveMatch::ReferencedPathOverlapsRoot {
                    referenced_path,
                    root: &self.target_root,
                });
            }

            if let Some(build_root) = &self.build_root
                && paths_overlap(referenced_path, build_root)
            {
                return Some(ActiveMatch::ReferencedPathOverlapsRoot {
                    referenced_path,
                    root: build_root,
                });
            }
        }

        None
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum ActiveMatch<'a> {
    CwdUnderProject {
        cwd: &'a Path,
        project_root: &'a Path,
    },
    ReferencedPathOverlapsRoot {
        referenced_path: &'a Path,
        root: &'a Path,
    },
}

fn path_is_under(path: &Path, root: &Path) -> bool {
    path == root || path.starts_with(root)
}

fn paths_overlap(left: &Path, right: &Path) -> bool {
    left == right || left.starts_with(right) || right.starts_with(left)
}