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,116 @@
|
|
|
1
|
+
"""Split command display functions - output formatting and user interaction."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from erk.cli.commands.stack.split_old.plan import SplitPlan
|
|
8
|
+
from erk_shared.output.output import user_confirm, user_output
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def display_stack_preview(
|
|
12
|
+
stack_to_split: list[str],
|
|
13
|
+
trunk_branch: str,
|
|
14
|
+
current_branch: str | None,
|
|
15
|
+
plan: SplitPlan,
|
|
16
|
+
) -> None:
|
|
17
|
+
"""Display which branches will be split and their status.
|
|
18
|
+
|
|
19
|
+
Shows visual indicators for:
|
|
20
|
+
- Trunk branch (stays in root)
|
|
21
|
+
- Current branch (already checked out)
|
|
22
|
+
- Branches with existing worktrees
|
|
23
|
+
- Branches that will get new worktrees
|
|
24
|
+
"""
|
|
25
|
+
user_output("\n" + click.style("Stack to split:", bold=True))
|
|
26
|
+
for b in stack_to_split:
|
|
27
|
+
if b == trunk_branch:
|
|
28
|
+
marker = f" {click.style('←', fg='cyan')} trunk (stays in root)"
|
|
29
|
+
branch_display = click.style(b, fg="cyan")
|
|
30
|
+
elif b == current_branch:
|
|
31
|
+
marker = f" {click.style('←', fg='bright_green')} current (already checked out)"
|
|
32
|
+
branch_display = click.style(b, fg="bright_green", bold=True)
|
|
33
|
+
elif b in plan.existing_worktrees:
|
|
34
|
+
marker = f" {click.style('✓', fg='green')} already has worktree"
|
|
35
|
+
branch_display = click.style(b, fg="green")
|
|
36
|
+
elif b in plan.branches_to_split:
|
|
37
|
+
marker = f" {click.style('→', fg='yellow')} will create worktree"
|
|
38
|
+
branch_display = click.style(b, fg="yellow")
|
|
39
|
+
else:
|
|
40
|
+
marker = ""
|
|
41
|
+
branch_display = click.style(b, fg="white", dim=True)
|
|
42
|
+
|
|
43
|
+
user_output(f" {branch_display}{marker}")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def display_creation_preview(
|
|
47
|
+
plan: SplitPlan,
|
|
48
|
+
dry_run: bool,
|
|
49
|
+
) -> None:
|
|
50
|
+
"""Display which worktrees will be created.
|
|
51
|
+
|
|
52
|
+
Shows paths for each branch that needs a worktree.
|
|
53
|
+
Returns early if no worktrees need to be created.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
plan: The split plan containing branches to split
|
|
57
|
+
dry_run: Whether this is a dry run
|
|
58
|
+
"""
|
|
59
|
+
if plan.branches_to_split:
|
|
60
|
+
if dry_run:
|
|
61
|
+
user_output(f"\n{click.style('[DRY RUN] Would create:', fg='yellow', bold=True)}")
|
|
62
|
+
else:
|
|
63
|
+
user_output(f"\n{click.style('Will create:', bold=True)}")
|
|
64
|
+
|
|
65
|
+
for branch in plan.branches_to_split:
|
|
66
|
+
worktree_path = plan.target_paths[branch]
|
|
67
|
+
path_text = click.style(str(worktree_path), fg="cyan")
|
|
68
|
+
branch_text = click.style(branch, fg="yellow")
|
|
69
|
+
user_output(f" - {branch_text} at {path_text}")
|
|
70
|
+
else:
|
|
71
|
+
user_output("\n✅ All branches already have worktrees or are excluded")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def confirm_split(force: bool, dry_run: bool) -> None:
|
|
75
|
+
"""Prompt user for confirmation unless --force or --dry-run.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
force: Whether to skip confirmation
|
|
79
|
+
dry_run: Whether this is a dry run
|
|
80
|
+
|
|
81
|
+
Raises:
|
|
82
|
+
SystemExit: If user declines
|
|
83
|
+
"""
|
|
84
|
+
if not force and not dry_run:
|
|
85
|
+
user_output("")
|
|
86
|
+
if not user_confirm("Proceed with creating worktrees?", default=False):
|
|
87
|
+
user_output(click.style("⭕ Aborted", fg="yellow"))
|
|
88
|
+
raise SystemExit(1)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def display_results(
|
|
92
|
+
results: list[tuple[str, Path]],
|
|
93
|
+
dry_run: bool,
|
|
94
|
+
) -> None:
|
|
95
|
+
"""Display results of split operation.
|
|
96
|
+
|
|
97
|
+
Shows created worktrees or dry-run simulation results.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
results: List of (branch, worktree_path) tuples
|
|
101
|
+
dry_run: Whether this is a dry run
|
|
102
|
+
"""
|
|
103
|
+
if results:
|
|
104
|
+
for branch, worktree_path in results:
|
|
105
|
+
path_text = click.style(str(worktree_path), fg="green")
|
|
106
|
+
branch_text = click.style(branch, fg="yellow")
|
|
107
|
+
if dry_run:
|
|
108
|
+
user_output(f"[DRY RUN] Would create worktree for {branch_text} at {path_text}")
|
|
109
|
+
else:
|
|
110
|
+
user_output(f"✅ Created worktree for {branch_text} at {path_text}")
|
|
111
|
+
|
|
112
|
+
# Summary message
|
|
113
|
+
if dry_run:
|
|
114
|
+
user_output(f"\n{click.style('[DRY RUN] No changes made', fg='yellow')}")
|
|
115
|
+
else:
|
|
116
|
+
user_output(f"\n✅ Split complete: created {len(results)} worktree(s)")
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""Split command planning logic - models, branch identification, plan creation, and execution."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from erk.core.context import ErkContext
|
|
8
|
+
from erk_shared.git.abc import WorktreeInfo
|
|
9
|
+
from erk_shared.output.output import user_output
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class SplitPlan:
|
|
14
|
+
"""Plan for splitting a stack into individual worktrees.
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
stack_branches: Full list of branches in the stack (trunk to leaf)
|
|
18
|
+
branches_to_split: Subset of branches needing worktrees
|
|
19
|
+
existing_worktrees: Branches that already have worktrees (informational)
|
|
20
|
+
target_paths: Mapping of branch names to their target worktree paths
|
|
21
|
+
source_worktree_path: Path to the source worktree where we're splitting from
|
|
22
|
+
repo_root: Path to the repository root
|
|
23
|
+
skipped_current: True if current branch was skipped (already checked out)
|
|
24
|
+
skipped_trunk: True if trunk branch was skipped (stays in root worktree)
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
stack_branches: list[str]
|
|
28
|
+
branches_to_split: list[str]
|
|
29
|
+
existing_worktrees: list[str]
|
|
30
|
+
target_paths: dict[str, Path]
|
|
31
|
+
source_worktree_path: Path
|
|
32
|
+
repo_root: Path
|
|
33
|
+
skipped_current: bool
|
|
34
|
+
skipped_trunk: bool
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_stack_branches(
|
|
38
|
+
ctx: ErkContext,
|
|
39
|
+
repo_root: Path,
|
|
40
|
+
current_branch: str | None,
|
|
41
|
+
trunk_branch: str,
|
|
42
|
+
) -> list[str]:
|
|
43
|
+
"""Get the worktree stack for the current or trunk branch.
|
|
44
|
+
|
|
45
|
+
Handles detached HEAD state by falling back to trunk branch stack.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
List of branches in the stack (trunk to leaf)
|
|
49
|
+
|
|
50
|
+
Raises:
|
|
51
|
+
SystemExit: If branch is not tracked by Graphite
|
|
52
|
+
"""
|
|
53
|
+
if current_branch is None:
|
|
54
|
+
# In detached HEAD state, get the full stack from trunk
|
|
55
|
+
stack_branches = ctx.graphite.get_branch_stack(ctx.git, repo_root, trunk_branch)
|
|
56
|
+
if stack_branches is None:
|
|
57
|
+
user_output(f"Error: Trunk branch '{trunk_branch}' is not tracked by Graphite")
|
|
58
|
+
raise SystemExit(1)
|
|
59
|
+
else:
|
|
60
|
+
# Get current branch's stack
|
|
61
|
+
stack_branches = ctx.graphite.get_branch_stack(ctx.git, repo_root, current_branch)
|
|
62
|
+
if stack_branches is None:
|
|
63
|
+
user_output(f"Error: Branch '{current_branch}' is not tracked by Graphite")
|
|
64
|
+
user_output(
|
|
65
|
+
"Run 'gt repo init' to initialize Graphite, or use 'gt track' to track this branch"
|
|
66
|
+
)
|
|
67
|
+
raise SystemExit(1)
|
|
68
|
+
|
|
69
|
+
return stack_branches
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def identify_splittable_branches(
|
|
73
|
+
stack_branches: list[str],
|
|
74
|
+
trunk_branch: str,
|
|
75
|
+
current_branch: str | None,
|
|
76
|
+
all_worktrees: list[WorktreeInfo],
|
|
77
|
+
) -> tuple[list[str], list[str], bool, bool]:
|
|
78
|
+
"""Identify which branches need new worktrees.
|
|
79
|
+
|
|
80
|
+
A branch needs a worktree if:
|
|
81
|
+
1. It's not the trunk branch (trunk stays in root worktree)
|
|
82
|
+
2. It's not the currently checked out branch (git prevents duplicate checkouts)
|
|
83
|
+
3. It doesn't already have a worktree
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
stack_branches: Full stack from trunk to leaf
|
|
87
|
+
trunk_branch: The trunk branch name (main or master)
|
|
88
|
+
current_branch: Currently checked out branch (None if detached)
|
|
89
|
+
all_worktrees: All existing worktrees in the repository
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Tuple of (branches_to_split, existing_worktrees, skipped_current, skipped_trunk)
|
|
93
|
+
- branches_to_split: Branches that need new worktrees
|
|
94
|
+
- existing_worktrees: Branches that already have worktrees
|
|
95
|
+
- skipped_current: True if current branch was skipped
|
|
96
|
+
- skipped_trunk: True if trunk branch was skipped
|
|
97
|
+
"""
|
|
98
|
+
branches_to_split = []
|
|
99
|
+
existing_worktrees = []
|
|
100
|
+
skipped_current = False
|
|
101
|
+
skipped_trunk = False
|
|
102
|
+
|
|
103
|
+
# Build set of branches that already have worktrees
|
|
104
|
+
branches_with_worktrees = {wt.branch for wt in all_worktrees if wt.branch is not None}
|
|
105
|
+
|
|
106
|
+
for branch in stack_branches:
|
|
107
|
+
# Track if this branch is trunk and/or current (can be both)
|
|
108
|
+
is_trunk = branch == trunk_branch
|
|
109
|
+
is_current = branch == current_branch
|
|
110
|
+
|
|
111
|
+
# Skip trunk branch - it stays in root worktree
|
|
112
|
+
if is_trunk:
|
|
113
|
+
skipped_trunk = True
|
|
114
|
+
|
|
115
|
+
# Skip current branch - can't create worktree for checked out branch
|
|
116
|
+
if is_current:
|
|
117
|
+
skipped_current = True
|
|
118
|
+
|
|
119
|
+
# Skip this branch if it's trunk or current
|
|
120
|
+
if is_trunk or is_current:
|
|
121
|
+
continue
|
|
122
|
+
|
|
123
|
+
# Check if branch already has a worktree
|
|
124
|
+
if branch in branches_with_worktrees:
|
|
125
|
+
existing_worktrees.append(branch)
|
|
126
|
+
else:
|
|
127
|
+
branches_to_split.append(branch)
|
|
128
|
+
|
|
129
|
+
return branches_to_split, existing_worktrees, skipped_current, skipped_trunk
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def create_split_plan(
|
|
133
|
+
stack_branches: list[str],
|
|
134
|
+
trunk_branch: str,
|
|
135
|
+
current_branch: str | None,
|
|
136
|
+
all_worktrees: list[WorktreeInfo],
|
|
137
|
+
worktrees_dir: Path,
|
|
138
|
+
sanitize_worktree_name: Callable[[str], str],
|
|
139
|
+
source_worktree_path: Path,
|
|
140
|
+
repo_root: Path,
|
|
141
|
+
) -> SplitPlan:
|
|
142
|
+
"""Create a complete split plan.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
stack_branches: Full stack from trunk to leaf
|
|
146
|
+
trunk_branch: The trunk branch name (main or master)
|
|
147
|
+
current_branch: Currently checked out branch (None if detached)
|
|
148
|
+
all_worktrees: All existing worktrees in the repository
|
|
149
|
+
worktrees_dir: Directory containing worktrees
|
|
150
|
+
sanitize_worktree_name: Function to convert branch name to valid worktree name
|
|
151
|
+
source_worktree_path: Path to the current worktree we're splitting from
|
|
152
|
+
repo_root: Path to the repository root
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Complete split plan with all information needed for execution
|
|
156
|
+
"""
|
|
157
|
+
result = identify_splittable_branches(
|
|
158
|
+
stack_branches, trunk_branch, current_branch, all_worktrees
|
|
159
|
+
)
|
|
160
|
+
branches_to_split, existing_worktrees, skipped_current, skipped_trunk = result
|
|
161
|
+
|
|
162
|
+
# Build target paths for branches needing worktrees
|
|
163
|
+
target_paths = {}
|
|
164
|
+
for branch in branches_to_split:
|
|
165
|
+
worktree_name = sanitize_worktree_name(branch)
|
|
166
|
+
target_paths[branch] = worktrees_dir / worktree_name
|
|
167
|
+
|
|
168
|
+
return SplitPlan(
|
|
169
|
+
stack_branches=stack_branches,
|
|
170
|
+
branches_to_split=branches_to_split,
|
|
171
|
+
existing_worktrees=existing_worktrees,
|
|
172
|
+
target_paths=target_paths,
|
|
173
|
+
source_worktree_path=source_worktree_path,
|
|
174
|
+
repo_root=repo_root,
|
|
175
|
+
skipped_current=skipped_current,
|
|
176
|
+
skipped_trunk=skipped_trunk,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def execute_split_plan(
|
|
181
|
+
plan: SplitPlan,
|
|
182
|
+
git_ops, # Type annotation omitted to avoid circular import
|
|
183
|
+
) -> list[tuple[str, Path]]:
|
|
184
|
+
"""Execute a split plan by creating worktrees.
|
|
185
|
+
|
|
186
|
+
The actual execution depends on the Git implementation passed in:
|
|
187
|
+
- RealGit: Actually creates the worktrees
|
|
188
|
+
- DryRunGit: No-op execution for dry-run mode
|
|
189
|
+
- PrintingGit: Prints operations (wraps either Real or Noop)
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
plan: The split plan to execute
|
|
193
|
+
git_ops: Git instance for performing git operations
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
List of (branch, worktree_path) tuples indicating what was processed
|
|
197
|
+
"""
|
|
198
|
+
results = []
|
|
199
|
+
|
|
200
|
+
for branch in plan.branches_to_split:
|
|
201
|
+
target_path = plan.target_paths[branch]
|
|
202
|
+
|
|
203
|
+
# Create worktree for existing branch
|
|
204
|
+
# Using create_branch=False since branch already exists
|
|
205
|
+
# The actual behavior depends on the injected Git implementation
|
|
206
|
+
git_ops.add_worktree(
|
|
207
|
+
plan.repo_root,
|
|
208
|
+
target_path,
|
|
209
|
+
branch=branch,
|
|
210
|
+
ref=None,
|
|
211
|
+
create_branch=False,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
results.append((branch, target_path))
|
|
215
|
+
|
|
216
|
+
return results
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Status command implementation."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from erk.cli.core import discover_repo_context
|
|
6
|
+
from erk.cli.ensure import Ensure
|
|
7
|
+
from erk.core.context import ErkContext
|
|
8
|
+
from erk.status.collectors.git import GitStatusCollector
|
|
9
|
+
from erk.status.collectors.github import GitHubPRCollector
|
|
10
|
+
from erk.status.collectors.graphite import GraphiteStackCollector
|
|
11
|
+
from erk.status.collectors.impl import PlanFileCollector
|
|
12
|
+
from erk.status.orchestrator import StatusOrchestrator
|
|
13
|
+
from erk.status.renderers.simple import SimpleRenderer
|
|
14
|
+
from erk_shared.gateway.parallel.real import RealParallelTaskRunner
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@click.command("status")
|
|
18
|
+
@click.pass_obj
|
|
19
|
+
def status_cmd(ctx: ErkContext) -> None:
|
|
20
|
+
"""Show comprehensive status of current worktree."""
|
|
21
|
+
# Discover repository context
|
|
22
|
+
repo = discover_repo_context(ctx, ctx.cwd)
|
|
23
|
+
current_dir = ctx.cwd.resolve()
|
|
24
|
+
|
|
25
|
+
# Find which worktree we're in
|
|
26
|
+
worktrees = ctx.git.list_worktrees(repo.root)
|
|
27
|
+
current_worktree_path = None
|
|
28
|
+
|
|
29
|
+
for wt in worktrees:
|
|
30
|
+
# Check path exists before resolution/comparison to avoid OSError
|
|
31
|
+
if wt.path.exists():
|
|
32
|
+
wt_path_resolved = wt.path.resolve()
|
|
33
|
+
# Use is_relative_to only after confirming path exists
|
|
34
|
+
if current_dir == wt_path_resolved or current_dir.is_relative_to(wt_path_resolved):
|
|
35
|
+
current_worktree_path = wt_path_resolved
|
|
36
|
+
break
|
|
37
|
+
|
|
38
|
+
Ensure.in_git_worktree(ctx, current_worktree_path)
|
|
39
|
+
# After Ensure check, we know current_worktree_path is not None
|
|
40
|
+
assert current_worktree_path is not None
|
|
41
|
+
|
|
42
|
+
# Create collectors
|
|
43
|
+
collectors = [
|
|
44
|
+
GitStatusCollector(),
|
|
45
|
+
GraphiteStackCollector(),
|
|
46
|
+
GitHubPRCollector(),
|
|
47
|
+
PlanFileCollector(),
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
# Create orchestrator
|
|
51
|
+
orchestrator = StatusOrchestrator(collectors, runner=RealParallelTaskRunner())
|
|
52
|
+
|
|
53
|
+
# Collect status
|
|
54
|
+
status = orchestrator.collect_status(ctx, current_worktree_path, repo.root)
|
|
55
|
+
|
|
56
|
+
# Render status
|
|
57
|
+
renderer = SimpleRenderer()
|
|
58
|
+
renderer.render(status)
|