grov 0.5.0

An opinionated bare-repo-only git worktree manager
Documentation
use std::io;
use std::path::Path;

use console::style;

use crate::config::{GrovConfig, WorktreeConfig, write_config};
use crate::git::executor::run_git_ok;
use crate::git::repo::default_branch;
use crate::git::worktree::add_worktree;
use crate::paths::{relative_from, repo_name_from_url, worktree_dir};
use crate::ui::prompt;

pub fn execute(
    url: Option<&str>,
    name: Option<&str>,
    prefix: Option<&str>,
    branch: Option<&str>,
    path: Option<&Path>,
) -> anyhow::Result<()> {
    let stdin = io::stdin();
    let mut reader = stdin.lock();

    // 1. URL — use flag or prompt
    let url = match url {
        Some(u) => u.to_string(),
        None => {
            let line = prompt("Repository URL", None, &mut reader)?;
            if line.is_empty() {
                anyhow::bail!("URL is required");
            }
            line
        }
    };

    // 2. Project name — derive from URL, allow override
    let derived_name = repo_name_from_url(&url);
    let project_name = match name {
        Some(n) => n.to_string(),
        None => {
            let line = prompt("Project name", Some(&derived_name), &mut reader)?;
            if line.is_empty() { derived_name } else { line }
        }
    };

    // 3. Prefix — use flag or prompt
    let prefix = match prefix {
        Some(p) => p.to_string(),
        None => prompt(
            "Worktree prefix (e.g. short alias, blank for none)",
            Some(""),
            &mut reader,
        )?,
    };

    let parent = match path {
        Some(p) => p.to_path_buf(),
        None => std::env::current_dir()?,
    };

    // Create project directory (like git clone creates a directory)
    let project_dir = parent.join(&project_name);
    if project_dir.exists() {
        anyhow::bail!("directory already exists: {}", project_dir.display());
    }
    std::fs::create_dir_all(&project_dir)?;

    // Clone bare into <project>/repo.git
    let bare_path = project_dir.join("repo.git");
    let bare_str = bare_path.to_string_lossy().to_string();
    run_git_ok(None, &["clone", "--bare", &url, &bare_str])?;

    // Write .grov.toml
    let config = GrovConfig {
        worktree: WorktreeConfig {
            prefix: prefix.clone(),
        },
    };
    write_config(&bare_path, &config)?;

    // Fix fetch refspec so `git fetch` works properly
    run_git_ok(
        Some(&bare_path),
        &[
            "config",
            "remote.origin.fetch",
            "+refs/heads/*:refs/remotes/origin/*",
        ],
    )?;

    // Fetch to populate remote tracking branches
    run_git_ok(Some(&bare_path), &["fetch", "origin"])?;

    // 4. Branch — use flag, prompt, or auto-detect
    let detected = match default_branch(&bare_path) {
        Ok(branch) => branch,
        Err(e) => {
            eprintln!(
                "{} Could not detect default branch ({}), assuming \"main\"",
                style("!").yellow().bold(),
                e
            );
            "main".to_string()
        }
    };
    let branch = match branch {
        Some(b) => b.to_string(),
        None => {
            let line = prompt("Default branch", Some(&detected), &mut reader)?;
            if line.is_empty() { detected } else { line }
        }
    };

    // Create initial worktree as sibling of repo.git
    let wt_path = worktree_dir(&bare_path, &branch, &prefix);
    add_worktree(&bare_path, &wt_path, Some(&branch), &[])?;

    println!(
        "\n{} Initialized {}/\n\n    {:<12}{}\n    {:<12}{}",
        style("").green().bold(),
        style(&project_name).bold(),
        "bare repo",
        style(format!("{}/repo.git", project_name)).dim(),
        "worktree",
        style(
            wt_path
                .file_name()
                .map(|n| format!("{}/{}", project_name, n.to_string_lossy()))
                .expect("worktree path must have a file name")
        )
        .dim(),
    );

    // Print cd hints
    let cwd = std::env::current_dir()?;
    let project_rel = relative_from(&project_dir, &cwd);
    if project_rel != Path::new(".") {
        let display = project_rel.display().to_string();
        let cd_arg = if display.contains(' ') {
            format!("\"{}\"", display)
        } else {
            display
        };
        println!(
            "{}",
            style(format!("  To enter the project:  cd {cd_arg}")).dim()
        );
    }
    let wt_rel = relative_from(&wt_path, &cwd);
    if wt_rel != Path::new(".") {
        let display = wt_rel.display().to_string();
        let cd_arg = if display.contains(' ') {
            format!("\"{}\"", display)
        } else {
            display
        };
        println!(
            "{}",
            style(format!("  To start working:      cd {cd_arg}")).dim()
        );
    }

    Ok(())
}