feat: Convert SAL to a Rust monorepo
	
		
			
	
		
	
	
		
	
		
			Some checks are pending
		
		
	
	
		
			
				
	
				Rhai Tests / Run Rhai Tests (push) Waiting to run
				
			
		
		
	
	
				
					
				
			
		
			Some checks are pending
		
		
	
	Rhai Tests / Run Rhai Tests (push) Waiting to run
				
			- Migrate SAL project from single-crate to monorepo structure - Create independent packages for individual modules - Improve build efficiency and testing capabilities - Update documentation to reflect new structure - Successfully convert the git module to an independent package.
This commit is contained in:
		
							
								
								
									
										484
									
								
								git/src/git.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										484
									
								
								git/src/git.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,484 @@ | ||||
| use std::process::Command; | ||||
| use std::path::Path; | ||||
| use std::fs; | ||||
| use regex::Regex; | ||||
| use std::fmt; | ||||
| use std::error::Error; | ||||
|  | ||||
| // Define a custom error type for git operations | ||||
| #[derive(Debug)] | ||||
| pub enum GitError { | ||||
|     GitNotInstalled(std::io::Error), | ||||
|     InvalidUrl(String), | ||||
|     InvalidBasePath(String), | ||||
|     HomeDirectoryNotFound(std::env::VarError), | ||||
|     FileSystemError(std::io::Error), | ||||
|     GitCommandFailed(String), | ||||
|     CommandExecutionError(std::io::Error), | ||||
|     NoRepositoriesFound, | ||||
|     RepositoryNotFound(String), | ||||
|     MultipleRepositoriesFound(String, usize), | ||||
|     NotAGitRepository(String), | ||||
|     LocalChangesExist(String), | ||||
| } | ||||
|  | ||||
| // Implement Display for GitError | ||||
| impl fmt::Display for GitError { | ||||
|     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||||
|         match self { | ||||
|             GitError::GitNotInstalled(e) => write!(f, "Git is not installed: {}", e), | ||||
|             GitError::InvalidUrl(url) => write!(f, "Could not parse git URL: {}", url), | ||||
|             GitError::InvalidBasePath(path) => write!(f, "Invalid base path: {}", path), | ||||
|             GitError::HomeDirectoryNotFound(e) => write!(f, "Could not determine home directory: {}", e), | ||||
|             GitError::FileSystemError(e) => write!(f, "Error creating directory structure: {}", e), | ||||
|             GitError::GitCommandFailed(e) => write!(f, "{}", e), | ||||
|             GitError::CommandExecutionError(e) => write!(f, "Error executing command: {}", e), | ||||
|             GitError::NoRepositoriesFound => write!(f, "No repositories found"), | ||||
|             GitError::RepositoryNotFound(pattern) => write!(f, "No repositories found matching '{}'", pattern), | ||||
|             GitError::MultipleRepositoriesFound(pattern, count) =>  | ||||
|                 write!(f, "Multiple repositories ({}) found matching '{}'. Use '*' suffix for multiple matches.", count, pattern), | ||||
|             GitError::NotAGitRepository(path) => write!(f, "Not a git repository at {}", path), | ||||
|             GitError::LocalChangesExist(path) => write!(f, "Repository at {} has local changes", path), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Implement Error trait for GitError | ||||
| impl Error for GitError { | ||||
|     fn source(&self) -> Option<&(dyn Error + 'static)> { | ||||
|         match self { | ||||
|             GitError::GitNotInstalled(e) => Some(e), | ||||
|             GitError::HomeDirectoryNotFound(e) => Some(e), | ||||
|             GitError::FileSystemError(e) => Some(e), | ||||
|             GitError::CommandExecutionError(e) => Some(e), | ||||
|             _ => None, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Parses a git URL to extract the server, account, and repository name. | ||||
| ///  | ||||
| /// # Arguments | ||||
| ///  | ||||
| /// * `url` - The URL of the git repository to parse. Can be in HTTPS format  | ||||
| ///   (https://github.com/username/repo.git) or SSH format (git@github.com:username/repo.git). | ||||
| ///  | ||||
| /// # Returns | ||||
| ///  | ||||
| /// A tuple containing: | ||||
| /// * `server` - The server name (e.g., "github.com") | ||||
| /// * `account` - The account or organization name (e.g., "username") | ||||
| /// * `repo` - The repository name (e.g., "repo") | ||||
| ///  | ||||
| /// If the URL cannot be parsed, all three values will be empty strings. | ||||
| pub fn parse_git_url(url: &str) -> (String, String, String) { | ||||
|     // HTTP(S) URL format: https://github.com/username/repo.git | ||||
|     let https_re = Regex::new(r"https?://([^/]+)/([^/]+)/([^/\.]+)(?:\.git)?").unwrap(); | ||||
|      | ||||
|     // SSH URL format: git@github.com:username/repo.git | ||||
|     let ssh_re = Regex::new(r"git@([^:]+):([^/]+)/([^/\.]+)(?:\.git)?").unwrap(); | ||||
|      | ||||
|     if let Some(caps) = https_re.captures(url) { | ||||
|         let server = caps.get(1).map_or("", |m| m.as_str()).to_string(); | ||||
|         let account = caps.get(2).map_or("", |m| m.as_str()).to_string(); | ||||
|         let repo = caps.get(3).map_or("", |m| m.as_str()).to_string(); | ||||
|          | ||||
|         return (server, account, repo); | ||||
|     } else if let Some(caps) = ssh_re.captures(url) { | ||||
|         let server = caps.get(1).map_or("", |m| m.as_str()).to_string(); | ||||
|         let account = caps.get(2).map_or("", |m| m.as_str()).to_string(); | ||||
|         let repo = caps.get(3).map_or("", |m| m.as_str()).to_string(); | ||||
|          | ||||
|         return (server, account, repo); | ||||
|     } | ||||
|      | ||||
|     (String::new(), String::new(), String::new()) | ||||
| } | ||||
|  | ||||
| /// Checks if git is installed on the system. | ||||
| ///  | ||||
| /// # Returns | ||||
| ///  | ||||
| /// * `Ok(())` - If git is installed | ||||
| /// * `Err(GitError)` - If git is not installed | ||||
| fn check_git_installed() -> Result<(), GitError> { | ||||
|     Command::new("git") | ||||
|         .arg("--version") | ||||
|         .output() | ||||
|         .map_err(GitError::GitNotInstalled)?; | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| /// Represents a collection of git repositories under a base path. | ||||
| #[derive(Clone)] | ||||
| pub struct GitTree { | ||||
|     base_path: String, | ||||
| } | ||||
|  | ||||
| impl GitTree { | ||||
|     /// Creates a new GitTree with the specified base path. | ||||
|     ///  | ||||
|     /// # Arguments | ||||
|     ///  | ||||
|     /// * `base_path` - The base path where all git repositories are located | ||||
|     ///  | ||||
|     /// # Returns | ||||
|     ///  | ||||
|     /// * `Ok(GitTree)` - A new GitTree instance | ||||
|     /// * `Err(GitError)` - If the base path is invalid or cannot be created | ||||
|     pub fn new(base_path: &str) -> Result<Self, GitError> { | ||||
|         // Check if git is installed | ||||
|         check_git_installed()?; | ||||
|          | ||||
|         // Validate the base path | ||||
|         let path = Path::new(base_path); | ||||
|         if !path.exists() { | ||||
|             fs::create_dir_all(path).map_err(|e| { | ||||
|                 GitError::FileSystemError(e) | ||||
|             })?; | ||||
|         } else if !path.is_dir() { | ||||
|             return Err(GitError::InvalidBasePath(base_path.to_string())); | ||||
|         } | ||||
|          | ||||
|         Ok(GitTree { | ||||
|             base_path: base_path.to_string(), | ||||
|         }) | ||||
|     } | ||||
|      | ||||
|     /// Lists all git repositories under the base path. | ||||
|     ///  | ||||
|     /// # Returns | ||||
|     ///  | ||||
|     /// * `Ok(Vec<String>)` - A vector of paths to git repositories | ||||
|     /// * `Err(GitError)` - If the operation failed | ||||
|     pub fn list(&self) -> Result<Vec<String>, GitError> { | ||||
|         let base_path = Path::new(&self.base_path); | ||||
|          | ||||
|         if !base_path.exists() || !base_path.is_dir() { | ||||
|             return Ok(Vec::new()); | ||||
|         } | ||||
|          | ||||
|         let mut repos = Vec::new(); | ||||
|          | ||||
|         // Find all directories with .git subdirectories | ||||
|         let output = Command::new("find") | ||||
|             .args(&[&self.base_path, "-type", "d", "-name", ".git"]) | ||||
|             .output() | ||||
|             .map_err(GitError::CommandExecutionError)?; | ||||
|              | ||||
|         if output.status.success() { | ||||
|             let stdout = String::from_utf8_lossy(&output.stdout); | ||||
|             for line in stdout.lines() { | ||||
|                 // Get the parent directory of .git which is the repo root | ||||
|                 if let Some(parent) = Path::new(line).parent() { | ||||
|                     if let Some(path_str) = parent.to_str() { | ||||
|                         repos.push(path_str.to_string()); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } else { | ||||
|             let error = String::from_utf8_lossy(&output.stderr); | ||||
|             return Err(GitError::GitCommandFailed(format!("Failed to find git repositories: {}", error))); | ||||
|         } | ||||
|          | ||||
|         Ok(repos) | ||||
|     } | ||||
|      | ||||
|     /// Finds repositories matching a pattern or partial path. | ||||
|     ///  | ||||
|     /// # Arguments | ||||
|     ///  | ||||
|     /// * `pattern` - The pattern to match against repository paths | ||||
|     ///   - If the pattern ends with '*', all matching repositories are returned | ||||
|     ///   - Otherwise, exactly one matching repository must be found | ||||
|     ///  | ||||
|     /// # Returns | ||||
|     ///  | ||||
|     /// * `Ok(Vec<String>)` - A vector of paths to matching repositories | ||||
|     /// * `Err(GitError)` - If no matching repositories are found, | ||||
|     ///   or if multiple repositories match a non-wildcard pattern | ||||
|     pub fn find(&self, pattern: &str) -> Result<Vec<GitRepo>, GitError> { | ||||
|         let repo_names = self.list()?; // list() already ensures these are git repo names | ||||
|  | ||||
|         if repo_names.is_empty() { | ||||
|             return Ok(Vec::new()); // If no repos listed, find results in an empty list | ||||
|         } | ||||
|  | ||||
|         let mut matched_repos: Vec<GitRepo> = Vec::new(); | ||||
|  | ||||
|         if pattern == "*" { | ||||
|             for name in repo_names { | ||||
|                 let full_path = format!("{}/{}", self.base_path, name); | ||||
|                 matched_repos.push(GitRepo::new(full_path)); | ||||
|             } | ||||
|         } else if pattern.ends_with('*') { | ||||
|             let prefix = &pattern[0..pattern.len()-1]; | ||||
|             for name in repo_names { | ||||
|                 if name.starts_with(prefix) { | ||||
|                     let full_path = format!("{}/{}", self.base_path, name); | ||||
|                     matched_repos.push(GitRepo::new(full_path)); | ||||
|                 } | ||||
|             } | ||||
|         } else { | ||||
|             // Exact match for the name | ||||
|             for name in repo_names { | ||||
|                 if name == pattern { | ||||
|                     let full_path = format!("{}/{}", self.base_path, name); | ||||
|                     matched_repos.push(GitRepo::new(full_path)); | ||||
|                     // `find` returns all exact matches. If names aren't unique (unlikely from `list`), | ||||
|                     // it could return more than one. For an exact name, typically one is expected. | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         Ok(matched_repos) | ||||
|     } | ||||
|      | ||||
|     /// Gets one or more GitRepo objects based on a path pattern or URL. | ||||
|     ///  | ||||
|     /// # Arguments | ||||
|     ///  | ||||
|     /// * `path_or_url` - The path pattern to match against repository paths or a git URL | ||||
|     ///   - If it's a URL, the repository will be cloned if it doesn't exist | ||||
|     ///   - If it's a path pattern, it will find matching repositories | ||||
|     ///  | ||||
|     /// # Returns | ||||
|     ///  | ||||
|     /// * `Ok(Vec<GitRepo>)` - A vector of GitRepo objects | ||||
|     /// * `Err(GitError)` - If no matching repositories are found or the clone operation failed | ||||
|     pub fn get(&self, path_or_url: &str) -> Result<Vec<GitRepo>, GitError> { | ||||
|         // Check if it's a URL | ||||
|         if path_or_url.starts_with("http") || path_or_url.starts_with("git@") { | ||||
|             // Parse the URL | ||||
|             let (server, account, repo) = parse_git_url(path_or_url); | ||||
|             if server.is_empty() || account.is_empty() || repo.is_empty() { | ||||
|                 return Err(GitError::InvalidUrl(path_or_url.to_string())); | ||||
|             } | ||||
|              | ||||
|             // Create the target directory | ||||
|             let clone_path = format!("{}/{}/{}/{}", self.base_path, server, account, repo); | ||||
|             let clone_dir = Path::new(&clone_path); | ||||
|              | ||||
|             // Check if repo already exists | ||||
|             if clone_dir.exists() { | ||||
|                 return Ok(vec![GitRepo::new(clone_path)]); | ||||
|             } | ||||
|              | ||||
|             // Create parent directory | ||||
|             if let Some(parent) = clone_dir.parent() { | ||||
|                 fs::create_dir_all(parent).map_err(GitError::FileSystemError)?; | ||||
|             } | ||||
|              | ||||
|             // Clone the repository | ||||
|             let output = Command::new("git") | ||||
|                 .args(&["clone", "--depth", "1", path_or_url, &clone_path]) | ||||
|                 .output() | ||||
|                 .map_err(GitError::CommandExecutionError)?; | ||||
|                  | ||||
|             if output.status.success() { | ||||
|                 Ok(vec![GitRepo::new(clone_path)]) | ||||
|             } else { | ||||
|                 let error = String::from_utf8_lossy(&output.stderr); | ||||
|                 Err(GitError::GitCommandFailed(format!("Git clone error: {}", error))) | ||||
|             } | ||||
|         } else { | ||||
|             // It's a path pattern, find matching repositories using the updated self.find() | ||||
|             // which now directly returns Result<Vec<GitRepo>, GitError>. | ||||
|             let repos = self.find(path_or_url)?; | ||||
|             Ok(repos) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Represents a git repository. | ||||
| pub struct GitRepo { | ||||
|     path: String, | ||||
| } | ||||
|  | ||||
| impl GitRepo { | ||||
|     /// Creates a new GitRepo with the specified path. | ||||
|     /// | ||||
|     /// # Arguments | ||||
|     /// | ||||
|     /// * `path` - The path to the git repository | ||||
|     pub fn new(path: String) -> Self { | ||||
|         GitRepo { path } | ||||
|     } | ||||
|  | ||||
|     /// Gets the path of the repository. | ||||
|     /// | ||||
|     /// # Returns | ||||
|     /// | ||||
|     /// * The path to the git repository | ||||
|     pub fn path(&self) -> &str { | ||||
|         &self.path | ||||
|     } | ||||
|  | ||||
|     /// Checks if the repository has uncommitted changes. | ||||
|     /// | ||||
|     /// # Returns | ||||
|     /// | ||||
|     /// * `Ok(bool)` - True if the repository has uncommitted changes, false otherwise | ||||
|     /// * `Err(GitError)` - If the operation failed | ||||
|     pub fn has_changes(&self) -> Result<bool, GitError> { | ||||
|         let output = Command::new("git") | ||||
|             .args(&["-C", &self.path, "status", "--porcelain"]) | ||||
|             .output() | ||||
|             .map_err(GitError::CommandExecutionError)?; | ||||
|  | ||||
|         Ok(!output.stdout.is_empty()) | ||||
|     } | ||||
|  | ||||
|     /// Pulls the latest changes from the remote repository. | ||||
|     /// | ||||
|     /// # Returns | ||||
|     /// | ||||
|     /// * `Ok(Self)` - The GitRepo object for method chaining | ||||
|     /// * `Err(GitError)` - If the pull operation failed | ||||
|     pub fn pull(&self) -> Result<Self, GitError> { | ||||
|         // Check if repository exists and is a git repository | ||||
|         let git_dir = Path::new(&self.path).join(".git"); | ||||
|         if !git_dir.exists() || !git_dir.is_dir() { | ||||
|             return Err(GitError::NotAGitRepository(self.path.clone())); | ||||
|         } | ||||
|  | ||||
|         // Check for local changes | ||||
|         if self.has_changes()? { | ||||
|             return Err(GitError::LocalChangesExist(self.path.clone())); | ||||
|         } | ||||
|  | ||||
|         // Pull the latest changes | ||||
|         let output = Command::new("git") | ||||
|             .args(&["-C", &self.path, "pull"]) | ||||
|             .output() | ||||
|             .map_err(GitError::CommandExecutionError)?; | ||||
|  | ||||
|         if output.status.success() { | ||||
|             Ok(self.clone()) | ||||
|         } else { | ||||
|             let error = String::from_utf8_lossy(&output.stderr); | ||||
|             Err(GitError::GitCommandFailed(format!("Git pull error: {}", error))) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// Resets any local changes in the repository. | ||||
|     /// | ||||
|     /// # Returns | ||||
|     /// | ||||
|     /// * `Ok(Self)` - The GitRepo object for method chaining | ||||
|     /// * `Err(GitError)` - If the reset operation failed | ||||
|     pub fn reset(&self) -> Result<Self, GitError> { | ||||
|         // Check if repository exists and is a git repository | ||||
|         let git_dir = Path::new(&self.path).join(".git"); | ||||
|         if !git_dir.exists() || !git_dir.is_dir() { | ||||
|             return Err(GitError::NotAGitRepository(self.path.clone())); | ||||
|         } | ||||
|  | ||||
|         // Reset any local changes | ||||
|         let reset_output = Command::new("git") | ||||
|             .args(&["-C", &self.path, "reset", "--hard", "HEAD"]) | ||||
|             .output() | ||||
|             .map_err(GitError::CommandExecutionError)?; | ||||
|  | ||||
|         if !reset_output.status.success() { | ||||
|             let error = String::from_utf8_lossy(&reset_output.stderr); | ||||
|             return Err(GitError::GitCommandFailed(format!("Git reset error: {}", error))); | ||||
|         } | ||||
|  | ||||
|         // Clean untracked files | ||||
|         let clean_output = Command::new("git") | ||||
|             .args(&["-C", &self.path, "clean", "-fd"]) | ||||
|             .output() | ||||
|             .map_err(GitError::CommandExecutionError)?; | ||||
|  | ||||
|         if !clean_output.status.success() { | ||||
|             let error = String::from_utf8_lossy(&clean_output.stderr); | ||||
|             return Err(GitError::GitCommandFailed(format!("Git clean error: {}", error))); | ||||
|         } | ||||
|  | ||||
|         Ok(self.clone()) | ||||
|     } | ||||
|  | ||||
|     /// Commits changes in the repository. | ||||
|     /// | ||||
|     /// # Arguments | ||||
|     /// | ||||
|     /// * `message` - The commit message | ||||
|     /// | ||||
|     /// # Returns | ||||
|     /// | ||||
|     /// * `Ok(Self)` - The GitRepo object for method chaining | ||||
|     /// * `Err(GitError)` - If the commit operation failed | ||||
|     pub fn commit(&self, message: &str) -> Result<Self, GitError> { | ||||
|         // Check if repository exists and is a git repository | ||||
|         let git_dir = Path::new(&self.path).join(".git"); | ||||
|         if !git_dir.exists() || !git_dir.is_dir() { | ||||
|             return Err(GitError::NotAGitRepository(self.path.clone())); | ||||
|         } | ||||
|  | ||||
|         // Check for local changes | ||||
|         if !self.has_changes()? { | ||||
|             return Ok(self.clone()); | ||||
|         } | ||||
|  | ||||
|         // Add all changes | ||||
|         let add_output = Command::new("git") | ||||
|             .args(&["-C", &self.path, "add", "."]) | ||||
|             .output() | ||||
|             .map_err(GitError::CommandExecutionError)?; | ||||
|  | ||||
|         if !add_output.status.success() { | ||||
|             let error = String::from_utf8_lossy(&add_output.stderr); | ||||
|             return Err(GitError::GitCommandFailed(format!("Git add error: {}", error))); | ||||
|         } | ||||
|  | ||||
|         // Commit the changes | ||||
|         let commit_output = Command::new("git") | ||||
|             .args(&["-C", &self.path, "commit", "-m", message]) | ||||
|             .output() | ||||
|             .map_err(GitError::CommandExecutionError)?; | ||||
|  | ||||
|         if !commit_output.status.success() { | ||||
|             let error = String::from_utf8_lossy(&commit_output.stderr); | ||||
|             return Err(GitError::GitCommandFailed(format!("Git commit error: {}", error))); | ||||
|         } | ||||
|  | ||||
|         Ok(self.clone()) | ||||
|     } | ||||
|  | ||||
|     /// Pushes changes to the remote repository. | ||||
|     /// | ||||
|     /// # Returns | ||||
|     /// | ||||
|     /// * `Ok(Self)` - The GitRepo object for method chaining | ||||
|     /// * `Err(GitError)` - If the push operation failed | ||||
|     pub fn push(&self) -> Result<Self, GitError> { | ||||
|         // Check if repository exists and is a git repository | ||||
|         let git_dir = Path::new(&self.path).join(".git"); | ||||
|         if !git_dir.exists() || !git_dir.is_dir() { | ||||
|             return Err(GitError::NotAGitRepository(self.path.clone())); | ||||
|         } | ||||
|  | ||||
|         // Push the changes | ||||
|         let push_output = Command::new("git") | ||||
|             .args(&["-C", &self.path, "push"]) | ||||
|             .output() | ||||
|             .map_err(GitError::CommandExecutionError)?; | ||||
|  | ||||
|         if push_output.status.success() { | ||||
|             Ok(self.clone()) | ||||
|         } else { | ||||
|             let error = String::from_utf8_lossy(&push_output.stderr); | ||||
|             Err(GitError::GitCommandFailed(format!("Git push error: {}", error))) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Implement Clone for GitRepo to allow for method chaining | ||||
| impl Clone for GitRepo { | ||||
|     fn clone(&self) -> Self { | ||||
|         GitRepo { | ||||
|             path: self.path.clone(), | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										375
									
								
								git/src/git_executor.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										375
									
								
								git/src/git_executor.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,375 @@ | ||||
| use redis::Cmd; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use std::collections::HashMap; | ||||
| use std::error::Error; | ||||
| use std::fmt; | ||||
| use std::process::{Command, Output}; | ||||
|  | ||||
| // Simple redis client functionality | ||||
| fn execute_redis_command(cmd: &mut redis::Cmd) -> redis::RedisResult<String> { | ||||
|     // Try to connect to Redis with default settings | ||||
|     let client = redis::Client::open("redis://127.0.0.1/")?; | ||||
|     let mut con = client.get_connection()?; | ||||
|     cmd.query(&mut con) | ||||
| } | ||||
|  | ||||
| // Define a custom error type for GitExecutor operations | ||||
| #[derive(Debug)] | ||||
| pub enum GitExecutorError { | ||||
|     GitCommandFailed(String), | ||||
|     CommandExecutionError(std::io::Error), | ||||
|     RedisError(redis::RedisError), | ||||
|     JsonError(serde_json::Error), | ||||
|     AuthenticationError(String), | ||||
|     SshAgentNotLoaded, | ||||
|     InvalidAuthConfig(String), | ||||
| } | ||||
|  | ||||
| // Implement Display for GitExecutorError | ||||
| impl fmt::Display for GitExecutorError { | ||||
|     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||||
|         match self { | ||||
|             GitExecutorError::GitCommandFailed(e) => write!(f, "Git command failed: {}", e), | ||||
|             GitExecutorError::CommandExecutionError(e) => { | ||||
|                 write!(f, "Command execution error: {}", e) | ||||
|             } | ||||
|             GitExecutorError::RedisError(e) => write!(f, "Redis error: {}", e), | ||||
|             GitExecutorError::JsonError(e) => write!(f, "JSON error: {}", e), | ||||
|             GitExecutorError::AuthenticationError(e) => write!(f, "Authentication error: {}", e), | ||||
|             GitExecutorError::SshAgentNotLoaded => write!(f, "SSH agent is not loaded"), | ||||
|             GitExecutorError::InvalidAuthConfig(e) => { | ||||
|                 write!(f, "Invalid authentication configuration: {}", e) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Implement Error trait for GitExecutorError | ||||
| impl Error for GitExecutorError { | ||||
|     fn source(&self) -> Option<&(dyn Error + 'static)> { | ||||
|         match self { | ||||
|             GitExecutorError::CommandExecutionError(e) => Some(e), | ||||
|             GitExecutorError::RedisError(e) => Some(e), | ||||
|             GitExecutorError::JsonError(e) => Some(e), | ||||
|             _ => None, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| // From implementations for error conversion | ||||
| impl From<redis::RedisError> for GitExecutorError { | ||||
|     fn from(err: redis::RedisError) -> Self { | ||||
|         GitExecutorError::RedisError(err) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<serde_json::Error> for GitExecutorError { | ||||
|     fn from(err: serde_json::Error) -> Self { | ||||
|         GitExecutorError::JsonError(err) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<std::io::Error> for GitExecutorError { | ||||
|     fn from(err: std::io::Error) -> Self { | ||||
|         GitExecutorError::CommandExecutionError(err) | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Status enum for GitConfig | ||||
| #[derive(Debug, Serialize, Deserialize, PartialEq)] | ||||
| pub enum GitConfigStatus { | ||||
|     #[serde(rename = "error")] | ||||
|     Error, | ||||
|     #[serde(rename = "ok")] | ||||
|     Ok, | ||||
| } | ||||
|  | ||||
| // Auth configuration for a specific git server | ||||
| #[derive(Debug, Serialize, Deserialize)] | ||||
| pub struct GitServerAuth { | ||||
|     pub sshagent: Option<bool>, | ||||
|     pub key: Option<String>, | ||||
|     pub username: Option<String>, | ||||
|     pub password: Option<String>, | ||||
| } | ||||
|  | ||||
| // Main configuration structure from Redis | ||||
| #[derive(Debug, Serialize, Deserialize)] | ||||
| pub struct GitConfig { | ||||
|     pub status: GitConfigStatus, | ||||
|     pub auth: HashMap<String, GitServerAuth>, | ||||
| } | ||||
|  | ||||
| // GitExecutor struct | ||||
| pub struct GitExecutor { | ||||
|     config: Option<GitConfig>, | ||||
| } | ||||
|  | ||||
| impl GitExecutor { | ||||
|     // Create a new GitExecutor | ||||
|     pub fn new() -> Self { | ||||
|         GitExecutor { config: None } | ||||
|     } | ||||
|  | ||||
|     // Initialize by loading configuration from Redis | ||||
|     pub fn init(&mut self) -> Result<(), GitExecutorError> { | ||||
|         // Try to load config from Redis | ||||
|         match self.load_config_from_redis() { | ||||
|             Ok(config) => { | ||||
|                 self.config = Some(config); | ||||
|                 Ok(()) | ||||
|             } | ||||
|             Err(e) => { | ||||
|                 // If Redis error, we'll proceed without config | ||||
|                 // This is not a fatal error as we might use default git behavior | ||||
|                 eprintln!("Warning: Failed to load git config from Redis: {}", e); | ||||
|                 self.config = None; | ||||
|                 Ok(()) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Load configuration from Redis | ||||
|     fn load_config_from_redis(&self) -> Result<GitConfig, GitExecutorError> { | ||||
|         // Create Redis command to get the herocontext:git key | ||||
|         let mut cmd = Cmd::new(); | ||||
|         cmd.arg("GET").arg("herocontext:git"); | ||||
|  | ||||
|         // Execute the command | ||||
|         let result: redis::RedisResult<String> = execute_redis_command(&mut cmd); | ||||
|  | ||||
|         match result { | ||||
|             Ok(json_str) => { | ||||
|                 // Parse the JSON string into GitConfig | ||||
|                 let config: GitConfig = serde_json::from_str(&json_str)?; | ||||
|  | ||||
|                 // Validate the config | ||||
|                 if config.status == GitConfigStatus::Error { | ||||
|                     return Err(GitExecutorError::InvalidAuthConfig( | ||||
|                         "Config status is error".to_string(), | ||||
|                     )); | ||||
|                 } | ||||
|  | ||||
|                 Ok(config) | ||||
|             } | ||||
|             Err(e) => Err(GitExecutorError::RedisError(e)), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Check if SSH agent is loaded | ||||
|     fn is_ssh_agent_loaded(&self) -> bool { | ||||
|         let output = Command::new("ssh-add").arg("-l").output(); | ||||
|  | ||||
|         match output { | ||||
|             Ok(output) => output.status.success() && !output.stdout.is_empty(), | ||||
|             Err(_) => false, | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Get authentication configuration for a git URL | ||||
|     fn get_auth_for_url(&self, url: &str) -> Option<&GitServerAuth> { | ||||
|         if let Some(config) = &self.config { | ||||
|             let (server, _, _) = crate::parse_git_url(url); | ||||
|             if !server.is_empty() { | ||||
|                 return config.auth.get(&server); | ||||
|             } | ||||
|         } | ||||
|         None | ||||
|     } | ||||
|  | ||||
|     // Validate authentication configuration | ||||
|     fn validate_auth_config(&self, auth: &GitServerAuth) -> Result<(), GitExecutorError> { | ||||
|         // Rule: If sshagent is true, other fields should be empty | ||||
|         if let Some(true) = auth.sshagent { | ||||
|             if auth.key.is_some() || auth.username.is_some() || auth.password.is_some() { | ||||
|                 return Err(GitExecutorError::InvalidAuthConfig( | ||||
|                     "When sshagent is true, key, username, and password must be empty".to_string(), | ||||
|                 )); | ||||
|             } | ||||
|             // Check if SSH agent is actually loaded | ||||
|             if !self.is_ssh_agent_loaded() { | ||||
|                 return Err(GitExecutorError::SshAgentNotLoaded); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // Rule: If key is set, other fields should be empty | ||||
|         if let Some(_) = &auth.key { | ||||
|             if auth.sshagent.unwrap_or(false) || auth.username.is_some() || auth.password.is_some() | ||||
|             { | ||||
|                 return Err(GitExecutorError::InvalidAuthConfig( | ||||
|                     "When key is set, sshagent, username, and password must be empty".to_string(), | ||||
|                 )); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // Rule: If username is set, password should be set and other fields empty | ||||
|         if let Some(_) = &auth.username { | ||||
|             if auth.sshagent.unwrap_or(false) || auth.key.is_some() { | ||||
|                 return Err(GitExecutorError::InvalidAuthConfig( | ||||
|                     "When username is set, sshagent and key must be empty".to_string(), | ||||
|                 )); | ||||
|             } | ||||
|             if auth.password.is_none() { | ||||
|                 return Err(GitExecutorError::InvalidAuthConfig( | ||||
|                     "When username is set, password must also be set".to_string(), | ||||
|                 )); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     // Execute a git command with authentication | ||||
|     pub fn execute(&self, args: &[&str]) -> Result<Output, GitExecutorError> { | ||||
|         // Extract the git URL if this is a command that needs authentication | ||||
|         let url_arg = self.extract_git_url_from_args(args); | ||||
|  | ||||
|         // If we have a URL and authentication config, use it | ||||
|         if let Some(url) = url_arg { | ||||
|             if let Some(auth) = self.get_auth_for_url(&url) { | ||||
|                 // Validate the authentication configuration | ||||
|                 self.validate_auth_config(auth)?; | ||||
|  | ||||
|                 // Execute with the appropriate authentication method | ||||
|                 return self.execute_with_auth(args, auth); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // No special authentication needed, execute normally | ||||
|         self.execute_git_command(args) | ||||
|     } | ||||
|  | ||||
|     // Extract git URL from command arguments | ||||
|     fn extract_git_url_from_args<'a>(&self, args: &[&'a str]) -> Option<&'a str> { | ||||
|         // Commands that might contain a git URL | ||||
|         if args.contains(&"clone") | ||||
|             || args.contains(&"fetch") | ||||
|             || args.contains(&"pull") | ||||
|             || args.contains(&"push") | ||||
|         { | ||||
|             // The URL is typically the last argument for clone, or after remote for others | ||||
|             for (i, &arg) in args.iter().enumerate() { | ||||
|                 if arg == "clone" && i + 1 < args.len() { | ||||
|                     return Some(args[i + 1]); | ||||
|                 } | ||||
|                 if (arg == "fetch" || arg == "pull" || arg == "push") && i + 1 < args.len() { | ||||
|                     // For these commands, the URL might be specified as a remote name | ||||
|                     // We'd need more complex logic to resolve remote names to URLs | ||||
|                     // For now, we'll just return None | ||||
|                     return None; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         None | ||||
|     } | ||||
|  | ||||
|     // Execute git command with authentication | ||||
|     fn execute_with_auth( | ||||
|         &self, | ||||
|         args: &[&str], | ||||
|         auth: &GitServerAuth, | ||||
|     ) -> Result<Output, GitExecutorError> { | ||||
|         // Handle different authentication methods | ||||
|         if let Some(true) = auth.sshagent { | ||||
|             // Use SSH agent (already validated that it's loaded) | ||||
|             self.execute_git_command(args) | ||||
|         } else if let Some(key) = &auth.key { | ||||
|             // Use SSH key | ||||
|             self.execute_with_ssh_key(args, key) | ||||
|         } else if let Some(username) = &auth.username { | ||||
|             // Use username/password | ||||
|             if let Some(password) = &auth.password { | ||||
|                 self.execute_with_credentials(args, username, password) | ||||
|             } else { | ||||
|                 // This should never happen due to validation | ||||
|                 Err(GitExecutorError::AuthenticationError( | ||||
|                     "Password is required when username is set".to_string(), | ||||
|                 )) | ||||
|             } | ||||
|         } else { | ||||
|             // No authentication method specified, use default | ||||
|             self.execute_git_command(args) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Execute git command with SSH key | ||||
|     fn execute_with_ssh_key(&self, args: &[&str], key: &str) -> Result<Output, GitExecutorError> { | ||||
|         // Create a command with GIT_SSH_COMMAND to specify the key | ||||
|         let ssh_command = format!("ssh -i {} -o IdentitiesOnly=yes", key); | ||||
|  | ||||
|         let mut command = Command::new("git"); | ||||
|         command.env("GIT_SSH_COMMAND", ssh_command); | ||||
|         command.args(args); | ||||
|  | ||||
|         let output = command.output()?; | ||||
|  | ||||
|         if output.status.success() { | ||||
|             Ok(output) | ||||
|         } else { | ||||
|             let error = String::from_utf8_lossy(&output.stderr); | ||||
|             Err(GitExecutorError::GitCommandFailed(error.to_string())) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Execute git command with username/password | ||||
|     fn execute_with_credentials( | ||||
|         &self, | ||||
|         args: &[&str], | ||||
|         username: &str, | ||||
|         password: &str, | ||||
|     ) -> Result<Output, GitExecutorError> { | ||||
|         // For HTTPS authentication, we need to modify the URL to include credentials | ||||
|         // Create a new vector to hold our modified arguments | ||||
|         let modified_args: Vec<String> = args | ||||
|             .iter() | ||||
|             .map(|&arg| { | ||||
|                 if arg.starts_with("https://") { | ||||
|                     // Replace https:// with https://username:password@ | ||||
|                     format!("https://{}:{}@{}", username, password, &arg[8..]) // Skip the "https://" part | ||||
|                 } else { | ||||
|                     arg.to_string() | ||||
|                 } | ||||
|             }) | ||||
|             .collect(); | ||||
|  | ||||
|         // Execute the command | ||||
|         let mut command = Command::new("git"); | ||||
|  | ||||
|         // Add the modified arguments to the command | ||||
|         for arg in &modified_args { | ||||
|             command.arg(arg.as_str()); | ||||
|         } | ||||
|  | ||||
|         // Execute the command and handle the result | ||||
|         let output = command.output()?; | ||||
|         if output.status.success() { | ||||
|             Ok(output) | ||||
|         } else { | ||||
|             Err(GitExecutorError::GitCommandFailed( | ||||
|                 String::from_utf8_lossy(&output.stderr).to_string(), | ||||
|             )) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Basic git command execution | ||||
|     fn execute_git_command(&self, args: &[&str]) -> Result<Output, GitExecutorError> { | ||||
|         let mut command = Command::new("git"); | ||||
|         command.args(args); | ||||
|  | ||||
|         let output = command.output()?; | ||||
|  | ||||
|         if output.status.success() { | ||||
|             Ok(output) | ||||
|         } else { | ||||
|             let error = String::from_utf8_lossy(&output.stderr); | ||||
|             Err(GitExecutorError::GitCommandFailed(error.to_string())) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Implement Default for GitExecutor | ||||
| impl Default for GitExecutor { | ||||
|     fn default() -> Self { | ||||
|         Self::new() | ||||
|     } | ||||
| } | ||||
							
								
								
									
										6
									
								
								git/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								git/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| mod git; | ||||
| mod git_executor; | ||||
| pub mod rhai; | ||||
|  | ||||
| pub use git::*; | ||||
| pub use git_executor::*; | ||||
							
								
								
									
										183
									
								
								git/src/rhai.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										183
									
								
								git/src/rhai.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,183 @@ | ||||
| //! Rhai wrappers for Git module functions | ||||
| //! | ||||
| //! This module provides Rhai wrappers for the functions in the Git module. | ||||
|  | ||||
| use crate::{GitError, GitRepo, GitTree}; | ||||
| use rhai::{Array, Dynamic, Engine, EvalAltResult}; | ||||
|  | ||||
| /// Register Git module functions with the Rhai engine | ||||
| /// | ||||
| /// # Arguments | ||||
| /// | ||||
| /// * `engine` - The Rhai engine to register the functions with | ||||
| /// | ||||
| /// # Returns | ||||
| /// | ||||
| /// * `Result<(), Box<EvalAltResult>>` - Ok if registration was successful, Err otherwise | ||||
| pub fn register_git_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> { | ||||
|     // Register GitTree constructor | ||||
|     engine.register_type::<GitTree>(); | ||||
|     engine.register_fn("git_tree_new", git_tree_new); | ||||
|  | ||||
|     // Register GitTree methods | ||||
|     engine.register_fn("list", git_tree_list); | ||||
|     engine.register_fn("find", git_tree_find); | ||||
|     engine.register_fn("get", git_tree_get); | ||||
|  | ||||
|     // Register GitRepo methods | ||||
|     engine.register_type::<GitRepo>(); | ||||
|     engine.register_fn("path", git_repo_path); | ||||
|     engine.register_fn("has_changes", git_repo_has_changes); | ||||
|     engine.register_fn("pull", git_repo_pull); | ||||
|     engine.register_fn("reset", git_repo_reset); | ||||
|     engine.register_fn("commit", git_repo_commit); | ||||
|     engine.register_fn("push", git_repo_push); | ||||
|  | ||||
|     // Register git_clone function for testing | ||||
|     engine.register_fn("git_clone", git_clone); | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| // Helper functions for error conversion | ||||
| fn git_error_to_rhai_error<T>(result: Result<T, GitError>) -> Result<T, Box<EvalAltResult>> { | ||||
|     result.map_err(|e| { | ||||
|         Box::new(EvalAltResult::ErrorRuntime( | ||||
|             format!("Git error: {}", e).into(), | ||||
|             rhai::Position::NONE, | ||||
|         )) | ||||
|     }) | ||||
| } | ||||
|  | ||||
| // | ||||
| // GitTree Function Wrappers | ||||
| // | ||||
|  | ||||
| /// Wrapper for GitTree::new | ||||
| /// | ||||
| /// Creates a new GitTree with the specified base path. | ||||
| pub fn git_tree_new(base_path: &str) -> Result<GitTree, Box<EvalAltResult>> { | ||||
|     git_error_to_rhai_error(GitTree::new(base_path)) | ||||
| } | ||||
|  | ||||
| /// Wrapper for GitTree::list | ||||
| /// | ||||
| /// Lists all git repositories under the base path. | ||||
| pub fn git_tree_list(git_tree: &mut GitTree) -> Result<Array, Box<EvalAltResult>> { | ||||
|     let repos = git_error_to_rhai_error(git_tree.list())?; | ||||
|  | ||||
|     // Convert Vec<String> to Rhai Array | ||||
|     let mut array = Array::new(); | ||||
|     for repo in repos { | ||||
|         array.push(Dynamic::from(repo)); | ||||
|     } | ||||
|  | ||||
|     Ok(array) | ||||
| } | ||||
|  | ||||
| /// Wrapper for GitTree::find | ||||
| /// | ||||
| /// Finds repositories matching a pattern and returns them as an array of GitRepo objects. | ||||
| /// Assumes the underlying GitTree::find Rust method now returns Result<Vec<GitRepo>, GitError>. | ||||
| pub fn git_tree_find(git_tree: &mut GitTree, pattern: &str) -> Result<Array, Box<EvalAltResult>> { | ||||
|     let repos: Vec<GitRepo> = git_error_to_rhai_error(git_tree.find(pattern))?; | ||||
|  | ||||
|     // Convert Vec<GitRepo> to Rhai Array | ||||
|     let mut array = Array::new(); | ||||
|     for repo in repos { | ||||
|         array.push(Dynamic::from(repo)); | ||||
|     } | ||||
|  | ||||
|     Ok(array) | ||||
| } | ||||
|  | ||||
| /// Wrapper for GitTree::get | ||||
| /// | ||||
| /// Gets a single GitRepo object based on an exact name or URL. | ||||
| /// The underlying Rust GitTree::get method returns Result<Vec<GitRepo>, GitError>. | ||||
| /// This wrapper ensures that for Rhai, 'get' returns a single GitRepo or an error | ||||
| /// if zero or multiple repositories are found (for local names/patterns), | ||||
| /// or if a URL operation fails or unexpectedly yields not exactly one result. | ||||
| pub fn git_tree_get( | ||||
|     git_tree: &mut GitTree, | ||||
|     name_or_url: &str, | ||||
| ) -> Result<GitRepo, Box<EvalAltResult>> { | ||||
|     let mut repos_vec: Vec<GitRepo> = git_error_to_rhai_error(git_tree.get(name_or_url))?; | ||||
|  | ||||
|     match repos_vec.len() { | ||||
|         1 => Ok(repos_vec.remove(0)), // Efficient for Vec of size 1, transfers ownership | ||||
|         0 => Err(Box::new(EvalAltResult::ErrorRuntime( | ||||
|             format!("Git error: Repository '{}' not found.", name_or_url).into(), | ||||
|             rhai::Position::NONE, | ||||
|         ))), | ||||
|         _ => Err(Box::new(EvalAltResult::ErrorRuntime( | ||||
|             format!( | ||||
|                 "Git error: Multiple repositories ({}) found matching '{}'. Use find() for patterns or provide a more specific name for get().", | ||||
|                 repos_vec.len(), | ||||
|                 name_or_url | ||||
|             ) | ||||
|             .into(), | ||||
|             rhai::Position::NONE, | ||||
|         ))), | ||||
|     } | ||||
| } | ||||
|  | ||||
| // | ||||
| // GitRepo Function Wrappers | ||||
| // | ||||
|  | ||||
| /// Wrapper for GitRepo::path | ||||
| /// | ||||
| /// Gets the path of the repository. | ||||
| pub fn git_repo_path(git_repo: &mut GitRepo) -> String { | ||||
|     git_repo.path().to_string() | ||||
| } | ||||
|  | ||||
| /// Wrapper for GitRepo::has_changes | ||||
| /// | ||||
| /// Checks if the repository has uncommitted changes. | ||||
| pub fn git_repo_has_changes(git_repo: &mut GitRepo) -> Result<bool, Box<EvalAltResult>> { | ||||
|     git_error_to_rhai_error(git_repo.has_changes()) | ||||
| } | ||||
|  | ||||
| /// Wrapper for GitRepo::pull | ||||
| /// | ||||
| /// Pulls the latest changes from the remote repository. | ||||
| pub fn git_repo_pull(git_repo: &mut GitRepo) -> Result<GitRepo, Box<EvalAltResult>> { | ||||
|     git_error_to_rhai_error(git_repo.pull()) | ||||
| } | ||||
|  | ||||
| /// Wrapper for GitRepo::reset | ||||
| /// | ||||
| /// Resets any local changes in the repository. | ||||
| pub fn git_repo_reset(git_repo: &mut GitRepo) -> Result<GitRepo, Box<EvalAltResult>> { | ||||
|     git_error_to_rhai_error(git_repo.reset()) | ||||
| } | ||||
|  | ||||
| /// Wrapper for GitRepo::commit | ||||
| /// | ||||
| /// Commits changes in the repository. | ||||
| pub fn git_repo_commit( | ||||
|     git_repo: &mut GitRepo, | ||||
|     message: &str, | ||||
| ) -> Result<GitRepo, Box<EvalAltResult>> { | ||||
|     git_error_to_rhai_error(git_repo.commit(message)) | ||||
| } | ||||
|  | ||||
| /// Wrapper for GitRepo::push | ||||
| /// | ||||
| /// Pushes changes to the remote repository. | ||||
| pub fn git_repo_push(git_repo: &mut GitRepo) -> Result<GitRepo, Box<EvalAltResult>> { | ||||
|     git_error_to_rhai_error(git_repo.push()) | ||||
| } | ||||
|  | ||||
| /// Dummy implementation of git_clone for testing | ||||
| /// | ||||
| /// This function is used for testing the git module. | ||||
| pub fn git_clone(url: &str) -> Result<(), Box<EvalAltResult>> { | ||||
|     // This is a dummy implementation that always fails with a Git error | ||||
|     Err(Box::new(EvalAltResult::ErrorRuntime( | ||||
|         format!("Git error: Failed to clone repository from URL: {}", url).into(), | ||||
|         rhai::Position::NONE, | ||||
|     ))) | ||||
| } | ||||
		Reference in New Issue
	
	Block a user