thor-wt 0.2.0

Worktree workflow commands for Thor
Documentation
use thor_core::{find_repo, list_worktrees};
use std::path::PathBuf;

/// What the user wants to navigate to.
pub enum GoQuery {
    /// Fuzzy substring match on branch name.
    Fuzzy(String),
    /// Jump by 1-based index in the worktree list.
    Index(usize),
    /// Return to the previous worktree.
    Previous,
}

/// Result of a go operation.
pub enum GoResult {
    Found { path: PathBuf, branch: String },
    NotFound(String),
    Ambiguous(Vec<String>),
}

/// Persist the current worktree path for `go -`.
pub fn save_last_worktree(path: &std::path::Path) {
    if let Some(data_dir) = dirs::data_dir() {
        let thor_dir = data_dir.join("thor");
        let _ = std::fs::create_dir_all(&thor_dir);
        let _ = std::fs::write(thor_dir.join("last_worktree"), path.display().to_string());
    }
}

fn load_last_worktree() -> Option<PathBuf> {
    let data_dir = dirs::data_dir()?;
    let path_str = std::fs::read_to_string(data_dir.join("thor").join("last_worktree")).ok()?;
    let path = PathBuf::from(path_str.trim());
    if path.is_dir() { Some(path) } else { None }
}

/// Navigate to a worktree by fuzzy match, index, or previous.
pub async fn go(query: &GoQuery) -> anyhow::Result<GoResult> {
    let repo = find_repo()?;
    let worktrees = list_worktrees(&repo).await?;

    match query {
        GoQuery::Previous => {
            match load_last_worktree() {
                Some(saved) => {
                    // Match saved path against worktree roots (cwd may be a subdirectory)
                    match worktrees.iter().find(|wt| saved.starts_with(&wt.path)) {
                        Some(wt) => Ok(GoResult::Found {
                            path: wt.path.clone(),
                            branch: wt.display_name(),
                        }),
                        None => Ok(GoResult::Found { path: saved, branch: "unknown".to_string() }),
                    }
                }
                None => Ok(GoResult::NotFound("No previous worktree".to_string())),
            }
        }
        GoQuery::Index(idx) => {
            match worktrees.get(idx.saturating_sub(1)) {
                Some(wt) => Ok(GoResult::Found {
                    path: wt.path.clone(),
                    branch: wt.display_name(),
                }),
                None => Ok(GoResult::NotFound(format!("Index {} out of range (1-{})", idx, worktrees.len()))),
            }
        }
        GoQuery::Fuzzy(query) => {
            let query_lower = query.to_lowercase();

            // Try exact match first
            if let Some(wt) = worktrees.iter().find(|wt| {
                wt.branch.as_ref().map(|b| b == query).unwrap_or(false)
            }) {
                return Ok(GoResult::Found {
                    path: wt.path.clone(),
                    branch: wt.display_name(),
                });
            }

            // Try substring match (case-insensitive)
            let matches: Vec<_> = worktrees.iter()
                .filter(|wt| {
                    wt.branch.as_ref()
                        .map(|b| b.to_lowercase().contains(&query_lower))
                        .unwrap_or(false)
                })
                .collect();

            match matches.len() {
                0 => Ok(GoResult::NotFound(format!("No worktree matching '{}'", query))),
                1 => Ok(GoResult::Found {
                    path: matches[0].path.clone(),
                    branch: matches[0].display_name(),
                }),
                _ => Ok(GoResult::Ambiguous(
                    matches.iter().map(|wt| wt.display_name()).collect()
                )),
            }
        }
    }
}