limb 0.1.0

A focused CLI for git worktree management
Documentation
//! Implements `limb list`. Prints a table of worktrees.

use anyhow::Result;

use crate::cli::ListArgs;
use crate::context::Context;
use crate::fmt::col_width;
use crate::style;
use crate::worktree::{self, RepoWorktree, Worktree};

/// Runs `limb list`.
///
/// Dispatches on `--all`: without it, lists the current repo's worktrees;
/// with it, scans every repo under `projects.roots`. Output is a
/// human-readable aligned table, or JSON when `--json` is set.
///
/// # Errors
///
/// Returns an error if the repo (or global config, with `--all`) cannot
/// be resolved, or if `--all` is requested with no configured roots.
pub fn run(ctx: &Context, args: &ListArgs) -> Result<()> {
    if args.all {
        run_all(ctx, args.verbose)
    } else {
        run_single(ctx, args.verbose)
    }
}

fn run_single(ctx: &Context, verbose: bool) -> Result<()> {
    let repo = ctx.repo()?;
    let trees = worktree::list(&repo)?;
    if ctx.json {
        println!("{}", serde_json::to_string_pretty(&trees)?);
    } else {
        print_single(&trees, verbose);
    }
    Ok(())
}

fn run_all(ctx: &Context, verbose: bool) -> Result<()> {
    let global = ctx.global()?;
    if global.projects_roots.is_empty() {
        anyhow::bail!(
            "--all: no projects.roots configured; \
             try: set `projects.roots = [\"~/dev/myrepos\"]` in ~/.config/limb/config.toml"
        );
    }
    let rows = worktree::list_all(&global.projects_roots);
    if ctx.json {
        println!("{}", serde_json::to_string_pretty(&rows)?);
    } else {
        print_all(&rows, verbose);
    }
    Ok(())
}

fn print_single(trees: &[Worktree], verbose: bool) {
    if trees.is_empty() {
        anstream::eprintln!("no worktrees");
        return;
    }
    let name_w = col_width(trees, |w| w.name.len(), 4);
    let branch_w = col_width(
        trees,
        |w| w.branch.as_deref().unwrap_or("(detached)").len(),
        6,
    );

    let h = style::DIM;
    anstream::println!("{h}{:<name_w$}  {:<branch_w$}  PATH{h:#}", "NAME", "BRANCH");
    for w in trees {
        let branch = w.branch.as_deref().unwrap_or("(detached)");
        anstream::println!(
            "{:<name_w$}  {:<branch_w$}  {}{}",
            w.name,
            branch,
            w.path.display(),
            render_tag(w, verbose),
        );
    }
}

fn print_all(rows: &[RepoWorktree], verbose: bool) {
    if rows.is_empty() {
        anstream::eprintln!("no worktrees");
        return;
    }
    let repo_w = col_width(rows, |r| r.repo.len(), 4);
    let name_w = col_width(rows, |r| r.worktree.name.len(), 4);
    let branch_w = col_width(
        rows,
        |r| r.worktree.branch.as_deref().unwrap_or("(detached)").len(),
        6,
    );

    let h = style::DIM;
    anstream::println!(
        "{h}{:<repo_w$}  {:<name_w$}  {:<branch_w$}  PATH{h:#}",
        "REPO",
        "NAME",
        "BRANCH"
    );
    let a = style::ACCENT;
    for r in rows {
        anstream::println!(
            "{a}{:<repo_w$}{a:#}  {:<name_w$}  {:<branch_w$}  {}{}",
            r.repo,
            r.worktree.name,
            r.worktree.branch.as_deref().unwrap_or("(detached)"),
            r.worktree.path.display(),
            render_tag(&r.worktree, verbose),
        );
    }
}

fn render_tag(w: &Worktree, verbose: bool) -> String {
    let (label, reason) = if w.bare {
        ("bare", None)
    } else if w.locked {
        ("locked", w.locked_reason.as_deref())
    } else if w.prunable {
        ("prunable", w.prunable_reason.as_deref())
    } else {
        return String::new();
    };
    let s = style::WARN;
    if verbose
        && let Some(r) = reason
        && !r.is_empty()
    {
        format!(" {s}({label}: {r}){s:#}")
    } else {
        format!(" {s}({label}){s:#}")
    }
}