git working

This commit is contained in:
despiegk 2025-05-08 09:05:44 +03:00
parent 21893ce225
commit 104097ee4b
36 changed files with 311 additions and 221 deletions

218
docs/git/git.md Normal file
View File

@ -0,0 +1,218 @@
# Rhai Git Module Manual
## Core Concepts
The Git module in Rhai allows interaction with Git repositories through two main objects:
- **`GitTree`**: Represents a collection of Git repositories under a specified base directory. Use it to manage, find, and access these repositories.
- **`GitRepo`**: Represents a single Git repository. Use it to perform common Git operations like `pull`, `commit`, and `push`.
## Error Handling
Methods performing Git operations (e.g., `pull`, `GitTree.get` when cloning) return a `Result`. If an operation fails and the error is not handled within the Rhai script, the script execution will halt, and Rhai will report the error. The examples below show direct usage, relying on this default error-halting behavior.
## `GitTree` Object
The `GitTree` object is the entry point for working with Git repositories.
### `git_tree_new(base_path: String) -> GitTree`
Creates a `GitTree` instance.
- **Description**: Initializes a `GitTree` to operate within the `base_path`. This directory is where repositories are located or will be cloned. It's created if it doesn't exist.
- **Parameters**:
- `base_path: String` - Path to the directory for Git repositories.
- **Returns**: `GitTree` - A new `GitTree` object. Halts on error (e.g., invalid path).
- **Rhai Example**:
```rhai
let git_tree = git_tree_new("./my_projects");
print("GitTree created.");
// To access the base path from Rhai, a `base_path()` getter would need to be exposed.
// // print(`GitTree base path: ${git_tree.base_path()}`);
```
### `list() -> Array`
Lists names of all Git repositories in the `GitTree`'s `base_path`.
- **Description**: Scans `base_path` for immediate subdirectories that are Git repositories and returns their names.
- **Returns**: `Array` - An array of strings (repository names). Returns an empty array if no repositories are found. Halts on other errors.
- **Rhai Example**:
```rhai
let git_tree = git_tree_new("./my_projects");
let repo_names = git_tree.list();
print(`Found ${repo_names.len()} repositories: ${repo_names}`);
```
### `find(pattern: String) -> Array`
Finds Git repositories matching `pattern` and returns them as `GitRepo` objects.
- **Description**: Searches `base_path` for Git repository subdirectories whose names match the `pattern` (e.g., `*`, `service-*`).
- **Parameters**:
- `pattern: String` - A pattern to match repository names.
- **Returns**: `Array` - An array of `GitRepo` objects. Returns an empty array if no repositories match. Halts on other errors (e.g. invalid pattern).
- **Rhai Example**:
```rhai
let git_tree = git_tree_new("./my_projects");
let api_repos = git_tree.find("api-*");
print(`Found ${api_repos.len()} API repositories.`);
for repo in api_repos {
print(`- Path: ${repo.path()}, Has Changes: ${repo.has_changes()}`);
}
```
### `get(name_or_url: String) -> GitRepo`
Retrieves a single `GitRepo` object by its exact local name or by a remote URL.
- **Description**:
- **Local Name**: If `name_or_url` is an exact subdirectory name in `base_path` (e.g., `"myrepo"`), opens that repository.
- **Remote URL**: If `name_or_url` is a Git URL (e.g., `"https://github.com/user/repo.git"`), it clones the repository (if not present) into `base_path` or opens it if it already exists.
- **Note**: Does not support wildcards for local names. Use `find()` for pattern matching.
- **Parameters**:
- `name_or_url: String` - The exact local repository name or a full Git URL.
- **Returns**: `GitRepo` - A single `GitRepo` object.
- **Halts on error if**:
- The local `name` is not found or is ambiguous.
- The `url` is invalid, or the clone/access operation fails.
- The target is not a valid Git repository.
- **Rhai Examples**:
*Get specific local repository by name:*
```rhai
let git_tree = git_tree_new("./my_projects");
// Assumes "my_service_a" is a git repo in "./my_projects/my_service_a"
// Script halts if "my_service_a" is not found or not a git repo.
let service_a_repo = git_tree.get("my_service_a");
print(`Opened repo: ${service_a_repo.path()}`);
service_a_repo.pull(); // Example operation
```
*Clone or get repository by URL:*
```rhai
let git_tree = git_tree_new("./cloned_repos_dest");
let url = "https://github.com/rhai-script/rhai.git";
// Clones if not present, otherwise opens. Halts on error.
let rhai_repo = git_tree.get(url);
print(`Rhai repository path: ${rhai_repo.path()}`);
print(`Rhai repo has changes: ${rhai_repo.has_changes()}`);
```
## `GitRepo` Object
Represents a single Git repository. Obtained from `GitTree.get()` or `GitTree.find()`.
### `path() -> String`
Returns the full file system path of the repository.
- **Returns**: `String` - The absolute path to the repository's root directory.
- **Rhai Example**:
```rhai
let git_tree = git_tree_new("./my_projects");
// Assumes "my_app" exists and is a Git repository.
// get() will halt if "my_app" is not found.
let app_repo = git_tree.get("my_app");
print(`App repository is at: ${app_repo.path()}`);
```
### `has_changes() -> bool`
Checks if the repository has any uncommitted local changes.
- **Description**: Checks for uncommitted modifications in the working directory or staged changes.
- **Returns**: `bool` - `true` if uncommitted changes exist, `false` otherwise. Halts on error.
- **Rhai Example** (assuming `app_repo` is a `GitRepo` object):
```rhai
if app_repo.has_changes() {
print(`Repository ${app_repo.path()} has uncommitted changes.`);
} else {
print(`Repository ${app_repo.path()} is clean.`);
}
```
### `pull() -> GitRepo`
Pulls latest changes from the remote.
- **Description**: Fetches changes from the default remote and merges them into the current local branch (`git pull`).
- **Returns**: `GitRepo` - The same `GitRepo` object for chaining. Halts on error (e.g., network issues, merge conflicts).
- **Rhai Example** (assuming `app_repo` is a `GitRepo` object):
```rhai
print(`Pulling latest changes for ${app_repo.path()}...`);
app_repo.pull(); // Halts on error
print("Pull successful.");
```
### `reset() -> GitRepo`
Resets local changes. **Caution: Discards uncommitted work.**
- **Description**: Discards local modifications and staged changes, resetting the working directory to match the last commit (`git reset --hard HEAD` or similar).
- **Returns**: `GitRepo` - The same `GitRepo` object for chaining. Halts on error.
- **Rhai Example** (assuming `app_repo` is a `GitRepo` object):
```rhai
print(`Resetting local changes in ${app_repo.path()}...`);
app_repo.reset(); // Halts on error
print("Reset successful.");
```
### `commit(message: String) -> GitRepo`
Commits staged changes.
- **Description**: Performs `git commit -m "message"`. Assumes changes are staged. Behavior regarding auto-staging of tracked files depends on the underlying Rust implementation.
- **Parameters**:
- `message: String` - The commit message.
- **Returns**: `GitRepo` - The same `GitRepo` object for chaining. Halts on error (e.g., nothing to commit).
- **Rhai Example** (assuming `app_repo` is a `GitRepo` object):
```rhai
// Ensure there are changes to commit.
if app_repo.has_changes() {
print(`Committing changes in ${app_repo.path()}...`);
app_repo.commit("Automated commit via Rhai script"); // Halts on error
print("Commit successful.");
} else {
print("No changes to commit.");
}
```
### `push() -> GitRepo`
Pushes committed changes to the remote.
- **Description**: Performs `git push` to the default remote and branch.
- **Returns**: `GitRepo` - The same `GitRepo` object for chaining. Halts on error (e.g., network issues, push rejected).
- **Rhai Example** (assuming `app_repo` is a `GitRepo` object with committed changes):
```rhai
print(`Pushing changes for ${app_repo.path()}...`);
app_repo.push(); // Halts on error
print("Push successful.");
```
## Chaining Operations
`GitRepo` methods like `pull`, `reset`, `commit`, and `push` return the `GitRepo` object, allowing for chained operations. If any operation in the chain fails, script execution halts.
- **Rhai Example**:
```rhai
let git_tree = git_tree_new("./my_projects");
// Assumes "my_writable_app" exists and you have write access.
// get() will halt if not found.
let app_repo = git_tree.get("my_writable_app");
print(`Performing chained operations on ${app_repo.path()}`);
// This example demonstrates a common workflow.
// Ensure the repo state is suitable (e.g., changes exist for commit/push).
app_repo.pull()
.commit("Rhai: Chained operations - automated update") // Commits if pull results in changes or local changes existed and were staged.
.push();
print("Chained pull, commit, and push reported successful.");
// Alternative:
// app_repo.pull();
// if app_repo.has_changes() {
// app_repo.commit("Updates").push();
// }
```

View File

@ -0,0 +1,28 @@
// Simplified Git Basic Operations Example
let git_tree = git_tree_new("/tmp/git"); // Using /tmp/git as base path
print("--- Git Basic Operations ---");
// print(`Base path: ${git_tree.base_path()}`); // base_path() getter would need to be exposed from Rust
let all_repos = git_tree.list();
print(`Listed ${all_repos.len()} repos.`);
// Find repos starting with "home" (adjust pattern if /tmp/git might contain other "home*" repos)
let found_repos = git_tree.find("home*");
print(`Found ${found_repos.len()} repos matching "home*".`);
for r in found_repos {
print(` - Found: ${r.path()}`);
}
print("Getting/Cloning 'https://github.com/freeflowuniverse/home'...");
let repo = git_tree.get("https://github.com/freeflowuniverse/home");
print(`Repo path: ${repo.path()}`);
print(`Has changes: ${repo.has_changes()}`);
print("Performing pull & reset...");
repo.pull().reset();
print("Pull and reset complete.");
print(`Has changes after pull/reset: ${repo.has_changes()}`);
print("--- Example Finished ---");

View File

@ -197,40 +197,41 @@ impl GitTree {
/// * `Ok(Vec<String>)` - A vector of paths to matching repositories /// * `Ok(Vec<String>)` - A vector of paths to matching repositories
/// * `Err(GitError)` - If no matching repositories are found, /// * `Err(GitError)` - If no matching repositories are found,
/// or if multiple repositories match a non-wildcard pattern /// or if multiple repositories match a non-wildcard pattern
pub fn find(&self, pattern: &str) -> Result<Vec<String>, GitError> { pub fn find(&self, pattern: &str) -> Result<Vec<GitRepo>, GitError> {
// Get all repos let repo_names = self.list()?; // list() already ensures these are git repo names
let repos = self.list()?;
if repos.is_empty() { if repo_names.is_empty() {
return Err(GitError::NoRepositoriesFound); return Ok(Vec::new()); // If no repos listed, find results in an empty list
} }
// Check if pattern ends with wildcard let mut matched_repos: Vec<GitRepo> = Vec::new();
if pattern.ends_with('*') {
let search_pattern = &pattern[0..pattern.len()-1]; // Remove the *
let matching: Vec<String> = repos.iter()
.filter(|repo| repo.contains(search_pattern))
.cloned()
.collect();
if matching.is_empty() { if pattern == "*" {
return Err(GitError::RepositoryNotFound(pattern.to_string())); 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));
}
} }
Ok(matching)
} else { } else {
// No wildcard, need to find exactly one match // Exact match for the name
let matching: Vec<String> = repos.iter() for name in repo_names {
.filter(|repo| repo.contains(pattern)) if name == pattern {
.cloned() let full_path = format!("{}/{}", self.base_path, name);
.collect(); matched_repos.push(GitRepo::new(full_path));
// `find` returns all exact matches. If names aren't unique (unlikely from `list`),
match matching.len() { // it could return more than one. For an exact name, typically one is expected.
0 => Err(GitError::RepositoryNotFound(pattern.to_string())), }
1 => Ok(matching),
_ => Err(GitError::MultipleRepositoriesFound(pattern.to_string(), matching.len())),
} }
} }
Ok(matched_repos)
} }
/// Gets one or more GitRepo objects based on a path pattern or URL. /// Gets one or more GitRepo objects based on a path pattern or URL.
@ -281,14 +282,9 @@ impl GitTree {
Err(GitError::GitCommandFailed(format!("Git clone error: {}", error))) Err(GitError::GitCommandFailed(format!("Git clone error: {}", error)))
} }
} else { } else {
// It's a path pattern, find matching repositories // It's a path pattern, find matching repositories using the updated self.find()
let repo_paths = self.find(path_or_url)?; // which now directly returns Result<Vec<GitRepo>, GitError>.
let repos = self.find(path_or_url)?;
// Convert paths to GitRepo objects
let repos: Vec<GitRepo> = repo_paths.into_iter()
.map(GitRepo::new)
.collect();
Ok(repos) Ok(repos)
} }
} }

View File

@ -16,7 +16,8 @@ use crate::git::{GitTree, GitRepo, GitError};
/// * `Result<(), Box<EvalAltResult>>` - Ok if registration was successful, Err otherwise /// * `Result<(), Box<EvalAltResult>>` - Ok if registration was successful, Err otherwise
pub fn register_git_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> { pub fn register_git_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
// Register GitTree constructor // Register GitTree constructor
engine.register_fn("gittree_new", git_tree_new); engine.register_type::<GitTree>();
engine.register_fn("git_tree_new", git_tree_new);
// Register GitTree methods // Register GitTree methods
engine.register_fn("list", git_tree_list); engine.register_fn("list", git_tree_list);
@ -24,6 +25,7 @@ pub fn register_git_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>
engine.register_fn("get", git_tree_get); engine.register_fn("get", git_tree_get);
// Register GitRepo methods // Register GitRepo methods
engine.register_type::<GitRepo>();
engine.register_fn("path", git_repo_path); engine.register_fn("path", git_repo_path);
engine.register_fn("has_changes", git_repo_has_changes); engine.register_fn("has_changes", git_repo_has_changes);
engine.register_fn("pull", git_repo_pull); engine.register_fn("pull", git_repo_pull);
@ -72,11 +74,12 @@ pub fn git_tree_list(git_tree: &mut GitTree) -> Result<Array, Box<EvalAltResult>
/// Wrapper for GitTree::find /// Wrapper for GitTree::find
/// ///
/// Finds repositories matching a pattern or partial path. /// 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>> { pub fn git_tree_find(git_tree: &mut GitTree, pattern: &str) -> Result<Array, Box<EvalAltResult>> {
let repos = git_error_to_rhai_error(git_tree.find(pattern))?; let repos: Vec<GitRepo> = git_error_to_rhai_error(git_tree.find(pattern))?;
// Convert Vec<String> to Rhai Array // Convert Vec<GitRepo> to Rhai Array
let mut array = Array::new(); let mut array = Array::new();
for repo in repos { for repo in repos {
array.push(Dynamic::from(repo)); array.push(Dynamic::from(repo));
@ -87,17 +90,30 @@ pub fn git_tree_find(git_tree: &mut GitTree, pattern: &str) -> Result<Array, Box
/// Wrapper for GitTree::get /// Wrapper for GitTree::get
/// ///
/// Gets one or more GitRepo objects based on a path pattern or URL. /// Gets a single GitRepo object based on an exact name or URL.
pub fn git_tree_get(git_tree: &mut GitTree, path_or_url: &str) -> Result<Array, Box<EvalAltResult>> { /// The underlying Rust GitTree::get method returns Result<Vec<GitRepo>, GitError>.
let repos = git_error_to_rhai_error(git_tree.get(path_or_url))?; /// 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))?;
// Convert Vec<GitRepo> to Rhai Array match repos_vec.len() {
let mut array = Array::new(); 1 => Ok(repos_vec.remove(0)), // Efficient for Vec of size 1, transfers ownership
for repo in repos { 0 => Err(Box::new(EvalAltResult::ErrorRuntime(
array.push(Dynamic::from(repo)); 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,
))),
} }
Ok(array)
} }
// //

View File

@ -6,9 +6,7 @@ use rhai::{Engine, EvalAltResult, Array, Map, Position};
use std::collections::HashMap; use std::collections::HashMap;
use crate::text::{ use crate::text::{
TextReplacer, TextReplacerBuilder, TextReplacer, TextReplacerBuilder,
TemplateBuilder, TemplateBuilder
dedent, prefix,
name_fix, path_fix
}; };
/// Register Text module functions with the Rhai engine /// Register Text module functions with the Rhai engine

View File

@ -1,164 +0,0 @@
// Simplified test script for Git module functions
// Ensure test directory exists using a bash script
fn ensure_test_dir() {
print("Ensuring test directory exists at /tmp/code");
// Create a bash script to set up the test environment
let setup_script = `#!/bin/bash -ex
rm -rf /tmp/code
mkdir -p /tmp/code
cd /tmp/code
mkdir -p myserver.com/myaccount/repogreen
mkdir -p myserver.com/myaccount/repored
cd myserver.com/myaccount/repogreen
git init
echo 'Initial test file' > test.txt
git add test.txt
git config --local user.email 'test@example.com'
git config --local user.name 'Test User'
git commit -m 'Initial commit'
cd myserver.com/myaccount/repored
git init
echo 'Initial test file' > test2.txt
git add test2.txt
git config --local user.email 'test@example.com'
git config --local user.name 'Test User'
git commit -m 'Initial commit'
//now we have 2 repos
`;
// Run the setup script
let result = run(setup_script);
if !result.success {
print("Failed to set up test directory");
print(`Error: ${result.stderr}`);
throw "Test setup failed";
}
}
// Test GitTree creation
fn test_git_tree_creation() {
print("\n=== Testing GitTree creation ===");
let git_tree = gittree_new("/tmp/code");
print(`Created GitTree with base path: /tmp/code`);
}
// Test GitTree list method
fn test_git_tree_list() {
print("\n=== Testing GitTree list method ===");
let git_tree = gittree_new("/tmp/code");
let repos = git_tree.list();
print(`Found ${repos.len()} repositories`);
// Print repositories
for repo in repos {
print(` - ${repo}`);
}
if repos.len() == 0 {
print("No repositories found, which is unexpected");
throw "No repositories found";
}
if repos.len() != 2 {
print("No enough repositories found, needs to be 2");
throw "No enough repositories found";
}
}
// Test GitTree find method
fn test_git_tree_find() {
print("\n=== Testing GitTree find method ===");
let git_tree = gittree_new("/tmp/code");
// Search for repositories with "code" in the name
let search_pattern = "myaccount/repo"; //we need to check if we need *, would be better not
print(`Searching for repositories matching pattern: ${search_pattern}`);
let matching = git_tree.find(search_pattern);
print(`Found ${matching.len()} matching repositories`);
for repo in matching {
print(` - ${repo}`);
}
if matching.len() == 0 {
print("No matching repositories found, which is unexpected");
throw "No matching repositories found";
}
if repos.len() != 2 {
print("No enough repositories found, needs to be 2");
throw "No enough repositories found";
}
}
// Test GitRepo operations
fn test_git_repo_operations() {
print("\n=== Testing GitRepo operations ===");
let git_tree = gittree_new("/tmp/code");
let repos = git_tree.list();
if repos.len() == 0 {
print("No repositories found, which is unexpected");
throw "No repositories found";
}
// Get the first repo
let repo_path = repos[0];
print(`Testing operations on repository: ${repo_path}`);
// Get GitRepo object
let git_repos = git_tree.get(repo_path);
if git_repos.len() == 0 {
print("Failed to get GitRepo object");
throw "Failed to get GitRepo object";
}
let git_repo = git_repos[0];
// Test has_changes method
print("Testing has_changes method");
let has_changes = git_repo.has_changes();
print(`Repository has changes: ${has_changes}`);
// Create a change to test
print("Creating a change to test");
file_write("/tmp/code/test2.txt", "Another test file");
// Check if changes are detected
let has_changes_after = git_repo.has_changes();
print(`Repository has changes after modification: ${has_changes_after}`);
if !has_changes_after {
print("Changes not detected, which is unexpected");
throw "Changes not detected";
}
// Clean up the change
delete("/tmp/code/test2.txt");
}
// Run all tests
fn run_all_tests() {
print("Starting Git module tests...");
// Ensure test directory exists
ensure_test_dir();
// Run tests
test_git_tree_creation();
test_git_tree_list();
test_git_tree_find();
test_git_repo_operations();
print("\nAll tests completed successfully!");
}
// Run all tests
run_all_tests();

View File

@ -1,7 +1,7 @@
use regex::Regex; use regex::Regex;
use std::fs; use std::fs;
use std::io::{self, Read, Seek, SeekFrom}; use std::io::{self, Read};
use std::path::Path; use std::path::Path;
/// Represents the type of replacement to perform. /// Represents the type of replacement to perform.

View File

@ -3,7 +3,6 @@
use crate::virt::nerdctl::execute_nerdctl_command; use crate::virt::nerdctl::execute_nerdctl_command;
use crate::process::CommandResult; use crate::process::CommandResult;
use super::NerdctlError; use super::NerdctlError;
use serde_json::{self};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
/// Represents a container image /// Represents a container image

View File

@ -12,4 +12,3 @@ pub use mount::{list_mounts, unmount_all, unmount, get_mount_info};
pub use pack::{pack_directory, unpack, list_contents, verify}; pub use pack::{pack_directory, unpack, list_contents, verify};
// Re-export the execute_rfs_command function for use in other modules // Re-export the execute_rfs_command function for use in other modules
pub(crate) use cmd::execute_rfs_command;