1use std::collections::HashMap;
2use std::path::{Path, PathBuf};
3use std::process::Command;
4use std::fs;
5use std::time::{SystemTime, UNIX_EPOCH};
6use crate::edit_control::ModifiableEdit;
7use crate::classification::{ClassifiedEdit, EditCategory};
8
9
10#[derive(Debug, Clone)]
14pub struct GitIntegrationSystem {
15 pub repository_path: PathBuf,
17 pub config: GitConfig,
19 pub branch_manager: BranchManager,
21 pub commit_manager: CommitManager,
23 pub conflict_resolver: ConflictResolver,
25 pub backup_manager: BackupManager,
27 pub session_state: GitSessionState,
29}
30
31#[derive(Debug, Clone)]
33pub struct GitConfig {
34 pub auto_commit: bool,
36 pub session_branches: bool,
38 pub backup_frequency_minutes: u32,
40 pub max_backup_days: u32,
42 pub cicd_integration: bool,
44 pub conflict_strategy: ConflictStrategy,
46}
47
48#[derive(Debug, Clone)]
50pub struct GitSessionState {
51 pub session_id: String,
53 pub start_time: SystemTime,
55 pub current_branch: String,
57 pub original_branch: String,
59 pub edits_applied: Vec<String>,
61 pub commit_history: Vec<GitCommit>,
63 pub backup_points: Vec<BackupPoint>,
65}
66
67#[derive(Debug, Clone)]
69pub struct BranchManager {
70 pub repo_path: PathBuf,
72 pub naming_strategy: BranchNamingStrategy,
74 pub auto_cleanup: bool,
76 pub max_branch_lifetime_days: u32,
78}
79
80#[derive(Debug, Clone)]
82pub struct CommitManager {
83 pub repo_path: PathBuf,
85 pub message_templates: HashMap<String, String>,
87 pub auto_stage: bool,
89 pub sign_commits: bool,
91}
92
93#[derive(Debug, Clone)]
95pub struct ConflictResolver {
96 pub repo_path: PathBuf,
98 pub strategies: Vec<ConflictStrategy>,
100 pub use_classification: bool,
102 pub cognitive_resolution: bool,
104}
105
106#[derive(Debug, Clone)]
108pub struct BackupManager {
109 pub repo_path: PathBuf,
111 pub backup_dir: PathBuf,
113 pub compress_backups: bool,
115 pub incremental: bool,
117}
118
119#[derive(Debug, Clone)]
121pub struct GitCommit {
122 pub hash: String,
124 pub message: String,
126 pub timestamp: SystemTime,
128 pub files_changed: Vec<String>,
130 pub soma_edit_ids: Vec<String>,
132}
133
134#[derive(Debug, Clone)]
136pub struct BackupPoint {
137 pub id: String,
139 pub timestamp: SystemTime,
141 pub state_hash: String,
143 pub backup_path: PathBuf,
145 pub description: String,
147}
148
149#[derive(Debug, Clone)]
151pub enum BranchNamingStrategy {
152 SessionTimestamp,
154 SessionId,
156 UserTimestamp,
158 Custom(String),
160}
161
162#[derive(Debug, Clone)]
164pub enum ConflictStrategy {
165 PreferLocal,
167 PreferRemote,
169 Interactive,
171 ClassificationBased,
173 CognitiveResolution,
175}
176
177#[derive(Debug, Clone)]
179pub enum GitOperationResult {
180 Success(String),
182 Error(String),
184 RequiresIntervention(String),
186 Conflict(ConflictInfo),
188}
189
190#[derive(Debug, Clone)]
192pub struct ConflictInfo {
193 pub files: Vec<String>,
195 pub descriptions: Vec<String>,
197 pub suggested_strategy: ConflictStrategy,
199 pub classification_results: Vec<ClassifiedEdit>,
201}
202
203impl Default for GitConfig {
204 fn default() -> Self {
205 GitConfig {
206 auto_commit: true,
207 session_branches: true,
208 backup_frequency_minutes: 30,
209 max_backup_days: 7,
210 cicd_integration: false,
211 conflict_strategy: ConflictStrategy::Interactive,
212 }
213 }
214}
215
216impl Default for BranchNamingStrategy {
217 fn default() -> Self {
218 BranchNamingStrategy::SessionTimestamp
219 }
220}
221
222impl GitIntegrationSystem {
223 pub fn new(repository_path: PathBuf) -> Result<Self, String> {
225 if !Self::is_git_repository(&repository_path) {
227 return Err(format!("Path {:?} is not a Git repository", repository_path));
228 }
229
230 let config = GitConfig::default();
231 let branch_manager = BranchManager::new(repository_path.clone())?;
232 let commit_manager = CommitManager::new(repository_path.clone())?;
233 let conflict_resolver = ConflictResolver::new(repository_path.clone())?;
234 let backup_manager = BackupManager::new(repository_path.clone())?;
235
236 let session_id = Self::generate_session_id();
237 let current_branch = Self::get_current_branch(&repository_path)?;
238
239 let session_state = GitSessionState {
240 session_id,
241 start_time: SystemTime::now(),
242 current_branch: current_branch.clone(),
243 original_branch: current_branch,
244 edits_applied: Vec::new(),
245 commit_history: Vec::new(),
246 backup_points: Vec::new(),
247 };
248
249 Ok(GitIntegrationSystem {
250 repository_path,
251 config,
252 branch_manager,
253 commit_manager,
254 conflict_resolver,
255 backup_manager,
256 session_state,
257 })
258 }
259
260 pub fn start_session(&mut self, session_name: Option<String>) -> Result<String, String> {
262 if self.config.session_branches {
264 let branch_name = self.branch_manager.create_session_branch(
265 session_name.clone()
266 )?;
267 self.session_state.current_branch = branch_name.clone();
268
269 self.switch_branch(&branch_name)?;
271 }
272
273 let backup_point = self.backup_manager.create_backup_point(
275 "Session start".to_string()
276 )?;
277 self.session_state.backup_points.push(backup_point);
278
279 self.commit_manager.initialize_session(&self.session_state.session_id)?;
281
282 Ok(format!("Started SOMA Git session: {}", self.session_state.session_id))
283 }
284
285 pub fn apply_edits_with_git(
287 &mut self,
288 edits: Vec<ModifiableEdit>,
289 classifications: Vec<ClassifiedEdit>
290 ) -> Result<Vec<GitOperationResult>, String> {
291 let mut results = Vec::new();
292
293 let mut critical_edits = Vec::new();
295 let mut safe_edits = Vec::new();
296 let mut experimental_edits = Vec::new();
297
298 for (edit, classification) in edits.iter().zip(classifications.iter()) {
299 match &classification.category {
300 EditCategory::Critical { .. } => {
301 critical_edits.push((edit, classification));
302 }
303 EditCategory::Safe { .. } => {
304 safe_edits.push((edit, classification));
305 }
306 EditCategory::Experimental { .. } => {
307 experimental_edits.push((edit, classification));
308 }
309 EditCategory::Cosmetic { .. } => {
310 safe_edits.push((edit, classification));
311 }
312 }
313 }
314
315 for (edit, classification) in safe_edits {
317 let result = self.apply_single_edit_with_git(edit, classification)?;
318 results.push(result);
319 }
320
321 if !experimental_edits.is_empty() {
323 let backup_point = self.backup_manager.create_backup_point(
324 "Before experimental edits".to_string()
325 )?;
326 self.session_state.backup_points.push(backup_point);
327
328 for (edit, classification) in experimental_edits {
329 let result = self.apply_single_edit_with_git(edit, classification)?;
330 results.push(result);
331 }
332 }
333
334 if !critical_edits.is_empty() {
336 let backup_point = self.backup_manager.create_backup_point(
337 "Before critical edits".to_string()
338 )?;
339 self.session_state.backup_points.push(backup_point);
340
341 for (edit, classification) in critical_edits {
342 let result = self.apply_single_edit_with_git(edit, classification)?;
343 results.push(result.clone());
344
345 if matches!(result, GitOperationResult::Success(_)) {
347 self.commit_current_changes(&format!(
348 "SOMA Critical Edit: {}",
349 classification.reasoning
350 ))?;
351 }
352 }
353 }
354
355 Ok(results)
356 }
357
358 fn apply_single_edit_with_git(
360 &mut self,
361 edit: &ModifiableEdit,
362 classification: &ClassifiedEdit
363 ) -> Result<GitOperationResult, String> {
364 if let Some(conflicts) = self.check_for_conflicts(&edit.base_edit.file)? {
366 return Ok(GitOperationResult::Conflict(conflicts));
367 }
368
369 let file_path = &edit.base_edit.file;
372
373 self.session_state.edits_applied.push(format!("{:?}", edit.base_edit));
375
376 if self.config.auto_commit && !matches!(classification.category, EditCategory::Critical { .. }) {
378 let commit_message = self.commit_manager.generate_commit_message(edit, classification);
379 match self.commit_current_changes(&commit_message) {
380 Ok(commit_hash) => {
381 let commit = GitCommit {
382 hash: commit_hash.clone(),
383 message: commit_message,
384 timestamp: SystemTime::now(),
385 files_changed: vec![file_path.clone()],
386 soma_edit_ids: vec![format!("{:?}", edit)],
387 };
388 self.session_state.commit_history.push(commit);
389 Ok(GitOperationResult::Success(commit_hash))
390 }
391 Err(e) => Ok(GitOperationResult::Error(e)),
392 }
393 } else {
394 Ok(GitOperationResult::Success("Edit applied successfully".to_string()))
395 }
396 }
397
398 pub fn end_session(&mut self, merge_to_main: bool) -> Result<String, String> {
400 let final_backup = self.backup_manager.create_backup_point(
402 "Session end".to_string()
403 )?;
404 self.session_state.backup_points.push(final_backup);
405
406 if self.has_uncommitted_changes()? {
408 self.commit_current_changes("SOMA Session final commit")?;
409 }
410
411 let session_summary = format!(
412 "Session {} completed:\n- Edits applied: {}\n- Commits: {}\n- Backups: {}",
413 self.session_state.session_id,
414 self.session_state.edits_applied.len(),
415 self.session_state.commit_history.len(),
416 self.session_state.backup_points.len()
417 );
418
419 if merge_to_main && self.config.session_branches {
421 self.merge_session_to_main()?;
422 }
423
424 if self.config.session_branches {
426 self.switch_branch(&self.session_state.original_branch)?;
427 }
428
429 Ok(session_summary)
430 }
431
432 fn is_git_repository(path: &Path) -> bool {
434 path.join(".git").exists()
435 }
436
437 fn generate_session_id() -> String {
439 let timestamp = SystemTime::now()
440 .duration_since(UNIX_EPOCH)
441 .unwrap()
442 .as_secs();
443 format!("soma-{}", timestamp)
444 }
445
446 fn get_current_branch(repo_path: &Path) -> Result<String, String> {
448 let output = Command::new("git")
449 .arg("branch")
450 .arg("--show-current")
451 .current_dir(repo_path)
452 .output()
453 .map_err(|e| format!("Failed to get current branch: {}", e))?;
454
455 if output.status.success() {
456 let branch = String::from_utf8_lossy(&output.stdout).trim().to_string();
457 Ok(branch)
458 } else {
459 let error = String::from_utf8_lossy(&output.stderr);
460 Err(format!("Git error: {}", error))
461 }
462 }
463
464 fn switch_branch(&self, branch_name: &str) -> Result<(), String> {
466 let output = Command::new("git")
467 .arg("checkout")
468 .arg(branch_name)
469 .current_dir(&self.repository_path)
470 .output()
471 .map_err(|e| format!("Failed to switch branch: {}", e))?;
472
473 if output.status.success() {
474 Ok(())
475 } else {
476 let error = String::from_utf8_lossy(&output.stderr);
477 Err(format!("Failed to switch to branch {}: {}", branch_name, error))
478 }
479 }
480
481 pub fn check_for_conflicts(&self, file_path: &str) -> Result<Option<ConflictInfo>, String> {
483 let file_content = fs::read_to_string(file_path)
485 .map_err(|e| format!("Failed to read file {}: {}", file_path, e))?;
486
487 if file_content.contains("<<<<<<< HEAD") ||
488 file_content.contains("=======") ||
489 file_content.contains(">>>>>>> ") {
490
491 let conflict_info = ConflictInfo {
492 files: vec![file_path.to_string()],
493 descriptions: vec!["Merge conflict detected".to_string()],
494 suggested_strategy: ConflictStrategy::Interactive,
495 classification_results: Vec::new(),
496 };
497
498 Ok(Some(conflict_info))
499 } else {
500 Ok(None)
501 }
502 }
503
504 pub fn commit_current_changes(&self, message: &str) -> Result<String, String> {
506 let stage_output = Command::new("git")
508 .arg("add")
509 .arg(".")
510 .current_dir(&self.repository_path)
511 .output()
512 .map_err(|e| format!("Failed to stage changes: {}", e))?;
513
514 if !stage_output.status.success() {
515 let error = String::from_utf8_lossy(&stage_output.stderr);
516 return Err(format!("Failed to stage changes: {}", error));
517 }
518
519 let commit_output = Command::new("git")
521 .arg("commit")
522 .arg("-m")
523 .arg(message)
524 .current_dir(&self.repository_path)
525 .output()
526 .map_err(|e| format!("Failed to commit: {}", e))?;
527
528 if commit_output.status.success() {
529 let hash_output = Command::new("git")
531 .arg("rev-parse")
532 .arg("HEAD")
533 .current_dir(&self.repository_path)
534 .output()
535 .map_err(|e| format!("Failed to get commit hash: {}", e))?;
536
537 if hash_output.status.success() {
538 let hash = String::from_utf8_lossy(&hash_output.stdout).trim().to_string();
539 Ok(hash)
540 } else {
541 Ok("unknown".to_string())
542 }
543 } else {
544 let error = String::from_utf8_lossy(&commit_output.stderr);
545 Err(format!("Failed to commit: {}", error))
546 }
547 }
548
549 fn has_uncommitted_changes(&self) -> Result<bool, String> {
551 let output = Command::new("git")
552 .arg("status")
553 .arg("--porcelain")
554 .current_dir(&self.repository_path)
555 .output()
556 .map_err(|e| format!("Failed to check git status: {}", e))?;
557
558 if output.status.success() {
559 let status = String::from_utf8_lossy(&output.stdout);
560 Ok(!status.trim().is_empty())
561 } else {
562 let error = String::from_utf8_lossy(&output.stderr);
563 Err(format!("Git status error: {}", error))
564 }
565 }
566
567 pub fn merge_session_to_main(&self) -> Result<(), String> {
569 self.switch_branch("main")?;
571
572 let merge_output = Command::new("git")
574 .arg("merge")
575 .arg(&self.session_state.current_branch)
576 .current_dir(&self.repository_path)
577 .output()
578 .map_err(|e| format!("Failed to merge: {}", e))?;
579
580 if merge_output.status.success() {
581 Ok(())
582 } else {
583 let error = String::from_utf8_lossy(&merge_output.stderr);
584 Err(format!("Failed to merge session branch: {}", error))
585 }
586 }
587}
588
589impl BranchManager {
592 pub fn new(repo_path: PathBuf) -> Result<Self, String> {
593 Ok(BranchManager {
594 repo_path,
595 naming_strategy: BranchNamingStrategy::default(),
596 auto_cleanup: true,
597 max_branch_lifetime_days: 7,
598 })
599 }
600
601 pub fn create_session_branch(&self, session_name: Option<String>) -> Result<String, String> {
602 let branch_name = match &self.naming_strategy {
603 BranchNamingStrategy::SessionTimestamp => {
604 let timestamp = SystemTime::now()
605 .duration_since(UNIX_EPOCH)
606 .unwrap()
607 .as_secs();
608 format!("soma-session-{}", timestamp)
609 }
610 BranchNamingStrategy::SessionId => {
611 format!("soma-session-{}",
612 session_name.unwrap_or_else(|| "default".to_string()))
613 }
614 BranchNamingStrategy::UserTimestamp => {
615 let timestamp = SystemTime::now()
616 .duration_since(UNIX_EPOCH)
617 .unwrap()
618 .as_secs();
619 format!("soma-user-{}", timestamp)
620 }
621 BranchNamingStrategy::Custom(pattern) => {
622 pattern.clone()
623 }
624 };
625
626 let output = Command::new("git")
628 .arg("checkout")
629 .arg("-b")
630 .arg(&branch_name)
631 .current_dir(&self.repo_path)
632 .output()
633 .map_err(|e| format!("Failed to create branch: {}", e))?;
634
635 if output.status.success() {
636 Ok(branch_name)
637 } else {
638 let error = String::from_utf8_lossy(&output.stderr);
639 Err(format!("Failed to create branch: {}", error))
640 }
641 }
642}
643
644impl CommitManager {
645 pub fn new(repo_path: PathBuf) -> Result<Self, String> {
646 let mut message_templates = HashMap::new();
647 message_templates.insert("safe".to_string(), "SOMA Safe Edit: {}".to_string());
648 message_templates.insert("critical".to_string(), "SOMA Critical Edit: {}".to_string());
649 message_templates.insert("experimental".to_string(), "SOMA Experimental Edit: {}".to_string());
650 message_templates.insert("cosmetic".to_string(), "SOMA Cosmetic Edit: {}".to_string());
651
652 Ok(CommitManager {
653 repo_path,
654 message_templates,
655 auto_stage: true,
656 sign_commits: false,
657 })
658 }
659
660 pub fn initialize_session(&self, _session_id: &str) -> Result<(), String> {
661 Ok(())
663 }
664
665 pub fn generate_commit_message(&self, edit: &ModifiableEdit, classification: &ClassifiedEdit) -> String {
666 let edit_type = match &classification.category {
667 EditCategory::Critical { .. } => "critical",
668 EditCategory::Safe { .. } => "safe",
669 EditCategory::Experimental { .. } => "experimental",
670 EditCategory::Cosmetic { .. } => "cosmetic",
671 };
672
673 let default_template = "SOMA Edit: {}".to_string();
674 let template = self.message_templates.get(edit_type)
675 .unwrap_or(&default_template);
676
677 let description = if !classification.reasoning.is_empty() {
678 &classification.reasoning
679 } else {
680 &format!("Edit to {}", edit.base_edit.file)
681 };
682
683 template.replace("{}", description)
684 }
685}
686
687impl ConflictResolver {
688 pub fn new(repo_path: PathBuf) -> Result<Self, String> {
689 Ok(ConflictResolver {
690 repo_path,
691 strategies: vec![ConflictStrategy::Interactive],
692 use_classification: true,
693 cognitive_resolution: true,
694 })
695 }
696}
697
698impl BackupManager {
699 pub fn new(repo_path: PathBuf) -> Result<Self, String> {
700 let backup_dir = repo_path.join(".soma-backups");
701
702 if !backup_dir.exists() {
704 fs::create_dir_all(&backup_dir)
705 .map_err(|e| format!("Failed to create backup directory: {}", e))?;
706 }
707
708 Ok(BackupManager {
709 repo_path,
710 backup_dir,
711 compress_backups: true,
712 incremental: true,
713 })
714 }
715
716 pub fn create_backup_point(&self, description: String) -> Result<BackupPoint, String> {
717 let timestamp = SystemTime::now();
718 let id = format!("backup-{}", timestamp.duration_since(UNIX_EPOCH).unwrap().as_secs());
719
720 let hash_output = Command::new("git")
722 .arg("rev-parse")
723 .arg("HEAD")
724 .current_dir(&self.repo_path)
725 .output()
726 .map_err(|e| format!("Failed to get state hash: {}", e))?;
727
728 let state_hash = if hash_output.status.success() {
729 String::from_utf8_lossy(&hash_output.stdout).trim().to_string()
730 } else {
731 "unknown".to_string()
732 };
733
734 let _stash_output = Command::new("git")
736 .arg("stash")
737 .arg("push")
738 .arg("-m")
739 .arg(&format!("SOMA Backup: {}", description))
740 .current_dir(&self.repo_path)
741 .output()
742 .map_err(|e| format!("Failed to create backup stash: {}", e))?;
743
744 Ok(BackupPoint {
747 id,
748 timestamp,
749 state_hash,
750 backup_path: self.backup_dir.clone(),
751 description,
752 })
753 }
754}
755
756#[cfg(test)]
758mod tests {
759 use super::*;
760 use std::fs;
761 use tempfile::TempDir;
762
763 fn create_test_git_repo() -> (TempDir, PathBuf) {
764 let temp_dir = TempDir::new().unwrap();
765 let repo_path = temp_dir.path().to_path_buf();
766
767 Command::new("git")
769 .arg("init")
770 .current_dir(&repo_path)
771 .output()
772 .unwrap();
773
774 Command::new("git")
776 .args(&["config", "user.name", "SOMA Test"])
777 .current_dir(&repo_path)
778 .output()
779 .unwrap();
780
781 Command::new("git")
782 .args(&["config", "user.email", "soma@test.com"])
783 .current_dir(&repo_path)
784 .output()
785 .unwrap();
786
787 let initial_file = repo_path.join("README.md");
789 fs::write(&initial_file, "# SOMA Test Repository\n").unwrap();
790
791 Command::new("git")
792 .args(&["add", "README.md"])
793 .current_dir(&repo_path)
794 .output()
795 .unwrap();
796
797 Command::new("git")
798 .args(&["commit", "-m", "Initial commit"])
799 .current_dir(&repo_path)
800 .output()
801 .unwrap();
802
803 (temp_dir, repo_path)
804 }
805
806 #[test]
807 fn test_git_integration_system_creation() {
808 let (_temp_dir, repo_path) = create_test_git_repo();
809
810 let git_system = GitIntegrationSystem::new(repo_path);
811 assert!(git_system.is_ok());
812 }
813
814 #[test]
815 fn test_session_start_and_end() {
816 let (_temp_dir, repo_path) = create_test_git_repo();
817
818 let mut git_system = GitIntegrationSystem::new(repo_path).unwrap();
819
820 let session_result = git_system.start_session(Some("test-session".to_string()));
822 assert!(session_result.is_ok());
823
824 let end_result = git_system.end_session(false);
826 assert!(end_result.is_ok());
827 }
828
829 #[test]
830 fn test_branch_manager() {
831 let (_temp_dir, repo_path) = create_test_git_repo();
832
833 let branch_manager = BranchManager::new(repo_path).unwrap();
834 let branch_name = branch_manager.create_session_branch(Some("test".to_string()));
835 assert!(branch_name.is_ok());
836 }
837
838 #[test]
839 fn test_backup_manager() {
840 let (_temp_dir, repo_path) = create_test_git_repo();
841
842 let backup_manager = BackupManager::new(repo_path).unwrap();
843 let backup_point = backup_manager.create_backup_point("Test backup".to_string());
844 assert!(backup_point.is_ok());
845 }
846
847 #[test]
848 fn test_conflict_detection() {
849 let (_temp_dir, repo_path) = create_test_git_repo();
850
851 let test_file = repo_path.join("test_conflict.txt");
853 fs::write(&test_file, "line1\n<<<<<<< HEAD\nlocal change\n=======\nremote change\n>>>>>>> branch\nline3").unwrap();
854
855 let git_system = GitIntegrationSystem::new(repo_path).unwrap();
856 let conflicts = git_system.check_for_conflicts(&test_file.to_string_lossy()).unwrap();
857 assert!(conflicts.is_some());
858 }
859
860 #[test]
861 fn test_git_config_default() {
862 let config = GitConfig::default();
863 assert!(config.auto_commit);
864 assert!(config.session_branches);
865 assert_eq!(config.backup_frequency_minutes, 30);
866 }
867}