1use anyhow::{Context, Result};
10use colored::*;
11use serde::{Deserialize, Serialize};
12use sha2::{Digest, Sha256};
13use std::collections::HashMap;
14use std::fs;
15use std::path::{Path, PathBuf};
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct ComponentState {
20 pub path: String,
22
23 pub base_hash: String,
25
26 pub source: String,
28
29 pub name: String,
31
32 pub version: String,
34
35 pub installed_at: chrono::DateTime<chrono::Utc>,
37}
38
39#[derive(Debug, Clone, PartialEq)]
41pub enum TrafficBranch {
42 Green,
44
45 Yellow { conflicts: Vec<String> },
47
48 Red { conflicts: Vec<String> },
50}
51
52pub struct ComponentStateManager {
54 state_file: PathBuf,
55 states: HashMap<String, ComponentState>,
56}
57
58impl ComponentStateManager {
59 pub fn new(forge_dir: &Path) -> Result<Self> {
61 let state_file = forge_dir.join("component_state.json");
62
63 let states = if state_file.exists() {
64 let content =
65 fs::read_to_string(&state_file).context("Failed to read component state file")?;
66 serde_json::from_str(&content).unwrap_or_default()
67 } else {
68 HashMap::new()
69 };
70
71 Ok(Self { state_file, states })
72 }
73
74 pub fn register_component(
76 &mut self,
77 path: &Path,
78 source: &str,
79 name: &str,
80 version: &str,
81 content: &str,
82 ) -> Result<()> {
83 let base_hash = compute_hash(content);
84
85 let state = ComponentState {
86 path: path.display().to_string(),
87 base_hash,
88 source: source.to_string(),
89 name: name.to_string(),
90 version: version.to_string(),
91 installed_at: chrono::Utc::now(),
92 };
93
94 self.states.insert(path.display().to_string(), state);
95 self.save()?;
96
97 Ok(())
98 }
99
100 pub fn get_component(&self, path: &Path) -> Option<&ComponentState> {
102 self.states.get(&path.display().to_string())
103 }
104
105 pub fn is_managed(&self, path: &Path) -> bool {
107 self.states.contains_key(&path.display().to_string())
108 }
109
110 pub fn analyze_update(&self, path: &Path, remote_content: &str) -> Result<TrafficBranch> {
112 let state = self
113 .get_component(path)
114 .context("Component not registered")?;
115
116 let local_content = fs::read_to_string(path).context("Failed to read local component")?;
118
119 let base_hash = &state.base_hash;
120 let local_hash = compute_hash(&local_content);
121 let remote_hash = compute_hash(remote_content);
122
123 if local_hash == *base_hash {
125 return Ok(TrafficBranch::Green);
126 }
127
128 if remote_hash == *base_hash {
130 return Ok(TrafficBranch::Green);
132 }
133
134 let conflicts = detect_conflicts(&local_content, remote_content);
139
140 if conflicts.is_empty() {
141 Ok(TrafficBranch::Yellow { conflicts: vec![] })
143 } else {
144 Ok(TrafficBranch::Red { conflicts })
146 }
147 }
148
149 pub fn update_component(
151 &mut self,
152 path: &Path,
153 new_version: &str,
154 new_content: &str,
155 ) -> Result<()> {
156 if let Some(state) = self.states.get_mut(&path.display().to_string()) {
157 state.base_hash = compute_hash(new_content);
158 state.version = new_version.to_string();
159 self.save()?;
160 }
161
162 Ok(())
163 }
164
165 pub fn unregister_component(&mut self, path: &Path) -> Result<()> {
167 self.states.remove(&path.display().to_string());
168 self.save()?;
169 Ok(())
170 }
171
172 fn save(&self) -> Result<()> {
174 let content = serde_json::to_string_pretty(&self.states)?;
175 fs::write(&self.state_file, content)?;
176 Ok(())
177 }
178
179 pub fn list_components(&self) -> Vec<&ComponentState> {
181 self.states.values().collect()
182 }
183}
184
185fn compute_hash(content: &str) -> String {
187 let mut hasher = Sha256::new();
188 hasher.update(content.as_bytes());
189 format!("{:x}", hasher.finalize())
190}
191
192fn detect_conflicts(local: &str, remote: &str) -> Vec<String> {
195 use similar::{ChangeTag, TextDiff};
196
197 let diff = TextDiff::from_lines(local, remote);
198 let mut conflicts = Vec::new();
199 let mut current_conflict: Option<(usize, usize)> = None;
200
201 for (idx, change) in diff.iter_all_changes().enumerate() {
202 match change.tag() {
203 ChangeTag::Delete | ChangeTag::Insert => {
204 if let Some((start, _)) = current_conflict {
205 current_conflict = Some((start, idx));
206 } else {
207 current_conflict = Some((idx, idx));
208 }
209 }
210 ChangeTag::Equal => {
211 if let Some((start, end)) = current_conflict.take() {
212 conflicts.push(format!("lines {}-{}", start + 1, end + 1));
213 }
214 }
215 }
216 }
217
218 if let Some((start, end)) = current_conflict {
219 conflicts.push(format!("lines {}-{}", start + 1, end + 1));
220 }
221
222 conflicts
223}
224
225pub async fn apply_update(
227 path: &Path,
228 remote_content: &str,
229 remote_version: &str,
230 state_mgr: &mut ComponentStateManager,
231) -> Result<UpdateResult> {
232 let branch = state_mgr.analyze_update(path, remote_content)?;
233
234 match branch {
235 TrafficBranch::Green => {
236 fs::write(path, remote_content)?;
238 state_mgr.update_component(path, remote_version, remote_content)?;
239
240 println!(
241 "{} {} updated to v{} {}",
242 "🟢".bright_green(),
243 path.display().to_string().bright_cyan(),
244 remote_version.bright_white(),
245 "(auto-updated)".bright_black()
246 );
247
248 Ok(UpdateResult::AutoUpdated)
249 }
250
251 TrafficBranch::Yellow { .. } => {
252 let local_content = fs::read_to_string(path)?;
254
255 let merged = merge_contents(&local_content, remote_content)?;
258
259 fs::write(path, &merged)?;
260 state_mgr.update_component(path, remote_version, &merged)?;
261
262 println!(
263 "{} {} updated to v{} {}",
264 "🟡".bright_yellow(),
265 path.display().to_string().bright_cyan(),
266 remote_version.bright_white(),
267 "(merged with local changes)".yellow()
268 );
269
270 Ok(UpdateResult::Merged)
271 }
272
273 TrafficBranch::Red { conflicts } => {
274 println!(
276 "{} {} {} v{}",
277 "🔴".bright_red(),
278 "CONFLICT:".red().bold(),
279 path.display().to_string().bright_cyan(),
280 remote_version.bright_white()
281 );
282 println!(
283 " {} Update conflicts with your local changes:",
284 "│".bright_black()
285 );
286 for conflict in &conflicts {
287 println!(" {} Conflict at {}", "│".bright_black(), conflict.red());
288 }
289 println!(
290 " {} Run {} to resolve",
291 "â””".bright_black(),
292 "forge resolve".bright_white().bold()
293 );
294
295 Ok(UpdateResult::Conflict { conflicts })
296 }
297 }
298}
299
300#[derive(Debug)]
302pub enum UpdateResult {
303 AutoUpdated,
305
306 Merged,
308
309 Conflict { conflicts: Vec<String> },
311}
312
313fn merge_contents(_local: &str, remote: &str) -> Result<String> {
315 Ok(remote.to_string())
318}
319
320#[cfg(test)]
321mod tests {
322 use super::*;
323
324 #[test]
325 fn test_hash_computation() {
326 let content = "Hello, world!";
327 let hash1 = compute_hash(content);
328 let hash2 = compute_hash(content);
329 assert_eq!(hash1, hash2);
330
331 let different = compute_hash("Different content");
332 assert_ne!(hash1, different);
333 }
334
335 #[test]
336 fn test_conflict_detection() {
337 let local = "line1\nline2\nline3\n";
338 let remote = "line1\nmodified\nline3\n";
339
340 let conflicts = detect_conflicts(local, remote);
341 assert!(!conflicts.is_empty());
342 }
343}