feat: Convert SAL to a Rust monorepo
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:
Mahmoud-Emad
2025-06-18 14:12:36 +03:00
parent ba9103685f
commit e031b03e04
20 changed files with 790 additions and 194 deletions

484
git/src/git.rs Normal file
View 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
View 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
View 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
View 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,
)))
}