use anyhow::{Result, anyhow};
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use crate::config::MuxMode;
use crate::multiplexer::{Multiplexer, util};
use crate::state::StateStore;
use crate::util::canon_or_self;
use crate::{config, git, github, spinner};
use super::types::{AgentStatusSummary, WorktreeInfo};
fn filter_worktrees(
worktrees: Vec<(PathBuf, String)>,
filter: &[String],
) -> Vec<(PathBuf, String)> {
if filter.is_empty() {
return worktrees;
}
let mut matched_paths = HashSet::new();
for token in filter {
let handle_match = worktrees.iter().find(|(path, _)| {
path.file_name()
.and_then(|s| s.to_str())
.is_some_and(|name| name == token)
});
if let Some((path, _)) = handle_match {
matched_paths.insert(path.clone());
continue;
}
for (path, branch) in &worktrees {
if branch == token {
matched_paths.insert(path.clone());
}
}
}
worktrees
.into_iter()
.filter(|(path, _)| matched_paths.contains(path))
.collect()
}
pub fn list(
config: &config::Config,
mux: &dyn Multiplexer,
fetch_pr_status: bool,
filter: &[String],
) -> Result<Vec<WorktreeInfo>> {
list_in(config, mux, fetch_pr_status, filter, None)
}
pub fn list_in(
config: &config::Config,
mux: &dyn Multiplexer,
fetch_pr_status: bool,
filter: &[String],
repo: Option<&Path>,
) -> Result<Vec<WorktreeInfo>> {
if repo.is_none() && !git::is_git_repo()? {
return Err(anyhow!("Not in a git repository"));
}
let worktrees_data = git::list_worktrees_in(repo)?;
if worktrees_data.is_empty() {
return Ok(Vec::new());
}
let main_worktree_path = worktrees_data.first().map(|(p, _)| p.clone());
let worktrees_data = filter_worktrees(worktrees_data, filter);
if worktrees_data.is_empty() {
return Ok(Vec::new());
}
let mux_running = mux.is_running().unwrap_or(false);
let mux_windows: HashSet<String> = if mux_running {
mux.get_all_window_names().unwrap_or_default()
} else {
HashSet::new()
};
let mux_sessions: HashSet<String> = if mux_running {
mux.get_all_session_names().unwrap_or_default()
} else {
HashSet::new()
};
let main_branch = git::get_default_branch_in(repo).ok();
let unmerged_branches = main_branch
.as_deref()
.and_then(|main| git::get_merge_base_in(repo, main).ok())
.and_then(|base| git::get_unmerged_branches_in(repo, &base).ok())
.unwrap_or_default();
let pr_map = if fetch_pr_status {
spinner::with_spinner("Fetching PR status", || {
Ok(github::list_prs().unwrap_or_default())
})?
} else {
std::collections::HashMap::new()
};
let agent_panes = if mux_running {
StateStore::new()
.ok()
.and_then(|store| store.load_reconciled_agents(mux).ok())
.unwrap_or_default()
} else {
Vec::new()
};
let agent_panes_canon: Vec<_> = agent_panes
.iter()
.map(|a| (canon_or_self(&a.path), a.status))
.collect();
let worktree_modes = git::get_all_worktree_modes_in(repo);
let prefix = config.window_prefix();
let worktrees: Vec<WorktreeInfo> = worktrees_data
.into_iter()
.map(|(path, branch)| {
let handle = path
.file_name()
.and_then(|s| s.to_str())
.unwrap_or(&branch)
.to_string();
let prefixed_name = util::prefixed(prefix, &handle);
let mode = worktree_modes
.get(&handle)
.copied()
.unwrap_or(MuxMode::Window);
let has_mux_window = if mode == MuxMode::Session {
mux_sessions.contains(&prefixed_name)
} else {
mux_windows.contains(&prefixed_name)
};
let has_unmerged = if let Some(ref main) = main_branch {
if branch == *main || branch == "(detached)" {
false
} else {
unmerged_branches.contains(&branch)
}
} else {
false
};
let pr_info = pr_map.get(&branch).cloned();
let canon_wt_path = canon_or_self(&path);
let matching_statuses: Vec<_> = agent_panes_canon
.iter()
.filter(|(canon_agent_path, _)| {
*canon_agent_path == canon_wt_path
|| canon_agent_path.starts_with(&canon_wt_path)
})
.filter_map(|(_, status)| *status)
.collect();
let agent_status = if matching_statuses.is_empty() {
None
} else {
Some(AgentStatusSummary {
statuses: matching_statuses,
})
};
let is_main = main_worktree_path
.as_ref()
.is_some_and(|main_path| *main_path == path);
let created_at = std::fs::metadata(&path)
.ok()
.and_then(|m| m.created().ok())
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_secs());
let base_branch = git::get_branch_base_in(&branch, repo).ok();
WorktreeInfo {
handle,
branch,
path,
is_main,
mode,
has_mux_window,
has_unmerged,
pr_info,
agent_status,
created_at,
base_branch,
}
})
.collect();
Ok(worktrees)
}