Skip to content
Merged
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
6 changes: 4 additions & 2 deletions docs/content/list.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ An in-progress git operation, a worktree-location attribute, or a branch with no
| `↻` | `operation_state` `"rebase"`, `"merge"`, `"cherry_pick"`, `"revert"`, `"bisect"` | A git operation is in progress; `git status` names it |
| `⊟` | `worktree.state` `"prunable"` | Prunable (worktree directory missing) |
| `⊞` | `worktree.state` `"locked"` | Locked worktree |
| `⚑` | `worktree.state` `"duplicate_branch"` | Branch checked out in more than one worktree, so `wt` resolves it to whichever git lists first; every worktree on the branch is flagged |
| `⚑` | `worktree.state` `"branch_worktree_mismatch"` | Branch name doesn't match the worktree path |
| `/` | `kind` `"branch"` | Branch without a worktree (no `worktree` object) |

Expand Down Expand Up @@ -243,6 +244,7 @@ One envelope object. Items carry independent facts; rendered strings
"committed_at": "2025-01-01T08:00:00Z"},
"worktree": {"path": "/home/user/repo.feature", "main": false, "current": true,
"previous": false, "detached": false, "branch_mismatch": false,
"duplicate_branch": false,
"changes": {"staged": false, "modified": true, "untracked": false,
"renamed": false, "deleted": false, "conflicted": false,
"diff": {"added": 10, "deleted": 2}}},
Expand Down Expand Up @@ -274,7 +276,7 @@ Item fields:
| `branch` | Branch name; null for a detached-HEAD worktree. Remote rows carry the bare name with the remote in `remote` |
| `remote` | Remote name, present only on remote-only branch rows |
| `head` | `{sha, short_sha, subject, committed_at}`; null for unborn branches. `committed_at` is RFC 3339 UTC |
| `worktree` | `{path, main, current, previous, detached, locked, prunable, branch_mismatch, operation, changes}`; absent on branch-only rows. `locked`/`prunable` are `{reason}` objects and can co-occur; `operation` is `"rebase"` or `"merge"`; `changes` holds the five working-tree flags plus `conflicted` and `diff {added, deleted}` |
| `worktree` | `{path, main, current, previous, detached, locked, prunable, branch_mismatch, duplicate_branch, operation, changes}`; absent on branch-only rows. `locked`/`prunable` are `{reason}` objects and can co-occur; `operation` is `"rebase"` or `"merge"`; `changes` holds the five working-tree flags plus `conflicted` and `diff {added, deleted}` |
| `default_branch` | Relation to the default branch: `{ahead, behind, diff, orphan, integration, merge_conflicts}`; absent on the default branch itself. `integration.reason` is one of `same_commit`, `ancestor`, `no_added_changes`, `trees_match`, `merge_adds_nothing`, `patch_id_match`; a dirty tree skips the checks, leaving `integration` null |
| `upstream` | Tracking branch: `{remote, branch, ahead, behind}`; absent when none is configured |
| `pr` | Open PR/MR: `{number, url, review, mergeable, repo}`; collected with `--full` or a listed `ci` column. `review` uses the schema 1 `ci.review_state` vocabulary; `mergeable` is false when the forge reports conflicts, null otherwise |
Expand Down Expand Up @@ -374,7 +376,7 @@ Present only for worktree-kind items. `state` is the worktree-location attribute

| Field | Type | Description |
|-------|------|-------------|
| `state` | string | `"branch_worktree_mismatch"`, `"prunable"`, or `"locked"` (absent when normal) |
| `state` | string | `"branch_worktree_mismatch"`, `"duplicate_branch"`, `"prunable"`, or `"locked"` (absent when normal) |
| `reason` | string | Reason for locked/prunable state |
| `detached` | boolean | HEAD is detached |

Expand Down
6 changes: 4 additions & 2 deletions plugins/worktrunk/skills/worktrunk/reference/list.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ An in-progress git operation, a worktree-location attribute, or a branch with no
| `↻` | `operation_state` `"rebase"`, `"merge"`, `"cherry_pick"`, `"revert"`, `"bisect"` | A git operation is in progress; `git status` names it |
| `⊟` | `worktree.state` `"prunable"` | Prunable (worktree directory missing) |
| `⊞` | `worktree.state` `"locked"` | Locked worktree |
| `⚑` | `worktree.state` `"duplicate_branch"` | Branch checked out in more than one worktree, so `wt` resolves it to whichever git lists first; every worktree on the branch is flagged |
| `⚑` | `worktree.state` `"branch_worktree_mismatch"` | Branch name doesn't match the worktree path |
| `/` | `kind` `"branch"` | Branch without a worktree (no `worktree` object) |

Expand Down Expand Up @@ -228,6 +229,7 @@ One envelope object. Items carry independent facts; rendered strings
"committed_at": "2025-01-01T08:00:00Z"},
"worktree": {"path": "/home/user/repo.feature", "main": false, "current": true,
"previous": false, "detached": false, "branch_mismatch": false,
"duplicate_branch": false,
"changes": {"staged": false, "modified": true, "untracked": false,
"renamed": false, "deleted": false, "conflicted": false,
"diff": {"added": 10, "deleted": 2}}},
Expand Down Expand Up @@ -259,7 +261,7 @@ Item fields:
| `branch` | Branch name; null for a detached-HEAD worktree. Remote rows carry the bare name with the remote in `remote` |
| `remote` | Remote name, present only on remote-only branch rows |
| `head` | `{sha, short_sha, subject, committed_at}`; null for unborn branches. `committed_at` is RFC 3339 UTC |
| `worktree` | `{path, main, current, previous, detached, locked, prunable, branch_mismatch, operation, changes}`; absent on branch-only rows. `locked`/`prunable` are `{reason}` objects and can co-occur; `operation` is `"rebase"` or `"merge"`; `changes` holds the five working-tree flags plus `conflicted` and `diff {added, deleted}` |
| `worktree` | `{path, main, current, previous, detached, locked, prunable, branch_mismatch, duplicate_branch, operation, changes}`; absent on branch-only rows. `locked`/`prunable` are `{reason}` objects and can co-occur; `operation` is `"rebase"` or `"merge"`; `changes` holds the five working-tree flags plus `conflicted` and `diff {added, deleted}` |
| `default_branch` | Relation to the default branch: `{ahead, behind, diff, orphan, integration, merge_conflicts}`; absent on the default branch itself. `integration.reason` is one of `same_commit`, `ancestor`, `no_added_changes`, `trees_match`, `merge_adds_nothing`, `patch_id_match`; a dirty tree skips the checks, leaving `integration` null |
| `upstream` | Tracking branch: `{remote, branch, ahead, behind}`; absent when none is configured |
| `pr` | Open PR/MR: `{number, url, review, mergeable, repo}`; collected with `--full` or a listed `ci` column. `review` uses the schema 1 `ci.review_state` vocabulary; `mergeable` is false when the forge reports conflicts, null otherwise |
Expand Down Expand Up @@ -395,7 +397,7 @@ Present only for worktree-kind items. `state` is the worktree-location attribute

| Field | Type | Description |
|-------|------|-------------|
| `state` | string | `"branch_worktree_mismatch"`, `"prunable"`, or `"locked"` (absent when normal) |
| `state` | string | `"branch_worktree_mismatch"`, `"duplicate_branch"`, `"prunable"`, or `"locked"` (absent when normal) |
| `reason` | string | Reason for locked/prunable state |
| `detached` | boolean | HEAD is detached |

Expand Down
6 changes: 4 additions & 2 deletions skills/worktrunk/reference/list.md

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

6 changes: 4 additions & 2 deletions src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -913,6 +913,7 @@ An in-progress git operation, a worktree-location attribute, or a branch with no
| `↻` | `operation_state` `"rebase"`, `"merge"`, `"cherry_pick"`, `"revert"`, `"bisect"` | A git operation is in progress; `git status` names it |
| `⊟` | `worktree.state` `"prunable"` | Prunable (worktree directory missing) |
| `⊞` | `worktree.state` `"locked"` | Locked worktree |
| `⚑` | `worktree.state` `"duplicate_branch"` | Branch checked out in more than one worktree, so `wt` resolves it to whichever git lists first; every worktree on the branch is flagged |
| `⚑` | `worktree.state` `"branch_worktree_mismatch"` | Branch name doesn't match the worktree path |
| `/` | `kind` `"branch"` | Branch without a worktree (no `worktree` object) |

Expand Down Expand Up @@ -984,6 +985,7 @@ One envelope object. Items carry independent facts; rendered strings
"committed_at": "2025-01-01T08:00:00Z"},
"worktree": {"path": "/home/user/repo.feature", "main": false, "current": true,
"previous": false, "detached": false, "branch_mismatch": false,
"duplicate_branch": false,
"changes": {"staged": false, "modified": true, "untracked": false,
"renamed": false, "deleted": false, "conflicted": false,
"diff": {"added": 10, "deleted": 2}}},
Expand Down Expand Up @@ -1015,7 +1017,7 @@ Item fields:
| `branch` | Branch name; null for a detached-HEAD worktree. Remote rows carry the bare name with the remote in `remote` |
| `remote` | Remote name, present only on remote-only branch rows |
| `head` | `{sha, short_sha, subject, committed_at}`; null for unborn branches. `committed_at` is RFC 3339 UTC |
| `worktree` | `{path, main, current, previous, detached, locked, prunable, branch_mismatch, operation, changes}`; absent on branch-only rows. `locked`/`prunable` are `{reason}` objects and can co-occur; `operation` is `"rebase"` or `"merge"`; `changes` holds the five working-tree flags plus `conflicted` and `diff {added, deleted}` |
| `worktree` | `{path, main, current, previous, detached, locked, prunable, branch_mismatch, duplicate_branch, operation, changes}`; absent on branch-only rows. `locked`/`prunable` are `{reason}` objects and can co-occur; `operation` is `"rebase"` or `"merge"`; `changes` holds the five working-tree flags plus `conflicted` and `diff {added, deleted}` |
| `default_branch` | Relation to the default branch: `{ahead, behind, diff, orphan, integration, merge_conflicts}`; absent on the default branch itself. `integration.reason` is one of `same_commit`, `ancestor`, `no_added_changes`, `trees_match`, `merge_adds_nothing`, `patch_id_match`; a dirty tree skips the checks, leaving `integration` null |
| `upstream` | Tracking branch: `{remote, branch, ahead, behind}`; absent when none is configured |
| `pr` | Open PR/MR: `{number, url, review, mergeable, repo}`; collected with `--full` or a listed `ci` column. `review` uses the schema 1 `ci.review_state` vocabulary; `mergeable` is false when the forge reports conflicts, null otherwise |
Expand Down Expand Up @@ -1151,7 +1153,7 @@ Present only for worktree-kind items. `state` is the worktree-location attribute

| Field | Type | Description |
|-------|------|-------------|
| `state` | string | `"branch_worktree_mismatch"`, `"prunable"`, or `"locked"` (absent when normal) |
| `state` | string | `"branch_worktree_mismatch"`, `"duplicate_branch"`, `"prunable"`, or `"locked"` (absent when normal) |
| `reason` | string | Reason for locked/prunable state |
| `detached` | boolean | HEAD is detached |

Expand Down
9 changes: 9 additions & 0 deletions src/commands/list/collect/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1132,6 +1132,11 @@ pub fn collect(
// (paths from git worktree list may differ based on symlinks or working directory)
let main_worktree_canonical = canonicalize(&main_worktree.path).ok();

// Branches living in more than one worktree. Every row on such a branch
// is flagged, including the one `wt` resolves to: that choice is git's
// listing order, so no row is the legitimate one.
let duplicated = worktrunk::git::duplicated_branches(worktrees);

// URL template already fetched in parallel join (layout needs to know if column is needed)
// Initialize worktree items with identity fields and None for computed fields
let mut all_items: Vec<ListItem> = sorted_worktrees
Expand All @@ -1158,6 +1163,10 @@ pub fn collect(
let mut worktree_data =
WorktreeData::from_worktree(wt, is_main, is_current, is_previous);
worktree_data.branch_worktree_mismatch = branch_worktree_mismatch;
worktree_data.duplicate_branch = wt
.branch
.as_deref()
.is_some_and(|branch| duplicated.contains(branch));

// URL expanded post-skeleton to minimize time-to-skeleton
ListItem {
Expand Down
5 changes: 4 additions & 1 deletion src/commands/list/json_output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,8 @@ pub struct JsonRemote {
/// Worktree-specific state
#[derive(Debug, Clone, Serialize, JsonSchema)]
pub struct JsonWorktree {
/// Worktree state: "branch_worktree_mismatch", "prunable", "locked" (absent when normal)
/// Worktree state: "branch_worktree_mismatch", "duplicate_branch",
/// "prunable", "locked" (absent when normal)
#[serde(skip_serializing_if = "Option::is_none")]
pub state: Option<&'static str>,

Expand Down Expand Up @@ -455,6 +456,7 @@ fn worktree_state_to_json(
Some(WorktreeState::BranchWorktreeMismatch) => {
return (Some("branch_worktree_mismatch"), None);
}
Some(WorktreeState::DuplicateBranch) => return (Some("duplicate_branch"), None),
Some(WorktreeState::Prunable) => return (Some("prunable"), data.prunable.clone()),
Some(WorktreeState::Locked) => return (Some("locked"), data.locked.clone()),
}
Expand Down Expand Up @@ -825,6 +827,7 @@ mod tests {
has_working_tree_conflicts: None,
git_operation: Some(None),
branch_worktree_mismatch: false,
duplicate_branch: false,
}
}

Expand Down
8 changes: 6 additions & 2 deletions src/commands/list/json_v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,8 @@ pub struct JsonHead {
}

/// Worktree facts. Location attributes (`locked`, `prunable`,
/// `branch_mismatch`) are independent fields — unlike schema 1's single
/// `state`, they can co-occur.
/// `branch_mismatch`, `duplicate_branch`) are independent fields — unlike
/// schema 1's single `state`, they can co-occur.
#[derive(Debug, Serialize, JsonSchema)]
pub struct JsonWorktreeV2 {
/// Filesystem path.
Expand Down Expand Up @@ -210,6 +210,9 @@ pub struct JsonWorktreeV2 {
/// created for.
pub branch_mismatch: bool,

/// Another worktree has the same branch checked out.
pub duplicate_branch: bool,

/// In-progress operation: `"rebase"`, `"merge"`, `"cherry_pick"`,
/// `"revert"`, or `"bisect"`; absent when none, null while unresolved.
#[serde(skip_serializing_if = "Tri::is_absent")]
Expand Down Expand Up @@ -596,6 +599,7 @@ fn json_worktree(data: &WorktreeData) -> JsonWorktreeV2 {
locked: reason(&data.locked),
prunable: reason(&data.prunable),
branch_mismatch: data.branch_worktree_mismatch,
duplicate_branch: data.duplicate_branch,
operation,
changes,
}
Expand Down
Loading
Loading