diff --git a/Cargo.lock b/Cargo.lock index 321a57b..2da9807 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -137,8 +137,8 @@ version = "0.1.0" dependencies = [ "anyhow", "colored", - "git", "indicatif", + "thiserror", ] [[package]] @@ -161,8 +161,8 @@ dependencies = [ "anyhow", "clap", "colored", - "command-with-spinner", "dirs", + "git", "indicatif", "thiserror", ] @@ -209,6 +209,8 @@ dependencies = [ name = "git" version = "0.1.0" dependencies = [ + "anyhow", + "command-with-spinner", "thiserror", ] @@ -333,7 +335,7 @@ dependencies = [ "anyhow", "clap", "colored", - "command-with-spinner", + "git", "indicatif", ] diff --git a/command-with-spinner/Cargo.toml b/command-with-spinner/Cargo.toml index 132ae34..4efa47a 100644 --- a/command-with-spinner/Cargo.toml +++ b/command-with-spinner/Cargo.toml @@ -1,6 +1,7 @@ [package] name = "command-with-spinner" version = "0.1.0" + edition.workspace = true authors.workspace = true license-file.workspace = true @@ -9,5 +10,5 @@ repository.workspace = true [dependencies] anyhow = "1.0.98" colored = "3.0.0" -git = { version = "0.1.0", path = "../git" } indicatif = "0.17.11" +thiserror = "2.0" diff --git a/command-with-spinner/src/error.rs b/command-with-spinner/src/error.rs new file mode 100644 index 0000000..025dde4 --- /dev/null +++ b/command-with-spinner/src/error.rs @@ -0,0 +1,12 @@ +/// Git operations error type +#[derive(Debug, thiserror::Error)] +pub enum CommandError { + #[error("Command failed with exit code: {0}")] + Failed(i32), + + #[error("Command failed without exit code")] + FailedNoCode, + + #[error("Failed to execute command: {0}")] + ExecutionError(#[from] std::io::Error), +} diff --git a/command-with-spinner/src/lib.rs b/command-with-spinner/src/lib.rs index 61948e4..66ada84 100644 --- a/command-with-spinner/src/lib.rs +++ b/command-with-spinner/src/lib.rs @@ -4,7 +4,8 @@ use anyhow::{Context, Result}; use colored::*; use indicatif::{ProgressBar, ProgressStyle}; -use git::error::GitError; +mod error; +use error::CommandError; /// Runs a git command with a progress spinner pub fn run_command(command: &mut Command, message: &str) -> Result<()> { @@ -34,8 +35,8 @@ pub fn run_command(command: &mut Command, message: &str) -> Result<()> { let code = status.code(); match code { - Some(code) => Err(GitError::Failed(code).into()), - None => Err(GitError::FailedNoCode.into()), + Some(code) => Err(CommandError::Failed(code).into()), + None => Err(CommandError::FailedNoCode.into()), } } } diff --git a/create-worktree/Cargo.toml b/create-worktree/Cargo.toml index 40291d9..2baeaf0 100644 --- a/create-worktree/Cargo.toml +++ b/create-worktree/Cargo.toml @@ -9,10 +9,10 @@ license-file.workspace = true repository.workspace = true [dependencies] -clap = { version = "4.5", features = ["derive"] } anyhow = "1.0" +clap = { version = "4.5", features = ["derive"] } colored = "3.0" -indicatif = "0.17" dirs = "6.0" +git = { version = "0.1.0", path = "../git" } +indicatif = "0.17" thiserror = { version = "2.0" } -command-with-spinner = { version = "0.1.0", path = "../command-with-spinner" } diff --git a/create-worktree/src/main.rs b/create-worktree/src/main.rs index 0e33816..00b9b84 100644 --- a/create-worktree/src/main.rs +++ b/create-worktree/src/main.rs @@ -1,12 +1,11 @@ use std::env; use std::path::Path; -use std::process::Command; use anyhow::{Context, Result}; use clap::{ArgAction, Parser}; use colored::*; -use command_with_spinner::run_command; +use git::Git; /// Tool to create git worktrees with convenient branch management #[derive(Parser, Debug)] @@ -40,86 +39,6 @@ struct Args { no_color: bool, } -/// Git command wrapper -struct Git; - -impl Git { - /// Check if a branch exists locally - fn branch_exists_locally(branch: &str) -> Result { - let output = Command::new("git") - .args([ - "branch", - "--list", - branch - ]) - .output() - .context("Failed to check branch existence")?; - - Ok(!output.stdout.is_empty()) - } - - /// Check if a branch exists on the remote - fn branch_exists_on_remote(branch: &str) -> Result { - let output = Command::new("git") - .args([ - "ls-remote", - "--heads", - "origin", - branch - ]) - .output() - .context("Failed to check remote branch existence")?; - - Ok(!output.stdout.is_empty()) - } - - /// Create a new worktree with an existing branch - fn create_worktree_existing_branch(worktree_path: &str, branch: &str) -> Result<()> { - let mut cmd = Command::new("git"); - cmd.args([ - "worktree", - "add", - worktree_path, - branch - ]); - run_command(&mut cmd, &format!("Generating new worktree from existing branch: {branch}")) - } - - /// Create a new worktree with a new branch - fn create_worktree_new_branch(worktree_path: &str, branch: &str, base: &str) -> Result<()> { - let mut cmd = Command::new("git"); - cmd.args([ - "worktree", - "add", - "-b", branch, - worktree_path, - base - ]); - run_command(&mut cmd, &format!("Generating new worktree: {worktree_path}")) - } - - /// Create and push a new remote branch - fn create_remote_branch(branch: &str) -> Result<()> { - let mut cmd = Command::new("git"); - cmd.args([ - "push", - "-u", "origin", - branch - ]); - run_command(&mut cmd, &format!("Creating remote branch {branch}...")) - } - - /// Set the upstream branch - fn set_upstream_branch(branch: &str) -> Result<()> { - let mut cmd = Command::new("git"); - cmd.args([ - "branch", - "--set-upstream-to", &format!("origin/{branch}") - ]); - run_command(&mut cmd, &format!("Setting upstream branch to 'origin/{branch}'")) - } -} - /// Update or create the remote tracking branch fn update_remote(branch: &str, create_upstream: bool) -> Result<()> { // Do nothing if create_upstream is disabled diff --git a/git/Cargo.toml b/git/Cargo.toml index 694768e..0ef8928 100644 --- a/git/Cargo.toml +++ b/git/Cargo.toml @@ -8,4 +8,6 @@ license-file.workspace = true repository.workspace = true [dependencies] +anyhow = "1.0.98" +command-with-spinner = { version = "0.1.0", path = "../command-with-spinner" } thiserror = "2.0" diff --git a/git/src/lib.rs b/git/src/lib.rs index a91e735..61f0ad5 100644 --- a/git/src/lib.rs +++ b/git/src/lib.rs @@ -1 +1,170 @@ -pub mod error; +use std::{path::Path, process::Command}; + +use anyhow::{Context, Result}; + +use command_with_spinner::run_command; +use error::GitError; + +mod error; +/// Git command wrapper +pub struct Git; + +impl Git { + /// Clone a repository as a bare clone in a .bare directory + pub fn clone_bare_repo(repo_url: &str, target_dir: &str) -> Result<()> { + // Create the base directory first + std::fs::create_dir_all(target_dir).context("Failed to create target directory")?; + + // Create the .bare subdirectory path + let bare_dir = Path::new(target_dir).join(".bare"); + let bare_dir_str = bare_dir.to_string_lossy(); + + // Clone the repository as a bare clone into .bare directory + let mut cmd = Command::new("git"); + cmd.args([ + "clone", + "--bare", + repo_url, + &bare_dir_str + ]); + + match run_command(&mut cmd, &format!("Cloning repository as bare clone into {bare_dir_str}")) { + Ok(_) => Ok(()), + Err(_) => Err(GitError::FailedNoCode.into()) + } + } + + /// Set up the .git file to point to the .bare directory + pub fn setup_git_pointer(target_dir: &str) -> Result<()> { + let git_file_path = Path::new(target_dir).join(".git"); + std::fs::write(git_file_path, "gitdir: ./.bare") + .context("Failed to create .git file pointing to .bare directory") + } + + /// Configure remote.origin.fetch to fetch all references + pub fn configure_remote_fetch(target_dir: &str) -> Result<()> { + let bare_dir = Path::new(target_dir).join(".bare"); + let bare_dir_str = bare_dir.to_string_lossy(); + + let mut cmd = Command::new("git"); + cmd.args([ + "--git-dir", &bare_dir_str, + "config", + "remote.origin.fetch", "+refs/heads/*:refs/remotes/origin/*" + ]); + + match run_command(&mut cmd, "Configuring remote.origin.fetch") { + Ok(_) => Ok(()), + Err(_) => Err(GitError::FailedNoCode.into()) + } + } + + /// Fetch all remotes + pub fn fetch_remotes(target_dir: &str) -> Result<()> { + let bare_dir = Path::new(target_dir).join(".bare"); + let bare_dir_str = bare_dir.to_string_lossy(); + + let mut cmd = Command::new("git"); + cmd.args([ + "--git-dir", &bare_dir_str, + "fetch", + "--all" + ]); + + match run_command(&mut cmd, "Fetching all remotes") { + Ok(_) => Ok(()), + Err(_) => Err(GitError::FailedNoCode.into()) + } + } + + /// Check if a branch exists locally + pub fn branch_exists_locally(branch: &str) -> Result { + let output = Command::new("git") + .args([ + "branch", + "--list", + branch + ]) + .output() + .context("Failed to check branch existence")?; + + Ok(!output.stdout.is_empty()) + } + + /// Check if a branch exists on the remote + pub fn branch_exists_on_remote(branch: &str) -> Result { + let output = Command::new("git") + .args([ + "ls-remote", + "--heads", + "origin", + branch + ]) + .output() + .context("Failed to check remote branch existence")?; + + Ok(!output.stdout.is_empty()) + } + + /// Create a new worktree with an existing branch + pub fn create_worktree_existing_branch(worktree_path: &str, branch: &str) -> Result<()> { + let mut cmd = Command::new("git"); + cmd.args([ + "worktree", + "add", + worktree_path, + branch + ]); + + match run_command(&mut cmd, &format!("Generating new worktree from existing branch: {branch}")) { + Ok(_) => Ok(()), + Err(_) => Err(GitError::FailedNoCode.into()) + } + } + + /// Create a new worktree with a new branch + pub fn create_worktree_new_branch(worktree_path: &str, branch: &str, base: &str) -> Result<()> { + let mut cmd = Command::new("git"); + cmd.args([ + "worktree", + "add", + "-b", branch, + worktree_path, + base + ]); + + match run_command(&mut cmd, &format!("Generating new worktree: {worktree_path}")) { + Ok(_) => Ok(()), + Err(_) => Err(GitError::FailedNoCode.into()) + } + } + + /// Create and push a new remote branch + pub fn create_remote_branch(branch: &str) -> Result<()> { + let mut cmd = Command::new("git"); + cmd.args([ + "push", + "-u", "origin", + branch + ]); + + match run_command(&mut cmd, &format!("Creating remote branch {branch}...")) { + Ok(_) => Ok(()), + Err(_) => Err(GitError::FailedNoCode.into()) + } + } + + /// Set the upstream branch + pub fn set_upstream_branch(branch: &str) -> Result<()> { + let mut cmd = Command::new("git"); + cmd.args([ + "branch", + "--set-upstream-to", &format!("origin/{branch}") + ]); + + match run_command(&mut cmd, &format!("Setting upstream branch to 'origin/{branch}'")) { + Ok(_) => Ok(()), + Err(_) => Err(GitError::FailedNoCode.into()) + } + } +} diff --git a/setup-repo/Cargo.toml b/setup-repo/Cargo.toml index bcc1220..cb0f995 100644 --- a/setup-repo/Cargo.toml +++ b/setup-repo/Cargo.toml @@ -12,5 +12,5 @@ repository.workspace = true anyhow = "1.0" clap = { version = "4.5", features = ["derive"] } colored = "3.0" -command-with-spinner = { version = "0.1.0", path = "../command-with-spinner" } +git = { version = "0.1.0", path = "../git" } indicatif = "0.17" diff --git a/setup-repo/src/main.rs b/setup-repo/src/main.rs index 47d8be9..02269a6 100644 --- a/setup-repo/src/main.rs +++ b/setup-repo/src/main.rs @@ -1,11 +1,8 @@ -use std::path::Path; -use std::process::Command; - -use anyhow::{Context, Result}; +use anyhow::Result; use clap::Parser; use colored::*; -use command_with_spinner::run_command; +use git::Git; /// Tool to set up Git repositories for worktree development #[derive(Parser, Debug)] @@ -28,66 +25,6 @@ struct Args { no_color: bool, } -/// Git command wrapper -struct Git; - -impl Git { - /// Clone a repository as a bare clone in a .bare directory - fn clone_bare_repo(repo_url: &str, target_dir: &str) -> Result<()> { - // Create the base directory first - std::fs::create_dir_all(target_dir).context("Failed to create target directory")?; - - // Create the .bare subdirectory path - let bare_dir = Path::new(target_dir).join(".bare"); - let bare_dir_str = bare_dir.to_string_lossy(); - - // Clone the repository as a bare clone into .bare directory - let mut cmd = Command::new("git"); - cmd.args([ - "clone", - "--bare", - repo_url, - &bare_dir_str - ]); - run_command(&mut cmd, &format!("Cloning repository as bare clone into {bare_dir_str}")) - } - - /// Set up the .git file to point to the .bare directory - fn setup_git_pointer(target_dir: &str) -> Result<()> { - let git_file_path = Path::new(target_dir).join(".git"); - std::fs::write(git_file_path, "gitdir: ./.bare") - .context("Failed to create .git file pointing to .bare directory") - } - - /// Configure remote.origin.fetch to fetch all references - fn configure_remote_fetch(target_dir: &str) -> Result<()> { - let bare_dir = Path::new(target_dir).join(".bare"); - let bare_dir_str = bare_dir.to_string_lossy(); - - let mut cmd = Command::new("git"); - cmd.args([ - "--git-dir", &bare_dir_str, - "config", - "remote.origin.fetch", "+refs/heads/*:refs/remotes/origin/*" - ]); - run_command(&mut cmd, "Configuring remote.origin.fetch") - } - - /// Fetch all remotes - fn fetch_remotes(target_dir: &str) -> Result<()> { - let bare_dir = Path::new(target_dir).join(".bare"); - let bare_dir_str = bare_dir.to_string_lossy(); - - let mut cmd = Command::new("git"); - cmd.args([ - "--git-dir", &bare_dir_str, - "fetch", - "--all" - ]); - run_command(&mut cmd, "Fetching all remotes") - } -} - fn main() -> Result<()> { // Parse arguments let args = Args::parse();