...
This commit is contained in:
		
							
								
								
									
										506
									
								
								packages/system/git/src/git.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										506
									
								
								packages/system/git/src/git.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,506 @@
 | 
			
		||||
use regex::Regex;
 | 
			
		||||
use std::error::Error;
 | 
			
		||||
use std::fmt;
 | 
			
		||||
use std::fs;
 | 
			
		||||
use std::path::Path;
 | 
			
		||||
use std::process::Command;
 | 
			
		||||
 | 
			
		||||
// 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(),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										420
									
								
								packages/system/git/src/git_executor.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										420
									
								
								packages/system/git/src/git_executor.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,420 @@
 | 
			
		||||
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 with configurable connection
 | 
			
		||||
fn execute_redis_command(cmd: &mut redis::Cmd) -> redis::RedisResult<String> {
 | 
			
		||||
    // Get Redis URL from environment variables with fallback
 | 
			
		||||
    let redis_url = get_redis_url();
 | 
			
		||||
    log::debug!("Connecting to Redis at: {}", mask_redis_url(&redis_url));
 | 
			
		||||
 | 
			
		||||
    let client = redis::Client::open(redis_url)?;
 | 
			
		||||
    let mut con = client.get_connection()?;
 | 
			
		||||
    cmd.query(&mut con)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Get Redis URL from environment variables with secure fallbacks
 | 
			
		||||
fn get_redis_url() -> String {
 | 
			
		||||
    std::env::var("REDIS_URL")
 | 
			
		||||
        .or_else(|_| std::env::var("SAL_REDIS_URL"))
 | 
			
		||||
        .unwrap_or_else(|_| "redis://127.0.0.1/".to_string())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Mask sensitive information in Redis URL for logging
 | 
			
		||||
fn mask_redis_url(url: &str) -> String {
 | 
			
		||||
    if let Ok(parsed) = url::Url::parse(url) {
 | 
			
		||||
        if parsed.password().is_some() {
 | 
			
		||||
            format!(
 | 
			
		||||
                "{}://{}:***@{}:{}/{}",
 | 
			
		||||
                parsed.scheme(),
 | 
			
		||||
                parsed.username(),
 | 
			
		||||
                parsed.host_str().unwrap_or("unknown"),
 | 
			
		||||
                parsed.port().unwrap_or(6379),
 | 
			
		||||
                parsed.path().trim_start_matches('/')
 | 
			
		||||
            )
 | 
			
		||||
        } else {
 | 
			
		||||
            url.to_string()
 | 
			
		||||
        }
 | 
			
		||||
    } else {
 | 
			
		||||
        "redis://***masked***".to_string()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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
 | 
			
		||||
                log::warn!("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 using secure credential helper
 | 
			
		||||
    fn execute_with_credentials(
 | 
			
		||||
        &self,
 | 
			
		||||
        args: &[&str],
 | 
			
		||||
        username: &str,
 | 
			
		||||
        password: &str,
 | 
			
		||||
    ) -> Result<Output, GitExecutorError> {
 | 
			
		||||
        // Use git credential helper approach for security
 | 
			
		||||
        // Create a temporary credential helper script
 | 
			
		||||
        let temp_dir = std::env::temp_dir();
 | 
			
		||||
        let helper_script = temp_dir.join(format!("git_helper_{}", std::process::id()));
 | 
			
		||||
 | 
			
		||||
        // Create credential helper script content
 | 
			
		||||
        let script_content = format!(
 | 
			
		||||
            "#!/bin/bash\necho username={}\necho password={}\n",
 | 
			
		||||
            username, password
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Write the helper script
 | 
			
		||||
        std::fs::write(&helper_script, script_content)
 | 
			
		||||
            .map_err(|e| GitExecutorError::CommandExecutionError(e))?;
 | 
			
		||||
 | 
			
		||||
        // Make it executable
 | 
			
		||||
        #[cfg(unix)]
 | 
			
		||||
        {
 | 
			
		||||
            use std::os::unix::fs::PermissionsExt;
 | 
			
		||||
            let mut perms = std::fs::metadata(&helper_script)
 | 
			
		||||
                .map_err(|e| GitExecutorError::CommandExecutionError(e))?
 | 
			
		||||
                .permissions();
 | 
			
		||||
            perms.set_mode(0o755);
 | 
			
		||||
            std::fs::set_permissions(&helper_script, perms)
 | 
			
		||||
                .map_err(|e| GitExecutorError::CommandExecutionError(e))?;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Execute git command with credential helper
 | 
			
		||||
        let mut command = Command::new("git");
 | 
			
		||||
        command.args(args);
 | 
			
		||||
        command.env("GIT_ASKPASS", &helper_script);
 | 
			
		||||
        command.env("GIT_TERMINAL_PROMPT", "0"); // Disable terminal prompts
 | 
			
		||||
 | 
			
		||||
        log::debug!("Executing git command with credential helper");
 | 
			
		||||
        let output = command.output()?;
 | 
			
		||||
 | 
			
		||||
        // Clean up the temporary helper script
 | 
			
		||||
        let _ = std::fs::remove_file(&helper_script);
 | 
			
		||||
 | 
			
		||||
        if output.status.success() {
 | 
			
		||||
            Ok(output)
 | 
			
		||||
        } else {
 | 
			
		||||
            let error = String::from_utf8_lossy(&output.stderr);
 | 
			
		||||
            log::error!("Git command failed: {}", error);
 | 
			
		||||
            Err(GitExecutorError::GitCommandFailed(error.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
									
								
								packages/system/git/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								packages/system/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::*;
 | 
			
		||||
							
								
								
									
										207
									
								
								packages/system/git/src/rhai.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										207
									
								
								packages/system/git/src/rhai.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,207 @@
 | 
			
		||||
//! 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())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Clone a git repository to a temporary location
 | 
			
		||||
///
 | 
			
		||||
/// This function clones a repository from the given URL to a temporary directory
 | 
			
		||||
/// and returns the GitRepo object for further operations.
 | 
			
		||||
///
 | 
			
		||||
/// # Arguments
 | 
			
		||||
///
 | 
			
		||||
/// * `url` - The URL of the git repository to clone
 | 
			
		||||
///
 | 
			
		||||
/// # Returns
 | 
			
		||||
///
 | 
			
		||||
/// * `Ok(GitRepo)` - The cloned repository object
 | 
			
		||||
/// * `Err(Box<EvalAltResult>)` - If the clone operation failed
 | 
			
		||||
pub fn git_clone(url: &str) -> Result<GitRepo, Box<EvalAltResult>> {
 | 
			
		||||
    // Get base path from environment or use default temp directory
 | 
			
		||||
    let base_path = std::env::var("GIT_DEFAULT_BASE_PATH").unwrap_or_else(|_| {
 | 
			
		||||
        std::env::temp_dir()
 | 
			
		||||
            .join("sal_git_clones")
 | 
			
		||||
            .to_string_lossy()
 | 
			
		||||
            .to_string()
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Create GitTree and clone the repository
 | 
			
		||||
    let git_tree = git_error_to_rhai_error(GitTree::new(&base_path))?;
 | 
			
		||||
    let repos = git_error_to_rhai_error(git_tree.get(url))?;
 | 
			
		||||
 | 
			
		||||
    // Return the first (and should be only) repository
 | 
			
		||||
    repos.into_iter().next().ok_or_else(|| {
 | 
			
		||||
        Box::new(EvalAltResult::ErrorRuntime(
 | 
			
		||||
            "Git error: No repository was cloned".into(),
 | 
			
		||||
            rhai::Position::NONE,
 | 
			
		||||
        ))
 | 
			
		||||
    })
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user