use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::fs;
use std::time::{SystemTime, UNIX_EPOCH};
use crate::edit_control::ModifiableEdit;
use crate::classification::{ClassifiedEdit, EditCategory};
#[derive(Debug, Clone)]
pub struct GitIntegrationSystem {
pub repository_path: PathBuf,
pub config: GitConfig,
pub branch_manager: BranchManager,
pub commit_manager: CommitManager,
pub conflict_resolver: ConflictResolver,
pub backup_manager: BackupManager,
pub session_state: GitSessionState,
}
#[derive(Debug, Clone)]
pub struct GitConfig {
pub auto_commit: bool,
pub session_branches: bool,
pub backup_frequency_minutes: u32,
pub max_backup_days: u32,
pub cicd_integration: bool,
pub conflict_strategy: ConflictStrategy,
}
#[derive(Debug, Clone)]
pub struct GitSessionState {
pub session_id: String,
pub start_time: SystemTime,
pub current_branch: String,
pub original_branch: String,
pub edits_applied: Vec<String>,
pub commit_history: Vec<GitCommit>,
pub backup_points: Vec<BackupPoint>,
}
#[derive(Debug, Clone)]
pub struct BranchManager {
pub repo_path: PathBuf,
pub naming_strategy: BranchNamingStrategy,
pub auto_cleanup: bool,
pub max_branch_lifetime_days: u32,
}
#[derive(Debug, Clone)]
pub struct CommitManager {
pub repo_path: PathBuf,
pub message_templates: HashMap<String, String>,
pub auto_stage: bool,
pub sign_commits: bool,
}
#[derive(Debug, Clone)]
pub struct ConflictResolver {
pub repo_path: PathBuf,
pub strategies: Vec<ConflictStrategy>,
pub use_classification: bool,
pub cognitive_resolution: bool,
}
#[derive(Debug, Clone)]
pub struct BackupManager {
pub repo_path: PathBuf,
pub backup_dir: PathBuf,
pub compress_backups: bool,
pub incremental: bool,
}
#[derive(Debug, Clone)]
pub struct GitCommit {
pub hash: String,
pub message: String,
pub timestamp: SystemTime,
pub files_changed: Vec<String>,
pub soma_edit_ids: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct BackupPoint {
pub id: String,
pub timestamp: SystemTime,
pub state_hash: String,
pub backup_path: PathBuf,
pub description: String,
}
#[derive(Debug, Clone)]
pub enum BranchNamingStrategy {
SessionTimestamp,
SessionId,
UserTimestamp,
Custom(String),
}
#[derive(Debug, Clone)]
pub enum ConflictStrategy {
PreferLocal,
PreferRemote,
Interactive,
ClassificationBased,
CognitiveResolution,
}
#[derive(Debug, Clone)]
pub enum GitOperationResult {
Success(String),
Error(String),
RequiresIntervention(String),
Conflict(ConflictInfo),
}
#[derive(Debug, Clone)]
pub struct ConflictInfo {
pub files: Vec<String>,
pub descriptions: Vec<String>,
pub suggested_strategy: ConflictStrategy,
pub classification_results: Vec<ClassifiedEdit>,
}
impl Default for GitConfig {
fn default() -> Self {
GitConfig {
auto_commit: true,
session_branches: true,
backup_frequency_minutes: 30,
max_backup_days: 7,
cicd_integration: false,
conflict_strategy: ConflictStrategy::Interactive,
}
}
}
impl Default for BranchNamingStrategy {
fn default() -> Self {
BranchNamingStrategy::SessionTimestamp
}
}
impl GitIntegrationSystem {
pub fn new(repository_path: PathBuf) -> Result<Self, String> {
if !Self::is_git_repository(&repository_path) {
return Err(format!("Path {:?} is not a Git repository", repository_path));
}
let config = GitConfig::default();
let branch_manager = BranchManager::new(repository_path.clone())?;
let commit_manager = CommitManager::new(repository_path.clone())?;
let conflict_resolver = ConflictResolver::new(repository_path.clone())?;
let backup_manager = BackupManager::new(repository_path.clone())?;
let session_id = Self::generate_session_id();
let current_branch = Self::get_current_branch(&repository_path)?;
let session_state = GitSessionState {
session_id,
start_time: SystemTime::now(),
current_branch: current_branch.clone(),
original_branch: current_branch,
edits_applied: Vec::new(),
commit_history: Vec::new(),
backup_points: Vec::new(),
};
Ok(GitIntegrationSystem {
repository_path,
config,
branch_manager,
commit_manager,
conflict_resolver,
backup_manager,
session_state,
})
}
pub fn start_session(&mut self, session_name: Option<String>) -> Result<String, String> {
if self.config.session_branches {
let branch_name = self.branch_manager.create_session_branch(
session_name.clone()
)?;
self.session_state.current_branch = branch_name.clone();
self.switch_branch(&branch_name)?;
}
let backup_point = self.backup_manager.create_backup_point(
"Session start".to_string()
)?;
self.session_state.backup_points.push(backup_point);
self.commit_manager.initialize_session(&self.session_state.session_id)?;
Ok(format!("Started SOMA Git session: {}", self.session_state.session_id))
}
pub fn apply_edits_with_git(
&mut self,
edits: Vec<ModifiableEdit>,
classifications: Vec<ClassifiedEdit>
) -> Result<Vec<GitOperationResult>, String> {
let mut results = Vec::new();
let mut critical_edits = Vec::new();
let mut safe_edits = Vec::new();
let mut experimental_edits = Vec::new();
for (edit, classification) in edits.iter().zip(classifications.iter()) {
match &classification.category {
EditCategory::Critical { .. } => {
critical_edits.push((edit, classification));
}
EditCategory::Safe { .. } => {
safe_edits.push((edit, classification));
}
EditCategory::Experimental { .. } => {
experimental_edits.push((edit, classification));
}
EditCategory::Cosmetic { .. } => {
safe_edits.push((edit, classification));
}
}
}
for (edit, classification) in safe_edits {
let result = self.apply_single_edit_with_git(edit, classification)?;
results.push(result);
}
if !experimental_edits.is_empty() {
let backup_point = self.backup_manager.create_backup_point(
"Before experimental edits".to_string()
)?;
self.session_state.backup_points.push(backup_point);
for (edit, classification) in experimental_edits {
let result = self.apply_single_edit_with_git(edit, classification)?;
results.push(result);
}
}
if !critical_edits.is_empty() {
let backup_point = self.backup_manager.create_backup_point(
"Before critical edits".to_string()
)?;
self.session_state.backup_points.push(backup_point);
for (edit, classification) in critical_edits {
let result = self.apply_single_edit_with_git(edit, classification)?;
results.push(result.clone());
if matches!(result, GitOperationResult::Success(_)) {
self.commit_current_changes(&format!(
"SOMA Critical Edit: {}",
classification.reasoning
))?;
}
}
}
Ok(results)
}
fn apply_single_edit_with_git(
&mut self,
edit: &ModifiableEdit,
classification: &ClassifiedEdit
) -> Result<GitOperationResult, String> {
if let Some(conflicts) = self.check_for_conflicts(&edit.base_edit.file)? {
return Ok(GitOperationResult::Conflict(conflicts));
}
let file_path = &edit.base_edit.file;
self.session_state.edits_applied.push(format!("{:?}", edit.base_edit));
if self.config.auto_commit && !matches!(classification.category, EditCategory::Critical { .. }) {
let commit_message = self.commit_manager.generate_commit_message(edit, classification);
match self.commit_current_changes(&commit_message) {
Ok(commit_hash) => {
let commit = GitCommit {
hash: commit_hash.clone(),
message: commit_message,
timestamp: SystemTime::now(),
files_changed: vec![file_path.clone()],
soma_edit_ids: vec![format!("{:?}", edit)],
};
self.session_state.commit_history.push(commit);
Ok(GitOperationResult::Success(commit_hash))
}
Err(e) => Ok(GitOperationResult::Error(e)),
}
} else {
Ok(GitOperationResult::Success("Edit applied successfully".to_string()))
}
}
pub fn end_session(&mut self, merge_to_main: bool) -> Result<String, String> {
let final_backup = self.backup_manager.create_backup_point(
"Session end".to_string()
)?;
self.session_state.backup_points.push(final_backup);
if self.has_uncommitted_changes()? {
self.commit_current_changes("SOMA Session final commit")?;
}
let session_summary = format!(
"Session {} completed:\n- Edits applied: {}\n- Commits: {}\n- Backups: {}",
self.session_state.session_id,
self.session_state.edits_applied.len(),
self.session_state.commit_history.len(),
self.session_state.backup_points.len()
);
if merge_to_main && self.config.session_branches {
self.merge_session_to_main()?;
}
if self.config.session_branches {
self.switch_branch(&self.session_state.original_branch)?;
}
Ok(session_summary)
}
fn is_git_repository(path: &Path) -> bool {
path.join(".git").exists()
}
fn generate_session_id() -> String {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
format!("soma-{}", timestamp)
}
fn get_current_branch(repo_path: &Path) -> Result<String, String> {
let output = Command::new("git")
.arg("branch")
.arg("--show-current")
.current_dir(repo_path)
.output()
.map_err(|e| format!("Failed to get current branch: {}", e))?;
if output.status.success() {
let branch = String::from_utf8_lossy(&output.stdout).trim().to_string();
Ok(branch)
} else {
let error = String::from_utf8_lossy(&output.stderr);
Err(format!("Git error: {}", error))
}
}
fn switch_branch(&self, branch_name: &str) -> Result<(), String> {
let output = Command::new("git")
.arg("checkout")
.arg(branch_name)
.current_dir(&self.repository_path)
.output()
.map_err(|e| format!("Failed to switch branch: {}", e))?;
if output.status.success() {
Ok(())
} else {
let error = String::from_utf8_lossy(&output.stderr);
Err(format!("Failed to switch to branch {}: {}", branch_name, error))
}
}
pub fn check_for_conflicts(&self, file_path: &str) -> Result<Option<ConflictInfo>, String> {
let file_content = fs::read_to_string(file_path)
.map_err(|e| format!("Failed to read file {}: {}", file_path, e))?;
if file_content.contains("<<<<<<< HEAD") ||
file_content.contains("=======") ||
file_content.contains(">>>>>>> ") {
let conflict_info = ConflictInfo {
files: vec![file_path.to_string()],
descriptions: vec!["Merge conflict detected".to_string()],
suggested_strategy: ConflictStrategy::Interactive,
classification_results: Vec::new(),
};
Ok(Some(conflict_info))
} else {
Ok(None)
}
}
pub fn commit_current_changes(&self, message: &str) -> Result<String, String> {
let stage_output = Command::new("git")
.arg("add")
.arg(".")
.current_dir(&self.repository_path)
.output()
.map_err(|e| format!("Failed to stage changes: {}", e))?;
if !stage_output.status.success() {
let error = String::from_utf8_lossy(&stage_output.stderr);
return Err(format!("Failed to stage changes: {}", error));
}
let commit_output = Command::new("git")
.arg("commit")
.arg("-m")
.arg(message)
.current_dir(&self.repository_path)
.output()
.map_err(|e| format!("Failed to commit: {}", e))?;
if commit_output.status.success() {
let hash_output = Command::new("git")
.arg("rev-parse")
.arg("HEAD")
.current_dir(&self.repository_path)
.output()
.map_err(|e| format!("Failed to get commit hash: {}", e))?;
if hash_output.status.success() {
let hash = String::from_utf8_lossy(&hash_output.stdout).trim().to_string();
Ok(hash)
} else {
Ok("unknown".to_string())
}
} else {
let error = String::from_utf8_lossy(&commit_output.stderr);
Err(format!("Failed to commit: {}", error))
}
}
fn has_uncommitted_changes(&self) -> Result<bool, String> {
let output = Command::new("git")
.arg("status")
.arg("--porcelain")
.current_dir(&self.repository_path)
.output()
.map_err(|e| format!("Failed to check git status: {}", e))?;
if output.status.success() {
let status = String::from_utf8_lossy(&output.stdout);
Ok(!status.trim().is_empty())
} else {
let error = String::from_utf8_lossy(&output.stderr);
Err(format!("Git status error: {}", error))
}
}
pub fn merge_session_to_main(&self) -> Result<(), String> {
self.switch_branch("main")?;
let merge_output = Command::new("git")
.arg("merge")
.arg(&self.session_state.current_branch)
.current_dir(&self.repository_path)
.output()
.map_err(|e| format!("Failed to merge: {}", e))?;
if merge_output.status.success() {
Ok(())
} else {
let error = String::from_utf8_lossy(&merge_output.stderr);
Err(format!("Failed to merge session branch: {}", error))
}
}
}
impl BranchManager {
pub fn new(repo_path: PathBuf) -> Result<Self, String> {
Ok(BranchManager {
repo_path,
naming_strategy: BranchNamingStrategy::default(),
auto_cleanup: true,
max_branch_lifetime_days: 7,
})
}
pub fn create_session_branch(&self, session_name: Option<String>) -> Result<String, String> {
let branch_name = match &self.naming_strategy {
BranchNamingStrategy::SessionTimestamp => {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
format!("soma-session-{}", timestamp)
}
BranchNamingStrategy::SessionId => {
format!("soma-session-{}",
session_name.unwrap_or_else(|| "default".to_string()))
}
BranchNamingStrategy::UserTimestamp => {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
format!("soma-user-{}", timestamp)
}
BranchNamingStrategy::Custom(pattern) => {
pattern.clone()
}
};
let output = Command::new("git")
.arg("checkout")
.arg("-b")
.arg(&branch_name)
.current_dir(&self.repo_path)
.output()
.map_err(|e| format!("Failed to create branch: {}", e))?;
if output.status.success() {
Ok(branch_name)
} else {
let error = String::from_utf8_lossy(&output.stderr);
Err(format!("Failed to create branch: {}", error))
}
}
}
impl CommitManager {
pub fn new(repo_path: PathBuf) -> Result<Self, String> {
let mut message_templates = HashMap::new();
message_templates.insert("safe".to_string(), "SOMA Safe Edit: {}".to_string());
message_templates.insert("critical".to_string(), "SOMA Critical Edit: {}".to_string());
message_templates.insert("experimental".to_string(), "SOMA Experimental Edit: {}".to_string());
message_templates.insert("cosmetic".to_string(), "SOMA Cosmetic Edit: {}".to_string());
Ok(CommitManager {
repo_path,
message_templates,
auto_stage: true,
sign_commits: false,
})
}
pub fn initialize_session(&self, _session_id: &str) -> Result<(), String> {
Ok(())
}
pub fn generate_commit_message(&self, edit: &ModifiableEdit, classification: &ClassifiedEdit) -> String {
let edit_type = match &classification.category {
EditCategory::Critical { .. } => "critical",
EditCategory::Safe { .. } => "safe",
EditCategory::Experimental { .. } => "experimental",
EditCategory::Cosmetic { .. } => "cosmetic",
};
let default_template = "SOMA Edit: {}".to_string();
let template = self.message_templates.get(edit_type)
.unwrap_or(&default_template);
let description = if !classification.reasoning.is_empty() {
&classification.reasoning
} else {
&format!("Edit to {}", edit.base_edit.file)
};
template.replace("{}", description)
}
}
impl ConflictResolver {
pub fn new(repo_path: PathBuf) -> Result<Self, String> {
Ok(ConflictResolver {
repo_path,
strategies: vec![ConflictStrategy::Interactive],
use_classification: true,
cognitive_resolution: true,
})
}
}
impl BackupManager {
pub fn new(repo_path: PathBuf) -> Result<Self, String> {
let backup_dir = repo_path.join(".soma-backups");
if !backup_dir.exists() {
fs::create_dir_all(&backup_dir)
.map_err(|e| format!("Failed to create backup directory: {}", e))?;
}
Ok(BackupManager {
repo_path,
backup_dir,
compress_backups: true,
incremental: true,
})
}
pub fn create_backup_point(&self, description: String) -> Result<BackupPoint, String> {
let timestamp = SystemTime::now();
let id = format!("backup-{}", timestamp.duration_since(UNIX_EPOCH).unwrap().as_secs());
let hash_output = Command::new("git")
.arg("rev-parse")
.arg("HEAD")
.current_dir(&self.repo_path)
.output()
.map_err(|e| format!("Failed to get state hash: {}", e))?;
let state_hash = if hash_output.status.success() {
String::from_utf8_lossy(&hash_output.stdout).trim().to_string()
} else {
"unknown".to_string()
};
let _stash_output = Command::new("git")
.arg("stash")
.arg("push")
.arg("-m")
.arg(&format!("SOMA Backup: {}", description))
.current_dir(&self.repo_path)
.output()
.map_err(|e| format!("Failed to create backup stash: {}", e))?;
Ok(BackupPoint {
id,
timestamp,
state_hash,
backup_path: self.backup_dir.clone(),
description,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
fn create_test_git_repo() -> (TempDir, PathBuf) {
let temp_dir = TempDir::new().unwrap();
let repo_path = temp_dir.path().to_path_buf();
Command::new("git")
.arg("init")
.current_dir(&repo_path)
.output()
.unwrap();
Command::new("git")
.args(&["config", "user.name", "SOMA Test"])
.current_dir(&repo_path)
.output()
.unwrap();
Command::new("git")
.args(&["config", "user.email", "soma@test.com"])
.current_dir(&repo_path)
.output()
.unwrap();
let initial_file = repo_path.join("README.md");
fs::write(&initial_file, "# SOMA Test Repository\n").unwrap();
Command::new("git")
.args(&["add", "README.md"])
.current_dir(&repo_path)
.output()
.unwrap();
Command::new("git")
.args(&["commit", "-m", "Initial commit"])
.current_dir(&repo_path)
.output()
.unwrap();
(temp_dir, repo_path)
}
#[test]
fn test_git_integration_system_creation() {
let (_temp_dir, repo_path) = create_test_git_repo();
let git_system = GitIntegrationSystem::new(repo_path);
assert!(git_system.is_ok());
}
#[test]
fn test_session_start_and_end() {
let (_temp_dir, repo_path) = create_test_git_repo();
let mut git_system = GitIntegrationSystem::new(repo_path).unwrap();
let session_result = git_system.start_session(Some("test-session".to_string()));
assert!(session_result.is_ok());
let end_result = git_system.end_session(false);
assert!(end_result.is_ok());
}
#[test]
fn test_branch_manager() {
let (_temp_dir, repo_path) = create_test_git_repo();
let branch_manager = BranchManager::new(repo_path).unwrap();
let branch_name = branch_manager.create_session_branch(Some("test".to_string()));
assert!(branch_name.is_ok());
}
#[test]
fn test_backup_manager() {
let (_temp_dir, repo_path) = create_test_git_repo();
let backup_manager = BackupManager::new(repo_path).unwrap();
let backup_point = backup_manager.create_backup_point("Test backup".to_string());
assert!(backup_point.is_ok());
}
#[test]
fn test_conflict_detection() {
let (_temp_dir, repo_path) = create_test_git_repo();
let test_file = repo_path.join("test_conflict.txt");
fs::write(&test_file, "line1\n<<<<<<< HEAD\nlocal change\n=======\nremote change\n>>>>>>> branch\nline3").unwrap();
let git_system = GitIntegrationSystem::new(repo_path).unwrap();
let conflicts = git_system.check_for_conflicts(&test_file.to_string_lossy()).unwrap();
assert!(conflicts.is_some());
}
#[test]
fn test_git_config_default() {
let config = GitConfig::default();
assert!(config.auto_commit);
assert!(config.session_branches);
assert_eq!(config.backup_frequency_minutes, 30);
}
}