use std::process::{Command, Output}; use std::error::Error; use std::fmt; use std::collections::HashMap; use redis::Cmd; use serde::{Deserialize, Serialize}; use crate::env; use crate::git::git::parse_git_url; // 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 for GitExecutorError { fn from(err: redis::RedisError) -> Self { GitExecutorError::RedisError(err) } } impl From for GitExecutorError { fn from(err: serde_json::Error) -> Self { GitExecutorError::JsonError(err) } } impl From 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, pub key: Option, pub username: Option, pub password: Option, } // Main configuration structure from Redis #[derive(Debug, Serialize, Deserialize)] pub struct GitConfig { pub status: GitConfigStatus, pub auth: HashMap, } // GitExecutor struct pub struct GitExecutor { config: Option, } 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 { // 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 = env::execute(&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, _, _) = 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 { // 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 { // 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 { // 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 { // Helper method to execute a command and handle the result fn execute_command(command: &mut Command) -> Result { 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())) } } // 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 = 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 { 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() } }