169 lines
5.1 KiB
Rust
169 lines
5.1 KiB
Rust
|
|
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(())
|
||
|
|
}
|