Skip to content

feat: wt list --porcelain + [unadopted] indicator#35

Open
bezhermoso wants to merge 7 commits intoblock:mainfrom
bezhermoso:list.porcelain
Open

feat: wt list --porcelain + [unadopted] indicator#35
bezhermoso wants to merge 7 commits intoblock:mainfrom
bezhermoso:list.porcelain

Conversation

@bezhermoso
Copy link
Contributor

@bezhermoso bezhermoso commented Mar 17, 2026

wt list --porcelain: machine-readable worktree metadata + new indicators

Summary

  • wt list --porcelain — augmented git worktree list --porcelain with wt-specific metadata lines
  • [unadopted] indicator — surfaces unadopted worktrees in wt list and wt switch picker
  • Bug fix — stale (deleted) worktrees no longer produce warnings and blank entries in the picker

--porcelain output

Passes through native git porcelain verbatim and appends wt.* lines per entry:

worktree /Users/me/Development/java
HEAD abc123def456
branch refs/heads/master
wt.active

worktree /Users/me/Development/java-worktrees/feature-foo
HEAD def456abc123
branch refs/heads/feature/foo
wt.adopted

worktree /Users/me/Development/java-worktrees/bugfix-bar
HEAD 789abc012345
branch refs/heads/bugfix/bar

With -v, adds status metadata (slower — runs git status per worktree):

worktree /Users/me/Development/java-worktrees/feature-foo
HEAD def456abc123
branch refs/heads/feature/foo
wt.adopted
wt.dirty
wt.ahead 3
Line Mode Meaning
wt.active always Target of WT_ACTIVE_WORKTREE symlink
wt.adopted always Managed by wt (adoption marker exists)
wt.dirty -v Uncommitted changes
wt.ahead N -v Commits ahead of upstream
wt.behind N -v Commits behind upstream

What --porcelain unlocks

The structured output makes wt data composable with unix tools and custom scripts. Examples:

Custom fzf worktree picker:

wt_fzf_switch() {
  local selected
  selected=$(
    wt list --porcelain | awk '
      /^worktree /  { path = substr($0, 10); flags = "" }
      /^branch /    { branch = $2; sub("refs/heads/", "", branch) }
      /^wt\.active/ { flags = flags " *" }
      /^wt\.adopted/{ flags = flags " ✓" }
      /^$/          { if (path != "") printf "%s\t(%s)%s\n", path, branch, flags }
    ' | fzf --with-nth=1.. --delimiter='\t' \
        --preview 'git -C {1} log --oneline -10'
  )
  [[ -n "$selected" ]] && wt switch "$(echo "$selected" | cut -f1)"
}

Shell completions that show adoption status:

_wt_complete_worktrees() {
  local branches=()
  while IFS= read -r line; do
    case "$line" in
      branch\ *) branches+=("${line#branch refs/heads/}") ;;
    esac
  done < <(wt list --porcelain)
  COMPREPLY=($(compgen -W "${branches[*]}" -- "${COMP_WORDS[COMP_CWORD]}"))
}

Scripted batch adoption:

# Adopt all unadopted worktrees
wt list --porcelain | awk '
  /^worktree / { path = substr($0, 10); adopted = 0 }
  /^wt\.adopted/ { adopted = 1 }
  /^$/ { if (path != "" && !adopted) print path }
' | while read -r wt; do
  wt adopt "$wt"
done

[unadopted] indicator in nice output

Worktrees not managed by wt now show [unadopted] in wt list and the wt switch picker. Color escalates based on severity:

  /Users/me/Development/java (master) [main]
  /Users/me/Development/java-wt/feature-foo (feature/foo) [unadopted]
* /Users/me/Development/java-wt/bugfix-bar (bugfix/bar) [linked] [unadopted]
  /Users/me/Development/java-wt/feature-baz (feature/baz)
  • Yellow [unadopted] — informational: worktree exists but isn't wt-managed
  • Red [unadopted] — when combined with [linked]: you're actively working in an unmanaged worktree

Fixes

  • Stale worktrees in picker: worktrees registered in git but with deleted directories no longer produce cd: No such file or directory warnings or blank entries in the wt switch picker. They are silently skipped, matching the existing behavior in wt list.

Test plan

  • 12 new unit tests for wt_list_porcelain() (wt.active, wt.adopted, wt.dirty, wt.ahead, verbose/non-verbose modes)
  • 7 new integration tests for wt list --porcelain (output format, color absence, flag combinations)
  • 5 new integration tests for [unadopted] indicator (adopted/unadopted/main/linked+unadopted)
  • All 256 existing tests continue to pass
  • Manual: run wt list --porcelain on a real repo with multiple worktrees
  • Manual: run wt list and verify [unadopted] appears on correct entries
  • Manual: run wt switch and verify picker shows [unadopted] badges

🤖 Generated with Claude Code

bezhermoso and others added 7 commits March 16, 2026 19:01
Adds a new library function that wraps `git worktree list --porcelain`
and appends wt-specific metadata lines per entry:
- wt.active — worktree is the WT_ACTIVE_WORKTREE symlink target
- wt.adopted — adoption marker exists
- wt.dirty, wt.ahead, wt.behind — with --verbose flag

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Claude Code <noreply@anthropic.com>
Ai-assisted: true
Exposes the augmented porcelain output via `wt list --porcelain`.
Combinable with -v for verbose metadata (dirty, ahead/behind).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Claude Code <noreply@anthropic.com>
Ai-assisted: true
Unit tests for wt_list_porcelain() covering wt.active, wt.adopted,
wt.dirty, wt.ahead indicators and verbose/non-verbose modes.
Integration tests for wt-list --porcelain flag including color code
absence check.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Claude Code <noreply@anthropic.com>
Ai-assisted: true
Non-adopted worktrees now show a yellow ? prefix and [unadopted] badge.
If the worktree is both linked and unadopted, the badge escalates to
red to signal that the active worktree is missing metadata/symlinks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Claude Code <noreply@anthropic.com>
Ai-assisted: true
Applies the same ? prefix and [unadopted] badge (yellow/red) to the
interactive worktree selection menu used by wt switch.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Claude Code <noreply@anthropic.com>
Ai-assisted: true
Add missing guard on cd failure when iterating worktrees, matching the
existing pattern in wt-list. Stale worktree registrations (directory
removed but still in git's list) are now silently skipped instead of
producing a warning and a blank entry in the picker.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Claude Code <noreply@anthropic.com>
Ai-assisted: true
Only the * prefix for active/linked worktrees remains. The [unadopted]
badge is sufficient on its own.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Claude Code <noreply@anthropic.com>
Ai-assisted: true
@bezhermoso bezhermoso changed the title feat: wt list --porcelain + [unadopted] indicator feat: wt list --porcelain + [unadopted] indicator Mar 17, 2026
@bezhermoso bezhermoso marked this pull request as ready for review March 17, 2026 02:52
Copy link
Collaborator

@guodong-sq guodong-sq left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this adds a new public --porcelain flag to wt list, but the bash/zsh completion definitions do not appear to advertise it yet. It would be good to update completions so the new flag is discoverable from both wt-list and unified wt list completion paths.

(same with the help text)


# Get absolute path of main repo
local main_repo_abs
main_repo_abs="$(cd "$WT_MAIN_REPO_ROOT" && pwd)"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

main_repo_abs is still normalized with pwd, but worktree entries below now use pwd -P. If WT_MAIN_REPO_ROOT contains symlink components, the main repo won’t compare equal to its own entry, which can break exclude_main and show the main repo as [unadopted]. I think this line should use pwd -P as well so both sides are compared in physical-path form.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added two convenience helpers to lib/wt-common that standardize all the cd && pwd -P patterns across the codebase:

  • _wt_realpath PATH — Resolves the entire path including following symlinks (equivalent to readlink -f but portable). Uses a while [[ -L ]] loop + cd -P && pwd -P.
  • _wt_resolve_parent PATH — Resolves only the parent directory to its physical path, preserving the basename as-is. Useful when you need a canonical parent path but must not dereference the final component (e.g., before a -L check).

else
wt_list_porcelain
fi
exit $?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This exit is before the validations and checks. if WT_MAIN_REPO_ROOT exists but is not actually a git repo, regular wt list still errors, while porcelain mode can return success with no records because the git failure gets swallowed inside wt_list_porcelain.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think porcelain mode should preserve the same non-git validation/error behavior as the normal listing path.

Comment on lines +61 to +67
# Source wt-adopt for adoption status checks
if [[ -f "$LIB_DIR/wt-adopt" ]]; then
. "$LIB_DIR/wt-adopt"
elif [[ -f "$HOME/.wt/lib/wt-adopt" ]]; then
. "$HOME/.wt/lib/wt-adopt"
fi

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wt_source wt-adopt?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants