feat: Migrate SAL to Cargo workspace
Some checks failed
Rhai Tests / Run Rhai Tests (push) Has been cancelled
Rhai Tests / Run Rhai Tests (pull_request) Has been cancelled

- Migrate individual modules to independent crates
- Refactor dependencies for improved modularity
- Update build system and testing infrastructure
- Update documentation to reflect new structure
This commit is contained in:
Mahmoud-Emad
2025-06-24 12:39:18 +03:00
parent 8012a66250
commit e125bb6511
54 changed files with 1196 additions and 1582 deletions

View File

@@ -8,13 +8,14 @@ repository = "https://git.threefold.info/herocode/sal"
license = "Apache-2.0"
[dependencies]
regex = "1.8.1"
redis = "0.31.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
rhai = { version = "1.12.0", features = ["sync"] }
log = "0.4"
url = "2.4"
# Use workspace dependencies for consistency
regex = { workspace = true }
redis = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
rhai = { workspace = true }
log = { workspace = true }
url = { workspace = true }
[dev-dependencies]
tempfile = "3.5"
tempfile = { workspace = true }

View File

@@ -1,9 +1,9 @@
use std::process::Command;
use std::path::Path;
use std::fs;
use regex::Regex;
use std::fmt;
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)]
@@ -35,7 +35,7 @@ impl fmt::Display for GitError {
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) =>
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),
@@ -57,48 +57,48 @@ impl Error for GitError {
}
/// 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
///
/// * `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> {
@@ -117,55 +117,53 @@ pub struct GitTree {
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)
})?;
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() {
@@ -178,22 +176,25 @@ impl GitTree {
}
} else {
let error = String::from_utf8_lossy(&output.stderr);
return Err(GitError::GitCommandFailed(format!("Failed to find git repositories: {}", error)));
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
@@ -212,7 +213,7 @@ impl GitTree {
matched_repos.push(GitRepo::new(full_path));
}
} else if pattern.ends_with('*') {
let prefix = &pattern[0..pattern.len()-1];
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);
@@ -233,17 +234,17 @@ impl GitTree {
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> {
@@ -254,32 +255,35 @@ impl GitTree {
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)))
Err(GitError::GitCommandFailed(format!(
"Git clone error: {}",
error
)))
}
} else {
// It's a path pattern, find matching repositories using the updated self.find()
@@ -357,7 +361,10 @@ impl GitRepo {
Ok(self.clone())
} else {
let error = String::from_utf8_lossy(&output.stderr);
Err(GitError::GitCommandFailed(format!("Git pull error: {}", error)))
Err(GitError::GitCommandFailed(format!(
"Git pull error: {}",
error
)))
}
}
@@ -382,7 +389,10 @@ impl GitRepo {
if !reset_output.status.success() {
let error = String::from_utf8_lossy(&reset_output.stderr);
return Err(GitError::GitCommandFailed(format!("Git reset error: {}", error)));
return Err(GitError::GitCommandFailed(format!(
"Git reset error: {}",
error
)));
}
// Clean untracked files
@@ -393,7 +403,10 @@ impl GitRepo {
if !clean_output.status.success() {
let error = String::from_utf8_lossy(&clean_output.stderr);
return Err(GitError::GitCommandFailed(format!("Git clean error: {}", error)));
return Err(GitError::GitCommandFailed(format!(
"Git clean error: {}",
error
)));
}
Ok(self.clone())
@@ -429,7 +442,10 @@ impl GitRepo {
if !add_output.status.success() {
let error = String::from_utf8_lossy(&add_output.stderr);
return Err(GitError::GitCommandFailed(format!("Git add error: {}", error)));
return Err(GitError::GitCommandFailed(format!(
"Git add error: {}",
error
)));
}
// Commit the changes
@@ -440,7 +456,10 @@ impl GitRepo {
if !commit_output.status.success() {
let error = String::from_utf8_lossy(&commit_output.stderr);
return Err(GitError::GitCommandFailed(format!("Git commit error: {}", error)));
return Err(GitError::GitCommandFailed(format!(
"Git commit error: {}",
error
)));
}
Ok(self.clone())
@@ -469,7 +488,10 @@ impl GitRepo {
Ok(self.clone())
} else {
let error = String::from_utf8_lossy(&push_output.stderr);
Err(GitError::GitCommandFailed(format!("Git push error: {}", error)))
Err(GitError::GitCommandFailed(format!(
"Git push error: {}",
error
)))
}
}
}

View File

@@ -1,19 +1,26 @@
use sal_git::rhai::*;
use rhai::Engine;
use sal_git::rhai::*;
#[test]
fn test_git_clone_with_various_url_formats() {
let mut engine = Engine::new();
register_git_module(&mut engine).unwrap();
let test_cases = vec![
("https://github.com/octocat/Hello-World.git", "HTTPS with .git"),
("https://github.com/octocat/Hello-World", "HTTPS without .git"),
(
"https://github.com/octocat/Hello-World.git",
"HTTPS with .git",
),
(
"https://github.com/octocat/Hello-World",
"HTTPS without .git",
),
// SSH would require key setup: ("git@github.com:octocat/Hello-World.git", "SSH format"),
];
for (url, description) in test_cases {
let script = format!(r#"
let script = format!(
r#"
let result = "";
try {{
let repo = git_clone("{}");
@@ -31,11 +38,18 @@ fn test_git_clone_with_various_url_formats() {
}}
}}
result
"#, url);
"#,
url
);
let result = engine.eval::<String>(&script);
assert!(result.is_ok(), "Failed to execute script for {}: {:?}", description, result);
assert!(
result.is_ok(),
"Failed to execute script for {}: {:?}",
description,
result
);
let outcome = result.unwrap();
// Accept success or git_error (network issues)
assert!(
@@ -51,7 +65,7 @@ fn test_git_clone_with_various_url_formats() {
fn test_git_tree_operations_comprehensive() {
let mut engine = Engine::new();
register_git_module(&mut engine).unwrap();
let script = r#"
let results = [];
@@ -74,7 +88,7 @@ fn test_git_tree_operations_comprehensive() {
results.len()
"#;
let result = engine.eval::<i64>(&script);
assert!(result.is_ok());
assert!(result.unwrap() >= 3, "Should execute at least 3 operations");
@@ -84,7 +98,7 @@ fn test_git_tree_operations_comprehensive() {
fn test_error_message_quality() {
let mut engine = Engine::new();
register_git_module(&mut engine).unwrap();
let script = r#"
let error_msg = "";
try {
@@ -94,11 +108,14 @@ fn test_error_message_quality() {
}
error_msg
"#;
let result = engine.eval::<String>(&script);
assert!(result.is_ok());
let error_msg = result.unwrap();
assert!(error_msg.contains("Git error"), "Error should contain 'Git error'");
assert!(
error_msg.contains("Git error"),
"Error should contain 'Git error'"
);
assert!(error_msg.len() > 10, "Error message should be descriptive");
}