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,75 @@
|
|
|
1
|
+
"""Workflow run display utilities for worktree listings.
|
|
2
|
+
|
|
3
|
+
This module provides helpers for fetching and formatting workflow run status
|
|
4
|
+
information for worktrees with associated GitHub issues.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from erk_shared.github.abc import GitHub
|
|
10
|
+
from erk_shared.github.issues import GitHubIssues
|
|
11
|
+
from erk_shared.github.status_history import extract_workflow_run_id
|
|
12
|
+
from erk_shared.github.types import WorkflowRun
|
|
13
|
+
from erk_shared.impl_folder import read_issue_reference
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_workflow_run_for_worktree(
|
|
17
|
+
worktree_path: Path,
|
|
18
|
+
github: GitHub,
|
|
19
|
+
github_issues: GitHubIssues,
|
|
20
|
+
repo_root: Path,
|
|
21
|
+
) -> tuple[WorkflowRun | None, str | None]:
|
|
22
|
+
"""Get workflow run information for a worktree.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
worktree_path: Path to the worktree directory
|
|
26
|
+
github: GitHub operations interface
|
|
27
|
+
github_issues: GitHub issues interface
|
|
28
|
+
repo_root: Repository root directory
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Tuple of (WorkflowRun, workflow_url) or (None, None) if no workflow found
|
|
32
|
+
"""
|
|
33
|
+
# Check if worktree has .impl/issue.json
|
|
34
|
+
impl_dir = worktree_path / ".impl"
|
|
35
|
+
# Handle sentinel paths in tests (they raise RuntimeError on .exists())
|
|
36
|
+
# This is acceptable here because we're just checking for existence
|
|
37
|
+
# and returning early if not found - not using exceptions for control flow
|
|
38
|
+
try:
|
|
39
|
+
if not impl_dir.exists():
|
|
40
|
+
return (None, None)
|
|
41
|
+
except RuntimeError:
|
|
42
|
+
# Sentinel path in tests - treat as non-existent
|
|
43
|
+
return (None, None)
|
|
44
|
+
|
|
45
|
+
issue_ref = read_issue_reference(impl_dir)
|
|
46
|
+
if issue_ref is None:
|
|
47
|
+
return (None, None)
|
|
48
|
+
|
|
49
|
+
# Fetch issue comments (returns list of comment body strings)
|
|
50
|
+
comment_bodies = github_issues.get_issue_comments(repo_root, issue_ref.issue_number)
|
|
51
|
+
if not comment_bodies:
|
|
52
|
+
return (None, None)
|
|
53
|
+
|
|
54
|
+
# Extract workflow run ID from comments
|
|
55
|
+
run_id = extract_workflow_run_id(comment_bodies)
|
|
56
|
+
if run_id is None:
|
|
57
|
+
return (None, None)
|
|
58
|
+
|
|
59
|
+
# Fetch workflow run details
|
|
60
|
+
workflow_run = github.get_workflow_run(repo_root, run_id)
|
|
61
|
+
if workflow_run is None:
|
|
62
|
+
return (None, None)
|
|
63
|
+
|
|
64
|
+
# Build workflow URL
|
|
65
|
+
# Extract owner/repo from issue URL if available
|
|
66
|
+
workflow_url = None
|
|
67
|
+
if issue_ref.issue_url:
|
|
68
|
+
# Parse owner/repo from URL like https://github.com/owner/repo/issues/123
|
|
69
|
+
parts = issue_ref.issue_url.split("/")
|
|
70
|
+
if len(parts) >= 5:
|
|
71
|
+
owner = parts[-4]
|
|
72
|
+
repo = parts[-3]
|
|
73
|
+
workflow_url = f"https://github.com/{owner}/{repo}/actions/runs/{run_id}"
|
|
74
|
+
|
|
75
|
+
return (workflow_run, workflow_url)
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""Worktree pool state management.
|
|
2
|
+
|
|
3
|
+
Provides dataclasses and persistence functions for managing a pool of
|
|
4
|
+
pre-created worktrees that can be assigned to branches on demand.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class SlotInfo:
|
|
16
|
+
"""Represents an initialized pool slot.
|
|
17
|
+
|
|
18
|
+
A slot that has been pre-created with a worktree and placeholder branch,
|
|
19
|
+
ready for assignment to feature branches.
|
|
20
|
+
|
|
21
|
+
Attributes:
|
|
22
|
+
name: The pool slot identifier (e.g., "erk-managed-wt-01")
|
|
23
|
+
last_objective_issue: Issue number of the last objective worked on in this slot.
|
|
24
|
+
Persists across assignment cycles so /erk:objective-next-plan can default to it.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
name: str
|
|
28
|
+
last_objective_issue: int | None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class SlotAssignment:
|
|
33
|
+
"""Represents a branch assignment to a worktree slot.
|
|
34
|
+
|
|
35
|
+
Attributes:
|
|
36
|
+
slot_name: The pool slot identifier (e.g., "erk-managed-wt-01")
|
|
37
|
+
branch_name: The git branch assigned to this slot
|
|
38
|
+
assigned_at: ISO timestamp when the assignment was made
|
|
39
|
+
worktree_path: Filesystem path to the worktree directory
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
slot_name: str
|
|
43
|
+
branch_name: str
|
|
44
|
+
assigned_at: str
|
|
45
|
+
worktree_path: Path
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class PoolState:
|
|
50
|
+
"""Represents the complete state of the worktree pool.
|
|
51
|
+
|
|
52
|
+
Attributes:
|
|
53
|
+
version: Schema version for forward compatibility
|
|
54
|
+
pool_size: Maximum number of slots in the pool
|
|
55
|
+
slots: Tuple of initialized slots (may or may not have assignments)
|
|
56
|
+
assignments: Tuple of current slot assignments (immutable)
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
version: str
|
|
60
|
+
pool_size: int
|
|
61
|
+
slots: tuple[SlotInfo, ...]
|
|
62
|
+
assignments: tuple[SlotAssignment, ...]
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def test(
|
|
66
|
+
cls,
|
|
67
|
+
*,
|
|
68
|
+
version: str | None = None,
|
|
69
|
+
pool_size: int | None = None,
|
|
70
|
+
slots: tuple[SlotInfo, ...] | None = None,
|
|
71
|
+
assignments: tuple[SlotAssignment, ...] | None = None,
|
|
72
|
+
) -> PoolState:
|
|
73
|
+
"""Create a PoolState with sensible test defaults.
|
|
74
|
+
|
|
75
|
+
All parameters are optional and use sensible defaults for testing:
|
|
76
|
+
- version: "1.0"
|
|
77
|
+
- pool_size: 4
|
|
78
|
+
- slots: ()
|
|
79
|
+
- assignments: ()
|
|
80
|
+
"""
|
|
81
|
+
return cls(
|
|
82
|
+
version=version if version is not None else "1.0",
|
|
83
|
+
pool_size=pool_size if pool_size is not None else 4,
|
|
84
|
+
slots=slots if slots is not None else (),
|
|
85
|
+
assignments=assignments if assignments is not None else (),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def load_pool_state(pool_json_path: Path) -> PoolState | None:
|
|
90
|
+
"""Load pool state from JSON file.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
pool_json_path: Path to the pool.json file
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
PoolState if file exists and is valid, None otherwise
|
|
97
|
+
"""
|
|
98
|
+
if not pool_json_path.exists():
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
content = pool_json_path.read_text(encoding="utf-8")
|
|
102
|
+
data = json.loads(content)
|
|
103
|
+
|
|
104
|
+
slots = tuple(
|
|
105
|
+
SlotInfo(name=s["name"], last_objective_issue=s.get("last_objective_issue"))
|
|
106
|
+
for s in data.get("slots", [])
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
assignments = tuple(
|
|
110
|
+
SlotAssignment(
|
|
111
|
+
slot_name=a["slot_name"],
|
|
112
|
+
branch_name=a["branch_name"],
|
|
113
|
+
assigned_at=a["assigned_at"],
|
|
114
|
+
worktree_path=Path(a["worktree_path"]),
|
|
115
|
+
)
|
|
116
|
+
for a in data.get("assignments", [])
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
return PoolState(
|
|
120
|
+
version=data.get("version", "1.0"),
|
|
121
|
+
pool_size=data.get("pool_size", 4),
|
|
122
|
+
slots=slots,
|
|
123
|
+
assignments=assignments,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def save_pool_state(pool_json_path: Path, state: PoolState) -> None:
|
|
128
|
+
"""Save pool state to JSON file.
|
|
129
|
+
|
|
130
|
+
Creates parent directories if they don't exist.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
pool_json_path: Path to the pool.json file
|
|
134
|
+
state: Pool state to persist
|
|
135
|
+
"""
|
|
136
|
+
pool_json_path.parent.mkdir(parents=True, exist_ok=True)
|
|
137
|
+
|
|
138
|
+
data = {
|
|
139
|
+
"version": state.version,
|
|
140
|
+
"pool_size": state.pool_size,
|
|
141
|
+
"slots": [
|
|
142
|
+
{"name": s.name, "last_objective_issue": s.last_objective_issue} for s in state.slots
|
|
143
|
+
],
|
|
144
|
+
"assignments": [
|
|
145
|
+
{
|
|
146
|
+
"slot_name": a.slot_name,
|
|
147
|
+
"branch_name": a.branch_name,
|
|
148
|
+
"assigned_at": a.assigned_at,
|
|
149
|
+
"worktree_path": str(a.worktree_path),
|
|
150
|
+
}
|
|
151
|
+
for a in state.assignments
|
|
152
|
+
],
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
pool_json_path.write_text(json.dumps(data, indent=2), encoding="utf-8")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def update_slot_objective(
|
|
159
|
+
state: PoolState, slot_name: str, objective_issue: int | None
|
|
160
|
+
) -> PoolState:
|
|
161
|
+
"""Return new PoolState with slot's last_objective_issue updated.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
state: Current pool state
|
|
165
|
+
slot_name: Name of the slot to update
|
|
166
|
+
objective_issue: Issue number to set, or None to clear
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
New PoolState with the updated slot. If slot_name is not found,
|
|
170
|
+
returns state unchanged.
|
|
171
|
+
"""
|
|
172
|
+
new_slots: list[SlotInfo] = []
|
|
173
|
+
found = False
|
|
174
|
+
|
|
175
|
+
for slot in state.slots:
|
|
176
|
+
if slot.name == slot_name:
|
|
177
|
+
new_slots.append(SlotInfo(name=slot.name, last_objective_issue=objective_issue))
|
|
178
|
+
found = True
|
|
179
|
+
else:
|
|
180
|
+
new_slots.append(slot)
|
|
181
|
+
|
|
182
|
+
if not found:
|
|
183
|
+
return state
|
|
184
|
+
|
|
185
|
+
return PoolState(
|
|
186
|
+
version=state.version,
|
|
187
|
+
pool_size=state.pool_size,
|
|
188
|
+
slots=tuple(new_slots),
|
|
189
|
+
assignments=state.assignments,
|
|
190
|
+
)
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"""Utility functions for worktree operations.
|
|
2
|
+
|
|
3
|
+
This module provides pure business logic functions for worktree operations,
|
|
4
|
+
separated from I/O and CLI concerns. These functions work with data objects
|
|
5
|
+
(WorktreeInfo) and enable fast unit testing.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from erk_shared.git.abc import WorktreeInfo
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MoveOperationType(Enum):
|
|
17
|
+
"""Type of move operation to perform between worktrees."""
|
|
18
|
+
|
|
19
|
+
MOVE = "move" # Source has branch, target doesn't exist or is detached
|
|
20
|
+
SWAP = "swap" # Both source and target have branches
|
|
21
|
+
CREATE = "create" # Target doesn't exist
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class MoveOperation:
|
|
26
|
+
"""Represents a move operation between worktrees.
|
|
27
|
+
|
|
28
|
+
Attributes:
|
|
29
|
+
operation_type: Type of operation (move, swap, or create)
|
|
30
|
+
source_path: Path to source worktree
|
|
31
|
+
target_path: Path to target worktree
|
|
32
|
+
source_branch: Branch in source worktree (None if detached)
|
|
33
|
+
target_branch: Branch in target worktree (None if doesn't exist or detached)
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
operation_type: MoveOperationType
|
|
37
|
+
source_path: Path
|
|
38
|
+
target_path: Path
|
|
39
|
+
source_branch: str | None
|
|
40
|
+
target_branch: str | None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def compute_relative_path_in_worktree(
|
|
44
|
+
worktrees: list[WorktreeInfo],
|
|
45
|
+
current_dir: Path,
|
|
46
|
+
) -> Path | None:
|
|
47
|
+
"""Compute relative path from current worktree root to current_dir.
|
|
48
|
+
|
|
49
|
+
Used to preserve the user's relative position when switching worktrees.
|
|
50
|
+
For example, if the user is at /repo/worktrees/feat/src/lib, switching
|
|
51
|
+
to another worktree should land them at <target>/src/lib if it exists.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
worktrees: List of WorktreeInfo objects to search
|
|
55
|
+
current_dir: Current directory path (will be resolved internally)
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Relative path from worktree root to current_dir, or None if:
|
|
59
|
+
- current_dir is at the worktree root (relative path would be '.')
|
|
60
|
+
- current_dir is not within any known worktree
|
|
61
|
+
|
|
62
|
+
Examples:
|
|
63
|
+
>>> worktrees = [WorktreeInfo(Path("/repo/wt/feat"), "feat", False)]
|
|
64
|
+
>>> compute_relative_path_in_worktree(worktrees, Path("/repo/wt/feat/src/lib"))
|
|
65
|
+
Path("src/lib")
|
|
66
|
+
>>> compute_relative_path_in_worktree(worktrees, Path("/repo/wt/feat"))
|
|
67
|
+
None # At worktree root
|
|
68
|
+
"""
|
|
69
|
+
worktree_root = find_worktree_containing_path(worktrees, current_dir)
|
|
70
|
+
if worktree_root is None:
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
# Resolve both paths for reliable comparison
|
|
74
|
+
resolved_current = current_dir.resolve()
|
|
75
|
+
resolved_root = worktree_root.resolve()
|
|
76
|
+
|
|
77
|
+
# If at worktree root, return None (no relative subpath)
|
|
78
|
+
if resolved_current == resolved_root:
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
# Compute relative path
|
|
82
|
+
relative = resolved_current.relative_to(resolved_root)
|
|
83
|
+
|
|
84
|
+
return relative
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def find_worktree_containing_path(worktrees: list[WorktreeInfo], target_path: Path) -> Path | None:
|
|
88
|
+
"""Find which worktree contains the given path.
|
|
89
|
+
|
|
90
|
+
Returns the most specific (deepest) match to handle nested worktrees correctly.
|
|
91
|
+
Handles symlink resolution differences (e.g., /var vs /private/var on macOS).
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
worktrees: List of WorktreeInfo objects to search
|
|
95
|
+
target_path: Path to check (will be resolved internally)
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Path to the worktree that contains target_path, or None if not found
|
|
99
|
+
|
|
100
|
+
Examples:
|
|
101
|
+
>>> worktrees = [WorktreeInfo(Path("/repo"), "main", True),
|
|
102
|
+
... WorktreeInfo(Path("/repo/erks/feat"), "feat", False)]
|
|
103
|
+
>>> find_worktree_containing_path(worktrees, Path("/repo/erks/feat/src"))
|
|
104
|
+
Path("/repo/erks/feat") # Returns deepest match
|
|
105
|
+
"""
|
|
106
|
+
best_match: Path | None = None
|
|
107
|
+
best_match_depth = -1
|
|
108
|
+
|
|
109
|
+
# Resolve target_path to handle symlinks consistently
|
|
110
|
+
resolved_target = target_path.resolve()
|
|
111
|
+
|
|
112
|
+
for wt in worktrees:
|
|
113
|
+
wt_path = wt.path.resolve()
|
|
114
|
+
|
|
115
|
+
# Check if target_path is within this worktree
|
|
116
|
+
# is_relative_to() returns True if target_path is under wt_path
|
|
117
|
+
if resolved_target.is_relative_to(wt_path):
|
|
118
|
+
# Count path depth to find most specific match
|
|
119
|
+
depth = len(wt_path.parts)
|
|
120
|
+
if depth > best_match_depth:
|
|
121
|
+
best_match = wt_path
|
|
122
|
+
best_match_depth = depth
|
|
123
|
+
|
|
124
|
+
return best_match
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def find_current_worktree(worktrees: list[WorktreeInfo], current_dir: Path) -> WorktreeInfo | None:
|
|
128
|
+
"""Find the WorktreeInfo object for the worktree containing current_dir.
|
|
129
|
+
|
|
130
|
+
Higher-level wrapper around find_worktree_containing_path that returns
|
|
131
|
+
the full WorktreeInfo object instead of just the path.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
worktrees: List of WorktreeInfo objects to search
|
|
135
|
+
current_dir: Current directory path (should be resolved)
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
WorktreeInfo object if found, None if not in any worktree
|
|
139
|
+
|
|
140
|
+
Examples:
|
|
141
|
+
>>> worktrees = [WorktreeInfo(Path("/repo"), "main", True)]
|
|
142
|
+
>>> find_current_worktree(worktrees, Path("/repo/src"))
|
|
143
|
+
WorktreeInfo(path=Path("/repo"), branch="main", is_root=True)
|
|
144
|
+
"""
|
|
145
|
+
wt_path = find_worktree_containing_path(worktrees, current_dir)
|
|
146
|
+
if wt_path is None:
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
# Find and return the matching WorktreeInfo object
|
|
150
|
+
for wt in worktrees:
|
|
151
|
+
if wt.path.resolve() == wt_path:
|
|
152
|
+
return wt
|
|
153
|
+
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def is_root_worktree(worktree_path: Path, repo_root: Path) -> bool:
|
|
158
|
+
"""Check if a worktree path is the repository root worktree.
|
|
159
|
+
|
|
160
|
+
Compares resolved paths to determine if the worktree is the root.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
worktree_path: Path to the worktree to check
|
|
164
|
+
repo_root: Path to the repository root
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
True if worktree_path is the root worktree, False otherwise
|
|
168
|
+
|
|
169
|
+
Examples:
|
|
170
|
+
>>> is_root_worktree(Path("/repo"), Path("/repo"))
|
|
171
|
+
True
|
|
172
|
+
>>> is_root_worktree(Path("/repo/erks/feat"), Path("/repo"))
|
|
173
|
+
False
|
|
174
|
+
"""
|
|
175
|
+
return worktree_path.resolve() == repo_root.resolve()
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def get_worktree_branch(worktrees: list[WorktreeInfo], wt_path: Path) -> str | None:
|
|
179
|
+
"""Get the branch checked out in a worktree.
|
|
180
|
+
|
|
181
|
+
Returns None if worktree is not found or is in detached HEAD state.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
worktrees: List of WorktreeInfo objects to search
|
|
185
|
+
wt_path: Path to the worktree
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Branch name if found and checked out, None otherwise
|
|
189
|
+
|
|
190
|
+
Examples:
|
|
191
|
+
>>> worktrees = [WorktreeInfo(Path("/repo/erks/feat"), "feature-x", False)]
|
|
192
|
+
>>> get_worktree_branch(worktrees, Path("/repo/erks/feat"))
|
|
193
|
+
"feature-x"
|
|
194
|
+
>>> get_worktree_branch(worktrees, Path("/repo/erks/other"))
|
|
195
|
+
None
|
|
196
|
+
"""
|
|
197
|
+
# Resolve paths for comparison to handle relative vs absolute paths
|
|
198
|
+
wt_path_resolved = wt_path.resolve()
|
|
199
|
+
for wt in worktrees:
|
|
200
|
+
if wt.path.resolve() == wt_path_resolved:
|
|
201
|
+
return wt.branch
|
|
202
|
+
return None
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def find_worktree_with_branch(worktrees: list[WorktreeInfo], branch: str) -> Path | None:
|
|
206
|
+
"""Find the worktree path containing the specified branch.
|
|
207
|
+
|
|
208
|
+
Returns None if the branch is not found in any worktree.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
worktrees: List of WorktreeInfo objects to search
|
|
212
|
+
branch: Branch name to find
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
Path to worktree containing the branch, or None if not found
|
|
216
|
+
|
|
217
|
+
Examples:
|
|
218
|
+
>>> worktrees = [WorktreeInfo(Path("/repo/erks/feat"), "feature-x", False)]
|
|
219
|
+
>>> find_worktree_with_branch(worktrees, "feature-x")
|
|
220
|
+
Path("/repo/erks/feat")
|
|
221
|
+
>>> find_worktree_with_branch(worktrees, "unknown")
|
|
222
|
+
None
|
|
223
|
+
"""
|
|
224
|
+
for wt in worktrees:
|
|
225
|
+
if wt.branch == branch:
|
|
226
|
+
return wt.path
|
|
227
|
+
return None
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def filter_non_trunk_branches(all_branches: dict[str, Any], stack: list[str]) -> list[str]:
|
|
231
|
+
"""Filter out trunk branches from a stack.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
all_branches: Dictionary mapping branch names to branch info (with is_trunk attribute)
|
|
235
|
+
stack: List of branch names to filter
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
List of non-trunk branches from the stack
|
|
239
|
+
|
|
240
|
+
Examples:
|
|
241
|
+
>>> branches = {"main": BranchInfo(is_trunk=True), "feat": BranchInfo(is_trunk=False)}
|
|
242
|
+
>>> filter_non_trunk_branches(branches, ["main", "feat"])
|
|
243
|
+
["feat"]
|
|
244
|
+
"""
|
|
245
|
+
return [b for b in stack if b in all_branches and not all_branches[b].is_trunk]
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def determine_move_operation(
|
|
249
|
+
worktrees: list[WorktreeInfo],
|
|
250
|
+
source_path: Path,
|
|
251
|
+
target_path: Path,
|
|
252
|
+
) -> MoveOperation:
|
|
253
|
+
"""Determine the type of move operation based on source and target states.
|
|
254
|
+
|
|
255
|
+
Pure function that analyzes worktree states to determine operation type:
|
|
256
|
+
- CREATE: Target doesn't exist
|
|
257
|
+
- SWAP: Both source and target have branches
|
|
258
|
+
- MOVE: Source has branch, target exists but is detached or doesn't exist
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
worktrees: List of all worktrees in the repository
|
|
262
|
+
source_path: Path to source worktree (must exist)
|
|
263
|
+
target_path: Path to target worktree (may not exist)
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
MoveOperation object describing the operation to perform
|
|
267
|
+
|
|
268
|
+
Examples:
|
|
269
|
+
>>> worktrees = [
|
|
270
|
+
... WorktreeInfo(Path("/repo/src"), "feat-1", False, False),
|
|
271
|
+
... ]
|
|
272
|
+
>>> determine_move_operation(worktrees, Path("/repo/src"), Path("/repo/new"))
|
|
273
|
+
MoveOperation(operation_type=MoveOperationType.CREATE, ...)
|
|
274
|
+
"""
|
|
275
|
+
# Get source branch
|
|
276
|
+
source_branch = get_worktree_branch(worktrees, source_path)
|
|
277
|
+
|
|
278
|
+
# Check if target exists in worktrees list
|
|
279
|
+
target_branch = get_worktree_branch(worktrees, target_path)
|
|
280
|
+
|
|
281
|
+
# Determine operation type
|
|
282
|
+
if target_branch is None:
|
|
283
|
+
# Target doesn't exist or is detached
|
|
284
|
+
# Check if any worktree exists at target_path
|
|
285
|
+
target_exists = any(wt.path.resolve() == target_path.resolve() for wt in worktrees)
|
|
286
|
+
if target_exists:
|
|
287
|
+
operation_type = MoveOperationType.MOVE
|
|
288
|
+
else:
|
|
289
|
+
operation_type = MoveOperationType.CREATE
|
|
290
|
+
else:
|
|
291
|
+
# Target exists with a branch - this is a swap
|
|
292
|
+
operation_type = MoveOperationType.SWAP
|
|
293
|
+
|
|
294
|
+
return MoveOperation(
|
|
295
|
+
operation_type=operation_type,
|
|
296
|
+
source_path=source_path,
|
|
297
|
+
target_path=target_path,
|
|
298
|
+
source_branch=source_branch,
|
|
299
|
+
target_branch=target_branch,
|
|
300
|
+
)
|