governor-core 1.3.0

Core domain and application logic for cargo-governor
Documentation
//! Workspace domain entity

use serde::{Deserialize, Serialize};
use std::path::PathBuf;

use super::version::SemanticVersion;

/// Cargo workspace metadata
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkspaceMetadata {
    /// Path to workspace root
    pub root: PathBuf,
    /// Workspace members (crate paths)
    pub members: Vec<String>,
    /// Workspace exclude list
    pub exclude: Vec<String>,
    /// Shared workspace version (if any)
    pub version: Option<SemanticVersion>,
    /// Workspace resolver version
    pub resolver: Option<String>,
    /// Workspace metadata
    pub metadata: WorkspaceMetadataExtra,
}

/// Extra workspace metadata
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct WorkspaceMetadataExtra {
    /// Governor-specific metadata
    pub governor: Option<GovernorMetadata>,
}

/// Governor configuration in workspace metadata
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GovernorMetadata {
    /// Workspace owners
    pub owners: Option<OwnersConfig>,
}

/// Owners configuration
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct OwnersConfig {
    /// User owners
    pub users: Vec<String>,
    /// Group owners
    pub groups: Vec<String>,
}

impl WorkspaceMetadata {
    /// Create new workspace metadata
    #[must_use]
    pub fn new(root: PathBuf) -> Self {
        Self {
            root,
            members: Vec::new(),
            exclude: Vec::new(),
            version: None,
            resolver: None,
            metadata: WorkspaceMetadataExtra::default(),
        }
    }

    /// Check if this is a shared version workspace
    #[must_use]
    pub const fn is_shared_version(&self) -> bool {
        self.version.is_some()
    }

    /// Get all member paths
    #[must_use]
    pub fn member_paths(&self) -> Vec<PathBuf> {
        self.members
            .iter()
            .map(|m| {
                if m.starts_with("./") || m.starts_with('/') {
                    PathBuf::from(m)
                } else {
                    self.root.join(m)
                }
            })
            .collect()
    }

    /// Check if a path is excluded
    #[must_use]
    pub fn is_excluded(&self, path: &std::path::Path) -> bool {
        self.exclude.iter().any(|e| {
            let exclude_path = if e.starts_with("./") || e.starts_with('/') {
                PathBuf::from(e)
            } else {
                self.root.join(e)
            };
            path.starts_with(exclude_path)
        })
    }
}

/// Working tree status
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkingTreeStatus {
    /// Whether there are uncommitted changes
    pub has_changes: bool,
    /// Modified files
    pub modified: Vec<String>,
    /// Added files
    pub added: Vec<String>,
    /// Deleted files
    pub deleted: Vec<String>,
    /// Untracked files
    pub untracked: Vec<String>,
}

impl WorkingTreeStatus {
    /// Create a clean status
    #[must_use]
    pub const fn clean() -> Self {
        Self {
            has_changes: false,
            modified: Vec::new(),
            added: Vec::new(),
            deleted: Vec::new(),
            untracked: Vec::new(),
        }
    }

    /// Check if the working tree is clean
    #[must_use]
    pub const fn is_clean(&self) -> bool {
        !self.has_changes
    }

    /// Get all changed files
    #[must_use]
    pub fn all_changes(&self) -> Vec<&String> {
        let mut changes = Vec::new();
        changes.extend(self.modified.iter());
        changes.extend(self.added.iter());
        changes.extend(self.deleted.iter());
        changes
    }

    /// Count total changes
    #[must_use]
    pub const fn total_changes(&self) -> usize {
        self.modified.len() + self.added.len() + self.deleted.len()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_workspace_metadata() {
        let mut meta = WorkspaceMetadata::new(PathBuf::from("/test"));
        meta.members.push("crate1".to_string());
        meta.members.push("crate2".to_string());

        let paths = meta.member_paths();
        assert_eq!(paths.len(), 2);
        assert!(paths[0].ends_with("crate1"));
    }

    #[test]
    fn test_working_tree_status() {
        let status = WorkingTreeStatus::clean();
        assert!(status.is_clean());
        assert_eq!(status.total_changes(), 0);
    }

    #[test]
    fn test_excluded_paths() {
        let mut meta = WorkspaceMetadata::new(PathBuf::from("/test"));
        meta.exclude.push("target".to_string());

        assert!(meta.is_excluded(&PathBuf::from("/test/target")));
        assert!(!meta.is_excluded(&PathBuf::from("/test/src")));
    }
}