libnoa 0.1.1

AI-native distributed version control system with per-agent workspace isolation, JSONL append-only logs, snapshot-based history, and full git protocol compatibility
Documentation
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum DiffKind {
    Added,
    Modified,
    Deleted,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FileDiff {
    pub path: String,
    pub kind: DiffKind,
}

pub fn diff_snapshots(
    old_entries: &[crate::object::TreeEntry],
    new_entries: &[crate::object::TreeEntry],
) -> Vec<FileDiff> {
    let mut diffs = Vec::new();

    let old_map: std::collections::HashMap<&str, &crate::object::TreeEntry> =
        old_entries.iter().map(|e| (e.name.as_str(), e)).collect();

    let new_map: std::collections::HashMap<&str, &crate::object::TreeEntry> =
        new_entries.iter().map(|e| (e.name.as_str(), e)).collect();

    for entry in new_entries {
        match old_map.get(entry.name.as_str()) {
            None => diffs.push(FileDiff {
                path: entry.name.clone(),
                kind: DiffKind::Added,
            }),
            Some(old) if old.id != entry.id => diffs.push(FileDiff {
                path: entry.name.clone(),
                kind: DiffKind::Modified,
            }),
            _ => {}
        }
    }

    for entry in old_entries {
        if !new_map.contains_key(entry.name.as_str()) {
            diffs.push(FileDiff {
                path: entry.name.clone(),
                kind: DiffKind::Deleted,
            });
        }
    }

    diffs
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::object::{EntryKind, TreeEntry};

    fn entry(name: &str, id: &str) -> TreeEntry {
        TreeEntry {
            name: name.to_string(),
            kind: EntryKind::Blob,
            id: id.to_string(),
        }
    }

    #[test]
    fn test_added() {
        let old = vec![];
        let new = vec![entry("a.rs", "hash1")];
        let diffs = diff_snapshots(&old, &new);
        assert_eq!(diffs.len(), 1);
        assert_eq!(diffs[0].kind, DiffKind::Added);
    }

    #[test]
    fn test_modified() {
        let old = vec![entry("a.rs", "hash1")];
        let new = vec![entry("a.rs", "hash2")];
        let diffs = diff_snapshots(&old, &new);
        assert_eq!(diffs.len(), 1);
        assert_eq!(diffs[0].kind, DiffKind::Modified);
    }

    #[test]
    fn test_deleted() {
        let old = vec![entry("a.rs", "hash1")];
        let new = vec![];
        let diffs = diff_snapshots(&old, &new);
        assert_eq!(diffs.len(), 1);
        assert_eq!(diffs[0].kind, DiffKind::Deleted);
    }

    #[test]
    fn test_unchanged() {
        let old = vec![entry("a.rs", "hash1")];
        let new = vec![entry("a.rs", "hash1")];
        let diffs = diff_snapshots(&old, &new);
        assert!(diffs.is_empty());
    }

    #[test]
    fn test_mixed() {
        let old = vec![
            entry("a.rs", "h1"),
            entry("b.rs", "h2"),
            entry("c.rs", "h3"),
        ];
        let new = vec![
            entry("a.rs", "h1"),
            entry("b.rs", "h2_changed"),
            entry("d.rs", "h4"),
        ];
        let diffs = diff_snapshots(&old, &new);
        assert_eq!(diffs.len(), 3);
        assert!(diffs
            .iter()
            .any(|d| d.path == "b.rs" && matches!(d.kind, DiffKind::Modified)));
        assert!(diffs
            .iter()
            .any(|d| d.path == "c.rs" && matches!(d.kind, DiffKind::Deleted)));
        assert!(diffs
            .iter()
            .any(|d| d.path == "d.rs" && matches!(d.kind, DiffKind::Added)));
    }
}