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,35 @@
|
|
|
1
|
+
"""Current command implementation - displays current erk name."""
|
|
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.core.repo_discovery import RepoContext
|
|
9
|
+
from erk.core.worktree_utils import find_current_worktree
|
|
10
|
+
from erk_shared.output.output import user_output
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@click.command("current", hidden=True)
|
|
14
|
+
@click.pass_obj
|
|
15
|
+
def current_wt(ctx: ErkContext) -> None:
|
|
16
|
+
"""Show current worktree name (hidden command for automation)."""
|
|
17
|
+
# Use ctx.repo if it's a valid RepoContext, otherwise discover
|
|
18
|
+
if isinstance(ctx.repo, RepoContext):
|
|
19
|
+
repo = ctx.repo
|
|
20
|
+
else:
|
|
21
|
+
# Discover repository context (handles None and NoRepoSentinel)
|
|
22
|
+
# If not in a git repo, FileNotFoundError will bubble up
|
|
23
|
+
repo = discover_repo_context(ctx, ctx.cwd)
|
|
24
|
+
|
|
25
|
+
current_dir = ctx.cwd
|
|
26
|
+
worktrees = ctx.git.list_worktrees(repo.root)
|
|
27
|
+
wt_info = Ensure.not_none(
|
|
28
|
+
find_current_worktree(worktrees, current_dir), "Not in an erk worktree"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# Use WorktreeInfo.is_root which is set by git when listing worktrees
|
|
32
|
+
if wt_info.is_root:
|
|
33
|
+
user_output("root")
|
|
34
|
+
else:
|
|
35
|
+
user_output(wt_info.path.name)
|
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
import subprocess
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from erk.cli.commands.branch.unassign_cmd import execute_unassign
|
|
8
|
+
from erk.cli.commands.completions import complete_worktree_names
|
|
9
|
+
from erk.cli.commands.navigation_helpers import (
|
|
10
|
+
check_pending_extraction_marker,
|
|
11
|
+
find_assignment_by_worktree_path,
|
|
12
|
+
)
|
|
13
|
+
from erk.cli.core import (
|
|
14
|
+
discover_repo_context,
|
|
15
|
+
validate_worktree_name_for_deletion,
|
|
16
|
+
worktree_path_for,
|
|
17
|
+
)
|
|
18
|
+
from erk.cli.ensure import Ensure
|
|
19
|
+
from erk.core.context import ErkContext, create_context, regenerate_context
|
|
20
|
+
from erk.core.repo_discovery import RepoContext, ensure_erk_metadata_dir
|
|
21
|
+
from erk.core.worktree_pool import load_pool_state
|
|
22
|
+
from erk.core.worktree_utils import (
|
|
23
|
+
find_worktree_containing_path,
|
|
24
|
+
get_worktree_branch,
|
|
25
|
+
)
|
|
26
|
+
from erk_shared.gateway.graphite.abc import Graphite
|
|
27
|
+
from erk_shared.git.abc import Git
|
|
28
|
+
from erk_shared.github.metadata.plan_header import extract_plan_header_worktree_name
|
|
29
|
+
from erk_shared.github.types import PRNotFound
|
|
30
|
+
from erk_shared.output.output import user_confirm, user_output
|
|
31
|
+
from erk_shared.plan_store.types import PlanQuery, PlanState
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _get_pr_info_for_branch(
|
|
35
|
+
ctx: ErkContext, repo_root: Path, branch: str
|
|
36
|
+
) -> tuple[int, str] | None:
|
|
37
|
+
"""Get PR info for display during planning phase.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
ctx: Erk context with GitHub operations
|
|
41
|
+
repo_root: Repository root directory
|
|
42
|
+
branch: Branch name to find PR for
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Tuple of (PR number, state) if found, None otherwise.
|
|
46
|
+
State is one of: "OPEN", "CLOSED", "MERGED"
|
|
47
|
+
"""
|
|
48
|
+
pr = ctx.github.get_pr_for_branch(repo_root, branch)
|
|
49
|
+
if isinstance(pr, PRNotFound):
|
|
50
|
+
return None
|
|
51
|
+
return (pr.number, pr.state)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _get_plan_info_for_worktree(
|
|
55
|
+
ctx: ErkContext, repo_root: Path, worktree_name: str
|
|
56
|
+
) -> tuple[int, PlanState] | None:
|
|
57
|
+
"""Find a plan associated with a worktree name (any state).
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
ctx: Erk context with plan store
|
|
61
|
+
repo_root: Repository root directory
|
|
62
|
+
worktree_name: Name of the worktree to find a plan for
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Tuple of (plan number, state) if found, None otherwise.
|
|
66
|
+
"""
|
|
67
|
+
# Search ALL states (open and closed) to find the plan
|
|
68
|
+
query = PlanQuery(labels=["erk-plan"])
|
|
69
|
+
plans = ctx.plan_store.list_plans(repo_root, query)
|
|
70
|
+
|
|
71
|
+
for plan in plans:
|
|
72
|
+
plan_worktree_name = extract_plan_header_worktree_name(plan.body)
|
|
73
|
+
if plan_worktree_name == worktree_name:
|
|
74
|
+
return (int(plan.plan_identifier), plan.state)
|
|
75
|
+
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _close_pr_for_branch(
|
|
80
|
+
ctx: ErkContext,
|
|
81
|
+
repo_root: Path,
|
|
82
|
+
branch: str,
|
|
83
|
+
) -> int | None:
|
|
84
|
+
"""Close the PR associated with a branch if it exists and is open.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
ctx: Erk context with GitHub operations
|
|
88
|
+
repo_root: Repository root directory
|
|
89
|
+
branch: Branch name to find PR for
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
PR number if closed, None otherwise
|
|
93
|
+
"""
|
|
94
|
+
pr = ctx.github.get_pr_for_branch(repo_root, branch)
|
|
95
|
+
|
|
96
|
+
if isinstance(pr, PRNotFound):
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
if pr.state == "OPEN":
|
|
100
|
+
ctx.github.close_pr(repo_root, pr.number)
|
|
101
|
+
user_output(
|
|
102
|
+
click.style("ℹ️ ", fg="blue", bold=True)
|
|
103
|
+
+ f"Closed PR #{pr.number}: {click.style(pr.title, fg='cyan')}"
|
|
104
|
+
)
|
|
105
|
+
return pr.number
|
|
106
|
+
|
|
107
|
+
# PR exists but is already closed/merged
|
|
108
|
+
state_color = "green" if pr.state == "MERGED" else "yellow"
|
|
109
|
+
user_output(
|
|
110
|
+
click.style("ℹ️ ", fg="blue", bold=True)
|
|
111
|
+
+ f"PR #{pr.number} already {click.style(pr.state.lower(), fg=state_color)}"
|
|
112
|
+
)
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _close_plan_for_worktree(
|
|
117
|
+
ctx: ErkContext,
|
|
118
|
+
repo_root: Path,
|
|
119
|
+
worktree_name: str,
|
|
120
|
+
) -> int | None:
|
|
121
|
+
"""Close the plan associated with a worktree name if it exists and is open.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
ctx: Erk context with plan store
|
|
125
|
+
repo_root: Repository root directory
|
|
126
|
+
worktree_name: Name of the worktree to find a plan for
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Plan issue number if closed, None otherwise
|
|
130
|
+
"""
|
|
131
|
+
plan_info = _get_plan_info_for_worktree(ctx, repo_root, worktree_name)
|
|
132
|
+
|
|
133
|
+
if plan_info is None:
|
|
134
|
+
user_output(click.style("ℹ️ ", fg="blue", bold=True) + "No associated plan found")
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
plan_number, state = plan_info
|
|
138
|
+
if state == PlanState.CLOSED:
|
|
139
|
+
user_output(
|
|
140
|
+
click.style("ℹ️ ", fg="blue", bold=True) + f"Plan #{plan_number} already closed"
|
|
141
|
+
)
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
ctx.plan_store.close_plan(repo_root, str(plan_number))
|
|
145
|
+
user_output(click.style("ℹ️ ", fg="blue", bold=True) + f"Closed plan #{plan_number}")
|
|
146
|
+
return plan_number
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _try_git_worktree_delete(git_ops: Git, repo_root: Path, wt_path: Path) -> bool:
|
|
150
|
+
"""Attempt git worktree remove, returning success status.
|
|
151
|
+
|
|
152
|
+
This function violates LBYL norms because there's no reliable way to
|
|
153
|
+
check a priori if git worktree remove will succeed. The worktree might be:
|
|
154
|
+
- Already removed from git metadata
|
|
155
|
+
- In a partially corrupted state
|
|
156
|
+
- Referenced by stale lock files
|
|
157
|
+
|
|
158
|
+
Git's own error handling is unreliable for these edge cases, so we use
|
|
159
|
+
try/except as an error boundary and rely on manual cleanup + prune.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
True if git removal succeeded, False otherwise
|
|
163
|
+
"""
|
|
164
|
+
try:
|
|
165
|
+
git_ops.remove_worktree(repo_root, wt_path, force=True)
|
|
166
|
+
return True
|
|
167
|
+
except Exception:
|
|
168
|
+
# Git removal failed - manual cleanup will handle it
|
|
169
|
+
return False
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _prune_worktrees_safe(git_ops: Git, repo_root: Path) -> None:
|
|
173
|
+
"""Prune worktree metadata, ignoring errors if nothing to prune.
|
|
174
|
+
|
|
175
|
+
This function violates LBYL norms because git worktree prune can fail
|
|
176
|
+
for various reasons (no stale worktrees, permission issues, etc.) that
|
|
177
|
+
are not easily detectable beforehand. Since pruning is a cleanup operation
|
|
178
|
+
and failure doesn't affect the primary operation, we allow silent failure.
|
|
179
|
+
"""
|
|
180
|
+
try:
|
|
181
|
+
git_ops.prune_worktrees(repo_root)
|
|
182
|
+
except Exception:
|
|
183
|
+
# Prune might fail if there's nothing to prune or other non-critical issues
|
|
184
|
+
pass
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _escape_worktree_if_inside(
|
|
188
|
+
ctx: ErkContext, repo_root: Path, wt_path: Path, dry_run: bool
|
|
189
|
+
) -> ErkContext:
|
|
190
|
+
"""Change to repository root if currently inside the worktree being deleted.
|
|
191
|
+
|
|
192
|
+
Prevents the shell from being left in a deleted directory. Returns a new
|
|
193
|
+
context if directory was changed (context is immutable), otherwise returns
|
|
194
|
+
the original context.
|
|
195
|
+
"""
|
|
196
|
+
if not ctx.git.path_exists(ctx.cwd):
|
|
197
|
+
return ctx
|
|
198
|
+
|
|
199
|
+
current_dir = ctx.cwd.resolve()
|
|
200
|
+
worktrees = ctx.git.list_worktrees(repo_root)
|
|
201
|
+
current_worktree_path = find_worktree_containing_path(worktrees, current_dir)
|
|
202
|
+
|
|
203
|
+
if current_worktree_path is None:
|
|
204
|
+
return ctx
|
|
205
|
+
|
|
206
|
+
if current_worktree_path.resolve() != wt_path.resolve():
|
|
207
|
+
return ctx
|
|
208
|
+
|
|
209
|
+
# Change to repository root before deletion
|
|
210
|
+
user_output(
|
|
211
|
+
click.style("ℹ️ ", fg="blue", bold=True)
|
|
212
|
+
+ f"Changing directory to repository root: {click.style(str(repo_root), fg='cyan')}"
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# Change directory using safe_chdir which handles both real and sentinel paths
|
|
216
|
+
if not dry_run and ctx.git.safe_chdir(repo_root):
|
|
217
|
+
# Regenerate context with new cwd (context is immutable)
|
|
218
|
+
return regenerate_context(ctx)
|
|
219
|
+
|
|
220
|
+
return ctx
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _collect_branch_to_delete(
|
|
224
|
+
ctx: ErkContext, repo_root: Path, wt_path: Path, name: str
|
|
225
|
+
) -> str | None:
|
|
226
|
+
"""Get the branch checked out on the worktree, if any.
|
|
227
|
+
|
|
228
|
+
Returns the branch name, or None if in detached HEAD state.
|
|
229
|
+
"""
|
|
230
|
+
worktrees = ctx.git.list_worktrees(repo_root)
|
|
231
|
+
worktree_branch = get_worktree_branch(worktrees, wt_path)
|
|
232
|
+
|
|
233
|
+
if worktree_branch is None:
|
|
234
|
+
user_output(
|
|
235
|
+
f"Warning: Worktree {name} is in detached HEAD state. Cannot delete branch.",
|
|
236
|
+
)
|
|
237
|
+
return None
|
|
238
|
+
|
|
239
|
+
return worktree_branch
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _display_planned_operations(
|
|
243
|
+
wt_path: Path,
|
|
244
|
+
branch_to_delete: str | None,
|
|
245
|
+
close_all: bool,
|
|
246
|
+
pr_info: tuple[int, str] | None,
|
|
247
|
+
plan_info: tuple[int, PlanState] | None,
|
|
248
|
+
) -> None:
|
|
249
|
+
"""Display the operations that will be performed.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
wt_path: Path to the worktree being deleted
|
|
253
|
+
branch_to_delete: Branch name to delete, or None if detached HEAD
|
|
254
|
+
close_all: Whether -a/--all flag was passed
|
|
255
|
+
pr_info: Tuple of (PR number, state) if found, None otherwise
|
|
256
|
+
plan_info: Tuple of (plan number, state) if found, None otherwise
|
|
257
|
+
"""
|
|
258
|
+
user_output(click.style("📋 Planning to perform the following operations:", bold=True))
|
|
259
|
+
worktree_text = click.style(str(wt_path), fg="cyan")
|
|
260
|
+
step = 1
|
|
261
|
+
user_output(f" {step}. 🗑️ Delete worktree: {worktree_text}")
|
|
262
|
+
|
|
263
|
+
if close_all and branch_to_delete:
|
|
264
|
+
step += 1
|
|
265
|
+
pr_text = _format_pr_plan_text(pr_info, "PR")
|
|
266
|
+
user_output(f" {step}. 🔒 {pr_text}")
|
|
267
|
+
step += 1
|
|
268
|
+
plan_text = _format_plan_text(plan_info)
|
|
269
|
+
user_output(f" {step}. 📝 {plan_text}")
|
|
270
|
+
|
|
271
|
+
if branch_to_delete:
|
|
272
|
+
step += 1
|
|
273
|
+
branch_text = click.style(branch_to_delete, fg="yellow")
|
|
274
|
+
user_output(f" {step}. 🌳 Delete branch: {branch_text}")
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _format_pr_plan_text(pr_info: tuple[int, str] | None, item_type: str) -> str:
|
|
278
|
+
"""Format PR info for display in planning phase."""
|
|
279
|
+
if pr_info is None:
|
|
280
|
+
return f"Close associated {item_type} (if any)"
|
|
281
|
+
|
|
282
|
+
number, state = pr_info
|
|
283
|
+
if state == "OPEN":
|
|
284
|
+
return f"Close {item_type} #{number} (currently open)"
|
|
285
|
+
elif state == "MERGED":
|
|
286
|
+
state_text = click.style("merged", fg="green")
|
|
287
|
+
return f"{item_type} #{number} already {state_text}"
|
|
288
|
+
else:
|
|
289
|
+
state_text = click.style("closed", fg="yellow")
|
|
290
|
+
return f"{item_type} #{number} already {state_text}"
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _format_plan_text(plan_info: tuple[int, PlanState] | None) -> str:
|
|
294
|
+
"""Format plan info for display in planning phase."""
|
|
295
|
+
if plan_info is None:
|
|
296
|
+
return "Close associated plan (if any)"
|
|
297
|
+
|
|
298
|
+
number, state = plan_info
|
|
299
|
+
if state == PlanState.OPEN:
|
|
300
|
+
return f"Close plan #{number} (currently open)"
|
|
301
|
+
else:
|
|
302
|
+
state_text = click.style("closed", fg="yellow")
|
|
303
|
+
return f"Plan #{number} already {state_text}"
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _confirm_operations(force: bool, dry_run: bool) -> bool:
|
|
307
|
+
"""Prompt for confirmation unless force or dry-run mode.
|
|
308
|
+
|
|
309
|
+
Returns True if operations should proceed, False if aborted.
|
|
310
|
+
"""
|
|
311
|
+
if force or dry_run:
|
|
312
|
+
return True
|
|
313
|
+
|
|
314
|
+
user_output()
|
|
315
|
+
if not user_confirm("Proceed with these operations?", default=True):
|
|
316
|
+
user_output(click.style("⭕ Aborted.", fg="red", bold=True))
|
|
317
|
+
return False
|
|
318
|
+
|
|
319
|
+
return True
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _delete_worktree_directory(ctx: ErkContext, repo: RepoContext, wt_path: Path) -> bool:
|
|
323
|
+
"""Delete the worktree directory from filesystem (slot-aware).
|
|
324
|
+
|
|
325
|
+
If worktree is a pool slot: unassigns slot (keeps directory for reuse).
|
|
326
|
+
If not a pool slot: removes worktree directory.
|
|
327
|
+
|
|
328
|
+
First attempts git worktree remove, then manually deletes if still present.
|
|
329
|
+
This function encapsulates the legitimate error boundary for shutil.rmtree
|
|
330
|
+
because in pure test mode, the path may be a sentinel that doesn't exist
|
|
331
|
+
on the real filesystem.
|
|
332
|
+
|
|
333
|
+
Returns:
|
|
334
|
+
True if this was a slot worktree (slot was unassigned), False otherwise.
|
|
335
|
+
"""
|
|
336
|
+
# Check if this is a slot worktree
|
|
337
|
+
state = load_pool_state(repo.pool_json_path)
|
|
338
|
+
assignment = None
|
|
339
|
+
if state is not None:
|
|
340
|
+
assignment = find_assignment_by_worktree_path(state, wt_path)
|
|
341
|
+
|
|
342
|
+
if assignment is not None:
|
|
343
|
+
# Slot worktree: unassign instead of delete
|
|
344
|
+
# state is guaranteed to be non-None since assignment was found in it
|
|
345
|
+
assert state is not None
|
|
346
|
+
execute_unassign(ctx, repo, state, assignment)
|
|
347
|
+
user_output(
|
|
348
|
+
click.style("✓", fg="green")
|
|
349
|
+
+ f" Unassigned slot {click.style(assignment.slot_name, fg='cyan')}"
|
|
350
|
+
)
|
|
351
|
+
return True
|
|
352
|
+
|
|
353
|
+
# Non-slot worktree: delete normally
|
|
354
|
+
# Try to delete via git first - this updates git's metadata when possible
|
|
355
|
+
_try_git_worktree_delete(ctx.git, repo.root, wt_path)
|
|
356
|
+
|
|
357
|
+
# Always manually delete directory if it still exists
|
|
358
|
+
if not ctx.git.path_exists(wt_path):
|
|
359
|
+
return False
|
|
360
|
+
|
|
361
|
+
if ctx.dry_run:
|
|
362
|
+
user_output(f"[DRY RUN] Would delete directory: {wt_path}")
|
|
363
|
+
return False
|
|
364
|
+
|
|
365
|
+
# Only call shutil.rmtree() if we're on a real filesystem.
|
|
366
|
+
# In pure test mode, we skip the actual deletion since it's a sentinel path.
|
|
367
|
+
# This violates LBYL because there's no reliable way to distinguish sentinel
|
|
368
|
+
# paths from real paths that have been deleted between the path_exists check
|
|
369
|
+
# and the rmtree call (race condition).
|
|
370
|
+
try:
|
|
371
|
+
shutil.rmtree(wt_path)
|
|
372
|
+
except OSError:
|
|
373
|
+
# Path doesn't exist on real filesystem (sentinel path), skip deletion
|
|
374
|
+
pass
|
|
375
|
+
|
|
376
|
+
# Prune worktree metadata to clean up any stale references
|
|
377
|
+
_prune_worktrees_safe(ctx.git, repo.root)
|
|
378
|
+
return False
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _delete_branch_at_error_boundary(
|
|
382
|
+
ctx: ErkContext,
|
|
383
|
+
repo_root: Path,
|
|
384
|
+
branch: str,
|
|
385
|
+
force: bool,
|
|
386
|
+
dry_run: bool,
|
|
387
|
+
graphite: Graphite,
|
|
388
|
+
) -> None:
|
|
389
|
+
"""Delete a branch after its worktree has been removed.
|
|
390
|
+
|
|
391
|
+
This function encapsulates a legitimate error boundary because:
|
|
392
|
+
1. `gt delete` prompts for user confirmation, which can be declined (exit 1)
|
|
393
|
+
2. `git branch -d` may fail if branch is not fully merged
|
|
394
|
+
3. There's no LBYL way to predict user's response to interactive prompt
|
|
395
|
+
4. This is a CLI error boundary - appropriate place per AGENTS.md
|
|
396
|
+
|
|
397
|
+
The exception handling distinguishes between user-declined (expected) and
|
|
398
|
+
actual errors (propagated as SystemExit).
|
|
399
|
+
|
|
400
|
+
Note: run_subprocess_with_context catches CalledProcessError and re-raises
|
|
401
|
+
as RuntimeError with the original exception in __cause__.
|
|
402
|
+
"""
|
|
403
|
+
use_graphite = ctx.global_config.use_graphite if ctx.global_config else False
|
|
404
|
+
|
|
405
|
+
# Determine if branch is tracked by Graphite (LBYL check using gt branch info)
|
|
406
|
+
branch_is_tracked = False
|
|
407
|
+
if use_graphite:
|
|
408
|
+
branch_is_tracked = graphite.is_branch_tracked(repo_root, branch)
|
|
409
|
+
|
|
410
|
+
try:
|
|
411
|
+
if branch_is_tracked:
|
|
412
|
+
ctx.git.delete_branch_with_graphite(repo_root, branch, force=force)
|
|
413
|
+
else:
|
|
414
|
+
ctx.git.delete_branch(repo_root, branch, force=force)
|
|
415
|
+
if not dry_run:
|
|
416
|
+
branch_text = click.style(branch, fg="green")
|
|
417
|
+
user_output(f"✅ Deleted branch: {branch_text}")
|
|
418
|
+
except RuntimeError as e:
|
|
419
|
+
_handle_branch_deletion_error(e, branch, force)
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _handle_branch_deletion_error(e: RuntimeError, branch: str, force: bool) -> None:
|
|
423
|
+
"""Handle errors from branch deletion commands.
|
|
424
|
+
|
|
425
|
+
This function encapsulates the error boundary logic for branch deletion.
|
|
426
|
+
Exit code 1 with --force off typically means user declined the confirmation
|
|
427
|
+
prompt, which is expected behavior. Other errors are propagated as SystemExit.
|
|
428
|
+
|
|
429
|
+
Args:
|
|
430
|
+
e: RuntimeError from run_subprocess_with_context, with the original
|
|
431
|
+
CalledProcessError accessible via e.__cause__
|
|
432
|
+
branch: Name of the branch that failed to delete
|
|
433
|
+
force: Whether --force flag was used
|
|
434
|
+
"""
|
|
435
|
+
branch_text = click.style(branch, fg="yellow")
|
|
436
|
+
|
|
437
|
+
# Extract returncode from the original CalledProcessError in __cause__
|
|
438
|
+
returncode: int | None = None
|
|
439
|
+
if isinstance(e.__cause__, subprocess.CalledProcessError):
|
|
440
|
+
returncode = e.__cause__.returncode
|
|
441
|
+
|
|
442
|
+
if returncode == 1 and not force:
|
|
443
|
+
# User declined - this is expected behavior, not an error
|
|
444
|
+
user_output(f"⭕ Skipped deletion of branch: {branch_text} (user declined or not eligible)")
|
|
445
|
+
else:
|
|
446
|
+
# Other error (branch doesn't exist, git failure, etc.)
|
|
447
|
+
# The RuntimeError message already contains stderr from run_subprocess_with_context
|
|
448
|
+
user_output(
|
|
449
|
+
click.style("Error: ", fg="red") + f"Failed to delete branch {branch_text}: {e}"
|
|
450
|
+
)
|
|
451
|
+
raise SystemExit(1) from e
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def _delete_worktree(
|
|
455
|
+
ctx: ErkContext,
|
|
456
|
+
name: str,
|
|
457
|
+
force: bool,
|
|
458
|
+
delete_branch: bool,
|
|
459
|
+
dry_run: bool,
|
|
460
|
+
quiet: bool = False,
|
|
461
|
+
close_all: bool = False,
|
|
462
|
+
) -> None:
|
|
463
|
+
"""Internal function to delete a worktree.
|
|
464
|
+
|
|
465
|
+
Args:
|
|
466
|
+
ctx: Erk context with git operations
|
|
467
|
+
name: Name of the worktree to delete
|
|
468
|
+
force: Skip confirmation prompts and use -D for branch deletion
|
|
469
|
+
delete_branch: Delete the branch checked out on the worktree
|
|
470
|
+
dry_run: Print what would be done without executing destructive operations
|
|
471
|
+
quiet: Suppress planning output (still shows final confirmation)
|
|
472
|
+
close_all: Also close associated PR and plan
|
|
473
|
+
"""
|
|
474
|
+
if dry_run:
|
|
475
|
+
ctx = create_context(dry_run=True)
|
|
476
|
+
|
|
477
|
+
validate_worktree_name_for_deletion(name)
|
|
478
|
+
|
|
479
|
+
repo = discover_repo_context(ctx, ctx.cwd)
|
|
480
|
+
ensure_erk_metadata_dir(repo)
|
|
481
|
+
wt_path = worktree_path_for(repo.worktrees_dir, name)
|
|
482
|
+
|
|
483
|
+
Ensure.path_exists(ctx, wt_path, f"Worktree not found: {wt_path}")
|
|
484
|
+
|
|
485
|
+
# Check for pending extraction marker
|
|
486
|
+
check_pending_extraction_marker(wt_path, force)
|
|
487
|
+
|
|
488
|
+
# main_repo_root is always set by RepoContext.__post_init__, but ty doesn't know
|
|
489
|
+
main_repo = repo.main_repo_root if repo.main_repo_root else repo.root
|
|
490
|
+
ctx = _escape_worktree_if_inside(ctx, main_repo, wt_path, dry_run)
|
|
491
|
+
|
|
492
|
+
branch_to_delete: str | None = None
|
|
493
|
+
if delete_branch:
|
|
494
|
+
branch_to_delete = _collect_branch_to_delete(ctx, repo.root, wt_path, name)
|
|
495
|
+
|
|
496
|
+
# Fetch PR/plan info before displaying plan (for informative planning output)
|
|
497
|
+
pr_info: tuple[int, str] | None = None
|
|
498
|
+
plan_info: tuple[int, PlanState] | None = None
|
|
499
|
+
if close_all and branch_to_delete:
|
|
500
|
+
pr_info = _get_pr_info_for_branch(ctx, repo.root, branch_to_delete)
|
|
501
|
+
plan_info = _get_plan_info_for_worktree(ctx, repo.root, name)
|
|
502
|
+
|
|
503
|
+
if not quiet:
|
|
504
|
+
_display_planned_operations(wt_path, branch_to_delete, close_all, pr_info, plan_info)
|
|
505
|
+
|
|
506
|
+
if not _confirm_operations(force, dry_run):
|
|
507
|
+
return
|
|
508
|
+
|
|
509
|
+
# Order of operations: worktree delete → PR close → plan close → branch delete
|
|
510
|
+
was_slot = _delete_worktree_directory(ctx, repo, wt_path)
|
|
511
|
+
|
|
512
|
+
if close_all and branch_to_delete:
|
|
513
|
+
# Close PR for the branch (if exists and open)
|
|
514
|
+
_close_pr_for_branch(ctx, repo.root, branch_to_delete)
|
|
515
|
+
# Close plan for the worktree (if exists and open)
|
|
516
|
+
_close_plan_for_worktree(ctx, repo.root, name)
|
|
517
|
+
|
|
518
|
+
if branch_to_delete:
|
|
519
|
+
# User already confirmed via _confirm_operations(), so force=True for branch deletion
|
|
520
|
+
# to avoid redundant Graphite prompt
|
|
521
|
+
_delete_branch_at_error_boundary(
|
|
522
|
+
ctx, repo.root, branch_to_delete, force=True, dry_run=dry_run, graphite=ctx.graphite
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
if not dry_run and not was_slot:
|
|
526
|
+
# Only show "Deleted worktree" message if not a slot (slot shows its own message)
|
|
527
|
+
path_text = click.style(str(wt_path), fg="green")
|
|
528
|
+
user_output(f"✅ Deleted worktree: {path_text}")
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
@click.command("delete")
|
|
532
|
+
@click.argument("name", metavar="NAME", shell_complete=complete_worktree_names)
|
|
533
|
+
@click.option("-f", "--force", is_flag=True, help="Do not prompt for confirmation.")
|
|
534
|
+
@click.option(
|
|
535
|
+
"-b",
|
|
536
|
+
"--branch",
|
|
537
|
+
is_flag=True,
|
|
538
|
+
help="Delete the branch checked out on the worktree.",
|
|
539
|
+
)
|
|
540
|
+
@click.option(
|
|
541
|
+
"-a",
|
|
542
|
+
"--all",
|
|
543
|
+
"close_all", # Use different name to avoid shadowing builtin
|
|
544
|
+
is_flag=True,
|
|
545
|
+
help="Delete branch, close associated PR and plan.",
|
|
546
|
+
)
|
|
547
|
+
@click.option(
|
|
548
|
+
"--dry-run",
|
|
549
|
+
is_flag=True,
|
|
550
|
+
# dry_run=False: Allow destructive operations by default
|
|
551
|
+
default=False,
|
|
552
|
+
help="Print what would be done without executing destructive operations.",
|
|
553
|
+
)
|
|
554
|
+
@click.pass_obj
|
|
555
|
+
def delete_wt(
|
|
556
|
+
ctx: ErkContext,
|
|
557
|
+
name: str,
|
|
558
|
+
force: bool,
|
|
559
|
+
branch: bool,
|
|
560
|
+
close_all: bool,
|
|
561
|
+
dry_run: bool,
|
|
562
|
+
) -> None:
|
|
563
|
+
"""Delete the worktree directory.
|
|
564
|
+
|
|
565
|
+
With `-f/--force`, skips the confirmation prompt and uses -D for branch deletion.
|
|
566
|
+
Attempts `git worktree remove` before deleting the directory.
|
|
567
|
+
|
|
568
|
+
With `-a/--all`, also closes the associated PR and plan (implies --branch).
|
|
569
|
+
"""
|
|
570
|
+
# --all implies --branch
|
|
571
|
+
if close_all:
|
|
572
|
+
branch = True
|
|
573
|
+
_delete_worktree(ctx, name, force, branch, dry_run, close_all=close_all)
|