diff --git a/.gitignore b/.gitignore index d0b9f14d10..2c348dec72 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /target +.DS_Store docs/static/assets/ docs/demos/out/ diff --git a/Cargo.lock b/Cargo.lock index 8b706d6cd3..cd1292eb6c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3199,6 +3199,7 @@ dependencies = [ "env_logger", "etcetera", "fs2", + "glob", "home", "humantime", "ignore", diff --git a/Cargo.toml b/Cargo.toml index 005628eb88..bcb06ab70e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -126,6 +126,7 @@ schemars = { version = "1.2.1", features = ["derive"] } tempfile = "3.27" wait-timeout = "0.2" +glob = "0.3.3" [target.'cfg(unix)'.dependencies] skim = "0.20" diff --git a/dev/config.example.toml b/dev/config.example.toml index f616901682..5c051cc9a6 100644 --- a/dev/config.example.toml +++ b/dev/config.example.toml @@ -85,6 +85,7 @@ # # task-timeout-ms = 0 # Kill individual git commands after N ms; 0 disables # timeout-ms = 0 # Wall-clock budget for the entire collect phase; 0 disables +# hidden = ["tmp-*"] # [experimental] Glob patterns to hide from output # # ### Commit # diff --git a/docs/content/config.md b/docs/content/config.md index 32490a6d1e..59d4d7f6f8 100644 --- a/docs/content/config.md +++ b/docs/content/config.md @@ -166,6 +166,7 @@ remotes = false # Include remote-only branches (--remotes) task-timeout-ms = 0 # Kill individual git commands after N ms; 0 disables timeout-ms = 0 # Wall-clock budget for the entire collect phase; 0 disables +hidden = ["tmp-*"] # Glob patterns to hide from output ``` ### Commit diff --git a/docs/content/list.md b/docs/content/list.md index 17c5944282..da971c46e5 100644 --- a/docs/content/list.md +++ b/docs/content/list.md @@ -132,6 +132,21 @@ These appear across all columns while the table is loading: --- +## Filtering with hidden patterns + + + +The `[list].hidden` config hides worktrees and branches from output. Patterns are matched against both the canonical worktree path and the branch name: + +```toml +[list] +hidden = ["tmp-*", "*/scratch/*"] +``` + +A worktree or branch is hidden if any pattern matches its path or branch name. Filtering applies to worktrees, local branches (`--branches`), and remote branches (`--remotes`). The summary line shows how many items were filtered (e.g. `Showing 3 worktrees, 1 hidden`). + +--- + ## JSON output Query structured data with `--format=json`: diff --git a/skills/worktrunk/reference/config.md b/skills/worktrunk/reference/config.md index 293d2a45b1..b1d1476760 100644 --- a/skills/worktrunk/reference/config.md +++ b/skills/worktrunk/reference/config.md @@ -165,6 +165,7 @@ remotes = false # Include remote-only branches (--remotes) task-timeout-ms = 0 # Kill individual git commands after N ms; 0 disables timeout-ms = 0 # Wall-clock budget for the entire collect phase; 0 disables +hidden = ["tmp-*"] # [experimental] Glob patterns to hide from output ``` ### Commit diff --git a/skills/worktrunk/reference/list.md b/skills/worktrunk/reference/list.md index d6e1cc2281..29ab2d7965 100644 --- a/skills/worktrunk/reference/list.md +++ b/skills/worktrunk/reference/list.md @@ -122,6 +122,19 @@ These appear across all columns while the table is loading: --- +## Filtering with hidden patterns [experimental] + +The `[list].hidden` config hides worktrees and branches from output. Patterns are matched against both the canonical worktree path and the branch name: + +```toml +[list] +hidden = ["tmp-*", "*/scratch/*"] +``` + +A worktree or branch is hidden if any pattern matches its path or branch name. Filtering applies to worktrees, local branches (`--branches`), and remote branches (`--remotes`). The summary line shows how many items were filtered (e.g. `Showing 3 worktrees, 1 hidden`). + +--- + ## JSON output Query structured data with `--format=json`: diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 2ab37e6c65..efce9f5751 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -390,6 +390,7 @@ pub(crate) struct ListArgs { #[arg(long)] pub(crate) full: bool, + // TODO: consider adding a flag to show hidden items (name TBD β€” `--hidden`, `--all`, etc.) /// Show fast info immediately, update with slow info /// /// Displays local data (branches, paths, status) first, then updates @@ -771,6 +772,19 @@ These appear across all columns while the table is loading: --- +## Filtering with hidden patterns [experimental] + +The `[list].hidden` config hides worktrees and branches from output. Patterns are matched against both the canonical worktree path and the branch name: + +```toml +[list] +hidden = ["tmp-*", "*/scratch/*"] +``` + +A worktree or branch is hidden if any pattern matches its path or branch name. Filtering applies to worktrees, local branches (`--branches`), and remote branches (`--remotes`). The summary line shows how many items were filtered (e.g. `Showing 3 worktrees, 1 hidden`). + +--- + ## JSON output Query structured data with `--format=json`: @@ -1790,6 +1804,7 @@ remotes = false # Include remote-only branches (--remotes) task-timeout-ms = 0 # Kill individual git commands after N ms; 0 disables timeout-ms = 0 # Wall-clock budget for the entire collect phase; 0 disables +hidden = ["tmp-*"] # [experimental] Glob patterns to hide from output ``` ### Commit diff --git a/src/commands/list/collect/mod.rs b/src/commands/list/collect/mod.rs index a8f0f11725..7fb9108e11 100644 --- a/src/commands/list/collect/mod.rs +++ b/src/commands/list/collect/mod.rs @@ -361,59 +361,108 @@ pub fn collect( let url_template = url_template_cell.into_inner().unwrap(); // Resolve show flags: merge CLI overrides with config (warmed in parallel phase) - let (show_branches, show_remotes, skip_tasks, command_timeout, collect_deadline) = - match show_config { - ShowConfig::Resolved { - show_branches, - show_remotes, - skip_tasks, - command_timeout, - collect_deadline, - } => ( + let ( + show_branches, + show_remotes, + skip_tasks, + command_timeout, + collect_deadline, + hidden_patterns, + ) = match show_config { + ShowConfig::Resolved { + show_branches, + show_remotes, + skip_tasks, + command_timeout, + collect_deadline, + } => ( + show_branches, + show_remotes, + skip_tasks, + command_timeout, + collect_deadline, + None, + ), + ShowConfig::DeferredToParallel { + cli_branches, + cli_remotes, + cli_full, + } => { + let config = repo.config(); + let show_branches = cli_branches || config.list.branches(); + let show_remotes = cli_remotes || config.list.remotes(); + let show_full = cli_full || config.list.full(); + let skip_tasks: HashSet = if show_full { + HashSet::new() + } else { + [ + TaskKind::BranchDiff, + TaskKind::CiStatus, + TaskKind::WorkingTreeConflicts, + TaskKind::SummaryGenerate, + ] + .into_iter() + .collect() + }; + // Resolve timeouts from merged config (--full disables both) + let (command_timeout, collect_deadline) = if show_full { + (None, None) + } else { + let task_timeout = config.list.task_timeout(); + let deadline = config.list.timeout().map(|d| std::time::Instant::now() + d); + (task_timeout, deadline) + }; + let hidden_patterns = config.list.hidden.clone(); + ( show_branches, show_remotes, skip_tasks, command_timeout, collect_deadline, - ), - ShowConfig::DeferredToParallel { - cli_branches, - cli_remotes, - cli_full, - } => { - let config = repo.config(); - let show_branches = cli_branches || config.list.branches(); - let show_remotes = cli_remotes || config.list.remotes(); - let show_full = cli_full || config.list.full(); - let skip_tasks: HashSet = if show_full { - HashSet::new() - } else { - [ - TaskKind::BranchDiff, - TaskKind::CiStatus, - TaskKind::WorkingTreeConflicts, - TaskKind::SummaryGenerate, - ] - .into_iter() - .collect() - }; - // Resolve timeouts from merged config (--full disables both) - let (command_timeout, collect_deadline) = if show_full { - (None, None) - } else { - let task_timeout = config.list.task_timeout(); - let deadline = config.list.timeout().map(|d| std::time::Instant::now() + d); - (task_timeout, deadline) - }; - ( - show_branches, - show_remotes, - skip_tasks, - command_timeout, - collect_deadline, - ) - } - }; + hidden_patterns, + ) + } + }; + + // Compile hidden patterns once for use across worktrees, branches, and remotes. + // Uses string matching (not path matching) for branch names since they contain + // `/` separators but aren't filesystem paths. + let compiled_hidden: Vec = hidden_patterns + .as_ref() + .map(|p| { + p.iter() + .filter_map(|s| match glob::Pattern::new(s) { + Ok(pat) => Some(pat), + Err(e) => { + log::warn!("Invalid [list].hidden pattern {:?}: {}", s, e); + None + } + }) + .collect() + }) + .unwrap_or_default(); + let branch_hidden = + |name: &str| -> bool { compiled_hidden.iter().any(|pat| pat.matches(name)) }; + + // Filter out hidden worktrees by path or branch name + let (worktrees, hidden_worktree_count) = if compiled_hidden.is_empty() { + (worktrees, 0) + } else { + let before = worktrees.len(); + let filtered: Vec<_> = worktrees + .into_iter() + .filter(|wt| { + let path = canonicalize(&wt.path) + .ok() + .unwrap_or_else(|| wt.path.clone()); + let path_matches = compiled_hidden.iter().any(|pat| pat.matches_path(&path)); + let name_matches = wt.branch.as_deref().is_some_and(&branch_hidden); + !(path_matches || name_matches) + }) + .collect(); + let hidden = before - filtered.len(); + (filtered, hidden) + }; // Filter local branches to those without worktrees (CPU-only, no git commands) let branches_without_worktrees = if show_branches { @@ -431,6 +480,20 @@ pub fn collect( } else { Vec::new() }; + + // Filter hidden local branches + let (branches_without_worktrees, hidden_branch_count) = if compiled_hidden.is_empty() { + (branches_without_worktrees, 0) + } else { + let before = branches_without_worktrees.len(); + let filtered: Vec<_> = branches_without_worktrees + .into_iter() + .filter(|(name, _)| !branch_hidden(name)) + .collect(); + let hidden = before - filtered.len(); + (filtered, hidden) + }; + let remote_branches = if show_remotes { if let Some(result) = remote_branches_cell.into_inner() { result? @@ -442,6 +505,21 @@ pub fn collect( Vec::new() }; + // Filter hidden remote branches + let (remote_branches, hidden_remote_count) = if compiled_hidden.is_empty() { + (remote_branches, 0) + } else { + let before = remote_branches.len(); + let filtered: Vec<_> = remote_branches + .into_iter() + .filter(|(name, _)| !branch_hidden(name)) + .collect(); + let hidden = before - filtered.len(); + (filtered, hidden) + }; + + let total_hidden = hidden_worktree_count + hidden_branch_count + hidden_remote_count; + // Detect current worktree using git rev-parse --show-toplevel (via WorkingTree::root). // This correctly handles worktrees placed inside other worktrees (e.g., .worktrees/ layout) // by letting git resolve the actual worktree root rather than using prefix matching. @@ -985,6 +1063,7 @@ pub fn collect( &all_items, show_branches || show_remotes, layout.hidden_column_count, + total_hidden, error_count, timed_out_count, ), diff --git a/src/commands/list/mod.rs b/src/commands/list/mod.rs index 076652693d..0c37b5aec8 100644 --- a/src/commands/list/mod.rs +++ b/src/commands/list/mod.rs @@ -206,6 +206,7 @@ pub(super) struct SummaryMetrics { worktrees: usize, local_branches: usize, remote_branches: usize, + hidden: usize, dirty_worktrees: usize, ahead_items: usize, } @@ -266,6 +267,10 @@ impl SummaryMetrics { parts.push(format!("{} worktree{}", self.worktrees, plural)); } + if self.hidden > 0 { + parts.push(format!("{} hidden", self.hidden)); + } + if self.dirty_worktrees > 0 { parts.push(format!("{} with changes", self.dirty_worktrees)); } @@ -292,10 +297,12 @@ pub(crate) fn format_summary_message( items: &[ListItem], show_branches: bool, hidden_column_count: usize, + hidden_count: usize, error_count: usize, timed_out_count: usize, ) -> String { - let metrics = SummaryMetrics::from_items(items); + let mut metrics = SummaryMetrics::from_items(items); + metrics.hidden = hidden_count; let dim = Style::new().dimmed(); let summary = metrics .summary_parts(show_branches, hidden_column_count) @@ -331,6 +338,7 @@ mod tests { assert_eq!(metrics.worktrees, 0); assert_eq!(metrics.local_branches, 0); assert_eq!(metrics.remote_branches, 0); + assert_eq!(metrics.hidden, 0); assert_eq!(metrics.dirty_worktrees, 0); assert_eq!(metrics.ahead_items, 0); } @@ -341,6 +349,7 @@ mod tests { worktrees: 1, local_branches: 0, remote_branches: 0, + hidden: 0, dirty_worktrees: 0, ahead_items: 0, }; @@ -354,6 +363,7 @@ mod tests { worktrees: 3, local_branches: 0, remote_branches: 0, + hidden: 0, dirty_worktrees: 0, ahead_items: 0, }; @@ -367,6 +377,7 @@ mod tests { worktrees: 2, local_branches: 5, remote_branches: 10, + hidden: 0, dirty_worktrees: 0, ahead_items: 0, }; @@ -383,6 +394,7 @@ mod tests { worktrees: 3, local_branches: 0, remote_branches: 0, + hidden: 0, dirty_worktrees: 2, ahead_items: 0, }; @@ -396,6 +408,7 @@ mod tests { worktrees: 2, local_branches: 0, remote_branches: 0, + hidden: 0, dirty_worktrees: 0, ahead_items: 1, }; @@ -409,6 +422,7 @@ mod tests { worktrees: 1, local_branches: 0, remote_branches: 0, + hidden: 0, dirty_worktrees: 0, ahead_items: 0, }; @@ -425,6 +439,7 @@ mod tests { worktrees: 2, local_branches: 0, remote_branches: 5, + hidden: 0, dirty_worktrees: 0, ahead_items: 0, }; @@ -438,6 +453,7 @@ mod tests { worktrees: 5, local_branches: 3, remote_branches: 8, + hidden: 0, dirty_worktrees: 2, ahead_items: 4, }; @@ -460,16 +476,18 @@ mod tests { use insta::assert_snapshot; // No errors - assert_snapshot!(format_summary_message(&[], false, 0, 0, 0), @"β—‹ Showing 0 worktrees"); + assert_snapshot!(format_summary_message(&[], false, 0, 0, 0, 0), @"β—‹ Showing 0 worktrees"); // All timeouts - assert_snapshot!(format_summary_message(&[], false, 0, 3, 3), @"β—‹ Showing 0 worktrees. 3 tasks timed out"); + assert_snapshot!(format_summary_message(&[], false, 0, 0, 3, 3), @"β—‹ Showing 0 worktrees. 3 tasks timed out"); // Mixed errors and timeouts - assert_snapshot!(format_summary_message(&[], false, 0, 5, 3), @"β—‹ Showing 0 worktrees. 5 tasks failed (3 timed out)"); + assert_snapshot!(format_summary_message(&[], false, 0, 0, 5, 3), @"β—‹ Showing 0 worktrees. 5 tasks failed (3 timed out)"); // Only failures, no timeouts - assert_snapshot!(format_summary_message(&[], false, 0, 2, 0), @"β—‹ Showing 0 worktrees. 2 tasks failed"); + assert_snapshot!(format_summary_message(&[], false, 0, 0, 2, 0), @"β—‹ Showing 0 worktrees. 2 tasks failed"); // Single error - assert_snapshot!(format_summary_message(&[], false, 0, 1, 0), @"β—‹ Showing 0 worktrees. 1 task failed"); + assert_snapshot!(format_summary_message(&[], false, 0, 0, 1, 0), @"β—‹ Showing 0 worktrees. 1 task failed"); + // With hidden items + assert_snapshot!(format_summary_message(&[], false, 0, 3, 0, 0), @"β—‹ Showing 0 worktrees, 3 hidden"); // Single timeout - assert_snapshot!(format_summary_message(&[], false, 0, 1, 1), @"β—‹ Showing 0 worktrees. 1 task timed out"); + assert_snapshot!(format_summary_message(&[], false, 0, 0, 1, 1), @"β—‹ Showing 0 worktrees. 1 task timed out"); } } diff --git a/src/config/user/sections.rs b/src/config/user/sections.rs index 8abe076304..f8d7e85b11 100644 --- a/src/config/user/sections.rs +++ b/src/config/user/sections.rs @@ -149,6 +149,11 @@ pub struct ListConfig { /// Disabled when --full is used. Default: no budget (wait for all results). #[serde(rename = "timeout-ms", skip_serializing_if = "Option::is_none")] pub timeout_ms: Option, + + /// Glob patterns for items to hide from list output. + /// Matches against worktree paths and branch names. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub hidden: Option>, } impl ListConfig { @@ -198,6 +203,7 @@ impl Merge for ListConfig { summary: other.summary.or(self.summary), task_timeout_ms: other.task_timeout_ms.or(self.task_timeout_ms), timeout_ms: other.timeout_ms.or(self.timeout_ms), + hidden: other.hidden.clone().or_else(|| self.hidden.clone()), } } } diff --git a/src/config/user/tests.rs b/src/config/user/tests.rs index 34c78cf2a1..5360e48f1a 100644 --- a/src/config/user/tests.rs +++ b/src/config/user/tests.rs @@ -292,6 +292,7 @@ fn test_list_config_serde() { summary: None, task_timeout_ms: Some(500), timeout_ms: None, + hidden: None, }; let json = serde_json::to_string(&config).unwrap(); let parsed: ListConfig = serde_json::from_str(&json).unwrap(); @@ -533,6 +534,7 @@ fn test_merge_list_config() { summary: Some(true), task_timeout_ms: Some(1000), timeout_ms: Some(2000), + hidden: None, }; let override_config = ListConfig { full: None, // Should fall back to base @@ -541,6 +543,7 @@ fn test_merge_list_config() { summary: None, // Should fall back to base task_timeout_ms: None, // Should fall back to base timeout_ms: None, // Should fall back to base + hidden: None, }; let merged = base.merge_with(&override_config); @@ -947,6 +950,7 @@ fn test_list_config_accessor_methods_with_values() { summary: Some(true), task_timeout_ms: Some(5000), timeout_ms: Some(3000), + hidden: None, }; assert!(config.full()); assert!(config.branches()); diff --git a/tests/integration_tests/list_config.rs b/tests/integration_tests/list_config.rs index 14130a2c4e..bd9bce98f9 100644 --- a/tests/integration_tests/list_config.rs +++ b/tests/integration_tests/list_config.rs @@ -1,7 +1,8 @@ //! Tests for `wt list` command with user config use crate::common::{ - TestRepo, repo, set_temp_home_env, setup_snapshot_settings_with_home, temp_home, wt_command, + TestRepo, list_snapshots, repo, set_temp_home_env, setup_snapshot_settings_with_home, + temp_home, wt_command, }; use insta_cmd::assert_cmd_snapshot; use rstest::rstest; @@ -473,3 +474,124 @@ task-timeout-ms = 1 stderr ); } + +#[rstest] +fn test_list_config_hidden_single_glob(mut repo: TestRepo) { + // Create worktrees: one matching the hidden pattern, one not + repo.add_worktree("feature"); + repo.add_worktree("tmp-scratch"); + + // Write config with hidden pattern to the test config path + // Pattern matches worktree directory names containing "tmp-" + repo.write_test_config( + r#"worktree-path = "../{{ repo }}.{{ branch }}" + +[list] +hidden = ["*/repo.tmp-*"] +"#, + ); + + assert_cmd_snapshot!(list_snapshots::command(&repo, repo.root_path())); +} + +#[rstest] +fn test_list_config_hidden_array_globs(mut repo: TestRepo) { + // Create worktrees matching different patterns + repo.add_worktree("feature"); + repo.add_worktree("tmp-one"); + repo.add_worktree("scratch-two"); + + // Write config with multiple hidden patterns + // Patterns match worktree directory names containing "tmp-" or "scratch-" + repo.write_test_config( + r#"worktree-path = "../{{ repo }}.{{ branch }}" + +[list] +hidden = ["*/repo.tmp-*", "*/repo.scratch-*"] +"#, + ); + + assert_cmd_snapshot!(list_snapshots::command(&repo, repo.root_path())); +} + +#[rstest] +fn test_list_config_hidden_by_parent_path(mut repo: TestRepo) { + // Create worktrees in different parent directories + let temp_dir = repo.root_path().parent().unwrap(); + let scratch_dir = temp_dir.join("scratch"); + let keep_dir = temp_dir.join("keep"); + + // Worktree in "scratch" folder (should be ignored) + repo.add_worktree_at_path("feature-scratch", &scratch_dir.join("repo.feature-scratch")); + + // Worktree in "keep" folder (should be kept) + repo.add_worktree_at_path("feature-keep", &keep_dir.join("repo.feature-keep")); + + // Configure hidden pattern matching the parent directory + repo.write_test_config( + r#"worktree-path = "{{ branch }}" + +[list] +hidden = ["*/scratch/*"] +"#, + ); + + assert_cmd_snapshot!(list_snapshots::command(&repo, repo.root_path())); +} + +#[rstest] +fn test_list_config_hidden_worktree_by_branch_name(mut repo: TestRepo) { + repo.add_worktree("feature"); + repo.add_worktree("tmp-scratch"); + + // Pattern matches branch name directly (no path glob needed) + repo.write_test_config( + r#"worktree-path = "../{{ repo }}.{{ branch }}" + +[list] +hidden = ["tmp-*"] +"#, + ); + + assert_cmd_snapshot!(list_snapshots::command(&repo, repo.root_path())); +} + +#[rstest] +fn test_list_config_hidden_local_branch(repo: TestRepo) { + // Remove fixture worktrees to isolate test (keep only main worktree) + for branch in &["feature-a", "feature-b", "feature-c"] { + let worktree_path = repo + .root_path() + .parent() + .unwrap() + .join(format!("repo.{}", branch)); + if worktree_path.exists() { + let _ = repo + .git_command() + .args([ + "worktree", + "remove", + "--force", + worktree_path.to_str().unwrap(), + ]) + .run(); + } + let _ = repo.git_command().args(["branch", "-D", branch]).run(); + } + + // Create branches without worktrees + repo.run_git(&["branch", "feature"]); + repo.run_git(&["branch", "tmp-experiment"]); + + repo.write_test_config( + r#"worktree-path = "../{{ repo }}.{{ branch }}" + +[list] +hidden = ["tmp-*"] +"#, + ); + + let mut cmd = list_snapshots::command(&repo, repo.root_path()); + cmd.arg("--branches"); + assert_cmd_snapshot!(cmd); +} diff --git a/tests/snapshots/integration__integration_tests__help__help_config_create.snap b/tests/snapshots/integration__integration_tests__help__help_config_create.snap index 080399b74c..af29cfb470 100644 --- a/tests/snapshots/integration__integration_tests__help__help_config_create.snap +++ b/tests/snapshots/integration__integration_tests__help__help_config_create.snap @@ -145,6 +145,7 @@ Creates ~/.config/worktrunk/config.toml with the following content:   #   # task-timeout-ms = 0 # Kill individual git commands after N ms; 0 disables   # timeout-ms = 0 # Wall-clock budget for the entire collect phase; 0 disables +  # hidden = ["tmp-*"] # [experimental] Glob patterns to hide from output   #   # ### Commit   # diff --git a/tests/snapshots/integration__integration_tests__help__help_config_long.snap b/tests/snapshots/integration__integration_tests__help__help_config_long.snap index ca747d898a..43f55cbc11 100644 --- a/tests/snapshots/integration__integration_tests__help__help_config_long.snap +++ b/tests/snapshots/integration__integration_tests__help__help_config_long.snap @@ -191,6 +191,7 @@ Persistent flag values for wt list. Override on command line as needed.     task-timeout-ms = 0 # Kill individual git commands after N ms; 0 disables   timeout-ms = 0 # Wall-clock budget for the entire collect phase; 0 disables +  hidden = ["tmp-*"] # [experimental] Glob patterns to hide from output Commit diff --git a/tests/snapshots/integration__integration_tests__help__help_list_long.snap b/tests/snapshots/integration__integration_tests__help__help_list_long.snap index 70d2b74e5e..320203d5de 100644 --- a/tests/snapshots/integration__integration_tests__help__help_list_long.snap +++ b/tests/snapshots/integration__integration_tests__help__help_list_long.snap @@ -182,6 +182,17 @@ These appear across all columns while the table is loading: ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +Filtering with hidden patterns [experimental] + +The [list].hidden config hides worktrees and branches from output. Patterns are matched against both the canonical worktree path and the branch name: + +  [list] +  hidden = ["tmp-*", "*/scratch/*"] + +A worktree or branch is hidden if any pattern matches its path or branch name. Filtering applies to worktrees, local branches (--branches), and remote branches (--remotes). The summary line shows how many items were filtered (e.g. Showing 3 worktrees, 1 hidden). + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + JSON output Query structured data with --format=json: diff --git a/tests/snapshots/integration__integration_tests__help__help_list_narrow_80.snap b/tests/snapshots/integration__integration_tests__help__help_list_narrow_80.snap index 18f5ce1654..940a022e5d 100644 --- a/tests/snapshots/integration__integration_tests__help__help_list_narrow_80.snap +++ b/tests/snapshots/integration__integration_tests__help__help_list_narrow_80.snap @@ -204,6 +204,21 @@ These appear across all columns while the table is loading: ──────────────────────────────────────────────────────────────────────────────── +Filtering with hidden patterns [experimental] + +The [list].hidden config hides worktrees and branches from output. Patterns are +matched against both the canonical worktree path and the branch name: + +  [list] +  hidden = ["tmp-*", "*/scratch/*"] + +A worktree or branch is hidden if any pattern matches its path or branch name. +Filtering applies to worktrees, local branches (--branches), and remote branches + (--remotes). The summary line shows how many items were filtered (e.g. Showing +3 worktrees, 1 hidden). + +──────────────────────────────────────────────────────────────────────────────── + JSON output Query structured data with --format=json: diff --git a/tests/snapshots/integration__integration_tests__list_config__list_config_hidden_array_globs.snap b/tests/snapshots/integration__integration_tests__list_config__list_config_hidden_array_globs.snap new file mode 100644 index 0000000000..919454d996 --- /dev/null +++ b/tests/snapshots/integration__integration_tests__list_config__list_config_hidden_array_globs.snap @@ -0,0 +1,51 @@ +--- +source: tests/integration_tests/list_config.rs +info: + program: wt + args: + - list + env: + APPDATA: "[TEST_CONFIG_HOME]" + CLICOLOR_FORCE: "1" + COLUMNS: "500" + GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z" + GIT_COMMITTER_DATE: "2025-01-01T00:00:00Z" + GIT_CONFIG_GLOBAL: "[TEST_GIT_CONFIG]" + GIT_CONFIG_SYSTEM: /dev/null + GIT_EDITOR: "" + GIT_TERMINAL_PROMPT: "0" + HOME: "[TEST_HOME]" + LANG: C + LC_ALL: C + MOCK_CONFIG_DIR: "[MOCK_CONFIG_DIR]" + NO_COLOR: "" + PATH: "[PATH]" + PSModulePath: "" + RUST_LOG: warn + SHELL: "" + TERM: alacritty + USERPROFILE: "[TEST_HOME]" + WORKTRUNK_APPROVALS_PATH: "[TEST_APPROVALS]" + WORKTRUNK_CONFIG_PATH: "[TEST_CONFIG]" + WORKTRUNK_SYSTEM_CONFIG_PATH: "[TEST_SYSTEM_CONFIG]" + WORKTRUNK_TEST_CLAUDE_INSTALLED: "0" + WORKTRUNK_TEST_DELAYED_STREAM_MS: "-1" + WORKTRUNK_TEST_EPOCH: "1735776000" + WORKTRUNK_TEST_NUSHELL_ENV: "0" + WORKTRUNK_TEST_POWERSHELL_ENV: "0" + WORKTRUNK_TEST_SKIP_URL_HEALTH_CHECK: "1" + XDG_CONFIG_HOME: "[TEST_CONFIG_HOME]" +--- +success: true +exit_code: 0 +----- stdout ----- + Branch Status HEADΒ± main↕ Remoteβ‡… Path Commit Age Message +@ main ^| | . 05a4a45d 16h Initial commit ++ feature _ ../repo.feature 05a4a45d 16h Initial commit ++ feature-a ↑ ↑1 ../repo.feature-a 1b87d473 16h Add feature-a file ++ feature-b ↑ ↑1 ../repo.feature-b f62940fc 16h Add feature-b file ++ feature-c ↑ ↑1 ../repo.feature-c 345c7c93 16h Add feature-c file + +β—‹ Showing 5 worktrees, 2 hidden, 3 ahead + +----- stderr ----- diff --git a/tests/snapshots/integration__integration_tests__list_config__list_config_hidden_by_parent_path.snap b/tests/snapshots/integration__integration_tests__list_config__list_config_hidden_by_parent_path.snap new file mode 100644 index 0000000000..9efdaac5e1 --- /dev/null +++ b/tests/snapshots/integration__integration_tests__list_config__list_config_hidden_by_parent_path.snap @@ -0,0 +1,51 @@ +--- +source: tests/integration_tests/list_config.rs +info: + program: wt + args: + - list + env: + APPDATA: "[TEST_CONFIG_HOME]" + CLICOLOR_FORCE: "1" + COLUMNS: "500" + GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z" + GIT_COMMITTER_DATE: "2025-01-01T00:00:00Z" + GIT_CONFIG_GLOBAL: "[TEST_GIT_CONFIG]" + GIT_CONFIG_SYSTEM: /dev/null + GIT_EDITOR: "" + GIT_TERMINAL_PROMPT: "0" + HOME: "[TEST_HOME]" + LANG: C + LC_ALL: C + MOCK_CONFIG_DIR: "[MOCK_CONFIG_DIR]" + NO_COLOR: "" + PATH: "[PATH]" + PSModulePath: "" + RUST_LOG: warn + SHELL: "" + TERM: alacritty + USERPROFILE: "[TEST_HOME]" + WORKTRUNK_APPROVALS_PATH: "[TEST_APPROVALS]" + WORKTRUNK_CONFIG_PATH: "[TEST_CONFIG]" + WORKTRUNK_SYSTEM_CONFIG_PATH: "[TEST_SYSTEM_CONFIG]" + WORKTRUNK_TEST_CLAUDE_INSTALLED: "0" + WORKTRUNK_TEST_DELAYED_STREAM_MS: "-1" + WORKTRUNK_TEST_EPOCH: "1735776000" + WORKTRUNK_TEST_NUSHELL_ENV: "0" + WORKTRUNK_TEST_POWERSHELL_ENV: "0" + WORKTRUNK_TEST_SKIP_URL_HEALTH_CHECK: "1" + XDG_CONFIG_HOME: "[TEST_CONFIG_HOME]" +--- +success: true +exit_code: 0 +----- stdout ----- + Branch Status HEADΒ± main↕ Remoteβ‡… Path Commit Age Message +@ main ^| | . 05a4a45d 16h Initial commit ++ feature-keep βš‘_ ../keep/repo.feature-keep 05a4a45d 16h Initial commit ++ feature-a βš‘↑ ↑1 ../repo.feature-a 1b87d473 16h Add feature-a file ++ feature-b βš‘↑ ↑1 ../repo.feature-b f62940fc 16h Add feature-b file ++ feature-c βš‘↑ ↑1 ../repo.feature-c 345c7c93 16h Add feature-c file + +β—‹ Showing 5 worktrees, 1 hidden, 3 ahead + +----- stderr ----- diff --git a/tests/snapshots/integration__integration_tests__list_config__list_config_hidden_local_branch.snap b/tests/snapshots/integration__integration_tests__list_config__list_config_hidden_local_branch.snap new file mode 100644 index 0000000000..2c768dead0 --- /dev/null +++ b/tests/snapshots/integration__integration_tests__list_config__list_config_hidden_local_branch.snap @@ -0,0 +1,49 @@ +--- +source: tests/integration_tests/list_config.rs +info: + program: wt + args: + - list + - "--branches" + env: + APPDATA: "[TEST_CONFIG_HOME]" + CLICOLOR_FORCE: "1" + COLUMNS: "500" + GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z" + GIT_COMMITTER_DATE: "2025-01-01T00:00:00Z" + GIT_CONFIG_GLOBAL: "[TEST_GIT_CONFIG]" + GIT_CONFIG_SYSTEM: /dev/null + GIT_EDITOR: "" + GIT_TERMINAL_PROMPT: "0" + HOME: "[TEST_HOME]" + LANG: C + LC_ALL: C + MOCK_CONFIG_DIR: "[MOCK_CONFIG_DIR]" + NO_COLOR: "" + PATH: "[PATH]" + PSModulePath: "" + RUST_LOG: warn + SHELL: "" + TERM: alacritty + USERPROFILE: "[TEST_HOME]" + WORKTRUNK_APPROVALS_PATH: "[TEST_APPROVALS]" + WORKTRUNK_CONFIG_PATH: "[TEST_CONFIG]" + WORKTRUNK_SYSTEM_CONFIG_PATH: "[TEST_SYSTEM_CONFIG]" + WORKTRUNK_TEST_CLAUDE_INSTALLED: "0" + WORKTRUNK_TEST_DELAYED_STREAM_MS: "-1" + WORKTRUNK_TEST_EPOCH: "1735776000" + WORKTRUNK_TEST_NUSHELL_ENV: "0" + WORKTRUNK_TEST_POWERSHELL_ENV: "0" + WORKTRUNK_TEST_SKIP_URL_HEALTH_CHECK: "1" + XDG_CONFIG_HOME: "[TEST_CONFIG_HOME]" +--- +success: true +exit_code: 0 +----- stdout ----- + Branch Status HEADΒ± main↕ Remoteβ‡… Path Commit Age Message +@ main ^| | . 05a4a45d 16h Initial commit + feature /_ 05a4a45d 16h Initial commit + +β—‹ Showing 1 worktrees, 1 branches, 1 hidden + +----- stderr ----- diff --git a/tests/snapshots/integration__integration_tests__list_config__list_config_hidden_single_glob.snap b/tests/snapshots/integration__integration_tests__list_config__list_config_hidden_single_glob.snap new file mode 100644 index 0000000000..b55c07641c --- /dev/null +++ b/tests/snapshots/integration__integration_tests__list_config__list_config_hidden_single_glob.snap @@ -0,0 +1,51 @@ +--- +source: tests/integration_tests/list_config.rs +info: + program: wt + args: + - list + env: + APPDATA: "[TEST_CONFIG_HOME]" + CLICOLOR_FORCE: "1" + COLUMNS: "500" + GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z" + GIT_COMMITTER_DATE: "2025-01-01T00:00:00Z" + GIT_CONFIG_GLOBAL: "[TEST_GIT_CONFIG]" + GIT_CONFIG_SYSTEM: /dev/null + GIT_EDITOR: "" + GIT_TERMINAL_PROMPT: "0" + HOME: "[TEST_HOME]" + LANG: C + LC_ALL: C + MOCK_CONFIG_DIR: "[MOCK_CONFIG_DIR]" + NO_COLOR: "" + PATH: "[PATH]" + PSModulePath: "" + RUST_LOG: warn + SHELL: "" + TERM: alacritty + USERPROFILE: "[TEST_HOME]" + WORKTRUNK_APPROVALS_PATH: "[TEST_APPROVALS]" + WORKTRUNK_CONFIG_PATH: "[TEST_CONFIG]" + WORKTRUNK_SYSTEM_CONFIG_PATH: "[TEST_SYSTEM_CONFIG]" + WORKTRUNK_TEST_CLAUDE_INSTALLED: "0" + WORKTRUNK_TEST_DELAYED_STREAM_MS: "-1" + WORKTRUNK_TEST_EPOCH: "1735776000" + WORKTRUNK_TEST_NUSHELL_ENV: "0" + WORKTRUNK_TEST_POWERSHELL_ENV: "0" + WORKTRUNK_TEST_SKIP_URL_HEALTH_CHECK: "1" + XDG_CONFIG_HOME: "[TEST_CONFIG_HOME]" +--- +success: true +exit_code: 0 +----- stdout ----- + Branch Status HEADΒ± main↕ Remoteβ‡… Path Commit Age Message +@ main ^| | . 05a4a45d 16h Initial commit ++ feature _ ../repo.feature 05a4a45d 16h Initial commit ++ feature-a ↑ ↑1 ../repo.feature-a 1b87d473 16h Add feature-a file ++ feature-b ↑ ↑1 ../repo.feature-b f62940fc 16h Add feature-b file ++ feature-c ↑ ↑1 ../repo.feature-c 345c7c93 16h Add feature-c file + +β—‹ Showing 5 worktrees, 1 hidden, 3 ahead + +----- stderr ----- diff --git a/tests/snapshots/integration__integration_tests__list_config__list_config_hidden_worktree_by_branch_name.snap b/tests/snapshots/integration__integration_tests__list_config__list_config_hidden_worktree_by_branch_name.snap new file mode 100644 index 0000000000..b55c07641c --- /dev/null +++ b/tests/snapshots/integration__integration_tests__list_config__list_config_hidden_worktree_by_branch_name.snap @@ -0,0 +1,51 @@ +--- +source: tests/integration_tests/list_config.rs +info: + program: wt + args: + - list + env: + APPDATA: "[TEST_CONFIG_HOME]" + CLICOLOR_FORCE: "1" + COLUMNS: "500" + GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z" + GIT_COMMITTER_DATE: "2025-01-01T00:00:00Z" + GIT_CONFIG_GLOBAL: "[TEST_GIT_CONFIG]" + GIT_CONFIG_SYSTEM: /dev/null + GIT_EDITOR: "" + GIT_TERMINAL_PROMPT: "0" + HOME: "[TEST_HOME]" + LANG: C + LC_ALL: C + MOCK_CONFIG_DIR: "[MOCK_CONFIG_DIR]" + NO_COLOR: "" + PATH: "[PATH]" + PSModulePath: "" + RUST_LOG: warn + SHELL: "" + TERM: alacritty + USERPROFILE: "[TEST_HOME]" + WORKTRUNK_APPROVALS_PATH: "[TEST_APPROVALS]" + WORKTRUNK_CONFIG_PATH: "[TEST_CONFIG]" + WORKTRUNK_SYSTEM_CONFIG_PATH: "[TEST_SYSTEM_CONFIG]" + WORKTRUNK_TEST_CLAUDE_INSTALLED: "0" + WORKTRUNK_TEST_DELAYED_STREAM_MS: "-1" + WORKTRUNK_TEST_EPOCH: "1735776000" + WORKTRUNK_TEST_NUSHELL_ENV: "0" + WORKTRUNK_TEST_POWERSHELL_ENV: "0" + WORKTRUNK_TEST_SKIP_URL_HEALTH_CHECK: "1" + XDG_CONFIG_HOME: "[TEST_CONFIG_HOME]" +--- +success: true +exit_code: 0 +----- stdout ----- + Branch Status HEADΒ± main↕ Remoteβ‡… Path Commit Age Message +@ main ^| | . 05a4a45d 16h Initial commit ++ feature _ ../repo.feature 05a4a45d 16h Initial commit ++ feature-a ↑ ↑1 ../repo.feature-a 1b87d473 16h Add feature-a file ++ feature-b ↑ ↑1 ../repo.feature-b f62940fc 16h Add feature-b file ++ feature-c ↑ ↑1 ../repo.feature-c 345c7c93 16h Add feature-c file + +β—‹ Showing 5 worktrees, 1 hidden, 3 ahead + +----- stderr -----