This commit is contained in:
2025-04-05 04:45:56 +02:00
parent 9f33c94020
commit e48063a79c
22 changed files with 2661 additions and 1384 deletions

View File

@@ -11,6 +11,7 @@ use std::error::Error;
pub enum GitError {
GitNotInstalled(std::io::Error),
InvalidUrl(String),
InvalidBasePath(String),
HomeDirectoryNotFound(std::env::VarError),
FileSystemError(std::io::Error),
GitCommandFailed(String),
@@ -28,6 +29,7 @@ impl fmt::Display for GitError {
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),
@@ -55,98 +57,21 @@ impl Error for GitError {
}
}
// Git utility functions
/**
* Clones a git repository to a standardized location in the user's home directory.
*
* # Arguments
*
* * `url` - The URL of the git repository to clone. Can be in HTTPS format
* (https://github.com/username/repo.git) or SSH format (git@github.com:username/repo.git).
*
* # Returns
*
* * `Ok(String)` - The path where the repository was cloned, formatted as
* ~/code/server/account/repo (e.g., ~/code/github.com/username/repo).
* * `Err(GitError)` - An error if the clone operation failed.
*
* # Examples
*
* ```
* let repo_path = git_clone("https://github.com/username/repo.git")?;
* println!("Repository cloned to: {}", repo_path);
* ```
*/
pub fn git_clone(url: &str) -> Result<String, GitError> {
// Check if git is installed
let _git_check = Command::new("git")
.arg("--version")
.output()
.map_err(GitError::GitNotInstalled)?;
// Parse the URL to determine the clone path
let (server, account, repo) = parse_git_url(url);
if server.is_empty() || account.is_empty() || repo.is_empty() {
return Err(GitError::InvalidUrl(url.to_string()));
}
// Create the target directory
let home_dir = env::var("HOME").map_err(GitError::HomeDirectoryNotFound)?;
let clone_path = format!("{}/code/{}/{}/{}", home_dir, server, account, repo);
let clone_dir = Path::new(&clone_path);
// Check if repo already exists
if clone_dir.exists() {
return Ok(format!("Repository already exists at {}", 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", url, &clone_path])
.output()
.map_err(GitError::CommandExecutionError)?;
if output.status.success() {
Ok(clone_path)
} else {
let error = String::from_utf8_lossy(&output.stderr);
Err(GitError::GitCommandFailed(format!("Git clone error: {}", error)))
}
}
/**
* 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.
*
* # Examples
*
* ```
* let (server, account, repo) = parse_git_url("https://github.com/username/repo.git");
* assert_eq!(server, "github.com");
* assert_eq!(account, "username");
* assert_eq!(repo, "repo");
* ```
*/
/// 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();
@@ -171,427 +96,394 @@ pub fn parse_git_url(url: &str) -> (String, String, String) {
(String::new(), String::new(), String::new())
}
/**
* Lists all git repositories found in the user's ~/code directory.
*
* This function searches for directories containing a .git subdirectory,
* which indicates a git repository.
*
* # Returns
*
* * `Ok(Vec<String>)` - A vector of paths to git repositories
* * `Err(GitError)` - An error if the operation failed
*
* # Examples
*
* ```
* let repos = git_list()?;
* for repo in repos {
* println!("Found repository: {}", repo);
* }
* ```
*/
pub fn git_list() -> Result<Vec<String>, GitError> {
// Get home directory
let home_dir = env::var("HOME").map_err(GitError::HomeDirectoryNotFound)?;
let code_dir = format!("{}/code", home_dir);
let code_path = Path::new(&code_dir);
if !code_path.exists() || !code_path.is_dir() {
return Ok(Vec::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(),
})
}
let mut repos = Vec::new();
// Find all directories with .git subdirectories
let output = Command::new("find")
.args(&[&code_dir, "-type", "d", "-name", ".git"])
.output()
.map_err(GitError::CommandExecutionError)?;
/// 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 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());
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)
}
/**
* Checks if a git repository has uncommitted changes.
*
* # Arguments
*
* * `repo_path` - The path to the git repository
*
* # Returns
*
* * `Ok(bool)` - True if the repository has uncommitted changes, false otherwise
* * `Err(GitError)` - An error if the operation failed
*
* # Examples
*
* ```
* if has_git_changes("/path/to/repo")? {
* println!("Repository has uncommitted changes");
* } else {
* println!("Repository is clean");
* }
* ```
*/
pub fn has_git_changes(repo_path: &str) -> Result<bool, GitError> {
let output = Command::new("git")
.args(&["-C", repo_path, "status", "--porcelain"])
.output()
.map_err(GitError::CommandExecutionError)?;
Ok(!output.stdout.is_empty())
}
/**
* 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)` - An error if no matching repositories are found,
* or if multiple repositories match a non-wildcard pattern
*
* # Examples
*
* ```
* // Find all repositories containing "project"
* let repos = find_matching_repos("project*")?;
*
* // Find exactly one repository containing "unique-project"
* let repo = find_matching_repos("unique-project")?[0];
* ```
*/
pub fn find_matching_repos(pattern: &str) -> Result<Vec<String>, GitError> {
// Get all repos
let repos = git_list()?;
if repos.is_empty() {
return Err(GitError::NoRepositoriesFound);
}
// Check if pattern ends with wildcard
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() {
return Err(GitError::RepositoryNotFound(pattern.to_string()));
}
Ok(matching)
} else {
// No wildcard, need to find exactly one match
let matching: Vec<String> = repos.iter()
.filter(|repo| repo.contains(pattern))
.cloned()
.collect();
match matching.len() {
0 => Err(GitError::RepositoryNotFound(pattern.to_string())),
1 => Ok(matching),
_ => Err(GitError::MultipleRepositoriesFound(pattern.to_string(), matching.len())),
}
}
}
/**
* Updates a git repository by pulling the latest changes.
*
* This function will fail if there are uncommitted changes in the repository.
*
* # Arguments
*
* * `repo_path` - The path to the git repository, or a partial path that uniquely identifies a repository
*
* # Returns
*
* * `Ok(String)` - A success message indicating the repository was updated
* * `Err(GitError)` - An error if the update failed
*
* # Examples
*
* ```
* let result = git_update("my-project")?;
* println!("{}", result); // "Successfully updated repository at /home/user/code/github.com/user/my-project"
* ```
*/
pub fn git_update(repo_path: &str) -> Result<String, GitError> {
// If repo_path may be a partial path, find the matching repository
let repos = find_matching_repos(repo_path)?;
// Should only be one repository at this point
let actual_path = &repos[0];
// Check if repository exists and is a git repository
let git_dir = Path::new(actual_path).join(".git");
if !git_dir.exists() || !git_dir.is_dir() {
return Err(GitError::NotAGitRepository(actual_path.clone()));
}
// Check for local changes
if has_git_changes(actual_path)? {
return Err(GitError::LocalChangesExist(actual_path.clone()));
}
// Pull the latest changes
let output = Command::new("git")
.args(&["-C", actual_path, "pull"])
.output()
.map_err(GitError::CommandExecutionError)?;
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
if stdout.contains("Already up to date") {
Ok(format!("Repository already up to date at {}", actual_path))
} else {
Ok(format!("Successfully updated repository at {}", actual_path))
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<String>, GitError> {
// Get all repos
let repos = self.list()?;
if repos.is_empty() {
return Err(GitError::NoRepositoriesFound);
}
// Check if pattern ends with wildcard
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() {
return Err(GitError::RepositoryNotFound(pattern.to_string()));
}
Ok(matching)
} else {
// No wildcard, need to find exactly one match
let matching: Vec<String> = repos.iter()
.filter(|repo| repo.contains(pattern))
.cloned()
.collect();
match matching.len() {
0 => Err(GitError::RepositoryNotFound(pattern.to_string())),
1 => Ok(matching),
_ => Err(GitError::MultipleRepositoriesFound(pattern.to_string(), matching.len())),
}
}
}
/// 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
let repo_paths = self.find(path_or_url)?;
// Convert paths to GitRepo objects
let repos: Vec<GitRepo> = repo_paths.into_iter()
.map(GitRepo::new)
.collect();
Ok(repos)
}
} else {
let error = String::from_utf8_lossy(&output.stderr);
Err(GitError::GitCommandFailed(format!("Git pull error: {}", error)))
}
}
/**
* Force updates a git repository by discarding local changes and pulling the latest changes.
*
* This function will reset any uncommitted changes and clean untracked files before pulling.
*
* # Arguments
*
* * `repo_path` - The path to the git repository, or a partial path that uniquely identifies a repository
*
* # Returns
*
* * `Ok(String)` - A success message indicating the repository was force-updated
* * `Err(GitError)` - An error if the update failed
*
* # Examples
*
* ```
* let result = git_update_force("my-project")?;
* println!("{}", result); // "Successfully force-updated repository at /home/user/code/github.com/user/my-project"
* ```
*/
pub fn git_update_force(repo_path: &str) -> Result<String, GitError> {
// If repo_path may be a partial path, find the matching repository
let repos = find_matching_repos(repo_path)?;
// Should only be one repository at this point
let actual_path = &repos[0];
// Check if repository exists and is a git repository
let git_dir = Path::new(actual_path).join(".git");
if !git_dir.exists() || !git_dir.is_dir() {
return Err(GitError::NotAGitRepository(actual_path.clone()));
/// 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 }
}
// Reset any local changes
let reset_output = Command::new("git")
.args(&["-C", actual_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)));
/// Gets the path of the repository.
///
/// # Returns
///
/// * The path to the git repository
pub fn path(&self) -> &str {
&self.path
}
// Clean untracked files
let clean_output = Command::new("git")
.args(&["-C", actual_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)));
/// 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 pull_output = Command::new("git")
.args(&["-C", actual_path, "pull"])
.output()
.map_err(GitError::CommandExecutionError)?;
// Pull the latest changes
let output = Command::new("git")
.args(&["-C", &self.path, "pull"])
.output()
.map_err(GitError::CommandExecutionError)?;
if pull_output.status.success() {
Ok(format!("Successfully force-updated repository at {}", actual_path))
} else {
let error = String::from_utf8_lossy(&pull_output.stderr);
Err(GitError::GitCommandFailed(format!("Git pull error: {}", error)))
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)))
}
}
}
/**
* Commits changes in a git repository and then updates it by pulling the latest changes.
*
* # Arguments
*
* * `repo_path` - The path to the git repository, or a partial path that uniquely identifies a repository
* * `message` - The commit message
*
* # Returns
*
* * `Ok(String)` - A success message indicating the repository was committed and updated
* * `Err(GitError)` - An error if the operation failed
*
* # Examples
*
* ```
* let result = git_update_commit("my-project", "Fix bug in login form")?;
* println!("{}", result); // "Successfully committed and updated repository at /home/user/code/github.com/user/my-project"
* ```
*/
pub fn git_update_commit(repo_path: &str, message: &str) -> Result<String, GitError> {
// If repo_path may be a partial path, find the matching repository
let repos = find_matching_repos(repo_path)?;
// Should only be one repository at this point
let actual_path = &repos[0];
// Check if repository exists and is a git repository
let git_dir = Path::new(actual_path).join(".git");
if !git_dir.exists() || !git_dir.is_dir() {
return Err(GitError::NotAGitRepository(actual_path.clone()));
}
// Check for local changes
if !has_git_changes(actual_path)? {
return Ok(format!("No changes to commit in repository at {}", actual_path));
}
// Add all changes
let add_output = Command::new("git")
.args(&["-C", actual_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", actual_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)));
}
// Pull the latest changes
let pull_output = Command::new("git")
.args(&["-C", actual_path, "pull"])
.output()
.map_err(GitError::CommandExecutionError)?;
if pull_output.status.success() {
Ok(format!("Successfully committed and updated repository at {}", actual_path))
} else {
let error = String::from_utf8_lossy(&pull_output.stderr);
Err(GitError::GitCommandFailed(format!("Git pull error: {}", error)))
}
}
/**
* Commits changes in a git repository and pushes them to the remote.
*
* # Arguments
*
* * `repo_path` - The path to the git repository, or a partial path that uniquely identifies a repository
* * `message` - The commit message
*
* # Returns
*
* * `Ok(String)` - A success message indicating the repository was committed and pushed
* * `Err(GitError)` - An error if the operation failed
*
* # Examples
*
* ```
* let result = git_update_commit_push("my-project", "Add new feature")?;
* println!("{}", result); // "Successfully committed and pushed repository at /home/user/code/github.com/user/my-project"
* ```
*/
pub fn git_update_commit_push(repo_path: &str, message: &str) -> Result<String, GitError> {
// If repo_path may be a partial path, find the matching repository
let repos = find_matching_repos(repo_path)?;
// Should only be one repository at this point
let actual_path = &repos[0];
// Check if repository exists and is a git repository
let git_dir = Path::new(actual_path).join(".git");
if !git_dir.exists() || !git_dir.is_dir() {
return Err(GitError::NotAGitRepository(actual_path.clone()));
}
// Check for local changes
if !has_git_changes(actual_path)? {
return Ok(format!("No changes to commit in repository at {}", actual_path));
}
// Add all changes
let add_output = Command::new("git")
.args(&["-C", actual_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", actual_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)));
}
// Push the changes
let push_output = Command::new("git")
.args(&["-C", actual_path, "push"])
.output()
.map_err(GitError::CommandExecutionError)?;
if push_output.status.success() {
Ok(format!("Successfully committed and pushed repository at {}", actual_path))
} 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(),
}
}
}

View File

@@ -160,7 +160,7 @@ impl GitExecutor {
// Get authentication configuration for a git URL
fn get_auth_for_url(&self, url: &str) -> Option<&GitServerAuth> {
if let Some(config) = &self.config {
let (server, _, _) = parse_git_url(url);
let (server, _, _) = crate::git::git::parse_git_url(url);
if !server.is_empty() {
return config.auth.get(&server);
}