use std::path::{Path, PathBuf};
use anyhow::{bail, Context, Result};
use crate::config::Config;
use crate::git::{self, Worktree};
pub const MAIN_ALIAS: &str = "@";
pub struct Repo {
pub cwd: PathBuf,
pub main: PathBuf,
pub config: Config,
}
impl Repo {
pub fn discover() -> Result<Self> {
let cwd = std::env::current_dir().context("failed to read the current directory")?;
let main = git::main_worktree(&cwd)?;
let config = Config::merge(Config::load_global()?, Config::load(&main)?);
Ok(Self { cwd, main, config })
}
pub fn worktrees(&self) -> Result<Vec<Worktree>> {
git::list_worktrees(&self.cwd)
}
pub fn base_dir(&self) -> Result<PathBuf> {
self.config.base_dir(&self.main)
}
pub fn worktree_path_for(&self, branch: &str) -> Result<PathBuf> {
Ok(self.base_dir()?.join(branch))
}
pub fn display_name(&self, wt: &Worktree, main: &Worktree) -> String {
if wt.path == main.path {
return MAIN_ALIAS.to_string();
}
wt.name()
}
pub fn resolve(&self, name: Option<&str>) -> Result<Worktree> {
let worktrees = self.worktrees()?;
let Some(main) = worktrees.first().cloned() else {
bail!("no worktrees found");
};
let name = match name {
None | Some(MAIN_ALIAS) => return Ok(main),
Some(name) => name.trim_end_matches('/'),
};
let base = self.base_dir()?;
let mut by_branch = Vec::new();
let mut by_relpath = Vec::new();
let mut by_dirname = Vec::new();
for wt in &worktrees {
if wt.branch.as_deref() == Some(name) {
by_branch.push(wt);
continue;
}
if wt
.path
.strip_prefix(&base)
.map(|p| p == Path::new(name))
.unwrap_or(false)
{
by_relpath.push(wt);
continue;
}
if wt.path.file_name().map(|f| f == name).unwrap_or(false) {
by_dirname.push(wt);
}
}
for candidates in [by_branch, by_relpath, by_dirname] {
match candidates.len() {
0 => continue,
1 => return Ok(candidates[0].clone()),
_ => {
let paths = candidates
.iter()
.map(|w| w.path.display().to_string())
.collect::<Vec<_>>()
.join("\n ");
bail!("`{name}` is ambiguous, it matches:\n {paths}");
}
}
}
let known = worktrees
.iter()
.map(|w| self.display_name(w, &main))
.collect::<Vec<_>>()
.join(", ");
bail!("no worktree named `{name}`. Available: {known}");
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::git::parse_worktree_list;
fn repo() -> Repo {
Repo {
cwd: PathBuf::from("/home/me/repo"),
main: PathBuf::from("/home/me/repo"),
config: Config::default(),
}
}
const PORCELAIN: &str = "\
worktree /home/me/repo
HEAD 1111111111111111111111111111111111111111
branch refs/heads/main
worktree /home/me/worktrees/feature/auth
HEAD 2222222222222222222222222222222222222222
branch refs/heads/feature/auth
worktree /home/me/worktrees/detached
HEAD 3333333333333333333333333333333333333333
detached
";
#[test]
fn parses_porcelain_output() {
let list = parse_worktree_list(PORCELAIN);
assert_eq!(list.len(), 3);
assert_eq!(list[0].branch.as_deref(), Some("main"));
assert_eq!(
list[1].path,
PathBuf::from("/home/me/worktrees/feature/auth")
);
assert!(list[2].detached);
assert_eq!(list[2].name(), "detached");
assert_eq!(list[0].short_head(), "1111111");
}
#[test]
fn worktree_path_follows_base_dir() {
assert_eq!(
repo().worktree_path_for("feature/auth").unwrap(),
PathBuf::from("/home/me/worktrees/feature/auth")
);
}
}