gitent-core 0.1.0

Core library for gitent - change tracking and storage
Documentation
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use uuid::Uuid;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ChangeType {
    Create,
    Modify,
    Delete,
    Rename,
}

impl ChangeType {
    pub fn as_str(&self) -> &str {
        match self {
            ChangeType::Create => "create",
            ChangeType::Modify => "modify",
            ChangeType::Delete => "delete",
            ChangeType::Rename => "rename",
        }
    }

    pub fn parse(s: &str) -> Option<Self> {
        match s {
            "create" => Some(ChangeType::Create),
            "modify" => Some(ChangeType::Modify),
            "delete" => Some(ChangeType::Delete),
            "rename" => Some(ChangeType::Rename),
            _ => None,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Change {
    pub id: Uuid,
    pub timestamp: DateTime<Utc>,
    pub change_type: ChangeType,
    pub path: PathBuf,
    pub old_path: Option<PathBuf>,
    pub content_before: Option<Vec<u8>>,
    pub content_after: Option<Vec<u8>>,
    pub content_hash_before: Option<String>,
    pub content_hash_after: Option<String>,
    pub agent_id: Option<String>,
    pub metadata: HashMap<String, String>,
    pub session_id: Uuid,
}

impl Change {
    pub fn new(change_type: ChangeType, path: PathBuf, session_id: Uuid) -> Self {
        Self {
            id: Uuid::new_v4(),
            timestamp: Utc::now(),
            change_type,
            path,
            old_path: None,
            content_before: None,
            content_after: None,
            content_hash_before: None,
            content_hash_after: None,
            agent_id: None,
            metadata: HashMap::new(),
            session_id,
        }
    }

    pub fn with_content_before(mut self, content: Vec<u8>) -> Self {
        self.content_hash_before = Some(Self::hash_content(&content));
        self.content_before = Some(content);
        self
    }

    pub fn with_content_after(mut self, content: Vec<u8>) -> Self {
        self.content_hash_after = Some(Self::hash_content(&content));
        self.content_after = Some(content);
        self
    }

    pub fn with_agent_id(mut self, agent_id: String) -> Self {
        self.agent_id = Some(agent_id);
        self
    }

    pub fn with_metadata(mut self, key: String, value: String) -> Self {
        self.metadata.insert(key, value);
        self
    }

    pub fn with_old_path(mut self, old_path: PathBuf) -> Self {
        self.old_path = Some(old_path);
        self
    }

    fn hash_content(content: &[u8]) -> String {
        use sha2::{Digest, Sha256};
        let mut hasher = Sha256::new();
        hasher.update(content);
        hex::encode(hasher.finalize())
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Commit {
    pub id: Uuid,
    pub parent: Option<Uuid>,
    pub timestamp: DateTime<Utc>,
    pub message: String,
    pub agent_id: String,
    pub changes: Vec<Uuid>,
    pub session_id: Uuid,
    pub metadata: HashMap<String, String>,
}

impl Commit {
    pub fn new(message: String, agent_id: String, changes: Vec<Uuid>, session_id: Uuid) -> Self {
        Self {
            id: Uuid::new_v4(),
            parent: None,
            timestamp: Utc::now(),
            message,
            agent_id,
            changes,
            session_id,
            metadata: HashMap::new(),
        }
    }

    pub fn with_parent(mut self, parent: Uuid) -> Self {
        self.parent = Some(parent);
        self
    }

    pub fn with_metadata(mut self, key: String, value: String) -> Self {
        self.metadata.insert(key, value);
        self
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Session {
    pub id: Uuid,
    pub root_path: PathBuf,
    pub started: DateTime<Utc>,
    pub ended: Option<DateTime<Utc>>,
    pub active: bool,
    pub ignore_patterns: Vec<String>,
}

impl Session {
    pub fn new(root_path: PathBuf) -> Self {
        Self {
            id: Uuid::new_v4(),
            root_path,
            started: Utc::now(),
            ended: None,
            active: true,
            ignore_patterns: vec![
                ".git".to_string(),
                "target".to_string(),
                "node_modules".to_string(),
                ".gitent".to_string(),
            ],
        }
    }

    pub fn with_ignore_patterns(mut self, patterns: Vec<String>) -> Self {
        self.ignore_patterns = patterns;
        self
    }

    pub fn end(&mut self) {
        self.active = false;
        self.ended = Some(Utc::now());
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommitInfo {
    pub commit: Commit,
    pub change_count: usize,
    pub files_affected: Vec<PathBuf>,
}

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

    #[test]
    fn test_change_creation() {
        let session_id = Uuid::new_v4();
        let change = Change::new(ChangeType::Create, PathBuf::from("test.txt"), session_id);

        assert_eq!(change.change_type, ChangeType::Create);
        assert_eq!(change.path, PathBuf::from("test.txt"));
        assert_eq!(change.session_id, session_id);
    }

    #[test]
    fn test_change_with_content() {
        let session_id = Uuid::new_v4();
        let content = b"Hello, World!".to_vec();
        let change = Change::new(ChangeType::Create, PathBuf::from("test.txt"), session_id)
            .with_content_after(content.clone());

        assert!(change.content_after.is_some());
        assert!(change.content_hash_after.is_some());
        assert_eq!(change.content_after.unwrap(), content);
    }

    #[test]
    fn test_commit_creation() {
        let session_id = Uuid::new_v4();
        let change_ids = vec![Uuid::new_v4(), Uuid::new_v4()];
        let commit = Commit::new(
            "Initial commit".to_string(),
            "test-agent".to_string(),
            change_ids.clone(),
            session_id,
        );

        assert_eq!(commit.message, "Initial commit");
        assert_eq!(commit.agent_id, "test-agent");
        assert_eq!(commit.changes, change_ids);
    }

    #[test]
    fn test_session_creation() {
        let session = Session::new(PathBuf::from("/test/path"));

        assert_eq!(session.root_path, PathBuf::from("/test/path"));
        assert!(session.active);
        assert!(session.ended.is_none());
        assert!(!session.ignore_patterns.is_empty());
    }
}