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};

use crate::object::TreeEntry;

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ConflictResolution {
    Ours,
    Theirs,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FileConflict {
    pub path: String,
    pub ours_id: Option<String>,
    pub theirs_id: Option<String>,
    pub base_id: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum MergedEntry {
    Clean(TreeEntry),
    Conflict {
        ours: Option<TreeEntry>,
        theirs: Option<TreeEntry>,
        base: Option<TreeEntry>,
    },
}

impl MergedEntry {
    pub fn path(&self) -> Option<&str> {
        match self {
            MergedEntry::Clean(entry) => Some(&entry.name),
            MergedEntry::Conflict { ours, theirs, .. } => {
                ours.as_ref().or(theirs.as_ref()).map(|e| e.name.as_str())
            }
        }
    }

    pub fn is_conflict(&self) -> bool {
        matches!(self, MergedEntry::Conflict { .. })
    }

    pub fn into_clean_entry(self) -> Option<TreeEntry> {
        match self {
            MergedEntry::Clean(entry) => Some(entry),
            _ => None,
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MergeOutput {
    pub entries: Vec<MergedEntry>,
    pub has_conflicts: bool,
}

impl MergeOutput {
    pub fn clean_entries(&self) -> Vec<&TreeEntry> {
        self.entries
            .iter()
            .filter_map(|e| match e {
                MergedEntry::Clean(entry) => Some(entry),
                _ => None,
            })
            .collect()
    }

    pub fn conflict_count(&self) -> usize {
        self.entries.iter().filter(|e| e.is_conflict()).count()
    }

    pub fn resolve_with_strategy(&self, resolution: &ConflictResolution) -> Vec<TreeEntry> {
        self.entries
            .iter()
            .filter_map(|entry| match entry {
                MergedEntry::Clean(e) => Some(e.clone()),
                MergedEntry::Conflict { ours, theirs, .. } => match resolution {
                    ConflictResolution::Ours => ours.clone(),
                    ConflictResolution::Theirs => theirs.clone(),
                },
            })
            .collect()
    }
}

pub struct ConflictDetector;

impl ConflictDetector {
    pub fn resolve_upstream_wins(conflicts: &[FileConflict]) -> Vec<(String, ConflictResolution)> {
        conflicts
            .iter()
            .map(|c| (c.path.clone(), ConflictResolution::Theirs))
            .collect()
    }

    pub fn resolve_ours_wins(conflicts: &[FileConflict]) -> Vec<(String, ConflictResolution)> {
        conflicts
            .iter()
            .map(|c| (c.path.clone(), ConflictResolution::Ours))
            .collect()
    }
}

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

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

    #[test]
    fn test_upstream_wins() {
        let conflicts = vec![FileConflict {
            path: "a.rs".to_string(),
            ours_id: Some("h1".to_string()),
            theirs_id: Some("h2".to_string()),
            base_id: None,
        }];
        let resolved = ConflictDetector::resolve_upstream_wins(&conflicts);
        assert_eq!(resolved[0].1, ConflictResolution::Theirs);
    }

    #[test]
    fn test_ours_wins() {
        let conflicts = vec![FileConflict {
            path: "b.rs".to_string(),
            ours_id: Some("h3".to_string()),
            theirs_id: Some("h4".to_string()),
            base_id: None,
        }];
        let resolved = ConflictDetector::resolve_ours_wins(&conflicts);
        assert_eq!(resolved[0].1, ConflictResolution::Ours);
    }

    #[test]
    fn test_merged_entry_clean() {
        let e = entry("a.rs", "h1");
        let merged = MergedEntry::Clean(e.clone());
        assert_eq!(merged.path(), Some("a.rs"));
        assert!(!merged.is_conflict());
        assert_eq!(merged.into_clean_entry(), Some(e));
    }

    #[test]
    fn test_merged_entry_conflict() {
        let e = entry("a.rs", "h1");
        let merged = MergedEntry::Conflict {
            ours: Some(e.clone()),
            theirs: Some(entry("a.rs", "h2")),
            base: Some(entry("a.rs", "h0")),
        };
        assert_eq!(merged.path(), Some("a.rs"));
        assert!(merged.is_conflict());
        assert!(merged.into_clean_entry().is_none());
    }

    #[test]
    fn test_merge_output_resolve_ours() {
        let output = MergeOutput {
            entries: vec![
                MergedEntry::Clean(entry("b.rs", "h3")),
                MergedEntry::Conflict {
                    ours: Some(entry("a.rs", "h1")),
                    theirs: Some(entry("a.rs", "h2")),
                    base: Some(entry("a.rs", "h0")),
                },
            ],
            has_conflicts: true,
        };
        let resolved = output.resolve_with_strategy(&ConflictResolution::Ours);
        assert_eq!(resolved.len(), 2);
        assert_eq!(resolved[0].id, "h3");
        assert_eq!(resolved[1].id, "h1");
    }

    #[test]
    fn test_merge_output_resolve_theirs() {
        let output = MergeOutput {
            entries: vec![
                MergedEntry::Clean(entry("b.rs", "h3")),
                MergedEntry::Conflict {
                    ours: Some(entry("a.rs", "h1")),
                    theirs: Some(entry("a.rs", "h2")),
                    base: Some(entry("a.rs", "h0")),
                },
            ],
            has_conflicts: true,
        };
        let resolved = output.resolve_with_strategy(&ConflictResolution::Theirs);
        assert_eq!(resolved.len(), 2);
        assert_eq!(resolved[1].id, "h2");
    }

    #[test]
    fn test_merge_output_clean_entries() {
        let output = MergeOutput {
            entries: vec![
                MergedEntry::Clean(entry("a.rs", "h1")),
                MergedEntry::Conflict {
                    ours: Some(entry("b.rs", "h2")),
                    theirs: Some(entry("b.rs", "h3")),
                    base: None,
                },
                MergedEntry::Clean(entry("c.rs", "h4")),
            ],
            has_conflicts: true,
        };
        let clean = output.clean_entries();
        assert_eq!(clean.len(), 2);
        assert_eq!(clean[0].name, "a.rs");
        assert_eq!(clean[1].name, "c.rs");
    }

    #[test]
    fn test_merge_output_conflict_count() {
        let output = MergeOutput {
            entries: vec![
                MergedEntry::Clean(entry("a.rs", "h1")),
                MergedEntry::Conflict {
                    ours: Some(entry("b.rs", "h2")),
                    theirs: Some(entry("b.rs", "h3")),
                    base: None,
                },
            ],
            has_conflicts: true,
        };
        assert_eq!(output.conflict_count(), 1);
    }
}