diff --git a/bin/wt-list b/bin/wt-list index 158c5ea..94ca36b 100755 --- a/bin/wt-list +++ b/bin/wt-list @@ -14,12 +14,13 @@ # # Indicators: # ----------- -# * = Currently linked worktree (symlink target) -# [main] = The main repository root -# [linked] = This worktree is currently linked at WT_ACTIVE_WORKTREE -# [dirty] = Has uncommitted changes (only with -v) -# [↑N] = N commits ahead of upstream (only with -v) -# [↓N] = N commits behind upstream (only with -v) +# * = Currently linked worktree (symlink target) +# [main] = The main repository root +# [linked] = This worktree is currently linked at WT_ACTIVE_WORKTREE +# [unadopted] = Worktree not adopted by wt (yellow; red if also linked) +# [dirty] = Has uncommitted changes (only with -v) +# [↑N] = N commits ahead of upstream (only with -v) +# [↓N] = N commits behind upstream (only with -v) # # Usage: # wt-list # Fast: shows path, branch, [main], [linked] @@ -44,26 +45,35 @@ else fi wt_require_valid_config || exit 1 +# Source wt-adopt for adoption status checks +wt_source wt-adopt + usage() { cat </dev/null 2>&1; then + error "$WT_MAIN_REPO_ROOT is not a git repository or worktree." + exit 1 +fi + +# Porcelain mode: output augmented porcelain directly +if [[ "$PORCELAIN" == true ]]; then + if [[ "$VERBOSE" == true ]]; then + wt_list_porcelain --verbose + else + wt_list_porcelain + fi + exit $? +fi + +# Show context banner if contexts are configured +wt_show_context_banner + # Get absolute path of main repo (use pwd -P to resolve symlinks for consistent comparison) # Note: git worktree list stores physical paths, so we need to match that behavior MAIN_REPO_ABS="$(cd "$WT_MAIN_REPO_ROOT" && pwd -P)" @@ -96,11 +122,6 @@ LINKED_WORKTREE="$(wt_get_linked_worktree)" ( cd "$WT_MAIN_REPO_ROOT" || exit 1 - if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then - error "$WT_MAIN_REPO_ROOT is not a git repository or worktree." - exit 1 - fi - echo while IFS= read -r line; do @@ -109,14 +130,34 @@ LINKED_WORKTREE="$(wt_get_linked_worktree)" wt="${line#worktree }" wt_abs="$(cd "$wt" 2>/dev/null && pwd -P)" || continue - # Determine prefix (linked indicator) - prefix=" " + # Check adoption status (skip for main repo — adoption doesn't apply) + is_linked=false + is_unadopted=false if [[ -n "$LINKED_WORKTREE" && "$wt_abs" == "$LINKED_WORKTREE" ]]; then + is_linked=true + fi + if [[ "$wt_abs" != "$MAIN_REPO_ABS" ]] && ! wt_is_adopted "$wt_abs" 2>/dev/null; then + is_unadopted=true + fi + + # Determine prefix: * for linked, space otherwise + prefix=" " + if [[ "$is_linked" == true ]]; then prefix="${GREEN}*${NC} " fi - # Print with shared formatting - printf "%s%s\n" "$prefix" "$(wt_format_worktree "$wt_abs" "$MAIN_REPO_ABS" "$LINKED_WORKTREE" "$VERBOSE")" + # Build unadopted badge (red if also linked, yellow otherwise) + unadopted_badge="" + if [[ "$is_unadopted" == true ]]; then + if [[ "$is_linked" == true ]]; then + unadopted_badge="${RED}[unadopted]${NC} " + else + unadopted_badge="${YELLOW}[unadopted]${NC} " + fi + fi + + # Print with shared formatting + unadopted badge + printf "%s%s%s\n" "$prefix" "$(wt_format_worktree "$wt_abs" "$MAIN_REPO_ABS" "$LINKED_WORKTREE" "$VERBOSE")" "$unadopted_badge" ;; esac done < <(git worktree list --porcelain) diff --git a/completion/wt.bash b/completion/wt.bash index ac0c4c1..51120b4 100644 --- a/completion/wt.bash +++ b/completion/wt.bash @@ -213,6 +213,14 @@ _wt_cd_complete() { fi } +_wt_list_complete() { + local cur + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + + COMPREPLY+=( $(compgen -W "-v --verbose --porcelain -h --help" -- "$cur") ) +} + # --- Helper: get context list --- _wt_context_list() { local repos_dir="$HOME/.wt/repos" @@ -303,6 +311,7 @@ type wt-adopt >/dev/null 2>&1 && complete -F _wt_switch_complete wt-adopt type wt-switch >/dev/null 2>&1 && complete -F _wt_switch_complete wt-switch type wt-remove >/dev/null 2>&1 && complete -F _wt_remove_complete wt-remove type wt-cd >/dev/null 2>&1 && complete -F _wt_cd_complete wt-cd +type wt-list >/dev/null 2>&1 && complete -F _wt_list_complete wt-list type wt-context >/dev/null 2>&1 && complete -F _wt_context_complete wt-context type wt-metadata-export >/dev/null 2>&1 && complete -F _wt_metadata_export_complete wt-metadata-export type wt-metadata-import >/dev/null 2>&1 && complete -F _wt_metadata_import_complete wt-metadata-import @@ -352,6 +361,9 @@ _wt_completion_bash() { COMPREPLY+=($(compgen -W "$branches" -- "$cur")) fi ;; + list) + COMPREPLY=($(compgen -W "-v --verbose --porcelain -h --help" -- "$cur")) + ;; context) local contexts contexts="$(_wt_context_list)" diff --git a/completion/wt.zsh b/completion/wt.zsh index b8288c7..6d28f33 100644 --- a/completion/wt.zsh +++ b/completion/wt.zsh @@ -123,6 +123,14 @@ _wt_cd() { esac } +# Completion for wt-list / wt list: flags +_wt_list() { + _arguments \ + '(-v --verbose)'{-v,--verbose}'[Show dirty/ahead/behind status]' \ + '--porcelain[Machine-readable output]' \ + '(-h --help)'{-h,--help}'[Show help]' +} + # Completion for wt-context / wt context _wt_context() { local context state @@ -298,6 +306,7 @@ compdef _wt_switch wt-adopt compdef _wt_switch wt-switch compdef _wt_remove wt-remove compdef _wt_cd wt-cd +compdef _wt_list wt-list compdef _wt_context wt-context compdef _wt_metadata_export wt-metadata-export compdef _wt_metadata_import wt-metadata-import @@ -348,6 +357,7 @@ _wt_completion() { adopt) _wt_switch ;; switch|cd) _wt_switch ;; remove) _wt_remove ;; + list) _wt_list ;; context) _wt_context ;; metadata-export|ijwb-export) _wt_metadata_export ;; metadata-import|ijwb-import) _wt_metadata_import ;; diff --git a/lib/wt-choose b/lib/wt-choose index 681cd25..48b53a8 100644 --- a/lib/wt-choose +++ b/lib/wt-choose @@ -58,6 +58,9 @@ else exit 1 fi +# Source wt-adopt for adoption status checks +wt_source wt-adopt + # Interactively pick a worktree from WT_MAIN_REPO_ROOT # Runs in a subshell to avoid changing the caller's working directory # Args: $1 = "exclude_main" to exclude the main repository from selection @@ -79,9 +82,10 @@ select_git_worktree() { exit 1 fi - # Get absolute path of main repo + # Get absolute path of main repo (use pwd -P so symlinks in the path + # resolve to the physical form matching worktree entries below) local main_repo_abs - main_repo_abs="$(cd "$WT_MAIN_REPO_ROOT" && pwd)" + main_repo_abs="$(cd "$WT_MAIN_REPO_ROOT" && pwd -P)" # Get currently linked worktree for display local linked_worktree @@ -94,7 +98,7 @@ select_git_worktree() { case "$line" in worktree\ *) wt="${line#worktree }" - wt_abs="$(cd "$wt" && pwd)" + wt_abs="$(cd "$wt" 2>/dev/null && pwd -P)" || continue # Skip main repo if exclude_main is set if [[ "$exclude_main" != "exclude_main" || "$wt_abs" != "$main_repo_abs" ]]; then WORKTREES[${#WORKTREES[@]}]="$wt_abs" @@ -116,13 +120,32 @@ select_git_worktree() { local i=1 for wt in "${WORKTREES[@]}"; do - # Determine prefix (* for currently linked worktree) - local prefix=" " + local is_linked=false is_unadopted=false if [[ -n "$linked_worktree" && "$wt" == "$linked_worktree" ]]; then + is_linked=true + fi + if [[ "$wt" != "$main_repo_abs" ]] && ! wt_is_adopted "$wt" 2>/dev/null; then + is_unadopted=true + fi + + # Determine prefix: * for linked, space otherwise + local prefix=" " + if [[ "$is_linked" == true ]]; then prefix="${GREEN}*${NC} " fi + + # Build unadopted badge (red if also linked, yellow otherwise) + local unadopted_badge="" + if [[ "$is_unadopted" == true ]]; then + if [[ "$is_linked" == true ]]; then + unadopted_badge="${RED}[unadopted]${NC} " + else + unadopted_badge="${YELLOW}[unadopted]${NC} " + fi + fi + # Use shared formatting with number prefix (fast mode - no status checks) - printf "%s%2d) %s\n" "$prefix" "$i" "$(wt_format_worktree "$wt" "$main_repo_abs" "$linked_worktree" "false")" >&2 + printf "%s%2d) %s%s\n" "$prefix" "$i" "$(wt_format_worktree "$wt" "$main_repo_abs" "$linked_worktree" "false")" "$unadopted_badge" >&2 i=$((i + 1)) done diff --git a/lib/wt-common b/lib/wt-common index 19a2bc5..d58bcee 100644 --- a/lib/wt-common +++ b/lib/wt-common @@ -576,6 +576,129 @@ wt_format_worktree() { printf "${BOLD}%s${NC} ${BLUE}(%s)${NC} %s" "$wt_abs" "$branch" "$indicators" } +# ───────────────────────────────────────────────────────────────────────────── +# Porcelain listing +# ───────────────────────────────────────────────────────────────────────────── + +# Output augmented porcelain listing of all worktrees. +# Passes through all native `git worktree list --porcelain` lines verbatim, +# then appends wt.* lines per entry before the blank separator. +# +# wt-specific lines: +# wt.active — this worktree is the WT_ACTIVE_WORKTREE symlink target +# wt.adopted — the adoption marker file exists +# wt.dirty — has uncommitted changes (only with --verbose) +# wt.ahead N — N commits ahead of upstream (only with --verbose) +# wt.behind N — N commits behind upstream (only with --verbose) +# +# Usage: wt_list_porcelain [--verbose] +# Outputs: augmented porcelain to stdout +wt_list_porcelain() { + local verbose=false + while [[ $# -gt 0 ]]; do + case "$1" in + --verbose) verbose=true ;; + *) ;; + esac + shift + done + + # Pre-resolve paths for comparison + local main_repo_abs + main_repo_abs="$(cd "$WT_MAIN_REPO_ROOT" && pwd -P)" || return 1 + + local active_worktree + active_worktree="$(wt_get_linked_worktree)" + + # Ensure wt_is_adopted is available + if ! declare -f wt_is_adopted >/dev/null 2>&1; then + local lib_dir="${LIB_DIR:-}" + if [[ -n "$lib_dir" && -f "$lib_dir/wt-adopt" ]]; then + . "$lib_dir/wt-adopt" + elif [[ -f "$HOME/.wt/lib/wt-adopt" ]]; then + . "$HOME/.wt/lib/wt-adopt" + fi + fi + + local wt_path="" branch="" is_detached=false + local in_entry=false + + while IFS= read -r line || [[ -n "$line" ]]; do + if [[ -z "$line" ]]; then + # End of entry — append wt.* lines, then blank separator + if [[ "$in_entry" == true ]]; then + local wt_abs + wt_abs="$(cd "$wt_path" 2>/dev/null && pwd -P)" || { echo; in_entry=false; continue; } + + # wt.active + if [[ -n "$active_worktree" && "$wt_abs" == "$active_worktree" ]]; then + echo "wt.active" + fi + + # wt.adopted + if declare -f wt_is_adopted >/dev/null 2>&1 && wt_is_adopted "$wt_abs" 2>/dev/null; then + echo "wt.adopted" + fi + + # Verbose: dirty, ahead/behind + if [[ "$verbose" == true ]]; then + if wt_has_uncommitted_changes "$wt_abs"; then + echo "wt.dirty" + fi + + if [[ "$is_detached" != true && -n "$branch" ]]; then + local short_branch="${branch#refs/heads/}" + local upstream + upstream="$(git -C "$wt_abs" rev-parse --abbrev-ref "@{upstream}" 2>/dev/null || true)" + if [[ -n "$upstream" ]]; then + local counts + counts="$(git -C "$wt_abs" rev-list --left-right --count "$short_branch...$upstream" 2>/dev/null || true)" + if [[ -n "$counts" ]]; then + local ahead behind + ahead="$(echo "$counts" | cut -f1)" + behind="$(echo "$counts" | cut -f2)" + [[ "$ahead" -gt 0 ]] && echo "wt.ahead $ahead" + [[ "$behind" -gt 0 ]] && echo "wt.behind $behind" + fi + fi + fi + fi + + echo # blank separator + in_entry=false + fi + continue + fi + + case "$line" in + worktree\ *) + wt_path="${line#worktree }" + branch="" + is_detached=false + in_entry=true + echo "$line" + ;; + branch\ *) + branch="${line#branch }" + echo "$line" + ;; + detached) + is_detached=true + echo "$line" + ;; + *) + # Pass through HEAD, bare, locked, prunable, etc. + echo "$line" + ;; + esac + done < <(git -C "$main_repo_abs" worktree list --porcelain; echo) + # The trailing `echo` ensures the last entry's blank line is processed +} + +# ───────────────────────────────────────────────────────────────────────────── +# Worktree status helpers +# ───────────────────────────────────────────────────────────────────────────── + # Check if a worktree has uncommitted changes # Usage: wt_has_uncommitted_changes # Returns: 0 if uncommitted changes exist, 1 if clean diff --git a/lib/wt-help b/lib/wt-help index dc5171e..60a2f07 100644 --- a/lib/wt-help +++ b/lib/wt-help @@ -27,8 +27,9 @@ Worktree Commands: remove [worktree] Remove a worktree - interactive worktree selection if omitted - use --merged to remove all merged-branch worktrees - list [-v] List all worktrees with status + list [-v] [--porcelain] List all worktrees with status - use -v for dirty/ahead/behind indicators + - use --porcelain for machine-readable output cd [worktree] Change directory to a worktree - interactive worktree selection if omitted @@ -60,6 +61,7 @@ Examples: wt cd # Interactive: cd to selected worktree wt list # Show all worktrees (fast) wt list -v # Show worktrees with dirty/ahead/behind + wt list --porcelain # Machine-readable output (for scripts) wt remove # Interactive: remove a worktree wt remove --merged # Remove all worktrees with merged branches diff --git a/test/integration/wt-list.bats b/test/integration/wt-list.bats index 6e735dd..8f72550 100644 --- a/test/integration/wt-list.bats +++ b/test/integration/wt-list.bats @@ -163,6 +163,163 @@ teardown() { # Edge cases # ============================================================================= +# ============================================================================= +# Unadopted indicator tests +# ============================================================================= + +@test "wt-list shows [unadopted] for non-adopted worktree" { + create_branch "$REPO" "feature-raw" + local wt_path="$WT_WORKTREES_BASE/feature-raw" + create_worktree "$REPO" "$wt_path" "feature-raw" + # Worktree is NOT adopted — no wt_mark_adopted call + + run "$TEST_HOME/.wt/bin/wt-list" + assert_success + + local raw_line + raw_line=$(echo "$output" | grep "feature-raw" | head -1) + [[ "$raw_line" == *"[unadopted]"* ]] || fail "Expected [unadopted] on unadopted worktree line, got: $raw_line" +} + +@test "wt-list does not show [unadopted] for adopted worktree" { + source "$TEST_HOME/.wt/lib/wt-adopt" + + create_branch "$REPO" "feature-adopted" + local wt_path="$WT_WORKTREES_BASE/feature-adopted" + create_worktree "$REPO" "$wt_path" "feature-adopted" + local norm_wt_path + norm_wt_path="$(cd "$wt_path" && pwd -P)" + wt_mark_adopted "$norm_wt_path" + + run "$TEST_HOME/.wt/bin/wt-list" + assert_success + refute_output --partial "[unadopted]" +} + +@test "wt-list does not show [unadopted] for main repo" { + run "$TEST_HOME/.wt/bin/wt-list" + assert_success + + local main_line + main_line=$(echo "$output" | grep -F "$REPO" | grep "\[main\]" | head -1) + [[ "$main_line" != *"[unadopted]"* ]] || fail "Main repo should not show [unadopted], got: $main_line" +} + +@test "wt-list shows * prefix and [unadopted] for linked unadopted worktree" { + create_branch "$REPO" "feature-linked-raw" + local wt_path="$WT_WORKTREES_BASE/feature-linked-raw" + create_worktree "$REPO" "$wt_path" "feature-linked-raw" + local norm_wt_path + norm_wt_path="$(cd "$wt_path" && pwd -P)" + + # Link but don't adopt + ln -s "$norm_wt_path" "$WT_ACTIVE_WORKTREE" + + run "$TEST_HOME/.wt/bin/wt-list" + assert_success + + local linked_line + linked_line=$(echo "$output" | grep "feature-linked-raw" | head -1) + [[ "$linked_line" == *"*"* ]] || fail "Expected * prefix on linked line, got: $linked_line" + [[ "$linked_line" == *"[linked]"* ]] || fail "Expected [linked] on linked line, got: $linked_line" + [[ "$linked_line" == *"[unadopted]"* ]] || fail "Expected [unadopted] on linked unadopted line, got: $linked_line" +} + +# ============================================================================= +# Porcelain mode tests (--porcelain) +# ============================================================================= + +@test "wt-list --porcelain outputs worktree lines" { + run "$TEST_HOME/.wt/bin/wt-list" --porcelain + assert_success + assert_output --partial "worktree $REPO" + assert_output --partial "branch refs/heads/main" +} + +@test "wt-list --porcelain includes wt.adopted for adopted worktrees" { + source "$TEST_HOME/.wt/lib/wt-adopt" + + create_branch "$REPO" "feature-adopted" + local wt_path="$WT_WORKTREES_BASE/feature-adopted" + create_worktree "$REPO" "$wt_path" "feature-adopted" + local norm_wt_path + norm_wt_path="$(cd "$wt_path" && pwd -P)" + wt_mark_adopted "$norm_wt_path" + + run "$TEST_HOME/.wt/bin/wt-list" --porcelain + assert_success + assert_output --partial "wt.adopted" +} + +@test "wt-list --porcelain includes wt.active when active symlink exists" { + create_branch "$REPO" "feature-active" + local wt_path="$WT_WORKTREES_BASE/feature-active" + create_worktree "$REPO" "$wt_path" "feature-active" + local norm_wt_path + norm_wt_path="$(cd "$wt_path" && pwd -P)" + + ln -s "$norm_wt_path" "$WT_ACTIVE_WORKTREE" + + run "$TEST_HOME/.wt/bin/wt-list" --porcelain + assert_success + assert_output --partial "wt.active" +} + +@test "wt-list --porcelain does not include color codes" { + run "$TEST_HOME/.wt/bin/wt-list" --porcelain + assert_success + + # ANSI escape codes start with \033[ or \e[ + if echo "$output" | grep -qP '\033\['; then + fail "Porcelain output should not contain ANSI color codes" + fi +} + +@test "wt-list --porcelain -v includes wt.dirty for dirty worktree" { + make_repo_dirty "$REPO" + + run "$TEST_HOME/.wt/bin/wt-list" --porcelain -v + assert_success + assert_output --partial "wt.dirty" +} + +@test "wt-list --porcelain -v does not include wt.dirty for clean worktree" { + run "$TEST_HOME/.wt/bin/wt-list" --porcelain -v + assert_success + refute_output --partial "wt.dirty" +} + +@test "wt-list --porcelain shows --porcelain in help" { + run "$TEST_HOME/.wt/bin/wt-list" -h + assert_success + assert_output --partial "--porcelain" +} + +@test "wt-list --porcelain errors when WT_MAIN_REPO_ROOT does not exist" { + rm -f "$TEST_HOME/.wt/current" + export WT_MAIN_REPO_ROOT="/nonexistent/path" + + run "$TEST_HOME/.wt/bin/wt-list" --porcelain + assert_failure + assert_output --partial "does not exist" +} + +@test "wt-list --porcelain errors when WT_MAIN_REPO_ROOT is not a git repo" { + local not_git="$BATS_TEST_TMPDIR/not-a-git-repo" + mkdir -p "$not_git" + + rm -f "$TEST_HOME/.wt/current" + export WT_MAIN_REPO_ROOT="$not_git" + + run "$TEST_HOME/.wt/bin/wt-list" --porcelain + assert_failure + assert_output --partial "not a git" +} + +# ============================================================================= +# Edge cases +# ============================================================================= + @test "wt-list handles worktree with special characters in path" { # Create a branch and worktree with spaces in directory name create_branch "$REPO" "feature-spaces" diff --git a/test/unit/wt-list-porcelain.bats b/test/unit/wt-list-porcelain.bats new file mode 100644 index 0000000..e713e67 --- /dev/null +++ b/test/unit/wt-list-porcelain.bats @@ -0,0 +1,175 @@ +#!/usr/bin/env bats + +# Unit tests for wt_list_porcelain (lib/wt-common) + +setup() { + load '../test_helper/common' + setup_test_env + source "$TEST_HOME/.wt/lib/wt-common" + source "$TEST_HOME/.wt/lib/wt-adopt" + + REPO=$(create_mock_repo "$BATS_TEST_TMPDIR/repo") + + create_test_context "test" "$REPO" + load_test_context "test" +} + +teardown() { + teardown_test_env +} + +# ============================================================================= +# Basic porcelain output +# ============================================================================= + +@test "wt_list_porcelain outputs native git porcelain lines verbatim" { + run wt_list_porcelain + assert_success + + # Should contain the worktree line with the repo path + assert_output --partial "worktree $REPO" + # Should contain HEAD line + assert_output --partial "HEAD " + # Should contain branch line + assert_output --partial "branch refs/heads/main" +} + +@test "wt_list_porcelain first entry is the main repo" { + create_branch "$REPO" "feature-1" + create_worktree "$REPO" "$WT_WORKTREES_BASE/feature-1" "feature-1" + + run wt_list_porcelain + assert_success + + # First worktree line should be the main repo + local first_worktree + first_worktree=$(echo "$output" | grep "^worktree " | head -1) + [[ "$first_worktree" == "worktree $REPO" ]] || fail "Expected first entry to be main repo, got: $first_worktree" +} + +@test "wt_list_porcelain entries separated by blank lines" { + create_branch "$REPO" "feature-1" + create_worktree "$REPO" "$WT_WORKTREES_BASE/feature-1" "feature-1" + + run wt_list_porcelain + assert_success + + # Should have exactly 2 worktree entries + local count + count=$(echo "$output" | grep -c "^worktree ") + [[ "$count" -eq 2 ]] || fail "Expected 2 worktree entries, got: $count" +} + +# ============================================================================= +# wt.active indicator +# ============================================================================= + +@test "wt_list_porcelain includes wt.active for active worktree" { + create_branch "$REPO" "feature-active" + local wt_path="$WT_WORKTREES_BASE/feature-active" + create_worktree "$REPO" "$wt_path" "feature-active" + local norm_wt_path + norm_wt_path="$(cd "$wt_path" && pwd -P)" + + # Create the active symlink + ln -s "$norm_wt_path" "$WT_ACTIVE_WORKTREE" + + run wt_list_porcelain + assert_success + assert_output --partial "wt.active" + + # wt.active should appear in the feature-active entry, not the main repo entry + local active_entry + active_entry=$(echo "$output" | awk "/^worktree .*feature-active/,/^$/" | head -10) + [[ "$active_entry" == *"wt.active"* ]] || fail "Expected wt.active in feature-active entry, got: $active_entry" +} + +@test "wt_list_porcelain omits wt.active when no symlink exists" { + run wt_list_porcelain + assert_success + refute_output --partial "wt.active" +} + +# ============================================================================= +# wt.adopted indicator +# ============================================================================= + +@test "wt_list_porcelain includes wt.adopted for adopted worktrees" { + create_branch "$REPO" "feature-adopted" + local wt_path="$WT_WORKTREES_BASE/feature-adopted" + create_worktree "$REPO" "$wt_path" "feature-adopted" + local norm_wt_path + norm_wt_path="$(cd "$wt_path" && pwd -P)" + + # Mark as adopted + wt_mark_adopted "$norm_wt_path" + + run wt_list_porcelain + assert_success + + # wt.adopted should appear in the adopted entry + local adopted_entry + adopted_entry=$(echo "$output" | awk "/^worktree .*feature-adopted/,/^$/" | head -10) + [[ "$adopted_entry" == *"wt.adopted"* ]] || fail "Expected wt.adopted in feature-adopted entry, got: $adopted_entry" +} + +@test "wt_list_porcelain omits wt.adopted for non-adopted worktrees" { + create_branch "$REPO" "feature-fresh" + create_worktree "$REPO" "$WT_WORKTREES_BASE/feature-fresh" "feature-fresh" + + run wt_list_porcelain + assert_success + refute_output --partial "wt.adopted" +} + +# ============================================================================= +# Verbose mode +# ============================================================================= + +@test "wt_list_porcelain without --verbose omits wt.dirty" { + make_repo_dirty "$REPO" + + run wt_list_porcelain + assert_success + refute_output --partial "wt.dirty" +} + +@test "wt_list_porcelain --verbose includes wt.dirty for dirty worktree" { + make_repo_dirty "$REPO" + + run wt_list_porcelain --verbose + assert_success + assert_output --partial "wt.dirty" +} + +@test "wt_list_porcelain --verbose omits wt.dirty for clean worktree" { + # Repo is clean by default + run wt_list_porcelain --verbose + assert_success + refute_output --partial "wt.dirty" +} + +@test "wt_list_porcelain without --verbose omits wt.ahead and wt.behind" { + REPO_WITH_REMOTE=$(create_mock_repo_with_remote "$BATS_TEST_TMPDIR/repo-remote") + export WT_MAIN_REPO_ROOT="$REPO_WITH_REMOTE" + + # Create a local commit ahead of origin + (cd "$REPO_WITH_REMOTE" && echo "new" >> file.txt && git add file.txt && git commit -m "ahead") >/dev/null 2>&1 + + run wt_list_porcelain + assert_success + refute_output --partial "wt.ahead" + refute_output --partial "wt.behind" +} + +@test "wt_list_porcelain --verbose includes wt.ahead when ahead of upstream" { + REPO_WITH_REMOTE=$(create_mock_repo_with_remote "$BATS_TEST_TMPDIR/repo-remote") + export WT_MAIN_REPO_ROOT="$REPO_WITH_REMOTE" + + # Create a local commit ahead of origin + (cd "$REPO_WITH_REMOTE" && echo "new" >> file.txt && git add file.txt && git commit -m "ahead") >/dev/null 2>&1 + + run wt_list_porcelain --verbose + assert_success + assert_output --partial "wt.ahead 1" +}