sara_core/diff/
service.rs1use crate::graph::{GraphBuilder, GraphDiff};
4use crate::repository::parse_directory;
5
6use super::DiffOptions;
7
8#[derive(Debug, thiserror::Error)]
10pub enum DiffError {
11 #[error("Failed to parse repository {path}: {reason}")]
13 ParseError { path: String, reason: String },
14
15 #[error("Failed to build graph: {0}")]
17 GraphBuildError(String),
18
19 #[error(
21 "Git reference comparison not fully implemented. Only current state comparison is available."
22 )]
23 GitRefNotSupported,
24
25 #[error("IO error: {0}")]
27 Io(#[from] std::io::Error),
28}
29
30#[derive(Debug)]
32pub struct DiffResult {
33 pub diff: GraphDiff,
35 pub ref1: String,
37 pub ref2: String,
39 pub is_full_comparison: bool,
41}
42
43impl DiffResult {
44 pub fn is_empty(&self) -> bool {
46 self.diff.is_empty()
47 }
48}
49
50#[derive(Debug, Default)]
52pub struct DiffService;
53
54impl DiffService {
55 pub fn new() -> Self {
57 Self
58 }
59
60 pub fn diff(&self, opts: &DiffOptions) -> Result<DiffResult, DiffError> {
65 let is_full_comparison = false;
69
70 let items = self.parse_repositories(&opts.repositories)?;
72
73 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 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 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 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 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}