use std::path::Path; use std::process::{Command, Stdio}; use anyhow::{Context, Result}; use clap::Parser; use colored::*; use indicatif::{ProgressBar, ProgressStyle}; /// Tool to set up Git repositories for worktree development #[derive(Parser, Debug)] #[command(author, version, about)] struct Args { /// Repository URL to clone #[arg(short, long)] repo_url: String, /// Target directory for the repository setup #[arg(short, long)] target_dir: String, /// Enable verbose output #[arg(short, long)] verbose: bool, /// Disable colored output #[arg(long)] 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(); // Enable or disable colored output colored::control::set_override(!args.no_color); // Print verbose information if enabled if args.verbose { println!("{}", "Verbose mode enabled".dimmed()); println!("{}", format!("Repository URL: {}", args.repo_url).dimmed()); println!("{}", format!("Target directory: {}", args.target_dir).dimmed()); } println!("{}", "Setting up repository for worktree development".blue()); // Clone the repository as a bare clone Git::clone_bare_repo(&args.repo_url, &args.target_dir)?; // Set up the .git file to point to the .bare directory Git::setup_git_pointer(&args.target_dir)?; // Configure the remote.origin.fetch setting Git::configure_remote_fetch(&args.target_dir)?; // Fetch all remotes Git::fetch_remotes(&args.target_dir)?; println!("{}", "Repository setup complete.".green()); println!("{}", format!("You can now create worktrees in '{}'.", args.target_dir).green()); Ok(()) }