pub mod ci_status;
pub(crate) mod collect;
pub(crate) mod columns;
pub mod json_output;
pub(crate) mod layout;
pub mod model;
pub mod progressive;
mod progressive_table;
pub(crate) mod render;
#[cfg(test)]
mod spacing_test;
use anstyle::Style;
use anyhow::Context;
use model::{ListData, ListItem};
use progressive::RenderMode;
use worktrunk::git::Repository;
use worktrunk::styling::INFO_SYMBOL;
pub use collect::{CollectOptions, build_worktree_item, populate_item};
pub use model::StatuslineSegment;
pub fn handle_list(
repo: Repository,
format: crate::OutputFormat,
cli_branches: bool,
cli_remotes: bool,
cli_full: bool,
render_mode: RenderMode,
) -> anyhow::Result<()> {
let show_progress = match format {
crate::OutputFormat::Table | crate::OutputFormat::ClaudeCode => {
render_mode == RenderMode::Progressive
}
crate::OutputFormat::Json => false, };
let render_table = matches!(
format,
crate::OutputFormat::Table | crate::OutputFormat::ClaudeCode
);
let list_data = collect::collect(
&repo,
collect::ShowConfig::DeferredToParallel {
cli_branches,
cli_remotes,
cli_full,
},
show_progress,
render_table,
)?;
let Some(ListData { items, .. }) = list_data else {
return Ok(());
};
match format {
crate::OutputFormat::Json => {
let json_items = json_output::to_json_items(&items, &repo);
let json =
serde_json::to_string_pretty(&json_items).context("Failed to serialize to JSON")?;
println!("{}", json);
}
crate::OutputFormat::Table | crate::OutputFormat::ClaudeCode => {
}
}
Ok(())
}
#[derive(Default)]
pub(super) struct SummaryMetrics {
worktrees: usize,
local_branches: usize,
remote_branches: usize,
dirty_worktrees: usize,
ahead_items: usize,
}
impl SummaryMetrics {
pub(super) fn from_items(items: &[ListItem]) -> Self {
let mut metrics = Self::default();
for item in items {
metrics.update(item);
}
metrics
}
fn update(&mut self, item: &ListItem) {
if let Some(_data) = item.worktree_data() {
self.worktrees += 1;
if item
.status_symbols
.working_tree
.is_some_and(|wt| wt.is_dirty())
{
self.dirty_worktrees += 1;
}
} else {
if item.branch.as_ref().is_some_and(|b| b.contains('/')) {
self.remote_branches += 1;
} else {
self.local_branches += 1;
}
}
if item.counts.is_some_and(|c| c.ahead > 0) {
self.ahead_items += 1;
}
}
pub(super) fn summary_parts(
&self,
include_branches: bool,
hidden_columns: usize,
) -> Vec<String> {
let mut parts = Vec::new();
if include_branches {
parts.push(format!("{} worktrees", self.worktrees));
if self.local_branches > 0 {
parts.push(format!("{} branches", self.local_branches));
}
if self.remote_branches > 0 {
parts.push(format!("{} remote branches", self.remote_branches));
}
} else {
let plural = if self.worktrees == 1 { "" } else { "s" };
parts.push(format!("{} worktree{}", self.worktrees, plural));
}
if self.dirty_worktrees > 0 {
parts.push(format!("{} with changes", self.dirty_worktrees));
}
if self.ahead_items > 0 {
parts.push(format!("{} ahead", self.ahead_items));
}
if hidden_columns > 0 {
let plural = if hidden_columns == 1 {
"column"
} else {
"columns"
};
parts.push(format!("{} {} hidden", hidden_columns, plural));
}
parts
}
}
pub(crate) fn format_summary_message(
items: &[ListItem],
show_branches: bool,
hidden_column_count: usize,
error_count: usize,
timed_out_count: usize,
) -> String {
let metrics = SummaryMetrics::from_items(items);
let dim = Style::new().dimmed();
let summary = metrics
.summary_parts(show_branches, hidden_column_count)
.join(", ");
if error_count > 0 {
let failure_msg = if error_count == timed_out_count {
let plural = if timed_out_count == 1 { "" } else { "s" };
format!("{timed_out_count} task{plural} timed out")
} else if timed_out_count > 0 {
let plural = if error_count == 1 { "" } else { "s" };
format!("{error_count} task{plural} failed ({timed_out_count} timed out)")
} else {
let plural = if error_count == 1 { "" } else { "s" };
format!("{error_count} task{plural} failed")
};
format!("{INFO_SYMBOL} {dim}Showing {summary}. {failure_msg}{dim:#}")
} else {
format!("{INFO_SYMBOL} {dim}Showing {summary}{dim:#}")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_summary_metrics_default() {
let metrics = SummaryMetrics::default();
assert_eq!(metrics.worktrees, 0);
assert_eq!(metrics.local_branches, 0);
assert_eq!(metrics.remote_branches, 0);
assert_eq!(metrics.dirty_worktrees, 0);
assert_eq!(metrics.ahead_items, 0);
}
#[test]
fn test_summary_metrics_summary_parts_single_worktree() {
let metrics = SummaryMetrics {
worktrees: 1,
local_branches: 0,
remote_branches: 0,
dirty_worktrees: 0,
ahead_items: 0,
};
let parts = metrics.summary_parts(false, 0);
assert_eq!(parts, vec!["1 worktree"]);
}
#[test]
fn test_summary_metrics_summary_parts_multiple_worktrees() {
let metrics = SummaryMetrics {
worktrees: 3,
local_branches: 0,
remote_branches: 0,
dirty_worktrees: 0,
ahead_items: 0,
};
let parts = metrics.summary_parts(false, 0);
assert_eq!(parts, vec!["3 worktrees"]);
}
#[test]
fn test_summary_metrics_summary_parts_with_branches() {
let metrics = SummaryMetrics {
worktrees: 2,
local_branches: 5,
remote_branches: 10,
dirty_worktrees: 0,
ahead_items: 0,
};
let parts = metrics.summary_parts(true, 0);
assert_eq!(
parts,
vec!["2 worktrees", "5 branches", "10 remote branches"]
);
}
#[test]
fn test_summary_metrics_summary_parts_with_dirty() {
let metrics = SummaryMetrics {
worktrees: 3,
local_branches: 0,
remote_branches: 0,
dirty_worktrees: 2,
ahead_items: 0,
};
let parts = metrics.summary_parts(false, 0);
assert_eq!(parts, vec!["3 worktrees", "2 with changes"]);
}
#[test]
fn test_summary_metrics_summary_parts_with_ahead() {
let metrics = SummaryMetrics {
worktrees: 2,
local_branches: 0,
remote_branches: 0,
dirty_worktrees: 0,
ahead_items: 1,
};
let parts = metrics.summary_parts(false, 0);
assert_eq!(parts, vec!["2 worktrees", "1 ahead"]);
}
#[test]
fn test_summary_metrics_summary_parts_with_hidden_columns() {
let metrics = SummaryMetrics {
worktrees: 1,
local_branches: 0,
remote_branches: 0,
dirty_worktrees: 0,
ahead_items: 0,
};
let parts = metrics.summary_parts(false, 1);
assert_eq!(parts, vec!["1 worktree", "1 column hidden"]);
let parts = metrics.summary_parts(false, 3);
assert_eq!(parts, vec!["1 worktree", "3 columns hidden"]);
}
#[test]
fn test_summary_metrics_summary_parts_branches_no_local() {
let metrics = SummaryMetrics {
worktrees: 2,
local_branches: 0,
remote_branches: 5,
dirty_worktrees: 0,
ahead_items: 0,
};
let parts = metrics.summary_parts(true, 0);
assert_eq!(parts, vec!["2 worktrees", "5 remote branches"]);
}
#[test]
fn test_summary_metrics_summary_parts_all_features() {
let metrics = SummaryMetrics {
worktrees: 5,
local_branches: 3,
remote_branches: 8,
dirty_worktrees: 2,
ahead_items: 4,
};
let parts = metrics.summary_parts(true, 2);
assert_eq!(
parts,
vec![
"5 worktrees",
"3 branches",
"8 remote branches",
"2 with changes",
"4 ahead",
"2 columns hidden"
]
);
}
#[test]
fn test_format_summary_message_error_variants() {
use insta::assert_snapshot;
assert_snapshot!(format_summary_message(&[], false, 0, 0, 0), @"[2mâ—‹[22m [2mShowing 0 worktrees[0m");
assert_snapshot!(format_summary_message(&[], false, 0, 3, 3), @"[2mâ—‹[22m [2mShowing 0 worktrees. 3 tasks timed out[0m");
assert_snapshot!(format_summary_message(&[], false, 0, 5, 3), @"[2mâ—‹[22m [2mShowing 0 worktrees. 5 tasks failed (3 timed out)[0m");
assert_snapshot!(format_summary_message(&[], false, 0, 2, 0), @"[2mâ—‹[22m [2mShowing 0 worktrees. 2 tasks failed[0m");
assert_snapshot!(format_summary_message(&[], false, 0, 1, 0), @"[2mâ—‹[22m [2mShowing 0 worktrees. 1 task failed[0m");
assert_snapshot!(format_summary_message(&[], false, 0, 1, 1), @"[2mâ—‹[22m [2mShowing 0 worktrees. 1 task timed out[0m");
}
}