erk 0.4.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- erk/__init__.py +12 -0
- erk/__main__.py +6 -0
- erk/agent_docs/__init__.py +5 -0
- erk/agent_docs/models.py +123 -0
- erk/agent_docs/operations.py +666 -0
- erk/artifacts/__init__.py +5 -0
- erk/artifacts/artifact_health.py +623 -0
- erk/artifacts/detection.py +16 -0
- erk/artifacts/discovery.py +343 -0
- erk/artifacts/models.py +63 -0
- erk/artifacts/staleness.py +56 -0
- erk/artifacts/state.py +100 -0
- erk/artifacts/sync.py +624 -0
- erk/cli/__init__.py +0 -0
- erk/cli/activation.py +132 -0
- erk/cli/alias.py +53 -0
- erk/cli/cli.py +221 -0
- erk/cli/commands/__init__.py +0 -0
- erk/cli/commands/admin.py +153 -0
- erk/cli/commands/artifact/__init__.py +1 -0
- erk/cli/commands/artifact/check.py +260 -0
- erk/cli/commands/artifact/group.py +31 -0
- erk/cli/commands/artifact/list_cmd.py +89 -0
- erk/cli/commands/artifact/show.py +62 -0
- erk/cli/commands/artifact/sync_cmd.py +39 -0
- erk/cli/commands/branch/__init__.py +26 -0
- erk/cli/commands/branch/assign_cmd.py +152 -0
- erk/cli/commands/branch/checkout_cmd.py +357 -0
- erk/cli/commands/branch/create_cmd.py +161 -0
- erk/cli/commands/branch/list_cmd.py +82 -0
- erk/cli/commands/branch/unassign_cmd.py +197 -0
- erk/cli/commands/cc/__init__.py +15 -0
- erk/cli/commands/cc/jsonl_cmd.py +20 -0
- erk/cli/commands/cc/session/AGENTS.md +30 -0
- erk/cli/commands/cc/session/CLAUDE.md +1 -0
- erk/cli/commands/cc/session/__init__.py +15 -0
- erk/cli/commands/cc/session/list_cmd.py +167 -0
- erk/cli/commands/cc/session/show_cmd.py +175 -0
- erk/cli/commands/completion.py +89 -0
- erk/cli/commands/completions.py +165 -0
- erk/cli/commands/config.py +327 -0
- erk/cli/commands/docs/__init__.py +1 -0
- erk/cli/commands/docs/group.py +16 -0
- erk/cli/commands/docs/sync.py +121 -0
- erk/cli/commands/docs/validate.py +102 -0
- erk/cli/commands/doctor.py +243 -0
- erk/cli/commands/down.py +171 -0
- erk/cli/commands/exec/__init__.py +1 -0
- erk/cli/commands/exec/group.py +164 -0
- erk/cli/commands/exec/scripts/AGENTS.md +79 -0
- erk/cli/commands/exec/scripts/CLAUDE.md +1 -0
- erk/cli/commands/exec/scripts/__init__.py +5 -0
- erk/cli/commands/exec/scripts/add_reaction_to_comment.py +69 -0
- erk/cli/commands/exec/scripts/add_remote_execution_note.py +68 -0
- erk/cli/commands/exec/scripts/check_impl.py +152 -0
- erk/cli/commands/exec/scripts/ci_update_pr_body.py +294 -0
- erk/cli/commands/exec/scripts/create_extraction_branch.py +138 -0
- erk/cli/commands/exec/scripts/create_extraction_plan.py +242 -0
- erk/cli/commands/exec/scripts/create_issue_from_session.py +103 -0
- erk/cli/commands/exec/scripts/create_plan_from_context.py +103 -0
- erk/cli/commands/exec/scripts/create_worker_impl_from_issue.py +93 -0
- erk/cli/commands/exec/scripts/detect_trunk_branch.py +121 -0
- erk/cli/commands/exec/scripts/exit_plan_mode_hook.py +777 -0
- erk/cli/commands/exec/scripts/extract_latest_plan.py +49 -0
- erk/cli/commands/exec/scripts/extract_session_from_issue.py +150 -0
- erk/cli/commands/exec/scripts/find_project_dir.py +214 -0
- erk/cli/commands/exec/scripts/generate_pr_summary.py +112 -0
- erk/cli/commands/exec/scripts/get_closing_text.py +98 -0
- erk/cli/commands/exec/scripts/get_embedded_prompt.py +62 -0
- erk/cli/commands/exec/scripts/get_plan_metadata.py +95 -0
- erk/cli/commands/exec/scripts/get_pr_body_footer.py +70 -0
- erk/cli/commands/exec/scripts/get_pr_discussion_comments.py +149 -0
- erk/cli/commands/exec/scripts/get_pr_review_comments.py +155 -0
- erk/cli/commands/exec/scripts/impl_init.py +158 -0
- erk/cli/commands/exec/scripts/impl_signal.py +375 -0
- erk/cli/commands/exec/scripts/impl_verify.py +49 -0
- erk/cli/commands/exec/scripts/issue_title_to_filename.py +34 -0
- erk/cli/commands/exec/scripts/list_sessions.py +296 -0
- erk/cli/commands/exec/scripts/mark_impl_ended.py +188 -0
- erk/cli/commands/exec/scripts/mark_impl_started.py +188 -0
- erk/cli/commands/exec/scripts/marker.py +163 -0
- erk/cli/commands/exec/scripts/objective_save_to_issue.py +109 -0
- erk/cli/commands/exec/scripts/plan_save_to_issue.py +269 -0
- erk/cli/commands/exec/scripts/plan_update_issue.py +147 -0
- erk/cli/commands/exec/scripts/post_extraction_comment.py +237 -0
- erk/cli/commands/exec/scripts/post_or_update_pr_summary.py +133 -0
- erk/cli/commands/exec/scripts/post_pr_inline_comment.py +143 -0
- erk/cli/commands/exec/scripts/post_workflow_started_comment.py +168 -0
- erk/cli/commands/exec/scripts/preprocess_session.py +777 -0
- erk/cli/commands/exec/scripts/quick_submit.py +32 -0
- erk/cli/commands/exec/scripts/rebase_with_conflict_resolution.py +260 -0
- erk/cli/commands/exec/scripts/reply_to_discussion_comment.py +173 -0
- erk/cli/commands/exec/scripts/resolve_review_thread.py +170 -0
- erk/cli/commands/exec/scripts/session_id_injector_hook.py +52 -0
- erk/cli/commands/exec/scripts/setup_impl_from_issue.py +159 -0
- erk/cli/commands/exec/scripts/slot_objective.py +102 -0
- erk/cli/commands/exec/scripts/tripwires_reminder_hook.py +20 -0
- erk/cli/commands/exec/scripts/update_dispatch_info.py +116 -0
- erk/cli/commands/exec/scripts/user_prompt_hook.py +113 -0
- erk/cli/commands/exec/scripts/validate_plan_content.py +98 -0
- erk/cli/commands/exec/scripts/wrap_plan_in_metadata_block.py +34 -0
- erk/cli/commands/implement.py +695 -0
- erk/cli/commands/implement_shared.py +649 -0
- erk/cli/commands/info/__init__.py +14 -0
- erk/cli/commands/info/release_notes_cmd.py +128 -0
- erk/cli/commands/init.py +801 -0
- erk/cli/commands/land_cmd.py +690 -0
- erk/cli/commands/log_cmd.py +137 -0
- erk/cli/commands/md/__init__.py +5 -0
- erk/cli/commands/md/check.py +118 -0
- erk/cli/commands/md/group.py +14 -0
- erk/cli/commands/navigation_helpers.py +430 -0
- erk/cli/commands/objective/__init__.py +16 -0
- erk/cli/commands/objective/list_cmd.py +47 -0
- erk/cli/commands/objective_helpers.py +132 -0
- erk/cli/commands/plan/__init__.py +32 -0
- erk/cli/commands/plan/check_cmd.py +174 -0
- erk/cli/commands/plan/close_cmd.py +69 -0
- erk/cli/commands/plan/create_cmd.py +120 -0
- erk/cli/commands/plan/docs/__init__.py +18 -0
- erk/cli/commands/plan/docs/extract_cmd.py +53 -0
- erk/cli/commands/plan/docs/unextract_cmd.py +38 -0
- erk/cli/commands/plan/docs/unextracted_cmd.py +72 -0
- erk/cli/commands/plan/extraction/__init__.py +16 -0
- erk/cli/commands/plan/extraction/complete_cmd.py +101 -0
- erk/cli/commands/plan/extraction/create_raw_cmd.py +63 -0
- erk/cli/commands/plan/get.py +71 -0
- erk/cli/commands/plan/list_cmd.py +754 -0
- erk/cli/commands/plan/log_cmd.py +440 -0
- erk/cli/commands/plan/start_cmd.py +459 -0
- erk/cli/commands/planner/__init__.py +40 -0
- erk/cli/commands/planner/configure_cmd.py +73 -0
- erk/cli/commands/planner/connect_cmd.py +96 -0
- erk/cli/commands/planner/create_cmd.py +148 -0
- erk/cli/commands/planner/list_cmd.py +51 -0
- erk/cli/commands/planner/register_cmd.py +105 -0
- erk/cli/commands/planner/set_default_cmd.py +23 -0
- erk/cli/commands/planner/unregister_cmd.py +43 -0
- erk/cli/commands/pr/__init__.py +23 -0
- erk/cli/commands/pr/check_cmd.py +112 -0
- erk/cli/commands/pr/checkout_cmd.py +165 -0
- erk/cli/commands/pr/fix_conflicts_cmd.py +82 -0
- erk/cli/commands/pr/parse_pr_reference.py +10 -0
- erk/cli/commands/pr/submit_cmd.py +360 -0
- erk/cli/commands/pr/sync_cmd.py +181 -0
- erk/cli/commands/prepare_cwd_recovery.py +60 -0
- erk/cli/commands/project/__init__.py +16 -0
- erk/cli/commands/project/init_cmd.py +91 -0
- erk/cli/commands/run/__init__.py +17 -0
- erk/cli/commands/run/list_cmd.py +189 -0
- erk/cli/commands/run/logs_cmd.py +54 -0
- erk/cli/commands/run/shared.py +19 -0
- erk/cli/commands/shell_integration.py +29 -0
- erk/cli/commands/slot/__init__.py +23 -0
- erk/cli/commands/slot/check_cmd.py +277 -0
- erk/cli/commands/slot/common.py +314 -0
- erk/cli/commands/slot/init_pool_cmd.py +157 -0
- erk/cli/commands/slot/list_cmd.py +228 -0
- erk/cli/commands/slot/repair_cmd.py +190 -0
- erk/cli/commands/stack/__init__.py +23 -0
- erk/cli/commands/stack/consolidate_cmd.py +470 -0
- erk/cli/commands/stack/list_cmd.py +79 -0
- erk/cli/commands/stack/move_cmd.py +309 -0
- erk/cli/commands/stack/split_old/README.md +64 -0
- erk/cli/commands/stack/split_old/__init__.py +5 -0
- erk/cli/commands/stack/split_old/command.py +233 -0
- erk/cli/commands/stack/split_old/display.py +116 -0
- erk/cli/commands/stack/split_old/plan.py +216 -0
- erk/cli/commands/status.py +58 -0
- erk/cli/commands/submit.py +768 -0
- erk/cli/commands/up.py +154 -0
- erk/cli/commands/upgrade.py +82 -0
- erk/cli/commands/wt/__init__.py +29 -0
- erk/cli/commands/wt/checkout_cmd.py +110 -0
- erk/cli/commands/wt/create_cmd.py +998 -0
- erk/cli/commands/wt/current_cmd.py +35 -0
- erk/cli/commands/wt/delete_cmd.py +573 -0
- erk/cli/commands/wt/list_cmd.py +332 -0
- erk/cli/commands/wt/rename_cmd.py +66 -0
- erk/cli/config.py +242 -0
- erk/cli/constants.py +29 -0
- erk/cli/core.py +65 -0
- erk/cli/debug.py +9 -0
- erk/cli/ensure-conversion-tasks.md +288 -0
- erk/cli/ensure.py +628 -0
- erk/cli/github_parsing.py +96 -0
- erk/cli/graphite.py +81 -0
- erk/cli/graphite_command.py +80 -0
- erk/cli/help_formatter.py +345 -0
- erk/cli/output.py +361 -0
- erk/cli/presets/dagster.toml +12 -0
- erk/cli/presets/generic.toml +12 -0
- erk/cli/prompt_hooks_templates/README.md +68 -0
- erk/cli/script_output.py +32 -0
- erk/cli/shell_integration/bash_wrapper.sh +32 -0
- erk/cli/shell_integration/fish_wrapper.fish +39 -0
- erk/cli/shell_integration/handler.py +338 -0
- erk/cli/shell_integration/zsh_wrapper.sh +32 -0
- erk/cli/shell_utils.py +171 -0
- erk/cli/subprocess_utils.py +92 -0
- erk/cli/uvx_detection.py +59 -0
- erk/core/__init__.py +0 -0
- erk/core/claude_executor.py +511 -0
- erk/core/claude_settings.py +317 -0
- erk/core/command_log.py +406 -0
- erk/core/commit_message_generator.py +234 -0
- erk/core/completion.py +10 -0
- erk/core/consolidation_utils.py +177 -0
- erk/core/context.py +570 -0
- erk/core/display/__init__.py +4 -0
- erk/core/display/abc.py +24 -0
- erk/core/display/real.py +30 -0
- erk/core/display_utils.py +526 -0
- erk/core/file_utils.py +87 -0
- erk/core/health_checks.py +1315 -0
- erk/core/health_checks_dogfooder/__init__.py +85 -0
- erk/core/health_checks_dogfooder/deprecated_dot_agent_config.py +64 -0
- erk/core/health_checks_dogfooder/legacy_claude_docs.py +69 -0
- erk/core/health_checks_dogfooder/legacy_config_locations.py +122 -0
- erk/core/health_checks_dogfooder/legacy_erk_docs_agent.py +61 -0
- erk/core/health_checks_dogfooder/legacy_erk_kits_folder.py +60 -0
- erk/core/health_checks_dogfooder/legacy_hook_settings.py +104 -0
- erk/core/health_checks_dogfooder/legacy_kit_yaml.py +78 -0
- erk/core/health_checks_dogfooder/legacy_kits_toml.py +43 -0
- erk/core/health_checks_dogfooder/outdated_erk_skill.py +43 -0
- erk/core/implementation_queue/__init__.py +1 -0
- erk/core/implementation_queue/github/__init__.py +8 -0
- erk/core/implementation_queue/github/abc.py +7 -0
- erk/core/implementation_queue/github/noop.py +38 -0
- erk/core/implementation_queue/github/printing.py +43 -0
- erk/core/implementation_queue/github/real.py +119 -0
- erk/core/init_utils.py +227 -0
- erk/core/output_filter.py +338 -0
- erk/core/plan_store/__init__.py +6 -0
- erk/core/planner/__init__.py +1 -0
- erk/core/planner/registry_abc.py +8 -0
- erk/core/planner/registry_fake.py +129 -0
- erk/core/planner/registry_real.py +195 -0
- erk/core/planner/types.py +7 -0
- erk/core/pr_utils.py +30 -0
- erk/core/release_notes.py +263 -0
- erk/core/repo_discovery.py +126 -0
- erk/core/script_writer.py +41 -0
- erk/core/services/__init__.py +1 -0
- erk/core/services/plan_list_service.py +94 -0
- erk/core/shell.py +51 -0
- erk/core/user_feedback.py +11 -0
- erk/core/version_check.py +55 -0
- erk/core/workflow_display.py +75 -0
- erk/core/worktree_pool.py +190 -0
- erk/core/worktree_utils.py +300 -0
- erk/data/CHANGELOG.md +438 -0
- erk/data/__init__.py +1 -0
- erk/data/claude/agents/devrun.md +180 -0
- erk/data/claude/commands/erk/__init__.py +0 -0
- erk/data/claude/commands/erk/create-extraction-plan.md +360 -0
- erk/data/claude/commands/erk/fix-conflicts.md +25 -0
- erk/data/claude/commands/erk/git-pr-push.md +345 -0
- erk/data/claude/commands/erk/implement-stacked-plan.md +96 -0
- erk/data/claude/commands/erk/land.md +193 -0
- erk/data/claude/commands/erk/objective-create.md +370 -0
- erk/data/claude/commands/erk/objective-list.md +34 -0
- erk/data/claude/commands/erk/objective-next-plan.md +220 -0
- erk/data/claude/commands/erk/objective-update-with-landed-pr.md +216 -0
- erk/data/claude/commands/erk/plan-implement.md +202 -0
- erk/data/claude/commands/erk/plan-save.md +45 -0
- erk/data/claude/commands/erk/plan-submit.md +39 -0
- erk/data/claude/commands/erk/pr-address.md +367 -0
- erk/data/claude/commands/erk/pr-submit.md +58 -0
- erk/data/claude/skills/dignified-python/SKILL.md +48 -0
- erk/data/claude/skills/dignified-python/cli-patterns.md +155 -0
- erk/data/claude/skills/dignified-python/dignified-python-core.md +1190 -0
- erk/data/claude/skills/dignified-python/subprocess.md +99 -0
- erk/data/claude/skills/dignified-python/versions/python-3.10.md +517 -0
- erk/data/claude/skills/dignified-python/versions/python-3.11.md +536 -0
- erk/data/claude/skills/dignified-python/versions/python-3.12.md +662 -0
- erk/data/claude/skills/dignified-python/versions/python-3.13.md +653 -0
- erk/data/claude/skills/erk-diff-analysis/SKILL.md +27 -0
- erk/data/claude/skills/erk-diff-analysis/references/commit-message-prompt.md +78 -0
- erk/data/claude/skills/learned-docs/SKILL.md +362 -0
- erk/data/github/actions/setup-claude-erk/action.yml +11 -0
- erk/data/github/prompts/dignified-python-review.md +125 -0
- erk/data/github/workflows/dignified-python-review.yml +61 -0
- erk/data/github/workflows/erk-impl.yml +251 -0
- erk/hooks/__init__.py +1 -0
- erk/hooks/decorators.py +319 -0
- erk/status/__init__.py +8 -0
- erk/status/collectors/__init__.py +9 -0
- erk/status/collectors/base.py +52 -0
- erk/status/collectors/git.py +76 -0
- erk/status/collectors/github.py +81 -0
- erk/status/collectors/graphite.py +80 -0
- erk/status/collectors/impl.py +145 -0
- erk/status/models/__init__.py +4 -0
- erk/status/models/status_data.py +404 -0
- erk/status/orchestrator.py +169 -0
- erk/status/renderers/__init__.py +5 -0
- erk/status/renderers/simple.py +322 -0
- erk/tui/AGENTS.md +193 -0
- erk/tui/CLAUDE.md +1 -0
- erk/tui/__init__.py +1 -0
- erk/tui/app.py +1404 -0
- erk/tui/commands/__init__.py +1 -0
- erk/tui/commands/executor.py +66 -0
- erk/tui/commands/provider.py +165 -0
- erk/tui/commands/real_executor.py +63 -0
- erk/tui/commands/registry.py +121 -0
- erk/tui/commands/types.py +36 -0
- erk/tui/data/__init__.py +1 -0
- erk/tui/data/provider.py +492 -0
- erk/tui/data/types.py +104 -0
- erk/tui/filtering/__init__.py +1 -0
- erk/tui/filtering/logic.py +43 -0
- erk/tui/filtering/types.py +55 -0
- erk/tui/jsonl_viewer/__init__.py +1 -0
- erk/tui/jsonl_viewer/app.py +61 -0
- erk/tui/jsonl_viewer/models.py +208 -0
- erk/tui/jsonl_viewer/widgets.py +204 -0
- erk/tui/sorting/__init__.py +6 -0
- erk/tui/sorting/logic.py +55 -0
- erk/tui/sorting/types.py +68 -0
- erk/tui/styles/dash.tcss +95 -0
- erk/tui/widgets/__init__.py +1 -0
- erk/tui/widgets/command_output.py +112 -0
- erk/tui/widgets/plan_table.py +276 -0
- erk/tui/widgets/status_bar.py +116 -0
- erk-0.4.5.dist-info/METADATA +376 -0
- erk-0.4.5.dist-info/RECORD +331 -0
- erk-0.4.5.dist-info/WHEEL +4 -0
- erk-0.4.5.dist-info/entry_points.txt +2 -0
- erk-0.4.5.dist-info/licenses/LICENSE.md +3 -0
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
"""Consolidate worktrees by removing others containing branches from current stack."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from erk.cli.activation import render_activation_script
|
|
9
|
+
from erk.cli.commands.branch.unassign_cmd import execute_unassign
|
|
10
|
+
from erk.cli.commands.navigation_helpers import find_assignment_by_worktree_path
|
|
11
|
+
from erk.cli.core import discover_repo_context, worktree_path_for
|
|
12
|
+
from erk.cli.graphite_command import GraphiteCommandWithHiddenOptions
|
|
13
|
+
from erk.cli.help_formatter import script_option
|
|
14
|
+
from erk.core.consolidation_utils import calculate_stack_range, create_consolidation_plan
|
|
15
|
+
from erk.core.context import ErkContext, create_context
|
|
16
|
+
from erk.core.repo_discovery import RepoContext, ensure_erk_metadata_dir
|
|
17
|
+
from erk.core.worktree_pool import load_pool_state
|
|
18
|
+
from erk_shared.git.abc import WorktreeInfo
|
|
19
|
+
from erk_shared.output.output import user_confirm, user_output
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _format_section_header(text: str, separator_length: int = 59) -> str:
|
|
23
|
+
"""Format a section header with styled text and separator line."""
|
|
24
|
+
header = click.style(text, bold=True)
|
|
25
|
+
separator = "─" * separator_length
|
|
26
|
+
return f"{header}\n{separator}"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _format_consolidation_plan(
|
|
30
|
+
stack_branches: list[str],
|
|
31
|
+
current_branch: str,
|
|
32
|
+
consolidated_branches: list[str],
|
|
33
|
+
target_name: str,
|
|
34
|
+
worktrees_to_remove: list[tuple[str, Path]],
|
|
35
|
+
) -> str:
|
|
36
|
+
"""Format the consolidation plan section with visual hierarchy."""
|
|
37
|
+
lines: list[str] = []
|
|
38
|
+
|
|
39
|
+
# Section header
|
|
40
|
+
lines.append(_format_section_header("📋 Consolidation Plan"))
|
|
41
|
+
lines.append("")
|
|
42
|
+
|
|
43
|
+
# Branches consolidating to current worktree
|
|
44
|
+
lines.append("Branches consolidating to current worktree:")
|
|
45
|
+
for branch in consolidated_branches:
|
|
46
|
+
if branch == current_branch:
|
|
47
|
+
branch_display = click.style(branch, fg="bright_green", bold=True)
|
|
48
|
+
lines.append(f" • {branch_display} ← (keeping this worktree)")
|
|
49
|
+
else:
|
|
50
|
+
lines.append(f" • {branch}")
|
|
51
|
+
|
|
52
|
+
lines.append("")
|
|
53
|
+
|
|
54
|
+
# Worktrees to remove
|
|
55
|
+
lines.append("Worktrees to remove:")
|
|
56
|
+
for branch, path in worktrees_to_remove:
|
|
57
|
+
lines.append(f" • {branch}")
|
|
58
|
+
lines.append(f" {click.style(str(path), fg='white', dim=True)}")
|
|
59
|
+
|
|
60
|
+
lines.append("")
|
|
61
|
+
lines.append("─" * 59)
|
|
62
|
+
|
|
63
|
+
return "\n".join(lines)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _format_removal_progress(removed_paths: list[Path], unassigned_slots: list[str]) -> str:
|
|
67
|
+
"""Format the removal execution output with grouped checkmarks."""
|
|
68
|
+
lines: list[str] = []
|
|
69
|
+
|
|
70
|
+
if removed_paths or unassigned_slots:
|
|
71
|
+
lines.append(_format_section_header("🗑️ Removing worktrees..."))
|
|
72
|
+
for path in removed_paths:
|
|
73
|
+
lines.append(f" ✓ {click.style(str(path), fg='green')}")
|
|
74
|
+
for slot in unassigned_slots:
|
|
75
|
+
lines.append(f" ✓ {click.style(slot, fg='cyan')} (slot unassigned)")
|
|
76
|
+
|
|
77
|
+
return "\n".join(lines)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _remove_worktree_slot_aware(
|
|
81
|
+
ctx: ErkContext,
|
|
82
|
+
repo: RepoContext,
|
|
83
|
+
wt: WorktreeInfo,
|
|
84
|
+
) -> tuple[Path | None, str | None]:
|
|
85
|
+
"""Remove a worktree with slot awareness.
|
|
86
|
+
|
|
87
|
+
If worktree is a pool slot: unassigns slot (keeps directory for reuse).
|
|
88
|
+
If not a pool slot: removes worktree directory.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
ctx: ErkContext with git operations
|
|
92
|
+
repo: Repository context
|
|
93
|
+
wt: WorktreeInfo for the worktree to remove
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Tuple of (removed_path, unassigned_slot_name):
|
|
97
|
+
- removed_path: Path if worktree was removed, None if slot unassigned
|
|
98
|
+
- unassigned_slot_name: Slot name if slot unassigned, None if worktree removed
|
|
99
|
+
"""
|
|
100
|
+
state = load_pool_state(repo.pool_json_path)
|
|
101
|
+
assignment = None
|
|
102
|
+
if state is not None:
|
|
103
|
+
assignment = find_assignment_by_worktree_path(state, wt.path)
|
|
104
|
+
|
|
105
|
+
if assignment is not None:
|
|
106
|
+
# Slot worktree: unassign instead of remove
|
|
107
|
+
# state is guaranteed to be non-None since assignment was found in it
|
|
108
|
+
assert state is not None
|
|
109
|
+
execute_unassign(ctx, repo, state, assignment)
|
|
110
|
+
return (None, assignment.slot_name)
|
|
111
|
+
else:
|
|
112
|
+
# Non-slot worktree: remove normally
|
|
113
|
+
ctx.git.remove_worktree(repo.root, wt.path, force=True)
|
|
114
|
+
return (wt.path, None)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@click.command("consolidate", cls=GraphiteCommandWithHiddenOptions)
|
|
118
|
+
@click.argument("branch", required=False, default=None)
|
|
119
|
+
@click.option(
|
|
120
|
+
"--name",
|
|
121
|
+
type=str,
|
|
122
|
+
default=None,
|
|
123
|
+
help="Create and consolidate into a new worktree with this name",
|
|
124
|
+
)
|
|
125
|
+
@click.option("-f", "--force", is_flag=True, help="Skip confirmation prompt")
|
|
126
|
+
@click.option(
|
|
127
|
+
"--dry-run",
|
|
128
|
+
is_flag=True,
|
|
129
|
+
default=False,
|
|
130
|
+
help="Show what would be removed without executing",
|
|
131
|
+
)
|
|
132
|
+
@click.option(
|
|
133
|
+
"--down",
|
|
134
|
+
is_flag=True,
|
|
135
|
+
help="Only consolidate downstack (trunk to current branch). Default is entire stack.",
|
|
136
|
+
)
|
|
137
|
+
@script_option
|
|
138
|
+
@click.pass_obj
|
|
139
|
+
def consolidate_stack(
|
|
140
|
+
ctx: ErkContext,
|
|
141
|
+
branch: str | None,
|
|
142
|
+
name: str | None,
|
|
143
|
+
force: bool,
|
|
144
|
+
dry_run: bool,
|
|
145
|
+
down: bool,
|
|
146
|
+
script: bool,
|
|
147
|
+
) -> None:
|
|
148
|
+
"""Consolidate stack branches into a single worktree.
|
|
149
|
+
|
|
150
|
+
By default, consolidates full stack (trunk to leaf). With --down, consolidates
|
|
151
|
+
only downstack branches (trunk to current).
|
|
152
|
+
|
|
153
|
+
This command removes other worktrees that contain branches from the stack,
|
|
154
|
+
ensuring branches exist in only one worktree. This is useful before
|
|
155
|
+
stack-wide operations like 'gt restack'.
|
|
156
|
+
|
|
157
|
+
BRANCH: Optional branch name. If provided, consolidate only from trunk up to
|
|
158
|
+
this branch (partial consolidation). Cannot be used with --down.
|
|
159
|
+
|
|
160
|
+
\b
|
|
161
|
+
Examples:
|
|
162
|
+
# Consolidate full stack into current worktree (default)
|
|
163
|
+
$ erk consolidate
|
|
164
|
+
|
|
165
|
+
# Consolidate only downstack (trunk to current)
|
|
166
|
+
$ erk consolidate --down
|
|
167
|
+
|
|
168
|
+
# Consolidate trunk → feat-2 only (leaves feat-3+ in separate worktrees)
|
|
169
|
+
$ erk consolidate feat-2
|
|
170
|
+
|
|
171
|
+
# Create new worktree "my-stack" and consolidate full stack into it
|
|
172
|
+
$ erk consolidate --name my-stack
|
|
173
|
+
|
|
174
|
+
# Consolidate downstack into new worktree
|
|
175
|
+
$ erk consolidate --down --name my-partial
|
|
176
|
+
|
|
177
|
+
# Preview changes without executing
|
|
178
|
+
$ erk consolidate --dry-run
|
|
179
|
+
|
|
180
|
+
# Skip confirmation prompt
|
|
181
|
+
$ erk consolidate --force
|
|
182
|
+
|
|
183
|
+
Safety checks:
|
|
184
|
+
- Aborts if any worktree being consolidated has uncommitted changes
|
|
185
|
+
- Preserves the current worktree (or creates new one with --name)
|
|
186
|
+
- Shows preview before removal (unless --force)
|
|
187
|
+
- Never removes root worktree
|
|
188
|
+
"""
|
|
189
|
+
# During dry-run, always show output regardless of shell integration
|
|
190
|
+
if dry_run:
|
|
191
|
+
script = False
|
|
192
|
+
|
|
193
|
+
# Validate that --down and BRANCH are not used together
|
|
194
|
+
if down and branch is not None:
|
|
195
|
+
user_output(click.style("❌ Error: Cannot use --down with BRANCH argument", fg="red"))
|
|
196
|
+
user_output(
|
|
197
|
+
"Use either --down (consolidate trunk to current) or "
|
|
198
|
+
"BRANCH (consolidate trunk to BRANCH)"
|
|
199
|
+
)
|
|
200
|
+
raise SystemExit(1)
|
|
201
|
+
|
|
202
|
+
# Get current worktree and branch
|
|
203
|
+
current_worktree = ctx.cwd
|
|
204
|
+
current_branch = ctx.git.get_current_branch(current_worktree)
|
|
205
|
+
|
|
206
|
+
if current_branch is None:
|
|
207
|
+
user_output("Error: Current worktree is in detached HEAD state")
|
|
208
|
+
user_output("Checkout a branch before running consolidate")
|
|
209
|
+
raise SystemExit(1)
|
|
210
|
+
|
|
211
|
+
# Get repository root
|
|
212
|
+
repo = discover_repo_context(ctx, current_worktree)
|
|
213
|
+
ensure_erk_metadata_dir(repo)
|
|
214
|
+
|
|
215
|
+
# Get current branch's stack
|
|
216
|
+
stack_branches = ctx.graphite.get_branch_stack(ctx.git, repo.root, current_branch)
|
|
217
|
+
if stack_branches is None:
|
|
218
|
+
user_output(f"Error: Branch '{current_branch}' is not tracked by Graphite")
|
|
219
|
+
user_output(
|
|
220
|
+
"Run 'gt repo init' to initialize Graphite, or use 'gt track' to track this branch"
|
|
221
|
+
)
|
|
222
|
+
raise SystemExit(1)
|
|
223
|
+
|
|
224
|
+
# Validate branch argument if provided
|
|
225
|
+
if branch is not None:
|
|
226
|
+
if branch not in stack_branches:
|
|
227
|
+
user_output(
|
|
228
|
+
click.style(f"❌ Error: Branch '{branch}' is not in the current stack", fg="red")
|
|
229
|
+
)
|
|
230
|
+
user_output("\nCurrent stack:")
|
|
231
|
+
for b in stack_branches:
|
|
232
|
+
marker = " ← current" if b == current_branch else ""
|
|
233
|
+
user_output(f" {click.style(b, fg='cyan')}{marker}")
|
|
234
|
+
raise SystemExit(1)
|
|
235
|
+
|
|
236
|
+
# Determine which portion of the stack to consolidate (now handled by utility)
|
|
237
|
+
# This will be used in create_consolidation_plan() below
|
|
238
|
+
|
|
239
|
+
# Get all worktrees
|
|
240
|
+
all_worktrees = ctx.git.list_worktrees(repo.root)
|
|
241
|
+
|
|
242
|
+
# Validate --name argument if provided
|
|
243
|
+
if name is not None:
|
|
244
|
+
# Check if a worktree with this name already exists
|
|
245
|
+
existing_names = [wt.path.name for wt in all_worktrees]
|
|
246
|
+
|
|
247
|
+
if name in existing_names:
|
|
248
|
+
user_output(click.style(f"❌ Error: Worktree '{name}' already exists", fg="red"))
|
|
249
|
+
user_output("\nSuggested action:")
|
|
250
|
+
user_output(" 1. Use a different name")
|
|
251
|
+
user_output(f" 2. Remove existing worktree: erk remove {name}")
|
|
252
|
+
user_output(" 3. Switch to existing: erk br co <branch>")
|
|
253
|
+
raise SystemExit(1)
|
|
254
|
+
|
|
255
|
+
# Calculate stack range early (needed for safety check)
|
|
256
|
+
# If --down is set, force end_branch to be current_branch
|
|
257
|
+
end_branch = current_branch if down else branch
|
|
258
|
+
stack_to_consolidate = calculate_stack_range(stack_branches, end_branch)
|
|
259
|
+
|
|
260
|
+
# Check worktrees in stack for uncommitted changes
|
|
261
|
+
# Only check worktrees that will actually be removed (skip root and current)
|
|
262
|
+
worktrees_with_changes: list[Path] = []
|
|
263
|
+
for wt in all_worktrees:
|
|
264
|
+
if wt.branch not in stack_to_consolidate:
|
|
265
|
+
continue
|
|
266
|
+
# Skip root worktree (never removed)
|
|
267
|
+
if wt.is_root:
|
|
268
|
+
continue
|
|
269
|
+
# Skip current worktree (consolidation target, never removed)
|
|
270
|
+
if wt.path.resolve() == current_worktree.resolve():
|
|
271
|
+
continue
|
|
272
|
+
if ctx.git.path_exists(wt.path) and ctx.git.has_uncommitted_changes(wt.path):
|
|
273
|
+
worktrees_with_changes.append(wt.path)
|
|
274
|
+
|
|
275
|
+
if worktrees_with_changes:
|
|
276
|
+
user_output(
|
|
277
|
+
click.style("Error: Uncommitted changes detected in worktrees:", fg="red", bold=True)
|
|
278
|
+
)
|
|
279
|
+
for wt_path in worktrees_with_changes:
|
|
280
|
+
user_output(f" - {wt_path}")
|
|
281
|
+
user_output("\nCommit or stash changes before running consolidate")
|
|
282
|
+
raise SystemExit(1)
|
|
283
|
+
|
|
284
|
+
# Safety check passed - all worktrees are clean
|
|
285
|
+
user_output(
|
|
286
|
+
click.style("✅ Safety check: All worktrees have no uncommitted changes", fg="green")
|
|
287
|
+
)
|
|
288
|
+
user_output()
|
|
289
|
+
|
|
290
|
+
# Create new worktree if --name is provided
|
|
291
|
+
# Track temp branch name for cleanup after source worktree removal
|
|
292
|
+
temp_branch_name: str | None = None
|
|
293
|
+
|
|
294
|
+
if name is not None:
|
|
295
|
+
if not dry_run:
|
|
296
|
+
# Generate temporary branch name to avoid "already used by worktree" error
|
|
297
|
+
# when the source worktree and new worktree would have the same branch checked out
|
|
298
|
+
temp_branch_name = f"temp-consolidate-{int(time.time())}"
|
|
299
|
+
|
|
300
|
+
# Use proper erks directory path resolution
|
|
301
|
+
new_worktree_path = worktree_path_for(repo.worktrees_dir, name)
|
|
302
|
+
|
|
303
|
+
# Create temporary branch on current commit (doesn't checkout)
|
|
304
|
+
# Git operations use check=True, so failures raise CalledProcessError
|
|
305
|
+
ctx.git.create_branch(current_worktree, temp_branch_name, current_branch)
|
|
306
|
+
|
|
307
|
+
# Checkout temporary branch in source worktree to free up the original branch
|
|
308
|
+
ctx.git.checkout_branch(current_worktree, temp_branch_name)
|
|
309
|
+
|
|
310
|
+
# Track temporary branch with Graphite
|
|
311
|
+
ctx.graphite.track_branch(current_worktree, temp_branch_name, current_branch)
|
|
312
|
+
|
|
313
|
+
# Create new worktree with original branch
|
|
314
|
+
# (now available since source is on temp branch)
|
|
315
|
+
ctx.git.add_worktree(
|
|
316
|
+
repo.root,
|
|
317
|
+
new_worktree_path,
|
|
318
|
+
branch=current_branch,
|
|
319
|
+
ref=None,
|
|
320
|
+
create_branch=False,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
user_output(click.style(f"✅ Created new worktree: {name}", fg="green"))
|
|
324
|
+
|
|
325
|
+
# Change to new worktree directory BEFORE removing source worktree
|
|
326
|
+
# This prevents the shell from being in a deleted directory
|
|
327
|
+
# Always change directory regardless of script mode to ensure we're not in
|
|
328
|
+
# the source worktree when it gets deleted
|
|
329
|
+
if ctx.git.safe_chdir(new_worktree_path):
|
|
330
|
+
# Regenerate context with new cwd (context is immutable)
|
|
331
|
+
ctx = create_context(dry_run=ctx.dry_run)
|
|
332
|
+
user_output(click.style("✅ Changed directory to new worktree", fg="green"))
|
|
333
|
+
|
|
334
|
+
target_worktree_path = new_worktree_path
|
|
335
|
+
else:
|
|
336
|
+
user_output(
|
|
337
|
+
click.style(f"[DRY RUN] Would create new worktree: {name}", fg="yellow", bold=True)
|
|
338
|
+
)
|
|
339
|
+
target_worktree_path = current_worktree # In dry-run, keep current path
|
|
340
|
+
else:
|
|
341
|
+
# Use current worktree as target (existing behavior)
|
|
342
|
+
target_worktree_path = current_worktree
|
|
343
|
+
|
|
344
|
+
# Create consolidation plan using utility function
|
|
345
|
+
# Use the same end_branch logic as calculated above
|
|
346
|
+
plan = create_consolidation_plan(
|
|
347
|
+
all_worktrees=all_worktrees,
|
|
348
|
+
stack_branches=stack_branches,
|
|
349
|
+
end_branch=end_branch,
|
|
350
|
+
target_worktree_path=target_worktree_path,
|
|
351
|
+
source_worktree_path=current_worktree if name is not None else None,
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
# Extract data from plan for easier reference
|
|
355
|
+
worktrees_to_remove = plan.worktrees_to_remove
|
|
356
|
+
stack_to_consolidate = plan.stack_to_consolidate
|
|
357
|
+
|
|
358
|
+
# Display preview
|
|
359
|
+
if not worktrees_to_remove:
|
|
360
|
+
# If using --name, we still need to remove source worktree even if no other worktrees exist
|
|
361
|
+
if name is None:
|
|
362
|
+
user_output("No other worktrees found containing branches from current stack")
|
|
363
|
+
user_output(f"\nCurrent stack branches: {', '.join(stack_branches)}")
|
|
364
|
+
return
|
|
365
|
+
# Continue to source worktree removal when using --name
|
|
366
|
+
|
|
367
|
+
# Collect data for formatted output
|
|
368
|
+
worktrees_to_remove_list: list[tuple[str, Path]] = [
|
|
369
|
+
(wt.branch or "detached", wt.path) for wt in worktrees_to_remove
|
|
370
|
+
]
|
|
371
|
+
|
|
372
|
+
# Add source worktree to removal list if creating new worktree
|
|
373
|
+
if name is not None:
|
|
374
|
+
worktrees_to_remove_list.append((current_branch, current_worktree))
|
|
375
|
+
|
|
376
|
+
# Display consolidation plan
|
|
377
|
+
user_output()
|
|
378
|
+
plan_output = _format_consolidation_plan(
|
|
379
|
+
stack_branches=stack_branches,
|
|
380
|
+
current_branch=current_branch,
|
|
381
|
+
consolidated_branches=stack_to_consolidate,
|
|
382
|
+
target_name=name if name is not None else str(current_worktree.name),
|
|
383
|
+
worktrees_to_remove=worktrees_to_remove_list,
|
|
384
|
+
)
|
|
385
|
+
user_output(plan_output)
|
|
386
|
+
|
|
387
|
+
# Exit if dry-run
|
|
388
|
+
if dry_run:
|
|
389
|
+
user_output(f"\n{click.style('[DRY RUN] No changes made', fg='yellow', bold=True)}")
|
|
390
|
+
return
|
|
391
|
+
|
|
392
|
+
# Get confirmation unless --force or --script
|
|
393
|
+
if not force and not script:
|
|
394
|
+
user_output()
|
|
395
|
+
if not user_confirm("All worktrees are clean. Proceed with removal?", default=False):
|
|
396
|
+
user_output(click.style("⭕ Aborted", fg="red", bold=True))
|
|
397
|
+
return
|
|
398
|
+
|
|
399
|
+
# Shell integration: generate script to activate new worktree BEFORE destructive operations
|
|
400
|
+
# This ensures the shell can navigate even if later steps fail (e.g., branch deletion).
|
|
401
|
+
# The handler will use this script instead of passthrough when available.
|
|
402
|
+
if name is not None and script and not dry_run:
|
|
403
|
+
script_content = render_activation_script(
|
|
404
|
+
worktree_path=target_worktree_path,
|
|
405
|
+
target_subpath=None,
|
|
406
|
+
post_cd_commands=None,
|
|
407
|
+
final_message='echo "✓ Went to consolidated worktree."',
|
|
408
|
+
comment="work activate-script (consolidate)",
|
|
409
|
+
)
|
|
410
|
+
activation_result = ctx.script_writer.write_activation_script(
|
|
411
|
+
script_content,
|
|
412
|
+
command_name="consolidate",
|
|
413
|
+
comment=f"activate {name}",
|
|
414
|
+
)
|
|
415
|
+
activation_result.output_for_shell_integration()
|
|
416
|
+
|
|
417
|
+
# Remove worktrees and collect paths for progress output
|
|
418
|
+
removed_paths: list[Path] = []
|
|
419
|
+
unassigned_slots: list[str] = []
|
|
420
|
+
|
|
421
|
+
for wt in worktrees_to_remove:
|
|
422
|
+
removed_path, slot_name = _remove_worktree_slot_aware(ctx, repo, wt)
|
|
423
|
+
if removed_path is not None:
|
|
424
|
+
removed_paths.append(removed_path)
|
|
425
|
+
if slot_name is not None:
|
|
426
|
+
unassigned_slots.append(slot_name)
|
|
427
|
+
|
|
428
|
+
# Remove source worktree if a new worktree was created
|
|
429
|
+
if name is not None:
|
|
430
|
+
# Create a temporary WorktreeInfo for the source worktree
|
|
431
|
+
source_wt = WorktreeInfo(
|
|
432
|
+
path=current_worktree.resolve(),
|
|
433
|
+
branch=current_branch,
|
|
434
|
+
is_root=False,
|
|
435
|
+
)
|
|
436
|
+
removed_path, slot_name = _remove_worktree_slot_aware(ctx, repo, source_wt)
|
|
437
|
+
if removed_path is not None:
|
|
438
|
+
removed_paths.append(removed_path)
|
|
439
|
+
if slot_name is not None:
|
|
440
|
+
unassigned_slots.append(slot_name)
|
|
441
|
+
|
|
442
|
+
# Delete temporary branch after source worktree is removed
|
|
443
|
+
# (can't delete while it's checked out in the source worktree)
|
|
444
|
+
if temp_branch_name is not None:
|
|
445
|
+
ctx.git.delete_branch(repo.root, temp_branch_name, force=True)
|
|
446
|
+
|
|
447
|
+
# Display grouped removal progress
|
|
448
|
+
user_output()
|
|
449
|
+
user_output(_format_removal_progress(removed_paths, unassigned_slots))
|
|
450
|
+
|
|
451
|
+
# Prune stale worktree metadata after all removals
|
|
452
|
+
# (explicit call now that remove_worktree no longer auto-prunes)
|
|
453
|
+
ctx.git.prune_worktrees(repo.root)
|
|
454
|
+
|
|
455
|
+
user_output(f"\n{click.style('✅ Consolidation complete', fg='green', bold=True)}")
|
|
456
|
+
user_output()
|
|
457
|
+
user_output("Next step:")
|
|
458
|
+
user_output(" Run 'gt restack' to update branch relationships")
|
|
459
|
+
|
|
460
|
+
# Early return when no worktree switch (consolidating into current worktree)
|
|
461
|
+
# Makes it explicit that no script is needed in this case
|
|
462
|
+
if name is None:
|
|
463
|
+
return # No script needed when not switching worktrees
|
|
464
|
+
|
|
465
|
+
# Manual cd instruction when not in script mode
|
|
466
|
+
# (Script mode already output activation script earlier, before destructive operations)
|
|
467
|
+
if not script and not dry_run:
|
|
468
|
+
user_output(f"Going to worktree: {click.style(name, fg='cyan', bold=True)}")
|
|
469
|
+
user_output(f"\n{click.style('ℹ️', fg='blue')} Run this command to switch:")
|
|
470
|
+
user_output(f" cd {target_worktree_path}")
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""List worktree stack with branch info."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from rich.table import Table
|
|
6
|
+
|
|
7
|
+
from erk.cli.alias import alias
|
|
8
|
+
from erk.cli.core import discover_repo_context
|
|
9
|
+
from erk.cli.graphite import find_worktrees_containing_branch
|
|
10
|
+
from erk.cli.graphite_command import GraphiteCommand
|
|
11
|
+
from erk.core.context import ErkContext
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@alias("ls")
|
|
15
|
+
@click.command("list", cls=GraphiteCommand)
|
|
16
|
+
@click.pass_obj
|
|
17
|
+
def list_stack(ctx: ErkContext) -> None:
|
|
18
|
+
"""List the worktree stack with branch info.
|
|
19
|
+
|
|
20
|
+
Shows branches in the current stack that have associated worktrees,
|
|
21
|
+
displayed top-to-bottom (upstack children at top, downstack trunk at bottom).
|
|
22
|
+
|
|
23
|
+
Table columns:
|
|
24
|
+
- Marker: → for current branch
|
|
25
|
+
- branch: Branch name
|
|
26
|
+
- worktree: Worktree directory name
|
|
27
|
+
"""
|
|
28
|
+
repo = discover_repo_context(ctx, ctx.cwd)
|
|
29
|
+
current_branch = ctx.git.get_current_branch(repo.root)
|
|
30
|
+
|
|
31
|
+
if current_branch is None:
|
|
32
|
+
click.echo("Error: Not on a branch (detached HEAD state)", err=True)
|
|
33
|
+
raise SystemExit(1)
|
|
34
|
+
|
|
35
|
+
# Get the stack for current branch
|
|
36
|
+
stack_branches = ctx.graphite.get_branch_stack(ctx.git, repo.root, current_branch)
|
|
37
|
+
if stack_branches is None:
|
|
38
|
+
click.echo(f"Error: Branch '{current_branch}' is not tracked by Graphite", err=True)
|
|
39
|
+
click.echo("Run 'gt track' to track this branch, or 'gt create' to create a new branch.")
|
|
40
|
+
raise SystemExit(1)
|
|
41
|
+
|
|
42
|
+
# Get worktrees for branch-to-worktree mapping
|
|
43
|
+
worktrees = ctx.git.list_worktrees(repo.root)
|
|
44
|
+
|
|
45
|
+
# Build table
|
|
46
|
+
table = Table(show_header=True, header_style="bold", box=None)
|
|
47
|
+
table.add_column("", no_wrap=True) # Marker column
|
|
48
|
+
table.add_column("branch", style="cyan", no_wrap=True)
|
|
49
|
+
table.add_column("worktree", no_wrap=True)
|
|
50
|
+
|
|
51
|
+
# Show all branches in stack, using ancestor worktree for branches without their own
|
|
52
|
+
# Reverse order: upstack (children) at top, downstack (trunk) at bottom
|
|
53
|
+
for branch in reversed(stack_branches):
|
|
54
|
+
matching_worktrees = find_worktrees_containing_branch(ctx, repo.root, worktrees, branch)
|
|
55
|
+
|
|
56
|
+
if matching_worktrees:
|
|
57
|
+
wt = matching_worktrees[0]
|
|
58
|
+
else:
|
|
59
|
+
# Branch has no direct worktree - find closest ancestor with one
|
|
60
|
+
wt = ctx.graphite.find_ancestor_worktree(ctx.git, repo.root, branch)
|
|
61
|
+
if wt is None:
|
|
62
|
+
continue # Only skip if truly no worktree found
|
|
63
|
+
|
|
64
|
+
wt_name = "root" if wt.is_root else wt.path.name
|
|
65
|
+
|
|
66
|
+
is_current = branch == current_branch
|
|
67
|
+
marker = "→" if is_current else ""
|
|
68
|
+
branch_display = f"[bold cyan]{branch}[/bold cyan]" if is_current else branch
|
|
69
|
+
|
|
70
|
+
table.add_row(marker, branch_display, wt_name)
|
|
71
|
+
|
|
72
|
+
# Check if table has any rows
|
|
73
|
+
if table.row_count == 0:
|
|
74
|
+
click.echo("No branches in stack have worktrees", err=True)
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
# Output table to stderr (consistent with erk wt list)
|
|
78
|
+
console = Console(stderr=True, force_terminal=True)
|
|
79
|
+
console.print(table)
|