sara_core/diff/
service.rs

1//! Diff service implementation.
2
3use crate::graph::{GraphBuilder, GraphDiff};
4use crate::repository::parse_directory;
5
6use super::DiffOptions;
7
8/// Errors that can occur during diff operations.
9#[derive(Debug, thiserror::Error)]
10pub enum DiffError {
11    /// Failed to parse repository.
12    #[error("Failed to parse repository {path}: {reason}")]
13    ParseError { path: String, reason: String },
14
15    /// Failed to build graph.
16    #[error("Failed to build graph: {0}")]
17    GraphBuildError(String),
18
19    /// Git reference not supported yet.
20    #[error(
21        "Git reference comparison not fully implemented. Only current state comparison is available."
22    )]
23    GitRefNotSupported,
24
25    /// IO error.
26    #[error("IO error: {0}")]
27    Io(#[from] std::io::Error),
28}
29
30/// Result of a diff operation.
31#[derive(Debug)]
32pub struct DiffResult {
33    /// The computed diff.
34    pub diff: GraphDiff,
35    /// The first reference used.
36    pub ref1: String,
37    /// The second reference used.
38    pub ref2: String,
39    /// Whether this was a full Git ref comparison or a workaround.
40    pub is_full_comparison: bool,
41}
42
43impl DiffResult {
44    /// Returns true if there are no changes.
45    pub fn is_empty(&self) -> bool {
46        self.diff.is_empty()
47    }
48}
49
50/// Service for computing diffs between knowledge graph states.
51#[derive(Debug, Default)]
52pub struct DiffService;
53
54impl DiffService {
55    /// Creates a new diff service.
56    pub fn new() -> Self {
57        Self
58    }
59
60    /// Computes the diff between two references.
61    ///
62    /// Note: Full Git reference support is not yet implemented. Currently,
63    /// this compares the current working directory state with itself.
64    pub fn diff(&self, opts: &DiffOptions) -> Result<DiffResult, DiffError> {
65        // TODO: Implement full Git reference support
66        // For now, we parse the current state and compare with itself
67
68        let is_full_comparison = false;
69
70        // Parse all repositories
71        let items = self.parse_repositories(&opts.repositories)?;
72
73        // Build graphs (currently identical since we don't have Git ref support)
74        let graph1 = GraphBuilder::new()
75            .add_items(items.clone())
76            .build()
77            .map_err(|e| DiffError::GraphBuildError(e.to_string()))?;
78
79        let graph2 = GraphBuilder::new()
80            .add_items(items)
81            .build()
82            .map_err(|e| DiffError::GraphBuildError(e.to_string()))?;
83
84        // Compute diff
85        let diff = GraphDiff::compute(&graph1, &graph2);
86
87        Ok(DiffResult {
88            diff,
89            ref1: opts.ref1.clone(),
90            ref2: opts.ref2.clone(),
91            is_full_comparison,
92        })
93    }
94
95    /// Computes the diff between two existing graphs.
96    ///
97    /// Use this method when you already have the graphs loaded.
98    pub fn diff_graphs(
99        &self,
100        old_graph: &crate::graph::KnowledgeGraph,
101        new_graph: &crate::graph::KnowledgeGraph,
102        ref1: impl Into<String>,
103        ref2: impl Into<String>,
104    ) -> DiffResult {
105        let diff = GraphDiff::compute(old_graph, new_graph);
106        DiffResult {
107            diff,
108            ref1: ref1.into(),
109            ref2: ref2.into(),
110            is_full_comparison: true,
111        }
112    }
113
114    /// Parses items from all repository paths.
115    fn parse_repositories(
116        &self,
117        repositories: &[std::path::PathBuf],
118    ) -> Result<Vec<crate::model::Item>, DiffError> {
119        let mut all_items = Vec::new();
120
121        for repo_path in repositories {
122            let items = parse_directory(repo_path).map_err(|e| DiffError::ParseError {
123                path: repo_path.display().to_string(),
124                reason: e.to_string(),
125            })?;
126            all_items.extend(items);
127        }
128
129        Ok(all_items)
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136    use std::fs;
137    use std::path::Path;
138    use tempfile::TempDir;
139
140    fn create_test_file(dir: &Path, name: &str, content: &str) {
141        fs::write(dir.join(name), content).unwrap();
142    }
143
144    #[test]
145    fn test_diff_empty_repositories() {
146        let temp_dir = TempDir::new().unwrap();
147
148        let opts = DiffOptions::new("HEAD~1", "HEAD")
149            .with_repositories(vec![temp_dir.path().to_path_buf()]);
150
151        let service = DiffService::new();
152        let result = service.diff(&opts).unwrap();
153
154        assert!(result.is_empty());
155        assert!(!result.is_full_comparison);
156    }
157
158    #[test]
159    fn test_diff_with_items() {
160        let temp_dir = TempDir::new().unwrap();
161
162        create_test_file(
163            temp_dir.path(),
164            "solution.md",
165            r#"---
166id: "SOL-001"
167type: solution
168name: "Test Solution"
169---
170# Solution
171"#,
172        );
173
174        let opts = DiffOptions::new("main", "feature")
175            .with_repositories(vec![temp_dir.path().to_path_buf()]);
176
177        let service = DiffService::new();
178        let result = service.diff(&opts).unwrap();
179
180        // Since we compare current state with itself, there should be no changes
181        assert!(result.is_empty());
182        assert_eq!(result.ref1, "main");
183        assert_eq!(result.ref2, "feature");
184    }
185
186    #[test]
187    fn test_diff_options_builder() {
188        let opts = DiffOptions::new("HEAD~1", "HEAD")
189            .add_repository("/path/to/repo1".into())
190            .add_repository("/path/to/repo2".into());
191
192        assert_eq!(opts.ref1, "HEAD~1");
193        assert_eq!(opts.ref2, "HEAD");
194        assert_eq!(opts.repositories.len(), 2);
195    }
196}