Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/target
.DS_Store
docs/static/assets/
docs/demos/out/

Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions dev/config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
#
Expand Down
1 change: 1 addition & 0 deletions docs/content/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-*"] # <span class="badge-experimental"></span> Glob patterns to hide from output
```

### Commit
Expand Down
15 changes: 15 additions & 0 deletions docs/content/list.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,21 @@ These appear across all columns while the table is loading:

---

## Filtering with hidden patterns

<span class="badge-experimental"></span>

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`:
Expand Down
1 change: 1 addition & 0 deletions skills/worktrunk/reference/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions skills/worktrunk/reference/list.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`:
Expand Down
15 changes: 15 additions & 0 deletions src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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`:
Expand Down Expand Up @@ -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
Expand Down
175 changes: 127 additions & 48 deletions src/commands/list/collect/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<TaskKind> = 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<TaskKind> = 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<glob::Pattern> = 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 {
Expand All @@ -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?
Expand All @@ -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.
Expand Down Expand Up @@ -985,6 +1063,7 @@ pub fn collect(
&all_items,
show_branches || show_remotes,
layout.hidden_column_count,
total_hidden,
error_count,
timed_out_count,
),
Expand Down
Loading
Loading