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,357 @@
|
|
|
1
|
+
"""Checkout command - find and switch to a worktree by branch name."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from erk.cli.activation import render_activation_script
|
|
8
|
+
from erk.cli.alias import alias
|
|
9
|
+
from erk.cli.commands.completions import complete_branch_names
|
|
10
|
+
from erk.cli.commands.wt.create_cmd import ensure_worktree_for_branch
|
|
11
|
+
from erk.cli.core import discover_repo_context
|
|
12
|
+
from erk.cli.graphite import find_worktrees_containing_branch
|
|
13
|
+
from erk.cli.help_formatter import CommandWithHiddenOptions, script_option
|
|
14
|
+
from erk.core.context import ErkContext
|
|
15
|
+
from erk.core.repo_discovery import RepoContext, ensure_erk_metadata_dir
|
|
16
|
+
from erk.core.worktree_utils import compute_relative_path_in_worktree
|
|
17
|
+
from erk_shared.git.abc import WorktreeInfo
|
|
18
|
+
from erk_shared.output.output import user_confirm, user_output
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def try_switch_root_worktree(ctx: ErkContext, repo: RepoContext, branch: str) -> Path | None:
|
|
22
|
+
"""Try to switch root worktree to branch if it's trunk and root is clean.
|
|
23
|
+
|
|
24
|
+
This implements the "takeover" behavior where checking out trunk in a clean root
|
|
25
|
+
worktree switches the root to trunk instead of creating a new dated worktree.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
ctx: Erk context with git operations
|
|
29
|
+
repo: Repository context
|
|
30
|
+
branch: Branch name to check
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Root worktree path if successful, None otherwise
|
|
34
|
+
"""
|
|
35
|
+
# Check if branch is trunk
|
|
36
|
+
if branch != ctx.trunk_branch:
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
# Find root worktree
|
|
40
|
+
worktrees = ctx.git.list_worktrees(repo.root)
|
|
41
|
+
root_worktree = None
|
|
42
|
+
for wt in worktrees:
|
|
43
|
+
if wt.is_root:
|
|
44
|
+
root_worktree = wt
|
|
45
|
+
break
|
|
46
|
+
|
|
47
|
+
if root_worktree is None:
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
# Check if root is clean
|
|
51
|
+
if not ctx.git.is_worktree_clean(root_worktree.path):
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
# Switch root to trunk branch
|
|
55
|
+
ctx.git.checkout_branch(root_worktree.path, branch)
|
|
56
|
+
|
|
57
|
+
return root_worktree.path
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _ensure_graphite_tracking(
|
|
61
|
+
ctx: ErkContext,
|
|
62
|
+
repo_root: Path,
|
|
63
|
+
target_path: Path,
|
|
64
|
+
branch: str,
|
|
65
|
+
script: bool,
|
|
66
|
+
) -> None:
|
|
67
|
+
"""Ensure branch is tracked by Graphite (idempotent), with user confirmation.
|
|
68
|
+
|
|
69
|
+
If the branch is not already tracked, prompts the user and tracks it with
|
|
70
|
+
trunk as parent if confirmed. This enables branches created without Graphite
|
|
71
|
+
(e.g., via erk-queue) to be managed with Graphite locally.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
ctx: Erk context
|
|
75
|
+
repo_root: Repository root path
|
|
76
|
+
target_path: Worktree path where `gt track` should run
|
|
77
|
+
branch: Target branch name
|
|
78
|
+
script: Whether to output only the activation script
|
|
79
|
+
"""
|
|
80
|
+
# Skip if Graphite is disabled
|
|
81
|
+
use_graphite = ctx.global_config.use_graphite if ctx.global_config else False
|
|
82
|
+
if not use_graphite:
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
trunk_branch = ctx.trunk_branch
|
|
86
|
+
# Skip if no trunk branch detected (shouldn't happen in checkout context)
|
|
87
|
+
if trunk_branch is None:
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
# Skip trunk branch - it's always implicitly tracked
|
|
91
|
+
if branch == trunk_branch:
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
# Check if already tracked (LBYL)
|
|
95
|
+
all_branches = ctx.graphite.get_all_branches(ctx.git, repo_root)
|
|
96
|
+
if branch in all_branches:
|
|
97
|
+
return # Already tracked, nothing to do
|
|
98
|
+
|
|
99
|
+
# In script mode, skip tracking (no interactive prompts allowed)
|
|
100
|
+
if script:
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
# Prompt user for confirmation
|
|
104
|
+
if not user_confirm(
|
|
105
|
+
f"Branch '{branch}' is not tracked by Graphite. Track it with parent '{trunk_branch}'?",
|
|
106
|
+
default=False,
|
|
107
|
+
):
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
# Track the branch with trunk as parent
|
|
111
|
+
ctx.graphite.track_branch(target_path, branch, trunk_branch)
|
|
112
|
+
user_output(f"Tracked '{branch}' with Graphite (parent: {trunk_branch})")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _format_worktree_info(wt: WorktreeInfo, repo_root: Path) -> str:
|
|
116
|
+
"""Format worktree information for display.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
wt: WorktreeInfo to format
|
|
120
|
+
repo_root: Path to repository root (used to identify root worktree)
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Formatted string like "root (currently on 'main')" or "wt-name (currently on 'feature')"
|
|
124
|
+
"""
|
|
125
|
+
current = wt.branch or "(detached HEAD)"
|
|
126
|
+
if wt.path == repo_root:
|
|
127
|
+
return f" - root (currently on '{current}')"
|
|
128
|
+
else:
|
|
129
|
+
# Get worktree name from path
|
|
130
|
+
wt_name = wt.path.name
|
|
131
|
+
return f" - {wt_name} (currently on '{current}')"
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _perform_checkout(
|
|
135
|
+
ctx: ErkContext,
|
|
136
|
+
*,
|
|
137
|
+
repo_root: Path,
|
|
138
|
+
target_worktree: WorktreeInfo,
|
|
139
|
+
branch: str,
|
|
140
|
+
script: bool,
|
|
141
|
+
is_newly_created: bool = False,
|
|
142
|
+
worktrees: list[WorktreeInfo] | None = None,
|
|
143
|
+
) -> None:
|
|
144
|
+
"""Perform the actual checkout and switch to a worktree.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
ctx: Erk context
|
|
148
|
+
repo_root: Repository root path
|
|
149
|
+
target_worktree: The worktree to switch to
|
|
150
|
+
branch: Target branch name
|
|
151
|
+
script: Whether to output only the activation script
|
|
152
|
+
is_newly_created: Whether the worktree was just created (default False)
|
|
153
|
+
worktrees: Optional list of worktrees (for relative path computation)
|
|
154
|
+
"""
|
|
155
|
+
target_path = target_worktree.path
|
|
156
|
+
current_branch_in_worktree = target_worktree.branch
|
|
157
|
+
current_cwd = ctx.cwd
|
|
158
|
+
|
|
159
|
+
# Compute relative path to preserve directory position
|
|
160
|
+
relative_path = compute_relative_path_in_worktree(worktrees, ctx.cwd) if worktrees else None
|
|
161
|
+
|
|
162
|
+
# Check if branch is already checked out in the worktree
|
|
163
|
+
need_checkout = current_branch_in_worktree != branch
|
|
164
|
+
|
|
165
|
+
# If we need to checkout, do it before generating the activation script
|
|
166
|
+
if need_checkout:
|
|
167
|
+
# Checkout the branch in the target worktree
|
|
168
|
+
ctx.git.checkout_branch(target_path, branch)
|
|
169
|
+
|
|
170
|
+
# Ensure branch is tracked with Graphite (idempotent)
|
|
171
|
+
_ensure_graphite_tracking(ctx, repo_root, target_path, branch, script)
|
|
172
|
+
|
|
173
|
+
if need_checkout:
|
|
174
|
+
# Show stack context
|
|
175
|
+
if not script:
|
|
176
|
+
stack = ctx.graphite.get_branch_stack(ctx.git, repo_root, branch)
|
|
177
|
+
if stack:
|
|
178
|
+
user_output(f"Stack: {' -> '.join(stack)}")
|
|
179
|
+
user_output(f"Checked out '{branch}' in worktree")
|
|
180
|
+
|
|
181
|
+
# Generate activation script
|
|
182
|
+
if script:
|
|
183
|
+
# Script mode: always generate script (for shell integration or manual sourcing)
|
|
184
|
+
is_switching_location = current_cwd != target_path
|
|
185
|
+
|
|
186
|
+
# Determine worktree name from path
|
|
187
|
+
worktree_name = target_path.name
|
|
188
|
+
|
|
189
|
+
# Four-case message logic:
|
|
190
|
+
if is_newly_created:
|
|
191
|
+
# Case 4: Switched to newly created worktree
|
|
192
|
+
styled_wt = click.style(worktree_name, fg="cyan", bold=True)
|
|
193
|
+
switch_message = f'echo "Switched to new worktree {styled_wt}"'
|
|
194
|
+
elif not is_switching_location:
|
|
195
|
+
# Case 1: Already on target branch in current worktree
|
|
196
|
+
styled_branch = click.style(branch, fg="yellow")
|
|
197
|
+
styled_wt = click.style(worktree_name, fg="cyan", bold=True)
|
|
198
|
+
switch_message = f'echo "Already on branch {styled_branch} in worktree {styled_wt}"'
|
|
199
|
+
elif not need_checkout:
|
|
200
|
+
# Case 2: Switched to existing worktree with branch already checked out
|
|
201
|
+
styled_wt = click.style(worktree_name, fg="cyan", bold=True)
|
|
202
|
+
if worktree_name == branch:
|
|
203
|
+
# Standard naming
|
|
204
|
+
switch_message = f'echo "Switched to worktree {styled_wt}"'
|
|
205
|
+
else:
|
|
206
|
+
# Edge case: non-standard naming
|
|
207
|
+
styled_branch = click.style(branch, fg="yellow")
|
|
208
|
+
switch_message = f'echo "Switched to worktree {styled_wt} (branch {styled_branch})"'
|
|
209
|
+
else:
|
|
210
|
+
# Case 3: Switched to existing worktree and checked out branch
|
|
211
|
+
styled_wt = click.style(worktree_name, fg="cyan", bold=True)
|
|
212
|
+
styled_branch = click.style(branch, fg="yellow")
|
|
213
|
+
switch_message = (
|
|
214
|
+
f'echo "Switched to worktree {styled_wt} and checked out branch {styled_branch}"'
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
script_content = render_activation_script(
|
|
218
|
+
worktree_path=target_path,
|
|
219
|
+
target_subpath=relative_path,
|
|
220
|
+
post_cd_commands=None,
|
|
221
|
+
final_message=switch_message,
|
|
222
|
+
comment="work activate-script",
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
result = ctx.script_writer.write_activation_script(
|
|
226
|
+
script_content,
|
|
227
|
+
command_name="checkout",
|
|
228
|
+
comment=f"checkout {branch}",
|
|
229
|
+
)
|
|
230
|
+
result.output_for_shell_integration()
|
|
231
|
+
else:
|
|
232
|
+
# Non-script mode: Apply same four-case logic with user_output()
|
|
233
|
+
worktree_name = target_path.name
|
|
234
|
+
|
|
235
|
+
if is_newly_created:
|
|
236
|
+
styled_wt = click.style(worktree_name, fg="cyan", bold=True)
|
|
237
|
+
user_output(f"Switched to new worktree {styled_wt}")
|
|
238
|
+
elif ctx.cwd == target_path:
|
|
239
|
+
styled_branch = click.style(branch, fg="yellow")
|
|
240
|
+
styled_wt = click.style(worktree_name, fg="cyan", bold=True)
|
|
241
|
+
user_output(f"Already on branch {styled_branch} in worktree {styled_wt}")
|
|
242
|
+
elif current_branch_in_worktree == branch:
|
|
243
|
+
styled_wt = click.style(worktree_name, fg="cyan", bold=True)
|
|
244
|
+
if worktree_name == branch:
|
|
245
|
+
user_output(f"Switched to worktree {styled_wt}")
|
|
246
|
+
else:
|
|
247
|
+
styled_branch = click.style(branch, fg="yellow")
|
|
248
|
+
user_output(f"Switched to worktree {styled_wt} (branch {styled_branch})")
|
|
249
|
+
else:
|
|
250
|
+
styled_wt = click.style(worktree_name, fg="cyan", bold=True)
|
|
251
|
+
styled_branch = click.style(branch, fg="yellow")
|
|
252
|
+
user_output(f"Switched to worktree {styled_wt} and checked out branch {styled_branch}")
|
|
253
|
+
|
|
254
|
+
# Show manual instructions
|
|
255
|
+
user_output("\nShell integration not detected. Run 'erk init --shell' to set up.")
|
|
256
|
+
user_output(f"Or use: source <(erk br co {branch} --script)")
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
@alias("co")
|
|
260
|
+
@click.command("checkout", cls=CommandWithHiddenOptions)
|
|
261
|
+
@click.argument("branch", metavar="BRANCH", shell_complete=complete_branch_names)
|
|
262
|
+
@script_option
|
|
263
|
+
@click.pass_obj
|
|
264
|
+
def branch_checkout(ctx: ErkContext, branch: str, script: bool) -> None:
|
|
265
|
+
"""Checkout BRANCH by finding and switching to its worktree.
|
|
266
|
+
|
|
267
|
+
This command finds which worktree has the specified branch checked out
|
|
268
|
+
and switches to it. If the branch exists but isn't checked out anywhere,
|
|
269
|
+
a worktree is automatically created. If the branch exists on origin but
|
|
270
|
+
not locally, a tracking branch and worktree are created automatically.
|
|
271
|
+
|
|
272
|
+
Examples:
|
|
273
|
+
|
|
274
|
+
erk br co feature/user-auth # Checkout existing worktree
|
|
275
|
+
|
|
276
|
+
erk br co unchecked-branch # Auto-create worktree
|
|
277
|
+
|
|
278
|
+
erk br co origin-only-branch # Create tracking branch + worktree
|
|
279
|
+
|
|
280
|
+
If multiple worktrees contain the branch, all options are shown.
|
|
281
|
+
"""
|
|
282
|
+
# Use existing repo from context if available (for tests), otherwise discover
|
|
283
|
+
if isinstance(ctx.repo, RepoContext):
|
|
284
|
+
repo = ctx.repo
|
|
285
|
+
else:
|
|
286
|
+
repo = discover_repo_context(ctx, ctx.cwd)
|
|
287
|
+
ensure_erk_metadata_dir(repo)
|
|
288
|
+
|
|
289
|
+
# Get all worktrees
|
|
290
|
+
worktrees = ctx.git.list_worktrees(repo.root)
|
|
291
|
+
|
|
292
|
+
# Find worktrees containing the target branch
|
|
293
|
+
matching_worktrees = find_worktrees_containing_branch(ctx, repo.root, worktrees, branch)
|
|
294
|
+
|
|
295
|
+
# Track whether we're creating a new worktree
|
|
296
|
+
is_newly_created = False
|
|
297
|
+
|
|
298
|
+
# Handle three cases: no match, one match, multiple matches
|
|
299
|
+
if len(matching_worktrees) == 0:
|
|
300
|
+
# No worktrees have this branch checked out
|
|
301
|
+
# First, try switching clean root worktree if checking out trunk
|
|
302
|
+
root_path = try_switch_root_worktree(ctx, repo, branch)
|
|
303
|
+
if root_path is not None:
|
|
304
|
+
# Successfully switched root to trunk - refresh and jump to it
|
|
305
|
+
worktrees = ctx.git.list_worktrees(repo.root)
|
|
306
|
+
matching_worktrees = find_worktrees_containing_branch(ctx, repo.root, worktrees, branch)
|
|
307
|
+
else:
|
|
308
|
+
# Root not available or not trunk - auto-create worktree
|
|
309
|
+
_worktree_path, is_newly_created = ensure_worktree_for_branch(
|
|
310
|
+
ctx, repo, branch, is_plan_derived=False
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
# Refresh worktree list to include the newly created worktree
|
|
314
|
+
worktrees = ctx.git.list_worktrees(repo.root)
|
|
315
|
+
matching_worktrees = find_worktrees_containing_branch(ctx, repo.root, worktrees, branch)
|
|
316
|
+
|
|
317
|
+
# Fall through to jump to the worktree
|
|
318
|
+
|
|
319
|
+
if len(matching_worktrees) == 1:
|
|
320
|
+
# Exactly one worktree contains this branch
|
|
321
|
+
target_worktree = matching_worktrees[0]
|
|
322
|
+
_perform_checkout(
|
|
323
|
+
ctx,
|
|
324
|
+
repo_root=repo.root,
|
|
325
|
+
target_worktree=target_worktree,
|
|
326
|
+
branch=branch,
|
|
327
|
+
script=script,
|
|
328
|
+
is_newly_created=is_newly_created,
|
|
329
|
+
worktrees=worktrees,
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
else:
|
|
333
|
+
# Multiple worktrees contain this branch
|
|
334
|
+
# Check if any worktree has the branch directly checked out
|
|
335
|
+
directly_checked_out = [wt for wt in matching_worktrees if wt.branch == branch]
|
|
336
|
+
|
|
337
|
+
if len(directly_checked_out) == 1:
|
|
338
|
+
# Exactly one worktree has the branch directly checked out - jump to it
|
|
339
|
+
target_worktree = directly_checked_out[0]
|
|
340
|
+
_perform_checkout(
|
|
341
|
+
ctx,
|
|
342
|
+
repo_root=repo.root,
|
|
343
|
+
target_worktree=target_worktree,
|
|
344
|
+
branch=branch,
|
|
345
|
+
script=script,
|
|
346
|
+
is_newly_created=is_newly_created,
|
|
347
|
+
worktrees=worktrees,
|
|
348
|
+
)
|
|
349
|
+
else:
|
|
350
|
+
# Zero or multiple worktrees have it directly checked out
|
|
351
|
+
# Show error message listing all options
|
|
352
|
+
user_output(f"Branch '{branch}' exists in multiple worktrees:")
|
|
353
|
+
for wt in matching_worktrees:
|
|
354
|
+
user_output(_format_worktree_info(wt, repo.root))
|
|
355
|
+
|
|
356
|
+
user_output("\nPlease specify which worktree to use.")
|
|
357
|
+
raise SystemExit(1)
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Branch create command - create a new branch with optional slot assignment."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from datetime import UTC, datetime
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from erk.cli.commands.slot.common import (
|
|
9
|
+
cleanup_worktree_artifacts,
|
|
10
|
+
find_branch_assignment,
|
|
11
|
+
find_inactive_slot,
|
|
12
|
+
find_next_available_slot,
|
|
13
|
+
generate_slot_name,
|
|
14
|
+
get_pool_size,
|
|
15
|
+
handle_pool_full_interactive,
|
|
16
|
+
)
|
|
17
|
+
from erk.cli.core import discover_repo_context
|
|
18
|
+
from erk.core.context import ErkContext
|
|
19
|
+
from erk.core.repo_discovery import ensure_erk_metadata_dir
|
|
20
|
+
from erk.core.worktree_pool import (
|
|
21
|
+
PoolState,
|
|
22
|
+
SlotAssignment,
|
|
23
|
+
load_pool_state,
|
|
24
|
+
save_pool_state,
|
|
25
|
+
)
|
|
26
|
+
from erk_shared.output.output import user_output
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@click.command("create")
|
|
30
|
+
@click.argument("branch_name", metavar="BRANCH")
|
|
31
|
+
@click.option("--no-slot", is_flag=True, help="Create branch without slot assignment")
|
|
32
|
+
@click.option("-f", "--force", is_flag=True, help="Auto-unassign oldest branch if pool is full")
|
|
33
|
+
@click.pass_obj
|
|
34
|
+
def branch_create(ctx: ErkContext, branch_name: str, no_slot: bool, force: bool) -> None:
|
|
35
|
+
"""Create a NEW branch and optionally assign it to a pool slot.
|
|
36
|
+
|
|
37
|
+
BRANCH is the name of the new git branch to create.
|
|
38
|
+
|
|
39
|
+
By default, the command will:
|
|
40
|
+
1. Verify the branch does NOT already exist (fails if it does)
|
|
41
|
+
2. Create the branch from trunk
|
|
42
|
+
3. Find the next available slot in the pool
|
|
43
|
+
4. Create a worktree for that slot
|
|
44
|
+
5. Assign the branch to the slot
|
|
45
|
+
|
|
46
|
+
Use --no-slot to create a branch without assigning it to a slot.
|
|
47
|
+
Use `erk br assign` to assign an EXISTING branch to a slot.
|
|
48
|
+
"""
|
|
49
|
+
repo = discover_repo_context(ctx, ctx.cwd)
|
|
50
|
+
ensure_erk_metadata_dir(repo)
|
|
51
|
+
|
|
52
|
+
# Check if branch already exists
|
|
53
|
+
local_branches = ctx.git.list_local_branches(repo.root)
|
|
54
|
+
if branch_name in local_branches:
|
|
55
|
+
user_output(
|
|
56
|
+
f"Error: Branch '{branch_name}' already exists.\n"
|
|
57
|
+
"Use `erk br assign` to assign an existing branch to a slot."
|
|
58
|
+
)
|
|
59
|
+
raise SystemExit(1) from None
|
|
60
|
+
|
|
61
|
+
# Create the new branch from trunk
|
|
62
|
+
trunk = ctx.git.detect_trunk_branch(repo.root)
|
|
63
|
+
ctx.git.create_branch(repo.root, branch_name, trunk)
|
|
64
|
+
ctx.graphite.track_branch(repo.root, branch_name, trunk)
|
|
65
|
+
user_output(f"Created branch: {branch_name}")
|
|
66
|
+
|
|
67
|
+
# If --no-slot is specified, we're done
|
|
68
|
+
if no_slot:
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
# Get pool size from config or default
|
|
72
|
+
pool_size = get_pool_size(ctx)
|
|
73
|
+
|
|
74
|
+
# Load or create pool state
|
|
75
|
+
state = load_pool_state(repo.pool_json_path)
|
|
76
|
+
if state is None:
|
|
77
|
+
state = PoolState(
|
|
78
|
+
version="1.0",
|
|
79
|
+
pool_size=pool_size,
|
|
80
|
+
slots=(),
|
|
81
|
+
assignments=(),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Check if branch is already assigned (shouldn't happen since we just created it)
|
|
85
|
+
existing = find_branch_assignment(state, branch_name)
|
|
86
|
+
if existing is not None:
|
|
87
|
+
user_output(f"Error: Branch '{branch_name}' already assigned to {existing.slot_name}")
|
|
88
|
+
raise SystemExit(1) from None
|
|
89
|
+
|
|
90
|
+
# First, prefer reusing existing worktrees (fast path)
|
|
91
|
+
inactive_slot = find_inactive_slot(state, ctx.git, repo.root)
|
|
92
|
+
if inactive_slot is not None:
|
|
93
|
+
slot_name, worktree_path = inactive_slot
|
|
94
|
+
|
|
95
|
+
# Checkout the branch in the existing worktree
|
|
96
|
+
ctx.git.checkout_branch(worktree_path, branch_name)
|
|
97
|
+
else:
|
|
98
|
+
# Fall back to on-demand slot creation
|
|
99
|
+
slot_num = find_next_available_slot(state, repo.worktrees_dir)
|
|
100
|
+
if slot_num is None:
|
|
101
|
+
# Pool is full - handle interactively or with --force
|
|
102
|
+
to_unassign = handle_pool_full_interactive(state, force, sys.stdin.isatty())
|
|
103
|
+
if to_unassign is None:
|
|
104
|
+
raise SystemExit(1) from None
|
|
105
|
+
|
|
106
|
+
# Remove the assignment from state
|
|
107
|
+
new_assignments = tuple(
|
|
108
|
+
a for a in state.assignments if a.slot_name != to_unassign.slot_name
|
|
109
|
+
)
|
|
110
|
+
state = PoolState(
|
|
111
|
+
version=state.version,
|
|
112
|
+
pool_size=state.pool_size,
|
|
113
|
+
slots=state.slots,
|
|
114
|
+
assignments=new_assignments,
|
|
115
|
+
)
|
|
116
|
+
save_pool_state(repo.pool_json_path, state)
|
|
117
|
+
user_output(
|
|
118
|
+
click.style("✓ ", fg="green")
|
|
119
|
+
+ f"Unassigned {click.style(to_unassign.branch_name, fg='yellow')} "
|
|
120
|
+
+ f"from {click.style(to_unassign.slot_name, fg='cyan')}"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# Reuse the unassigned slot - worktree exists, just checkout
|
|
124
|
+
slot_name = to_unassign.slot_name
|
|
125
|
+
worktree_path = to_unassign.worktree_path
|
|
126
|
+
cleanup_worktree_artifacts(worktree_path)
|
|
127
|
+
ctx.git.checkout_branch(worktree_path, branch_name)
|
|
128
|
+
else:
|
|
129
|
+
# Create new slot - no worktree exists yet
|
|
130
|
+
slot_name = generate_slot_name(slot_num)
|
|
131
|
+
worktree_path = repo.worktrees_dir / slot_name
|
|
132
|
+
worktree_path.mkdir(parents=True, exist_ok=True)
|
|
133
|
+
ctx.git.add_worktree(
|
|
134
|
+
repo.root,
|
|
135
|
+
worktree_path,
|
|
136
|
+
branch=branch_name,
|
|
137
|
+
ref=None,
|
|
138
|
+
create_branch=False,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Create new assignment
|
|
142
|
+
now = datetime.now(UTC).isoformat()
|
|
143
|
+
new_assignment = SlotAssignment(
|
|
144
|
+
slot_name=slot_name,
|
|
145
|
+
branch_name=branch_name,
|
|
146
|
+
assigned_at=now,
|
|
147
|
+
worktree_path=worktree_path,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# Update state with new assignment
|
|
151
|
+
new_state = PoolState(
|
|
152
|
+
version=state.version,
|
|
153
|
+
pool_size=state.pool_size,
|
|
154
|
+
slots=state.slots,
|
|
155
|
+
assignments=(*state.assignments, new_assignment),
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# Save state
|
|
159
|
+
save_pool_state(repo.pool_json_path, new_state)
|
|
160
|
+
|
|
161
|
+
user_output(click.style(f"✓ Assigned {branch_name} to {slot_name}", fg="green"))
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""List active branches - those with worktrees or open PRs."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from rich.table import Table
|
|
6
|
+
|
|
7
|
+
from erk.cli.alias import alias
|
|
8
|
+
from erk.cli.core import discover_repo_context
|
|
9
|
+
from erk.core.context import ErkContext
|
|
10
|
+
from erk.core.display_utils import format_relative_time, get_pr_status_emoji
|
|
11
|
+
from erk_shared.github.types import PullRequestInfo
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@alias("ls")
|
|
15
|
+
@click.command("list")
|
|
16
|
+
@click.pass_obj
|
|
17
|
+
def branch_list(ctx: ErkContext) -> None:
|
|
18
|
+
"""List active branches with their worktrees and PR status.
|
|
19
|
+
|
|
20
|
+
Active branches are those that have:
|
|
21
|
+
- A worktree checked out, OR
|
|
22
|
+
- An open pull request
|
|
23
|
+
|
|
24
|
+
Example:
|
|
25
|
+
erk br ls
|
|
26
|
+
"""
|
|
27
|
+
repo = discover_repo_context(ctx, ctx.cwd)
|
|
28
|
+
|
|
29
|
+
# Detect trunk branch for last commit calculation
|
|
30
|
+
trunk = ctx.git.detect_trunk_branch(repo.root)
|
|
31
|
+
|
|
32
|
+
# Get worktrees and PR info
|
|
33
|
+
worktrees = ctx.git.list_worktrees(repo.root)
|
|
34
|
+
prs = ctx.graphite.get_prs_from_graphite(ctx.git, repo.root)
|
|
35
|
+
|
|
36
|
+
# Build active branches map: branch -> (worktree_name, pr_info)
|
|
37
|
+
active_branches: dict[str, tuple[str | None, PullRequestInfo | None]] = {}
|
|
38
|
+
|
|
39
|
+
# Add branches from worktrees
|
|
40
|
+
for wt in worktrees:
|
|
41
|
+
if wt.branch is not None and wt.branch != ctx.trunk_branch:
|
|
42
|
+
wt_name = "root" if wt.is_root else wt.path.name
|
|
43
|
+
pr = prs.get(wt.branch)
|
|
44
|
+
active_branches[wt.branch] = (wt_name, pr)
|
|
45
|
+
|
|
46
|
+
# Add branches with open PRs (not already in worktrees)
|
|
47
|
+
for branch, pr in prs.items():
|
|
48
|
+
if branch not in active_branches and pr.state == "OPEN":
|
|
49
|
+
active_branches[branch] = (None, pr)
|
|
50
|
+
|
|
51
|
+
# Display table
|
|
52
|
+
console = Console(stderr=True, force_terminal=True)
|
|
53
|
+
table = Table(show_header=True, header_style="bold", box=None)
|
|
54
|
+
table.add_column("branch", style="yellow")
|
|
55
|
+
table.add_column("worktree", style="cyan")
|
|
56
|
+
table.add_column("pr")
|
|
57
|
+
table.add_column("last", no_wrap=True)
|
|
58
|
+
table.add_column("state", style="dim")
|
|
59
|
+
|
|
60
|
+
for branch in sorted(active_branches.keys()):
|
|
61
|
+
wt_name, pr = active_branches[branch]
|
|
62
|
+
|
|
63
|
+
pr_cell = "-"
|
|
64
|
+
state_cell = "-"
|
|
65
|
+
if pr is not None:
|
|
66
|
+
emoji = get_pr_status_emoji(pr)
|
|
67
|
+
pr_cell = f"{emoji} #{pr.number}"
|
|
68
|
+
state_cell = pr.state
|
|
69
|
+
|
|
70
|
+
# Get last commit time for this branch
|
|
71
|
+
timestamp = ctx.git.get_branch_last_commit_time(repo.root, branch, trunk)
|
|
72
|
+
last_cell = format_relative_time(timestamp) if timestamp is not None else "-"
|
|
73
|
+
|
|
74
|
+
table.add_row(
|
|
75
|
+
branch,
|
|
76
|
+
wt_name or "-",
|
|
77
|
+
pr_cell,
|
|
78
|
+
last_cell,
|
|
79
|
+
state_cell,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
console.print(table)
|