diff --git a/Cargo.lock b/Cargo.lock index fc2da55..2da9807 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -131,6 +131,16 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "command-with-spinner" +version = "0.1.0" +dependencies = [ + "anyhow", + "colored", + "indicatif", + "thiserror", +] + [[package]] name = "console" version = "0.15.11" @@ -152,6 +162,7 @@ dependencies = [ "clap", "colored", "dirs", + "git", "indicatif", "thiserror", ] @@ -194,6 +205,19 @@ dependencies = [ "wasi", ] +[[package]] +name = "git" +version = "0.1.0" +dependencies = [ + "anyhow", + "command-with-spinner", + "thiserror", +] + +[[package]] +name = "git-ssh-bitwarden" +version = "0.1.0" + [[package]] name = "heck" version = "0.5.0" @@ -311,8 +335,8 @@ dependencies = [ "anyhow", "clap", "colored", + "git", "indicatif", - "thiserror", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index e950ec3..f1e1be9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,9 @@ [workspace] resolver = "3" -members = [ - "create-worktree", +members = [ + "command-with-spinner", + "create-worktree", "git", + "git-ssh-bitwarden", "setup-repo", ] diff --git a/command-with-spinner/Cargo.toml b/command-with-spinner/Cargo.toml new file mode 100644 index 0000000..4efa47a --- /dev/null +++ b/command-with-spinner/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "command-with-spinner" +version = "0.1.0" + +edition.workspace = true +authors.workspace = true +license-file.workspace = true +repository.workspace = true + +[dependencies] +anyhow = "1.0.98" +colored = "3.0.0" +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 new file mode 100644 index 0000000..66ada84 --- /dev/null +++ b/command-with-spinner/src/lib.rs @@ -0,0 +1,42 @@ +use std::process::{Command, Stdio}; + +use anyhow::{Context, Result}; +use colored::*; +use indicatif::{ProgressBar, ProgressStyle}; + +mod error; +use error::CommandError; + +/// Runs a git command with a progress spinner +pub fn run_command(command: &mut Command, message: &str) -> Result<()> { + let spinner = ProgressBar::new_spinner(); + spinner.set_style( + ProgressStyle::default_spinner() + .tick_chars("⣾⣽⣻⢿⡿⣟⣯⣷") + .template("{spinner:.green} {msg}") + .expect("Invalid template format"), + ); + spinner.set_message(message.to_string()); + + // Configure the command to not show output + command.stdout(Stdio::null()).stderr(Stdio::null()); + + // Execute the command and wait for it to complete + spinner.enable_steady_tick(std::time::Duration::from_millis(100)); + + let status = command.status().context("Failed to execute command")?; + spinner.finish_and_clear(); + + if status.success() { + println!("{message} {}", "Done.".green()); + Ok(()) + } else { + println!("{message} {}", "FAILED.".red()); + + let code = status.code(); + match code { + 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 11b51fb..2baeaf0 100644 --- a/create-worktree/Cargo.toml +++ b/create-worktree/Cargo.toml @@ -9,9 +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" } diff --git a/create-worktree/src/main.rs b/create-worktree/src/main.rs index c299158..00b9b84 100644 --- a/create-worktree/src/main.rs +++ b/create-worktree/src/main.rs @@ -1,11 +1,11 @@ use std::env; use std::path::Path; -use std::process::{Command, Stdio}; use anyhow::{Context, Result}; use clap::{ArgAction, Parser}; use colored::*; -use indicatif::{ProgressBar, ProgressStyle}; + +use git::Git; /// Tool to create git worktrees with convenient branch management #[derive(Parser, Debug)] @@ -39,133 +39,6 @@ struct Args { no_color: bool, } -/// Git operations error type -#[derive(Debug, thiserror::Error)] -enum GitError { - #[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), -} - -/// Runs a git command with a progress spinner -fn run_command(command: &mut Command, message: &str) -> Result<()> { - let spinner = ProgressBar::new_spinner(); - spinner.set_style( - ProgressStyle::default_spinner() - .tick_chars("⣾⣽⣻⢿⡿⣟⣯⣷") - .template("{spinner:.green} {msg}") - .expect("Invalid template format"), - ); - spinner.set_message(message.to_string()); - - // Configure the command to not show output - command.stdout(Stdio::null()).stderr(Stdio::null()); - - // Execute the command and wait for it to complete - spinner.enable_steady_tick(std::time::Duration::from_millis(100)); - - let status = command.status().context("Failed to execute command")?; - spinner.finish_and_clear(); - - if status.success() { - println!("{message} {}", "Done.".green()); - Ok(()) - } else { - println!("{message} {}", "FAILED.".red()); - - let code = status.code(); - match code { - Some(code) => Err(GitError::Failed(code).into()), - None => Err(GitError::FailedNoCode.into()), - } - } -} - -/// 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-ssh-bitwarden/Cargo.toml b/git-ssh-bitwarden/Cargo.toml new file mode 100644 index 0000000..3878f46 --- /dev/null +++ b/git-ssh-bitwarden/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "git-ssh-bitwarden" +version = "0.1.0" + +edition.workspace = true +authors.workspace = true +license-file.workspace = true +repository.workspace = true + +[dependencies] diff --git a/git-ssh-bitwarden/src/main.rs b/git-ssh-bitwarden/src/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/git-ssh-bitwarden/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} diff --git a/git/Cargo.toml b/git/Cargo.toml new file mode 100644 index 0000000..0ef8928 --- /dev/null +++ b/git/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "git" +version = "0.1.0" + +edition.workspace = true +authors.workspace = true +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/error.rs b/git/src/error.rs new file mode 100644 index 0000000..7685bc8 --- /dev/null +++ b/git/src/error.rs @@ -0,0 +1,12 @@ +/// Git operations error type +#[derive(Debug, thiserror::Error)] +pub enum GitError { + #[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/git/src/lib.rs b/git/src/lib.rs new file mode 100644 index 0000000..61f0ad5 --- /dev/null +++ b/git/src/lib.rs @@ -0,0 +1,170 @@ +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 6b41a30..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" +git = { version = "0.1.0", path = "../git" } indicatif = "0.17" -thiserror = "2.0" diff --git a/setup-repo/src/main.rs b/setup-repo/src/main.rs index 5df8572..02269a6 100644 --- a/setup-repo/src/main.rs +++ b/setup-repo/src/main.rs @@ -1,10 +1,8 @@ -use std::path::Path; -use std::process::{Command, Stdio}; - -use anyhow::{Context, Result}; +use anyhow::Result; use clap::Parser; use colored::*; -use indicatif::{ProgressBar, ProgressStyle}; + +use git::Git; /// Tool to set up Git repositories for worktree development #[derive(Parser, Debug)] @@ -27,113 +25,6 @@ struct Args { no_color: bool, } -/// Git operations error type -#[derive(Debug, thiserror::Error)] -enum GitError { - #[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), -} - -/// Runs a command with a progress spinner -fn run_command(command: &mut Command, message: &str) -> Result<()> { - let spinner = ProgressBar::new_spinner(); - spinner.set_style( - ProgressStyle::default_spinner() - .tick_chars("⣾⣽⣻⢿⡿⣟⣯⣷") - .template("{spinner:.green} {msg}") - .expect("Invalid template format"), - ); - spinner.set_message(message.to_string()); - - // Configure the command to not show output - command.stdout(Stdio::null()).stderr(Stdio::null()); - - // Execute the command and wait for it to complete - spinner.enable_steady_tick(std::time::Duration::from_millis(100)); - - let status = command.status().context("Failed to execute command")?; - spinner.finish_and_clear(); - - if status.success() { - println!("{message} {}", "Done.".green()); - Ok(()) - } else { - println!("{message} {}", "FAILED.".red()); - - let code = status.code(); - match code { - Some(code) => Err(GitError::Failed(code).into()), - None => Err(GitError::FailedNoCode.into()), - } - } -} - -/// 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();