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,82 @@
|
|
|
1
|
+
"""Fix merge conflicts with AI-powered resolution.
|
|
2
|
+
|
|
3
|
+
Uses Claude to resolve merge conflicts on the current branch without
|
|
4
|
+
any Graphite stack manipulation. Invokes the /erk:fix-conflicts
|
|
5
|
+
Claude slash command.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
from erk.cli.output import stream_fix_conflicts
|
|
11
|
+
from erk.core.context import ErkContext
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@click.command("fix-conflicts")
|
|
15
|
+
@click.option(
|
|
16
|
+
"-f",
|
|
17
|
+
"--dangerous",
|
|
18
|
+
is_flag=True,
|
|
19
|
+
help="Acknowledge that this command invokes Claude with --dangerously-skip-permissions.",
|
|
20
|
+
)
|
|
21
|
+
@click.pass_obj
|
|
22
|
+
def pr_fix_conflicts(ctx: ErkContext, *, dangerous: bool) -> None:
|
|
23
|
+
"""Fix merge conflicts with AI-powered resolution.
|
|
24
|
+
|
|
25
|
+
Resolves merge conflicts on the current branch using Claude.
|
|
26
|
+
Does not require or interact with Graphite stacks.
|
|
27
|
+
|
|
28
|
+
Examples:
|
|
29
|
+
|
|
30
|
+
\b
|
|
31
|
+
# Fix conflicts with Claude
|
|
32
|
+
erk pr fix-conflicts --dangerous
|
|
33
|
+
|
|
34
|
+
To disable the --dangerous flag requirement:
|
|
35
|
+
|
|
36
|
+
\b
|
|
37
|
+
erk config set fix_conflicts_require_dangerous_flag false
|
|
38
|
+
"""
|
|
39
|
+
# Runtime validation: require --dangerous unless config disables requirement
|
|
40
|
+
if not dangerous:
|
|
41
|
+
require_flag = (
|
|
42
|
+
ctx.global_config is None or ctx.global_config.fix_conflicts_require_dangerous_flag
|
|
43
|
+
)
|
|
44
|
+
if require_flag:
|
|
45
|
+
raise click.UsageError(
|
|
46
|
+
"Missing option '--dangerous'.\n"
|
|
47
|
+
"To disable: erk config set fix_conflicts_require_dangerous_flag false"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
cwd = ctx.cwd
|
|
51
|
+
|
|
52
|
+
# Check for conflicts
|
|
53
|
+
conflicted_files = ctx.git.get_conflicted_files(cwd)
|
|
54
|
+
if not conflicted_files:
|
|
55
|
+
click.echo("No merge conflicts detected.")
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
# Check Claude availability
|
|
59
|
+
executor = ctx.claude_executor
|
|
60
|
+
if not executor.is_claude_available():
|
|
61
|
+
raise click.ClickException(
|
|
62
|
+
"Claude CLI is required for conflict resolution.\n\n"
|
|
63
|
+
"Install from: https://claude.com/download"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Show conflict info
|
|
67
|
+
click.echo(
|
|
68
|
+
click.style(
|
|
69
|
+
f"Found {len(conflicted_files)} conflicted file(s). Invoking Claude...",
|
|
70
|
+
fg="yellow",
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Execute conflict resolution
|
|
75
|
+
result = stream_fix_conflicts(executor, cwd)
|
|
76
|
+
|
|
77
|
+
if result.requires_interactive:
|
|
78
|
+
raise click.ClickException("Semantic conflict requires interactive resolution")
|
|
79
|
+
if not result.success:
|
|
80
|
+
raise click.ClickException(result.error_message or "Conflict resolution failed")
|
|
81
|
+
|
|
82
|
+
click.echo(click.style("\n✅ Conflicts resolved!", fg="green", bold=True))
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Parse PR reference from user input.
|
|
2
|
+
|
|
3
|
+
This module re-exports parse_pr_identifier from the centralized CLI parsing module.
|
|
4
|
+
Kept for backwards compatibility with existing imports.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from erk.cli.github_parsing import parse_pr_identifier
|
|
8
|
+
|
|
9
|
+
# Re-export with explicit assignment per PEP 484 to indicate intentional re-export
|
|
10
|
+
parse_pr_reference = parse_pr_identifier
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
"""Submit current branch as a pull request.
|
|
2
|
+
|
|
3
|
+
Unified PR submission with two-layer architecture:
|
|
4
|
+
1. Core layer: git push + gh pr create (works without Graphite)
|
|
5
|
+
2. Graphite layer: Optional enhancement via gt submit
|
|
6
|
+
|
|
7
|
+
The workflow:
|
|
8
|
+
1. Core submit: git push + gh pr create
|
|
9
|
+
2. Get diff for AI: GitHub API
|
|
10
|
+
3. Generate: AI-generated commit message via Claude CLI
|
|
11
|
+
4. Graphite enhance: Optional gt submit for stack metadata
|
|
12
|
+
5. Finalize: Update PR with AI-generated title/body
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
import uuid
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
import click
|
|
20
|
+
|
|
21
|
+
from erk.core.commit_message_generator import (
|
|
22
|
+
CommitMessageGenerator,
|
|
23
|
+
CommitMessageRequest,
|
|
24
|
+
CommitMessageResult,
|
|
25
|
+
)
|
|
26
|
+
from erk.core.context import ErkContext
|
|
27
|
+
from erk_shared.gateway.gt.events import CompletionEvent, ProgressEvent
|
|
28
|
+
from erk_shared.gateway.gt.operations.finalize import execute_finalize
|
|
29
|
+
from erk_shared.gateway.gt.types import FinalizeResult, PostAnalysisError
|
|
30
|
+
from erk_shared.gateway.pr.diff_extraction import execute_diff_extraction
|
|
31
|
+
from erk_shared.gateway.pr.graphite_enhance import execute_graphite_enhance
|
|
32
|
+
from erk_shared.gateway.pr.submit import execute_core_submit
|
|
33
|
+
from erk_shared.gateway.pr.types import (
|
|
34
|
+
CoreSubmitError,
|
|
35
|
+
CoreSubmitResult,
|
|
36
|
+
GraphiteEnhanceError,
|
|
37
|
+
GraphiteEnhanceResult,
|
|
38
|
+
GraphiteSkipped,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _render_progress(event: ProgressEvent) -> None:
|
|
43
|
+
"""Render a progress event to the CLI."""
|
|
44
|
+
message = f" {event.message}"
|
|
45
|
+
if event.style == "info":
|
|
46
|
+
click.echo(click.style(message, dim=True))
|
|
47
|
+
elif event.style == "success":
|
|
48
|
+
click.echo(click.style(message, fg="green"))
|
|
49
|
+
elif event.style == "warning":
|
|
50
|
+
click.echo(click.style(message, fg="yellow"))
|
|
51
|
+
elif event.style == "error":
|
|
52
|
+
click.echo(click.style(message, fg="red"))
|
|
53
|
+
else:
|
|
54
|
+
click.echo(message)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@click.command("submit")
|
|
58
|
+
@click.option("--debug", is_flag=True, help="Show diagnostic output")
|
|
59
|
+
@click.option(
|
|
60
|
+
"--no-graphite",
|
|
61
|
+
is_flag=True,
|
|
62
|
+
help="Skip Graphite enhancement (use git + gh only)",
|
|
63
|
+
)
|
|
64
|
+
@click.option(
|
|
65
|
+
"-f",
|
|
66
|
+
"--force",
|
|
67
|
+
is_flag=True,
|
|
68
|
+
help="Force push (use when branch has diverged from remote)",
|
|
69
|
+
)
|
|
70
|
+
@click.pass_obj
|
|
71
|
+
def pr_submit(ctx: ErkContext, debug: bool, no_graphite: bool, force: bool) -> None:
|
|
72
|
+
"""Submit PR with AI-generated commit message.
|
|
73
|
+
|
|
74
|
+
Uses a two-layer architecture:
|
|
75
|
+
- Core layer (always): git push + gh pr create
|
|
76
|
+
- Graphite layer (optional): gt submit for stack metadata
|
|
77
|
+
|
|
78
|
+
The core layer works without Graphite installed. When Graphite is
|
|
79
|
+
available and the branch is tracked, it will enhance the PR with
|
|
80
|
+
stack metadata unless --no-graphite is specified.
|
|
81
|
+
|
|
82
|
+
Examples:
|
|
83
|
+
|
|
84
|
+
\b
|
|
85
|
+
# Submit PR (with Graphite if available)
|
|
86
|
+
erk pr submit
|
|
87
|
+
|
|
88
|
+
# Submit PR without Graphite enhancement
|
|
89
|
+
erk pr submit --no-graphite
|
|
90
|
+
|
|
91
|
+
# Force push when branch has diverged
|
|
92
|
+
erk pr submit -f
|
|
93
|
+
"""
|
|
94
|
+
_execute_pr_submit(ctx, debug=debug, use_graphite=not no_graphite, force=force)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _execute_pr_submit(ctx: ErkContext, debug: bool, use_graphite: bool, force: bool) -> None:
|
|
98
|
+
"""Execute PR submission with positively-named parameters."""
|
|
99
|
+
# Verify Claude is available (needed for commit message generation)
|
|
100
|
+
if not ctx.claude_executor.is_claude_available():
|
|
101
|
+
raise click.ClickException(
|
|
102
|
+
"Claude CLI not found\n\nInstall from: https://claude.com/download"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
click.echo(click.style("🚀 Submitting PR...", bold=True))
|
|
106
|
+
click.echo("")
|
|
107
|
+
|
|
108
|
+
cwd = Path.cwd()
|
|
109
|
+
session_id = os.environ.get("SESSION_ID", str(uuid.uuid4()))
|
|
110
|
+
|
|
111
|
+
# Phase 1: Core submit (git push + gh pr create)
|
|
112
|
+
click.echo(click.style("Phase 1: Creating PR", bold=True))
|
|
113
|
+
core_result = _run_core_submit(ctx, cwd, debug, force)
|
|
114
|
+
|
|
115
|
+
if isinstance(core_result, CoreSubmitError):
|
|
116
|
+
raise click.ClickException(core_result.message)
|
|
117
|
+
|
|
118
|
+
click.echo(click.style(f" PR #{core_result.pr_number} created", fg="green"))
|
|
119
|
+
click.echo("")
|
|
120
|
+
|
|
121
|
+
# Phase 2: Get diff for AI
|
|
122
|
+
click.echo(click.style("Phase 2: Getting diff", bold=True))
|
|
123
|
+
diff_file = _run_diff_extraction(ctx, cwd, core_result.pr_number, session_id, debug)
|
|
124
|
+
|
|
125
|
+
if diff_file is None:
|
|
126
|
+
raise click.ClickException("Failed to extract diff for AI analysis")
|
|
127
|
+
|
|
128
|
+
click.echo("")
|
|
129
|
+
|
|
130
|
+
# Get branch info for AI context
|
|
131
|
+
repo_root = ctx.git.get_repository_root(cwd)
|
|
132
|
+
current_branch = ctx.git.get_current_branch(cwd) or core_result.branch_name
|
|
133
|
+
trunk_branch = ctx.git.detect_trunk_branch(repo_root)
|
|
134
|
+
|
|
135
|
+
# Get parent branch (Graphite-aware, falls back to trunk)
|
|
136
|
+
parent_branch = (
|
|
137
|
+
ctx.graphite.get_parent_branch(ctx.git, Path(repo_root), current_branch) or trunk_branch
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# Get commit messages for AI context (only from current branch)
|
|
141
|
+
commit_messages = ctx.git.get_commit_messages_since(cwd, parent_branch)
|
|
142
|
+
|
|
143
|
+
# Phase 3: Generate commit message
|
|
144
|
+
click.echo(click.style("Phase 3: Generating PR description", bold=True))
|
|
145
|
+
msg_gen = CommitMessageGenerator(ctx.claude_executor)
|
|
146
|
+
msg_result = _run_commit_message_generation(
|
|
147
|
+
msg_gen,
|
|
148
|
+
diff_file=diff_file,
|
|
149
|
+
repo_root=Path(repo_root),
|
|
150
|
+
current_branch=current_branch,
|
|
151
|
+
parent_branch=parent_branch,
|
|
152
|
+
commit_messages=commit_messages,
|
|
153
|
+
debug=debug,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
if not msg_result.success:
|
|
157
|
+
raise click.ClickException(f"Failed to generate message: {msg_result.error_message}")
|
|
158
|
+
|
|
159
|
+
click.echo("")
|
|
160
|
+
|
|
161
|
+
# Phase 4: Graphite enhancement (optional)
|
|
162
|
+
graphite_url: str | None = None
|
|
163
|
+
if use_graphite:
|
|
164
|
+
click.echo(click.style("Phase 4: Graphite enhancement", bold=True))
|
|
165
|
+
graphite_result = _run_graphite_enhance(ctx, cwd, core_result.pr_number, debug, force)
|
|
166
|
+
|
|
167
|
+
if isinstance(graphite_result, GraphiteEnhanceResult):
|
|
168
|
+
graphite_url = graphite_result.graphite_url
|
|
169
|
+
click.echo("")
|
|
170
|
+
elif isinstance(graphite_result, GraphiteSkipped):
|
|
171
|
+
if debug:
|
|
172
|
+
click.echo(click.style(f" {graphite_result.message}", dim=True))
|
|
173
|
+
click.echo("")
|
|
174
|
+
elif isinstance(graphite_result, GraphiteEnhanceError):
|
|
175
|
+
# Graphite errors are warnings, not fatal
|
|
176
|
+
click.echo(click.style(f" Warning: {graphite_result.message}", fg="yellow"))
|
|
177
|
+
click.echo("")
|
|
178
|
+
|
|
179
|
+
# Phase 5: Finalize (update PR metadata)
|
|
180
|
+
click.echo(click.style("Phase 5: Updating PR metadata", bold=True))
|
|
181
|
+
finalize_result = _run_finalize(
|
|
182
|
+
ctx,
|
|
183
|
+
cwd,
|
|
184
|
+
pr_number=core_result.pr_number,
|
|
185
|
+
title=msg_result.title or "Update",
|
|
186
|
+
body=msg_result.body or "",
|
|
187
|
+
diff_file=str(diff_file),
|
|
188
|
+
debug=debug,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
if isinstance(finalize_result, PostAnalysisError):
|
|
192
|
+
raise click.ClickException(finalize_result.message)
|
|
193
|
+
|
|
194
|
+
click.echo(click.style(" PR metadata updated", fg="green"))
|
|
195
|
+
click.echo("")
|
|
196
|
+
|
|
197
|
+
# Success output with clickable URL
|
|
198
|
+
styled_url = click.style(finalize_result.pr_url, fg="cyan", underline=True)
|
|
199
|
+
clickable_url = f"\033]8;;{finalize_result.pr_url}\033\\{styled_url}\033]8;;\033\\"
|
|
200
|
+
click.echo(f"✅ {clickable_url}")
|
|
201
|
+
|
|
202
|
+
# Show Graphite URL if available
|
|
203
|
+
if graphite_url:
|
|
204
|
+
styled_graphite = click.style(graphite_url, fg="cyan", underline=True)
|
|
205
|
+
clickable_graphite = f"\033]8;;{graphite_url}\033\\{styled_graphite}\033]8;;\033\\"
|
|
206
|
+
click.echo(f"📊 {clickable_graphite}")
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _run_core_submit(
|
|
210
|
+
ctx: ErkContext,
|
|
211
|
+
cwd: Path,
|
|
212
|
+
debug: bool,
|
|
213
|
+
force: bool,
|
|
214
|
+
) -> CoreSubmitResult | CoreSubmitError:
|
|
215
|
+
"""Run core submit phase (git push + gh pr create)."""
|
|
216
|
+
result: CoreSubmitResult | CoreSubmitError | None = None
|
|
217
|
+
plans_repo = ctx.local_config.plans_repo if ctx.local_config else None
|
|
218
|
+
|
|
219
|
+
for event in execute_core_submit(
|
|
220
|
+
ctx, cwd, pr_title="WIP", pr_body="", force=force, plans_repo=plans_repo
|
|
221
|
+
):
|
|
222
|
+
if isinstance(event, ProgressEvent):
|
|
223
|
+
if debug:
|
|
224
|
+
_render_progress(event)
|
|
225
|
+
elif isinstance(event, CompletionEvent):
|
|
226
|
+
result = event.result
|
|
227
|
+
|
|
228
|
+
if result is None:
|
|
229
|
+
return CoreSubmitError(
|
|
230
|
+
success=False,
|
|
231
|
+
error_type="submit-failed",
|
|
232
|
+
message="Core submit did not complete",
|
|
233
|
+
details={},
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
return result
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _run_diff_extraction(
|
|
240
|
+
ctx: ErkContext,
|
|
241
|
+
cwd: Path,
|
|
242
|
+
pr_number: int,
|
|
243
|
+
session_id: str,
|
|
244
|
+
debug: bool,
|
|
245
|
+
) -> Path | None:
|
|
246
|
+
"""Run diff extraction phase."""
|
|
247
|
+
result: Path | None = None
|
|
248
|
+
|
|
249
|
+
for event in execute_diff_extraction(ctx, cwd, pr_number, session_id):
|
|
250
|
+
if isinstance(event, ProgressEvent):
|
|
251
|
+
if debug:
|
|
252
|
+
_render_progress(event)
|
|
253
|
+
elif isinstance(event, CompletionEvent):
|
|
254
|
+
result = event.result
|
|
255
|
+
|
|
256
|
+
return result
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _run_graphite_enhance(
|
|
260
|
+
ctx: ErkContext,
|
|
261
|
+
cwd: Path,
|
|
262
|
+
pr_number: int,
|
|
263
|
+
debug: bool,
|
|
264
|
+
force: bool,
|
|
265
|
+
) -> GraphiteEnhanceResult | GraphiteEnhanceError | GraphiteSkipped:
|
|
266
|
+
"""Run Graphite enhancement phase."""
|
|
267
|
+
result: GraphiteEnhanceResult | GraphiteEnhanceError | GraphiteSkipped | None = None
|
|
268
|
+
|
|
269
|
+
for event in execute_graphite_enhance(ctx, cwd, pr_number, force=force):
|
|
270
|
+
if isinstance(event, ProgressEvent):
|
|
271
|
+
if debug:
|
|
272
|
+
_render_progress(event)
|
|
273
|
+
elif isinstance(event, CompletionEvent):
|
|
274
|
+
result = event.result
|
|
275
|
+
|
|
276
|
+
if result is None:
|
|
277
|
+
return GraphiteSkipped(
|
|
278
|
+
success=True,
|
|
279
|
+
reason="incomplete",
|
|
280
|
+
message="Graphite enhancement did not complete",
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
return result
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _run_finalize(
|
|
287
|
+
ctx: ErkContext,
|
|
288
|
+
cwd: Path,
|
|
289
|
+
pr_number: int,
|
|
290
|
+
title: str,
|
|
291
|
+
body: str,
|
|
292
|
+
diff_file: str,
|
|
293
|
+
debug: bool,
|
|
294
|
+
) -> FinalizeResult | PostAnalysisError:
|
|
295
|
+
"""Run finalize phase and return result."""
|
|
296
|
+
result: FinalizeResult | PostAnalysisError | None = None
|
|
297
|
+
|
|
298
|
+
plans_repo = ctx.local_config.plans_repo if ctx.local_config else None
|
|
299
|
+
for event in execute_finalize(
|
|
300
|
+
ctx,
|
|
301
|
+
cwd,
|
|
302
|
+
pr_number=pr_number,
|
|
303
|
+
pr_title=title,
|
|
304
|
+
pr_body=body,
|
|
305
|
+
pr_body_file=None,
|
|
306
|
+
diff_file=diff_file,
|
|
307
|
+
plans_repo=plans_repo,
|
|
308
|
+
):
|
|
309
|
+
if isinstance(event, ProgressEvent):
|
|
310
|
+
if debug:
|
|
311
|
+
_render_progress(event)
|
|
312
|
+
elif isinstance(event, CompletionEvent):
|
|
313
|
+
result = event.result
|
|
314
|
+
|
|
315
|
+
if result is None:
|
|
316
|
+
return PostAnalysisError(
|
|
317
|
+
success=False,
|
|
318
|
+
error_type="submit-failed",
|
|
319
|
+
message="Finalize did not complete",
|
|
320
|
+
details={},
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
return result
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _run_commit_message_generation(
|
|
327
|
+
generator: CommitMessageGenerator,
|
|
328
|
+
diff_file: Path,
|
|
329
|
+
repo_root: Path,
|
|
330
|
+
current_branch: str,
|
|
331
|
+
parent_branch: str,
|
|
332
|
+
commit_messages: list[str] | None,
|
|
333
|
+
debug: bool,
|
|
334
|
+
) -> CommitMessageResult:
|
|
335
|
+
"""Run commit message generation and return result."""
|
|
336
|
+
result: CommitMessageResult | None = None
|
|
337
|
+
|
|
338
|
+
for event in generator.generate(
|
|
339
|
+
CommitMessageRequest(
|
|
340
|
+
diff_file=diff_file,
|
|
341
|
+
repo_root=repo_root,
|
|
342
|
+
current_branch=current_branch,
|
|
343
|
+
parent_branch=parent_branch,
|
|
344
|
+
commit_messages=commit_messages,
|
|
345
|
+
)
|
|
346
|
+
):
|
|
347
|
+
if isinstance(event, ProgressEvent):
|
|
348
|
+
_render_progress(event)
|
|
349
|
+
elif isinstance(event, CompletionEvent):
|
|
350
|
+
result = event.result
|
|
351
|
+
|
|
352
|
+
if result is None:
|
|
353
|
+
return CommitMessageResult(
|
|
354
|
+
success=False,
|
|
355
|
+
title=None,
|
|
356
|
+
body=None,
|
|
357
|
+
error_message="Commit message generation did not complete",
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
return result
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""Synchronize current PR branch with Graphite.
|
|
2
|
+
|
|
3
|
+
This command registers a checked-out PR branch with Graphite so it can be managed
|
|
4
|
+
using gt commands (gt pr, gt land, etc.). Useful after checking out a PR from a
|
|
5
|
+
remote source (like GitHub Actions).
|
|
6
|
+
|
|
7
|
+
Flow:
|
|
8
|
+
1. Validate preconditions (gh/gt auth, on branch, PR exists and is OPEN)
|
|
9
|
+
2. Check if already tracked by Graphite (idempotent)
|
|
10
|
+
3. Get PR base branch from GitHub
|
|
11
|
+
4. Track with Graphite: gt track --branch <current> --parent <base>
|
|
12
|
+
5. Squash commits: gt squash --no-edit --no-interactive
|
|
13
|
+
6. Update local commit message with PR title/body from GitHub
|
|
14
|
+
7. Restack: gt restack (manual conflict resolution if needed)
|
|
15
|
+
8. Submit: gt submit --no-edit --no-interactive (force-push to link with Graphite)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
import click
|
|
21
|
+
|
|
22
|
+
from erk.cli.ensure import Ensure
|
|
23
|
+
from erk.cli.graphite_command import GraphiteCommand
|
|
24
|
+
from erk.core.context import ErkContext
|
|
25
|
+
from erk.core.repo_discovery import NoRepoSentinel, RepoContext
|
|
26
|
+
from erk_shared.gateway.gt.events import CompletionEvent
|
|
27
|
+
from erk_shared.gateway.gt.operations import execute_squash
|
|
28
|
+
from erk_shared.gateway.gt.types import RestackError, SquashError, SquashSuccess
|
|
29
|
+
from erk_shared.github.types import PRNotFound
|
|
30
|
+
from erk_shared.output.output import user_output
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _squash_commits(ctx: ErkContext, repo_root: Path) -> None:
|
|
34
|
+
"""Squash all commits on the current branch into one."""
|
|
35
|
+
user_output("Squashing commits...")
|
|
36
|
+
squash_result = None
|
|
37
|
+
for event in execute_squash(ctx, repo_root):
|
|
38
|
+
if isinstance(event, CompletionEvent):
|
|
39
|
+
squash_result = event.result
|
|
40
|
+
squash_result = Ensure.not_none(squash_result, "Squash operation produced no result")
|
|
41
|
+
if isinstance(squash_result, SquashError):
|
|
42
|
+
Ensure.invariant(False, squash_result.message)
|
|
43
|
+
assert isinstance(squash_result, SquashSuccess) # Type narrowing after error check
|
|
44
|
+
user_output(click.style("✓", fg="green") + f" {squash_result.message}")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _update_commit_message_from_pr(ctx: ErkContext, repo_root: Path, pr_number: int) -> None:
|
|
48
|
+
"""Update the commit message with PR title and body from GitHub."""
|
|
49
|
+
pr = ctx.github.get_pr(repo_root, pr_number)
|
|
50
|
+
if isinstance(pr, PRNotFound):
|
|
51
|
+
# PR was verified to exist earlier, so this shouldn't happen
|
|
52
|
+
return
|
|
53
|
+
if pr.title:
|
|
54
|
+
commit_message = pr.title
|
|
55
|
+
if pr.body:
|
|
56
|
+
commit_message = f"{pr.title}\n\n{pr.body}"
|
|
57
|
+
user_output("Updating commit message from PR...")
|
|
58
|
+
ctx.git.amend_commit(repo_root, commit_message)
|
|
59
|
+
user_output(click.style("✓", fg="green") + " Commit message updated")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@click.command("sync", cls=GraphiteCommand)
|
|
63
|
+
@click.option(
|
|
64
|
+
"--dangerous",
|
|
65
|
+
is_flag=True,
|
|
66
|
+
required=True,
|
|
67
|
+
help="Acknowledge that this command invokes Claude with --dangerously-skip-permissions.",
|
|
68
|
+
)
|
|
69
|
+
@click.pass_obj
|
|
70
|
+
def pr_sync(ctx: ErkContext, *, dangerous: bool) -> None:
|
|
71
|
+
"""Synchronize current PR branch with Graphite.
|
|
72
|
+
|
|
73
|
+
Registers the current PR branch with Graphite for stack management.
|
|
74
|
+
After syncing, you can use standard gt commands (gt pr, gt land, etc.).
|
|
75
|
+
|
|
76
|
+
This is typically used after 'erk pr checkout' to enable Graphite workflows
|
|
77
|
+
on a PR that was created elsewhere (like from a GitHub Actions run).
|
|
78
|
+
|
|
79
|
+
Examples:
|
|
80
|
+
|
|
81
|
+
# Checkout and sync a PR
|
|
82
|
+
erk pr checkout 1973
|
|
83
|
+
erk pr sync --dangerous
|
|
84
|
+
|
|
85
|
+
# Now you can use Graphite commands
|
|
86
|
+
gt pr
|
|
87
|
+
gt land
|
|
88
|
+
|
|
89
|
+
Requirements:
|
|
90
|
+
- On a branch (not detached HEAD)
|
|
91
|
+
- PR exists and is OPEN
|
|
92
|
+
- PR is not from a fork (cross-repo PRs cannot be tracked)
|
|
93
|
+
"""
|
|
94
|
+
# dangerous flag is required to indicate acknowledgment
|
|
95
|
+
_ = dangerous
|
|
96
|
+
# Step 1: Validate preconditions
|
|
97
|
+
Ensure.gh_authenticated(ctx)
|
|
98
|
+
Ensure.gt_authenticated(ctx)
|
|
99
|
+
Ensure.invariant(
|
|
100
|
+
not isinstance(ctx.repo, NoRepoSentinel),
|
|
101
|
+
"Not in a git repository",
|
|
102
|
+
)
|
|
103
|
+
assert not isinstance(ctx.repo, NoRepoSentinel) # Type narrowing for ty
|
|
104
|
+
repo: RepoContext = ctx.repo
|
|
105
|
+
|
|
106
|
+
# Check we're on a branch (not detached HEAD)
|
|
107
|
+
current_branch = Ensure.not_none(
|
|
108
|
+
ctx.git.get_current_branch(ctx.cwd),
|
|
109
|
+
"Not on a branch - checkout a branch before syncing",
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# Step 2: Check if PR exists and get status
|
|
113
|
+
pr = ctx.github.get_pr_for_branch(repo.root, current_branch)
|
|
114
|
+
Ensure.invariant(
|
|
115
|
+
not isinstance(pr, PRNotFound),
|
|
116
|
+
f"No pull request found for branch '{current_branch}'",
|
|
117
|
+
)
|
|
118
|
+
# Type narrowing after invariant check
|
|
119
|
+
assert not isinstance(pr, PRNotFound)
|
|
120
|
+
Ensure.invariant(
|
|
121
|
+
pr.state == "OPEN",
|
|
122
|
+
f"Cannot sync {pr.state} PR - only OPEN PRs can be synchronized",
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
pr_number = pr.number
|
|
126
|
+
|
|
127
|
+
# Check if PR is from a fork (cross-repo)
|
|
128
|
+
Ensure.invariant(
|
|
129
|
+
not pr.is_cross_repository,
|
|
130
|
+
"Cannot sync fork PRs - Graphite cannot track branches from forks",
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# Step 3: Check if already tracked by Graphite (idempotent)
|
|
134
|
+
parent_branch = ctx.graphite.get_parent_branch(ctx.git, repo.root, current_branch)
|
|
135
|
+
if parent_branch is not None:
|
|
136
|
+
user_output(
|
|
137
|
+
click.style("✓", fg="green")
|
|
138
|
+
+ f" Branch '{current_branch}' already tracked by Graphite (parent: {parent_branch})"
|
|
139
|
+
)
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
# Step 4: Get PR base branch from GitHub (use same PRDetails object)
|
|
143
|
+
base_branch = pr.base_ref_name
|
|
144
|
+
user_output(f"Base branch: {base_branch}")
|
|
145
|
+
|
|
146
|
+
# Step 5: Track with Graphite
|
|
147
|
+
user_output(f"Tracking branch '{current_branch}' with parent '{base_branch}'...")
|
|
148
|
+
ctx.graphite.track_branch(ctx.cwd, current_branch, base_branch)
|
|
149
|
+
user_output(click.style("✓", fg="green") + " Branch tracked with Graphite")
|
|
150
|
+
|
|
151
|
+
# Step 6: Squash commits (idempotent)
|
|
152
|
+
_squash_commits(ctx, repo.root)
|
|
153
|
+
|
|
154
|
+
# Step 6b: Update commit message with PR title/body
|
|
155
|
+
_update_commit_message_from_pr(ctx, repo.root, pr_number)
|
|
156
|
+
|
|
157
|
+
# Step 7: Restack with Graphite (manual conflict resolution if needed)
|
|
158
|
+
user_output("Restacking branch...")
|
|
159
|
+
restack_result = ctx.graphite.restack_idempotent(repo.root, no_interactive=True, quiet=False)
|
|
160
|
+
if isinstance(restack_result, RestackError):
|
|
161
|
+
if restack_result.error_type == "restack-conflict":
|
|
162
|
+
user_output(click.style("\nRestack paused due to merge conflicts.", fg="yellow"))
|
|
163
|
+
user_output("To resolve conflicts, run:")
|
|
164
|
+
user_output(click.style(" erk pr fix-conflicts --dangerous", fg="cyan"))
|
|
165
|
+
user_output("\nOr manually:")
|
|
166
|
+
user_output(" 1. Resolve conflicts in the listed files")
|
|
167
|
+
user_output(" 2. Run: gt add -A")
|
|
168
|
+
user_output(" 3. Run: gt continue")
|
|
169
|
+
raise SystemExit(1)
|
|
170
|
+
# Non-conflict error
|
|
171
|
+
raise click.ClickException(restack_result.message)
|
|
172
|
+
user_output(click.style("✓", fg="green") + " Branch restacked")
|
|
173
|
+
|
|
174
|
+
# Step 8: Submit to link with Graphite
|
|
175
|
+
# Force push is required because squashing rewrites history, causing divergence from remote
|
|
176
|
+
user_output("Submitting to link with Graphite...")
|
|
177
|
+
ctx.graphite.submit_stack(repo.root, quiet=True, force=True)
|
|
178
|
+
user_output(click.style("✓", fg="green") + f" PR #{pr_number} synchronized with Graphite")
|
|
179
|
+
|
|
180
|
+
user_output(f"\nBranch '{current_branch}' is now tracked by Graphite.")
|
|
181
|
+
user_output("You can now use: gt pr, gt land, etc.")
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Hidden command that pre-generates recovery scripts for passthrough flows."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from erk.cli.core import discover_repo_context
|
|
8
|
+
from erk.cli.shell_utils import render_navigation_script
|
|
9
|
+
from erk.core.context import ErkContext
|
|
10
|
+
from erk_shared.output.output import user_output
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def generate_recovery_script(ctx: ErkContext) -> Path | None:
|
|
14
|
+
"""Create a recovery script that returns to the repo root if cwd vanishes.
|
|
15
|
+
|
|
16
|
+
This helper intentionally guards against runtime cwd races:
|
|
17
|
+
- ctx.cwd is a snapshot from CLI entry; it may no longer exist by the time this runs.
|
|
18
|
+
- discover_repo_context() performs the authoritative repo lookup; probing earlier provides
|
|
19
|
+
no additional safety and merely repeats the work.
|
|
20
|
+
- Returning None signals that graceful degradation is preferred to exploding at the boundary.
|
|
21
|
+
"""
|
|
22
|
+
current_dir = ctx.cwd
|
|
23
|
+
|
|
24
|
+
if not current_dir.exists():
|
|
25
|
+
return None
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
repo = discover_repo_context(ctx, current_dir)
|
|
29
|
+
except (FileNotFoundError, ValueError):
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
script_content = render_navigation_script(
|
|
33
|
+
repo.root,
|
|
34
|
+
repo.root,
|
|
35
|
+
comment="erk passthrough recovery script",
|
|
36
|
+
success_message="",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
result = ctx.script_writer.write_activation_script(
|
|
40
|
+
script_content,
|
|
41
|
+
command_name="prepare",
|
|
42
|
+
comment="pre-generated by __prepare_cwd_recovery",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
return result.path
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@click.command(
|
|
49
|
+
"__prepare_cwd_recovery",
|
|
50
|
+
hidden=True,
|
|
51
|
+
add_help_option=False,
|
|
52
|
+
)
|
|
53
|
+
@click.pass_obj
|
|
54
|
+
def prepare_cwd_recovery_cmd(ctx: ErkContext) -> None:
|
|
55
|
+
"""Emit a recovery script if we are inside a managed repository."""
|
|
56
|
+
script_path = generate_recovery_script(ctx)
|
|
57
|
+
if script_path is None:
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
user_output(str(script_path), nl=False)
|