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,309 @@
|
|
|
1
|
+
"""Move branches between worktrees with explicit source specification."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from erk.cli.commands.completions import complete_worktree_names
|
|
8
|
+
from erk.cli.core import discover_repo_context, worktree_path_for
|
|
9
|
+
from erk.cli.ensure import Ensure
|
|
10
|
+
from erk.core.context import ErkContext
|
|
11
|
+
from erk.core.repo_discovery import ensure_erk_metadata_dir
|
|
12
|
+
from erk.core.worktree_utils import (
|
|
13
|
+
MoveOperationType,
|
|
14
|
+
determine_move_operation,
|
|
15
|
+
find_worktree_containing_path,
|
|
16
|
+
find_worktree_with_branch,
|
|
17
|
+
get_worktree_branch,
|
|
18
|
+
)
|
|
19
|
+
from erk_shared.output.output import user_confirm, user_output
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _resolve_current_worktree(ctx: ErkContext, repo_root: Path) -> Path:
|
|
23
|
+
"""Find worktree containing current directory.
|
|
24
|
+
|
|
25
|
+
Raises SystemExit if not in a git repository or not in any worktree.
|
|
26
|
+
"""
|
|
27
|
+
Ensure.not_none(ctx.git.get_git_common_dir(ctx.cwd), "Not in a git repository")
|
|
28
|
+
|
|
29
|
+
cwd = ctx.cwd.resolve()
|
|
30
|
+
worktrees = ctx.git.list_worktrees(repo_root)
|
|
31
|
+
wt_path = find_worktree_containing_path(worktrees, cwd)
|
|
32
|
+
if wt_path is None:
|
|
33
|
+
user_output(
|
|
34
|
+
f"Error: Current directory ({cwd}) is not in any worktree.\n"
|
|
35
|
+
f"Either run this from within a worktree, or use --worktree or "
|
|
36
|
+
f"--branch to specify the source."
|
|
37
|
+
)
|
|
38
|
+
raise SystemExit(1)
|
|
39
|
+
return wt_path
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def resolve_source_worktree(
|
|
43
|
+
ctx: ErkContext,
|
|
44
|
+
repo_root: Path,
|
|
45
|
+
*,
|
|
46
|
+
current: bool,
|
|
47
|
+
branch: str | None,
|
|
48
|
+
worktree: str | None,
|
|
49
|
+
worktrees_dir: Path,
|
|
50
|
+
) -> Path:
|
|
51
|
+
"""Determine source worktree from flags.
|
|
52
|
+
|
|
53
|
+
Defaults to current worktree if no flags provided.
|
|
54
|
+
Raises SystemExit if multiple flags specified or if source cannot be resolved.
|
|
55
|
+
"""
|
|
56
|
+
# Count how many source flags are specified
|
|
57
|
+
flag_count = sum([current, branch is not None, worktree is not None])
|
|
58
|
+
|
|
59
|
+
if flag_count > 1:
|
|
60
|
+
user_output("Error: Only one of --current, --branch, or --worktree can be specified")
|
|
61
|
+
raise SystemExit(1)
|
|
62
|
+
|
|
63
|
+
if flag_count == 0 or current:
|
|
64
|
+
# Default to current worktree (either no flags or --current explicitly set)
|
|
65
|
+
return _resolve_current_worktree(ctx, repo_root)
|
|
66
|
+
|
|
67
|
+
if branch:
|
|
68
|
+
# Find worktree containing this branch
|
|
69
|
+
worktrees = ctx.git.list_worktrees(repo_root)
|
|
70
|
+
wt = Ensure.not_none(
|
|
71
|
+
find_worktree_with_branch(worktrees, branch),
|
|
72
|
+
f"Branch '{branch}' not found in any worktree",
|
|
73
|
+
)
|
|
74
|
+
return wt
|
|
75
|
+
|
|
76
|
+
if worktree:
|
|
77
|
+
# Resolve worktree name to path
|
|
78
|
+
wt_path = worktree_path_for(worktrees_dir, worktree)
|
|
79
|
+
# Validate that the worktree exists
|
|
80
|
+
Ensure.path_exists(ctx, wt_path, f"Worktree '{worktree}' does not exist")
|
|
81
|
+
return wt_path
|
|
82
|
+
|
|
83
|
+
user_output("Error: Invalid state - no source specified")
|
|
84
|
+
raise SystemExit(1)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def detect_operation_type(
|
|
88
|
+
source_wt: Path, target_wt: Path, ctx: ErkContext, repo_root: Path
|
|
89
|
+
) -> MoveOperationType:
|
|
90
|
+
"""Determine whether to move, swap, or create based on target existence.
|
|
91
|
+
|
|
92
|
+
Returns MoveOperationType enum value.
|
|
93
|
+
"""
|
|
94
|
+
worktrees = ctx.git.list_worktrees(repo_root)
|
|
95
|
+
operation = determine_move_operation(worktrees, source_wt, target_wt)
|
|
96
|
+
return operation.operation_type
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def execute_move(
|
|
100
|
+
ctx: ErkContext,
|
|
101
|
+
repo_root: Path,
|
|
102
|
+
source_wt: Path,
|
|
103
|
+
target_wt: Path,
|
|
104
|
+
fallback_ref: str,
|
|
105
|
+
*,
|
|
106
|
+
force: bool,
|
|
107
|
+
) -> None:
|
|
108
|
+
"""Execute move operation (target doesn't exist or is in detached HEAD).
|
|
109
|
+
|
|
110
|
+
Moves the branch from source to target, then switches source to fallback_ref.
|
|
111
|
+
"""
|
|
112
|
+
# Validate source has a branch
|
|
113
|
+
worktrees = ctx.git.list_worktrees(repo_root)
|
|
114
|
+
source_branch = Ensure.not_none(
|
|
115
|
+
get_worktree_branch(worktrees, source_wt), "Source worktree is in detached HEAD state"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Check for uncommitted changes in source
|
|
119
|
+
if ctx.git.has_uncommitted_changes(source_wt) and not force:
|
|
120
|
+
user_output(
|
|
121
|
+
f"Error: Uncommitted changes in source worktree '{source_wt.name}'.\n"
|
|
122
|
+
f"Commit, stash, or use --force to override."
|
|
123
|
+
)
|
|
124
|
+
raise SystemExit(1)
|
|
125
|
+
|
|
126
|
+
target_exists = ctx.git.path_exists(target_wt)
|
|
127
|
+
|
|
128
|
+
# To move branch from source to target, we need to avoid having the same branch
|
|
129
|
+
# checked out in two places simultaneously. Strategy:
|
|
130
|
+
# 1. Detach HEAD in source worktree (frees up source_branch)
|
|
131
|
+
# 2. Create/checkout source_branch in target worktree
|
|
132
|
+
# 3. Checkout fallback_ref in source worktree
|
|
133
|
+
user_output(f"Moving '{source_branch}' from '{source_wt.name}' to '{target_wt.name}'")
|
|
134
|
+
ctx.git.checkout_detached(source_wt, source_branch)
|
|
135
|
+
|
|
136
|
+
if target_exists:
|
|
137
|
+
# Target exists - check for uncommitted changes
|
|
138
|
+
if ctx.git.has_uncommitted_changes(target_wt) and not force:
|
|
139
|
+
user_output(
|
|
140
|
+
f"Error: Uncommitted changes in target worktree '{target_wt.name}'.\n"
|
|
141
|
+
f"Commit, stash, or use --force to override."
|
|
142
|
+
)
|
|
143
|
+
raise SystemExit(1)
|
|
144
|
+
|
|
145
|
+
# Checkout branch in existing target
|
|
146
|
+
ctx.git.checkout_branch(target_wt, source_branch)
|
|
147
|
+
else:
|
|
148
|
+
# Create new worktree with branch
|
|
149
|
+
ctx.git.add_worktree(
|
|
150
|
+
repo_root, target_wt, branch=source_branch, ref=None, create_branch=False
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Check if fallback_ref is already checked out elsewhere, and detach it if needed
|
|
154
|
+
fallback_wt = ctx.git.is_branch_checked_out(repo_root, fallback_ref)
|
|
155
|
+
if fallback_wt is not None and fallback_wt.resolve() != source_wt.resolve():
|
|
156
|
+
# Fallback branch is checked out in another worktree, detach it first
|
|
157
|
+
ctx.git.checkout_detached(fallback_wt, fallback_ref)
|
|
158
|
+
|
|
159
|
+
# Switch source to fallback branch
|
|
160
|
+
ctx.git.checkout_branch(source_wt, fallback_ref)
|
|
161
|
+
|
|
162
|
+
user_output(f"✓ Moved '{source_branch}' from '{source_wt.name}' to '{target_wt.name}'")
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def execute_swap(
|
|
166
|
+
ctx: ErkContext,
|
|
167
|
+
repo_root: Path,
|
|
168
|
+
source_wt: Path,
|
|
169
|
+
target_wt: Path,
|
|
170
|
+
*,
|
|
171
|
+
force: bool,
|
|
172
|
+
) -> None:
|
|
173
|
+
"""Execute swap operation (both worktrees exist with branches).
|
|
174
|
+
|
|
175
|
+
Swaps the branches between source and target worktrees.
|
|
176
|
+
"""
|
|
177
|
+
worktrees = ctx.git.list_worktrees(repo_root)
|
|
178
|
+
source_branch = get_worktree_branch(worktrees, source_wt)
|
|
179
|
+
target_branch = get_worktree_branch(worktrees, target_wt)
|
|
180
|
+
|
|
181
|
+
if source_branch is None or target_branch is None:
|
|
182
|
+
user_output("Error: Both worktrees must have branches checked out for swap")
|
|
183
|
+
raise SystemExit(1)
|
|
184
|
+
|
|
185
|
+
# Check for uncommitted changes
|
|
186
|
+
if ctx.git.has_uncommitted_changes(source_wt) or ctx.git.has_uncommitted_changes(target_wt):
|
|
187
|
+
if not force:
|
|
188
|
+
user_output(
|
|
189
|
+
"Error: Uncommitted changes detected in one or more worktrees.\n"
|
|
190
|
+
"Commit, stash, or use --force to override."
|
|
191
|
+
)
|
|
192
|
+
raise SystemExit(1)
|
|
193
|
+
|
|
194
|
+
# Confirm swap unless --force
|
|
195
|
+
if not force:
|
|
196
|
+
user_output("This will swap branches between worktrees:")
|
|
197
|
+
user_output(f" '{source_wt.name}': '{source_branch}' → '{target_branch}'")
|
|
198
|
+
user_output(f" '{target_wt.name}': '{target_branch}' → '{source_branch}'")
|
|
199
|
+
if not user_confirm("Continue?", default=False):
|
|
200
|
+
user_output("Swap cancelled")
|
|
201
|
+
raise SystemExit(0)
|
|
202
|
+
|
|
203
|
+
user_output(f"Swapping branches between '{source_wt.name}' and '{target_wt.name}'")
|
|
204
|
+
|
|
205
|
+
# To swap branches between worktrees, we need to avoid having the same branch
|
|
206
|
+
# checked out in two places simultaneously. Strategy:
|
|
207
|
+
# 1. Detach HEAD in source worktree (frees up source_branch)
|
|
208
|
+
# 2. Checkout source_branch in target worktree
|
|
209
|
+
# 3. Checkout target_branch in source worktree
|
|
210
|
+
ctx.git.checkout_detached(source_wt, source_branch)
|
|
211
|
+
ctx.git.checkout_branch(target_wt, source_branch)
|
|
212
|
+
ctx.git.checkout_branch(source_wt, target_branch)
|
|
213
|
+
|
|
214
|
+
user_output(f"✓ Swapped '{source_branch}' ↔ '{target_branch}'")
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
@click.command("move")
|
|
218
|
+
@click.option("--current", is_flag=True, help="Use current worktree as source")
|
|
219
|
+
@click.option("--branch", help="Auto-detect worktree containing this branch")
|
|
220
|
+
@click.option("--worktree", help="Use specific worktree as source")
|
|
221
|
+
@click.option("--ref", default="main", help="Fallback branch for source after move (default: main)")
|
|
222
|
+
@click.option("-f", "--force", is_flag=True, help="Skip confirmation prompts")
|
|
223
|
+
@click.argument("target", required=True, shell_complete=complete_worktree_names)
|
|
224
|
+
@click.pass_obj
|
|
225
|
+
def move_stack(
|
|
226
|
+
ctx: ErkContext,
|
|
227
|
+
current: bool,
|
|
228
|
+
branch: str | None,
|
|
229
|
+
worktree: str | None,
|
|
230
|
+
ref: str,
|
|
231
|
+
force: bool,
|
|
232
|
+
target: str,
|
|
233
|
+
) -> None:
|
|
234
|
+
"""Move branches between worktrees with explicit source specification.
|
|
235
|
+
|
|
236
|
+
Examples:
|
|
237
|
+
|
|
238
|
+
\b
|
|
239
|
+
# Move current branch back to repository root
|
|
240
|
+
erk move root
|
|
241
|
+
|
|
242
|
+
\b
|
|
243
|
+
# Move from current worktree to new worktree
|
|
244
|
+
erk move target-wt
|
|
245
|
+
|
|
246
|
+
\b
|
|
247
|
+
# Move from current worktree (explicit)
|
|
248
|
+
erk move --current target-wt
|
|
249
|
+
|
|
250
|
+
\b
|
|
251
|
+
# Auto-detect source from branch name
|
|
252
|
+
erk move --branch feature-x new-wt
|
|
253
|
+
|
|
254
|
+
\b
|
|
255
|
+
# Move from specific source to target
|
|
256
|
+
erk move --worktree old-wt new-wt
|
|
257
|
+
|
|
258
|
+
\b
|
|
259
|
+
# Swap branches between current and another worktree
|
|
260
|
+
erk move --current existing-wt
|
|
261
|
+
|
|
262
|
+
\b
|
|
263
|
+
# Force operation without prompts (for scripts)
|
|
264
|
+
erk move --current target-wt --force
|
|
265
|
+
|
|
266
|
+
\b
|
|
267
|
+
# Specify custom fallback branch
|
|
268
|
+
erk move --current new-wt --ref develop
|
|
269
|
+
"""
|
|
270
|
+
# Discover repository context
|
|
271
|
+
repo = discover_repo_context(ctx, ctx.cwd)
|
|
272
|
+
ensure_erk_metadata_dir(repo)
|
|
273
|
+
|
|
274
|
+
# Resolve source worktree
|
|
275
|
+
source_wt = resolve_source_worktree(
|
|
276
|
+
ctx,
|
|
277
|
+
repo.root,
|
|
278
|
+
current=current,
|
|
279
|
+
branch=branch,
|
|
280
|
+
worktree=worktree,
|
|
281
|
+
worktrees_dir=repo.worktrees_dir,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
# Resolve target worktree path
|
|
285
|
+
# Special case: "root" refers to the main repository root (not current worktree)
|
|
286
|
+
if target == "root":
|
|
287
|
+
# main_repo_root is always set by RepoContext.__post_init__, but ty doesn't know
|
|
288
|
+
target_wt = repo.main_repo_root if repo.main_repo_root else repo.root
|
|
289
|
+
else:
|
|
290
|
+
target_wt = worktree_path_for(repo.worktrees_dir, target)
|
|
291
|
+
|
|
292
|
+
# Validate source and target are different
|
|
293
|
+
if source_wt.resolve() == target_wt.resolve():
|
|
294
|
+
user_output("Error: Source and target worktrees are the same")
|
|
295
|
+
raise SystemExit(1)
|
|
296
|
+
|
|
297
|
+
# Detect operation type
|
|
298
|
+
operation_type = detect_operation_type(source_wt, target_wt, ctx, repo.root)
|
|
299
|
+
|
|
300
|
+
# Execute operation
|
|
301
|
+
if operation_type == MoveOperationType.SWAP:
|
|
302
|
+
execute_swap(ctx, repo.root, source_wt, target_wt, force=force)
|
|
303
|
+
else:
|
|
304
|
+
# Auto-detect default branch if using 'main' default and it doesn't exist
|
|
305
|
+
if ref == "main":
|
|
306
|
+
detected_default = ctx.git.detect_trunk_branch(repo.root)
|
|
307
|
+
ref = detected_default
|
|
308
|
+
|
|
309
|
+
execute_move(ctx, repo.root, source_wt, target_wt, ref, force=force)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Split Command
|
|
2
|
+
|
|
3
|
+
The split command creates individual worktrees for each branch in a Graphite stack, implementing the ephemeral worktree pattern. It's the inverse of the consolidate command.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Split takes a consolidated worktree (where multiple branches exist in one worktree via `gt stack checkout`) and creates individual worktrees for each branch. This allows parallel development across multiple branches in a stack.
|
|
8
|
+
|
|
9
|
+
## Module Structure
|
|
10
|
+
|
|
11
|
+
- `command.py` - CLI entry point, validation, and orchestration
|
|
12
|
+
- `plan.py` - Models, planning logic, and worktree creation
|
|
13
|
+
- `display.py` - Output formatting and user interaction
|
|
14
|
+
|
|
15
|
+
## Key Concepts
|
|
16
|
+
|
|
17
|
+
### Exclusions
|
|
18
|
+
|
|
19
|
+
The split command automatically excludes:
|
|
20
|
+
|
|
21
|
+
- **Trunk branch** (main/master) - stays in the root worktree
|
|
22
|
+
- **Current branch** - already checked out, can't have duplicate worktrees
|
|
23
|
+
- **Existing worktrees** - idempotent operation, preserves existing worktrees
|
|
24
|
+
|
|
25
|
+
### Stack Filtering
|
|
26
|
+
|
|
27
|
+
- `--up` - Split only upstack (current branch to leaf)
|
|
28
|
+
- `--down` - Split only downstack (trunk to current branch)
|
|
29
|
+
- Default - Split entire stack (trunk to leaf)
|
|
30
|
+
|
|
31
|
+
## Workflow Phases
|
|
32
|
+
|
|
33
|
+
1. **Validation** - Check flags, trunk availability, uncommitted changes
|
|
34
|
+
2. **Discovery** - Get Graphite stack branches
|
|
35
|
+
3. **Filtering** - Apply --up/--down filters
|
|
36
|
+
4. **Planning** - Identify branches needing worktrees
|
|
37
|
+
5. **Preview** - Show what will be created
|
|
38
|
+
6. **Confirmation** - User approval (unless --force or --dry-run)
|
|
39
|
+
7. **Execution** - Create worktrees
|
|
40
|
+
8. **Results** - Display what was created
|
|
41
|
+
|
|
42
|
+
## Usage Examples
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# Split full stack into worktrees
|
|
46
|
+
erk split
|
|
47
|
+
|
|
48
|
+
# Split only upstack branches
|
|
49
|
+
erk split --up
|
|
50
|
+
|
|
51
|
+
# Preview without creating
|
|
52
|
+
erk split --dry-run
|
|
53
|
+
|
|
54
|
+
# Skip confirmation
|
|
55
|
+
erk split --force
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Integration
|
|
59
|
+
|
|
60
|
+
The split command integrates with:
|
|
61
|
+
|
|
62
|
+
- **Graphite** - Uses gt to determine stack structure
|
|
63
|
+
- **Git worktrees** - Creates standard git worktrees
|
|
64
|
+
- **Erks directory** - Places worktrees in `.erks/`
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""Split command - CLI entry point, validation, and orchestration."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from erk.cli.commands.stack.split_old.display import (
|
|
8
|
+
confirm_split,
|
|
9
|
+
display_creation_preview,
|
|
10
|
+
display_results,
|
|
11
|
+
display_stack_preview,
|
|
12
|
+
)
|
|
13
|
+
from erk.cli.commands.stack.split_old.plan import (
|
|
14
|
+
create_split_plan,
|
|
15
|
+
execute_split_plan,
|
|
16
|
+
get_stack_branches,
|
|
17
|
+
)
|
|
18
|
+
from erk.cli.core import discover_repo_context
|
|
19
|
+
from erk.cli.graphite_command import GraphiteCommand
|
|
20
|
+
from erk.core.context import ErkContext
|
|
21
|
+
from erk_shared.naming import sanitize_worktree_name
|
|
22
|
+
from erk_shared.output.output import user_output
|
|
23
|
+
|
|
24
|
+
# Validation functions
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def validate_flags(up: bool, down: bool) -> None:
|
|
28
|
+
"""Validate that --up and --down are not used together.
|
|
29
|
+
|
|
30
|
+
Raises:
|
|
31
|
+
SystemExit: If both flags are set
|
|
32
|
+
"""
|
|
33
|
+
if up and down:
|
|
34
|
+
user_output(click.style("❌ Error: Cannot use --up and --down together", fg="red"))
|
|
35
|
+
user_output(
|
|
36
|
+
"Use either --up (split upstack) or --down (split downstack) or neither (full stack)"
|
|
37
|
+
)
|
|
38
|
+
raise SystemExit(1)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def validate_trunk_branch(trunk_branch: str | None) -> None:
|
|
42
|
+
"""Validate trunk branch is available.
|
|
43
|
+
|
|
44
|
+
Raises:
|
|
45
|
+
SystemExit: If trunk branch cannot be determined
|
|
46
|
+
"""
|
|
47
|
+
if not trunk_branch:
|
|
48
|
+
user_output(click.style("❌ Error: Cannot determine trunk branch", fg="red"))
|
|
49
|
+
user_output("Initialize repository or configure trunk branch")
|
|
50
|
+
raise SystemExit(1)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def check_uncommitted_changes(
|
|
54
|
+
ctx: ErkContext,
|
|
55
|
+
current_worktree: Path,
|
|
56
|
+
force: bool,
|
|
57
|
+
dry_run: bool,
|
|
58
|
+
) -> None:
|
|
59
|
+
"""Check for uncommitted changes unless --force or --dry-run.
|
|
60
|
+
|
|
61
|
+
Raises:
|
|
62
|
+
SystemExit: If uncommitted changes detected
|
|
63
|
+
"""
|
|
64
|
+
if not force and not dry_run:
|
|
65
|
+
if ctx.git.has_uncommitted_changes(current_worktree):
|
|
66
|
+
user_output(click.style("❌ Error: Uncommitted changes detected", fg="red", bold=True))
|
|
67
|
+
user_output("\nCommit or stash changes before running split")
|
|
68
|
+
raise SystemExit(1)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# Stack filtering
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def apply_stack_filter(
|
|
75
|
+
stack_branches: list[str],
|
|
76
|
+
current_branch: str | None,
|
|
77
|
+
up: bool,
|
|
78
|
+
down: bool,
|
|
79
|
+
) -> list[str]:
|
|
80
|
+
"""Apply --up or --down filters to determine which branches to split.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
stack_branches: Full stack from trunk to leaf
|
|
84
|
+
current_branch: Currently checked out branch (None if detached)
|
|
85
|
+
up: If True, only split upstack (current to leaf)
|
|
86
|
+
down: If True, only split downstack (trunk to current)
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
Filtered list of branches to split
|
|
90
|
+
|
|
91
|
+
Notes:
|
|
92
|
+
- If both up and down are False, returns full stack
|
|
93
|
+
- If current_branch is None, filters have no effect
|
|
94
|
+
- If current_branch is not in stack, returns empty list
|
|
95
|
+
"""
|
|
96
|
+
if up and current_branch is not None:
|
|
97
|
+
# Only split upstack (from current to leaf)
|
|
98
|
+
if current_branch in stack_branches:
|
|
99
|
+
current_index = stack_branches.index(current_branch)
|
|
100
|
+
return stack_branches[current_index:]
|
|
101
|
+
else:
|
|
102
|
+
# Current branch not in stack, split nothing
|
|
103
|
+
return []
|
|
104
|
+
elif down and current_branch is not None:
|
|
105
|
+
# Only split downstack (from trunk to current)
|
|
106
|
+
if current_branch in stack_branches:
|
|
107
|
+
current_index = stack_branches.index(current_branch)
|
|
108
|
+
return stack_branches[: current_index + 1]
|
|
109
|
+
else:
|
|
110
|
+
# Current branch not in stack, split nothing
|
|
111
|
+
return []
|
|
112
|
+
else:
|
|
113
|
+
# Split entire stack
|
|
114
|
+
return stack_branches
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# Main CLI command
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@click.command("split", cls=GraphiteCommand)
|
|
121
|
+
@click.option("-f", "--force", is_flag=True, help="Skip confirmation prompt")
|
|
122
|
+
@click.option(
|
|
123
|
+
"--dry-run",
|
|
124
|
+
is_flag=True,
|
|
125
|
+
default=False,
|
|
126
|
+
help="Show what worktrees would be created without executing",
|
|
127
|
+
)
|
|
128
|
+
@click.option(
|
|
129
|
+
"--up",
|
|
130
|
+
is_flag=True,
|
|
131
|
+
help="Only split upstack (current branch to leaf). Default is entire stack.",
|
|
132
|
+
)
|
|
133
|
+
@click.option(
|
|
134
|
+
"--down",
|
|
135
|
+
is_flag=True,
|
|
136
|
+
help="Only split downstack (trunk to current branch). Default is entire stack.",
|
|
137
|
+
)
|
|
138
|
+
@click.pass_obj
|
|
139
|
+
def split_cmd(
|
|
140
|
+
ctx: ErkContext,
|
|
141
|
+
force: bool,
|
|
142
|
+
dry_run: bool,
|
|
143
|
+
up: bool,
|
|
144
|
+
down: bool,
|
|
145
|
+
) -> None:
|
|
146
|
+
"""Split a stack into individual worktrees per branch.
|
|
147
|
+
|
|
148
|
+
This is the inverse of consolidate - it creates individual worktrees for each
|
|
149
|
+
branch in the stack (except trunk and the current branch).
|
|
150
|
+
|
|
151
|
+
By default, splits the full stack (trunk to leaf). With --up or --down, splits
|
|
152
|
+
only a portion of the stack.
|
|
153
|
+
|
|
154
|
+
This command is useful after consolidating branches for operations like
|
|
155
|
+
'gt restack', allowing you to return to the ephemeral worktree pattern.
|
|
156
|
+
|
|
157
|
+
\b
|
|
158
|
+
Examples:
|
|
159
|
+
# Split full stack into individual worktrees (default)
|
|
160
|
+
$ erk split
|
|
161
|
+
|
|
162
|
+
# Split only upstack (current to leaf)
|
|
163
|
+
$ erk split --up
|
|
164
|
+
|
|
165
|
+
# Split only downstack (trunk to current)
|
|
166
|
+
$ erk split --down
|
|
167
|
+
|
|
168
|
+
# Preview changes without executing
|
|
169
|
+
$ erk split --dry-run
|
|
170
|
+
|
|
171
|
+
# Skip confirmation prompt
|
|
172
|
+
$ erk split --force
|
|
173
|
+
|
|
174
|
+
Notes:
|
|
175
|
+
- Trunk branch (main/master) stays in root worktree
|
|
176
|
+
- Current branch cannot get its own worktree (already checked out)
|
|
177
|
+
- Existing worktrees are preserved (idempotent operation)
|
|
178
|
+
- Creates worktrees in the .erks directory
|
|
179
|
+
"""
|
|
180
|
+
# 1. Validate input flags
|
|
181
|
+
validate_flags(up, down)
|
|
182
|
+
|
|
183
|
+
# 2. Gather repository context
|
|
184
|
+
current_worktree = ctx.cwd
|
|
185
|
+
current_branch = ctx.git.get_current_branch(current_worktree)
|
|
186
|
+
repo = discover_repo_context(ctx, current_worktree)
|
|
187
|
+
trunk_branch = ctx.trunk_branch
|
|
188
|
+
validate_trunk_branch(trunk_branch)
|
|
189
|
+
# After validation, trunk_branch is guaranteed to be non-None
|
|
190
|
+
assert trunk_branch is not None # Type narrowing for mypy/ty
|
|
191
|
+
|
|
192
|
+
# 3. Get stack branches
|
|
193
|
+
stack_branches = get_stack_branches(ctx, repo.root, current_branch, trunk_branch)
|
|
194
|
+
|
|
195
|
+
# 4. Apply stack filters
|
|
196
|
+
stack_to_split = apply_stack_filter(stack_branches, current_branch, up, down)
|
|
197
|
+
|
|
198
|
+
# 5. Safety checks
|
|
199
|
+
check_uncommitted_changes(ctx, current_worktree, force, dry_run)
|
|
200
|
+
|
|
201
|
+
# 6. Create split plan
|
|
202
|
+
all_worktrees = ctx.git.list_worktrees(repo.root)
|
|
203
|
+
plan = create_split_plan(
|
|
204
|
+
stack_branches=stack_to_split,
|
|
205
|
+
trunk_branch=trunk_branch,
|
|
206
|
+
current_branch=current_branch,
|
|
207
|
+
all_worktrees=all_worktrees,
|
|
208
|
+
worktrees_dir=repo.worktrees_dir,
|
|
209
|
+
sanitize_worktree_name=sanitize_worktree_name,
|
|
210
|
+
source_worktree_path=current_worktree,
|
|
211
|
+
repo_root=repo.root,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
# 7. Display preview
|
|
215
|
+
display_stack_preview(stack_to_split, trunk_branch, current_branch, plan)
|
|
216
|
+
display_creation_preview(plan, dry_run)
|
|
217
|
+
|
|
218
|
+
# Early exit if nothing to do
|
|
219
|
+
if not plan.branches_to_split:
|
|
220
|
+
return
|
|
221
|
+
|
|
222
|
+
# 8. Get user confirmation
|
|
223
|
+
confirm_split(force, dry_run)
|
|
224
|
+
|
|
225
|
+
# 9. Execute or simulate
|
|
226
|
+
user_output("")
|
|
227
|
+
if dry_run:
|
|
228
|
+
results = [(branch, plan.target_paths[branch]) for branch in plan.branches_to_split]
|
|
229
|
+
else:
|
|
230
|
+
results = execute_split_plan(plan, ctx.git)
|
|
231
|
+
|
|
232
|
+
# 10. Display results
|
|
233
|
+
display_results(results, dry_run)
|