...
This commit is contained in:
882
src/git/git.rs
882
src/git/git.rs
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
|
Reference in New Issue
Block a user