use std::collections::HashSet;
use crate::cli::ListArgs;
use crate::commands::open_session;
use crate::cx::Cx;
use crate::error::Result;
use crate::model::{SortSpec, Worktree};
use crate::output::render::RenderCtx;
use crate::output::table::render_table;
use crate::time::now_unix;
use crate::util::fuzzy;
use crate::worktree_service::{build_worktrees, sort_worktrees};
const DEFAULT_WIDTH: usize = 100;
pub(crate) fn run(cx: &mut Cx, args: &ListArgs, json: bool) -> Result<u8> {
let git = cx.git.clone();
let session = open_session(cx, git.as_ref())?;
let mut worktrees = build_worktrees(&session.repo, git.as_ref())?;
let spec = match &args.sort {
Some(field) => SortSpec::parse(field)?,
None => SortSpec::default(),
};
sort_worktrees(&mut worktrees, spec);
if let Some(filter) = &args.filter {
let haystacks: Vec<String> = worktrees.iter().map(haystack).collect();
let matching: HashSet<usize> = fuzzy::filter_indices(&haystacks, filter)
.into_iter()
.collect();
let mut index = 0;
worktrees.retain(|_| {
let keep = matching.contains(&index);
index += 1;
keep
});
}
if json {
for worktree in &worktrees {
cx.out.line(&worktree.to_json_line()?)?;
}
return Ok(0);
}
let color = cx.color_enabled(session.config.ui_color);
let width = terminal_width(cx);
let ctx = RenderCtx {
show_untracked: session.config.list_show_untracked,
now: now_unix(),
repo_root: &session.primary_root,
};
let table = render_table(&worktrees, &session.config.list_columns, &ctx, width, color);
crate::output::pager::page(cx, &table)?;
Ok(0)
}
fn haystack(worktree: &Worktree) -> String {
format!(
"{} {} {}",
worktree.branch.as_deref().unwrap_or(""),
worktree.slug.as_deref().unwrap_or(""),
worktree.path.display()
)
}
fn terminal_width(cx: &Cx) -> usize {
if cx.out.is_tty() {
crossterm::terminal::size()
.map(|(w, _)| usize::from(w))
.unwrap_or(DEFAULT_WIDTH)
} else {
DEFAULT_WIDTH
}
}
#[cfg(test)]
mod tests {
use crate::cli::ListArgs;
use crate::testutil::TestRepo;
fn args(sort: Option<&str>, filter: Option<&str>) -> ListArgs {
ListArgs {
sort: sort.map(str::to_string),
filter: filter.map(str::to_string),
}
}
#[test]
fn lists_worktrees_as_table() {
let repo = TestRepo::init();
repo.add_worktree("feature/x", "../wt-x");
let mut t = crate::testutil::test_cx(&[], repo.root().to_str().unwrap());
let code = super::run(&mut t.cx, &args(None, None), false).unwrap();
assert_eq!(code, 0);
let out = t.out.contents();
assert!(out.contains("main"));
assert!(out.contains("feature/x"));
assert!(out.lines().any(|l| l.starts_with('*')));
}
#[test]
fn json_output_one_object_per_line() {
let repo = TestRepo::init();
repo.add_worktree("feature/x", "../wt-x");
let mut t = crate::testutil::test_cx(&[], repo.root().to_str().unwrap());
super::run(&mut t.cx, &args(None, None), true).unwrap();
let out = t.out.contents();
assert_eq!(out.lines().count(), 2);
for line in out.lines() {
let _: serde_json::Value = serde_json::from_str(line).unwrap();
}
}
#[test]
fn filter_selects_subset() {
let repo = TestRepo::init();
repo.add_worktree("feature/login", "../wt-login");
repo.add_worktree("hotfix/crash", "../wt-crash");
let mut t = crate::testutil::test_cx(&[], repo.root().to_str().unwrap());
super::run(&mut t.cx, &args(None, Some("login")), true).unwrap();
let out = t.out.contents();
assert_eq!(out.lines().count(), 1);
assert!(out.contains("feature/login"));
}
#[test]
fn sort_by_branch_orders_rows() {
let repo = TestRepo::init();
repo.add_worktree("aaa", "../wt-a");
repo.add_worktree("zzz", "../wt-z");
let mut t = crate::testutil::test_cx(&[], repo.root().to_str().unwrap());
super::run(&mut t.cx, &args(Some("branch"), None), true).unwrap();
let out = t.out.contents();
let branches: Vec<String> = out
.lines()
.map(|l| {
let v: serde_json::Value = serde_json::from_str(l).unwrap();
v["branch"].as_str().unwrap_or("").to_string()
})
.collect();
assert_eq!(branches, vec!["aaa", "main", "zzz"]);
}
#[test]
fn invalid_sort_is_usage_error() {
let repo = TestRepo::init();
let mut t = crate::testutil::test_cx(&[], repo.root().to_str().unwrap());
let err = super::run(&mut t.cx, &args(Some("bogus"), None), false).unwrap_err();
assert_eq!(err.exit_code(), 2);
}
}