spec-kitty-cli 0.12.1__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.
- spec_kitty_cli-0.12.1.dist-info/METADATA +1767 -0
- spec_kitty_cli-0.12.1.dist-info/RECORD +242 -0
- spec_kitty_cli-0.12.1.dist-info/WHEEL +4 -0
- spec_kitty_cli-0.12.1.dist-info/entry_points.txt +2 -0
- spec_kitty_cli-0.12.1.dist-info/licenses/LICENSE +21 -0
- specify_cli/__init__.py +171 -0
- specify_cli/acceptance.py +627 -0
- specify_cli/agent_utils/README.md +157 -0
- specify_cli/agent_utils/__init__.py +9 -0
- specify_cli/agent_utils/status.py +356 -0
- specify_cli/cli/__init__.py +6 -0
- specify_cli/cli/commands/__init__.py +46 -0
- specify_cli/cli/commands/accept.py +189 -0
- specify_cli/cli/commands/agent/__init__.py +22 -0
- specify_cli/cli/commands/agent/config.py +382 -0
- specify_cli/cli/commands/agent/context.py +191 -0
- specify_cli/cli/commands/agent/feature.py +1057 -0
- specify_cli/cli/commands/agent/release.py +11 -0
- specify_cli/cli/commands/agent/tasks.py +1253 -0
- specify_cli/cli/commands/agent/workflow.py +801 -0
- specify_cli/cli/commands/context.py +246 -0
- specify_cli/cli/commands/dashboard.py +85 -0
- specify_cli/cli/commands/implement.py +973 -0
- specify_cli/cli/commands/init.py +827 -0
- specify_cli/cli/commands/init_help.py +62 -0
- specify_cli/cli/commands/merge.py +755 -0
- specify_cli/cli/commands/mission.py +240 -0
- specify_cli/cli/commands/ops.py +265 -0
- specify_cli/cli/commands/orchestrate.py +640 -0
- specify_cli/cli/commands/repair.py +175 -0
- specify_cli/cli/commands/research.py +165 -0
- specify_cli/cli/commands/sync.py +364 -0
- specify_cli/cli/commands/upgrade.py +249 -0
- specify_cli/cli/commands/validate_encoding.py +186 -0
- specify_cli/cli/commands/validate_tasks.py +186 -0
- specify_cli/cli/commands/verify.py +310 -0
- specify_cli/cli/helpers.py +123 -0
- specify_cli/cli/step_tracker.py +91 -0
- specify_cli/cli/ui.py +192 -0
- specify_cli/core/__init__.py +53 -0
- specify_cli/core/agent_context.py +311 -0
- specify_cli/core/config.py +96 -0
- specify_cli/core/context_validation.py +362 -0
- specify_cli/core/dependency_graph.py +351 -0
- specify_cli/core/git_ops.py +129 -0
- specify_cli/core/multi_parent_merge.py +323 -0
- specify_cli/core/paths.py +260 -0
- specify_cli/core/project_resolver.py +110 -0
- specify_cli/core/stale_detection.py +263 -0
- specify_cli/core/tool_checker.py +79 -0
- specify_cli/core/utils.py +43 -0
- specify_cli/core/vcs/__init__.py +114 -0
- specify_cli/core/vcs/detection.py +341 -0
- specify_cli/core/vcs/exceptions.py +85 -0
- specify_cli/core/vcs/git.py +1304 -0
- specify_cli/core/vcs/jujutsu.py +1208 -0
- specify_cli/core/vcs/protocol.py +285 -0
- specify_cli/core/vcs/types.py +249 -0
- specify_cli/core/version_checker.py +261 -0
- specify_cli/core/worktree.py +506 -0
- specify_cli/dashboard/__init__.py +28 -0
- specify_cli/dashboard/diagnostics.py +204 -0
- specify_cli/dashboard/handlers/__init__.py +17 -0
- specify_cli/dashboard/handlers/api.py +143 -0
- specify_cli/dashboard/handlers/base.py +65 -0
- specify_cli/dashboard/handlers/features.py +390 -0
- specify_cli/dashboard/handlers/router.py +81 -0
- specify_cli/dashboard/handlers/static.py +50 -0
- specify_cli/dashboard/lifecycle.py +541 -0
- specify_cli/dashboard/scanner.py +437 -0
- specify_cli/dashboard/server.py +123 -0
- specify_cli/dashboard/static/dashboard/dashboard.css +722 -0
- specify_cli/dashboard/static/dashboard/dashboard.js +1424 -0
- specify_cli/dashboard/static/spec-kitty.png +0 -0
- specify_cli/dashboard/templates/__init__.py +36 -0
- specify_cli/dashboard/templates/index.html +258 -0
- specify_cli/doc_generators.py +621 -0
- specify_cli/doc_state.py +408 -0
- specify_cli/frontmatter.py +384 -0
- specify_cli/gap_analysis.py +915 -0
- specify_cli/gitignore_manager.py +300 -0
- specify_cli/guards.py +145 -0
- specify_cli/legacy_detector.py +83 -0
- specify_cli/manifest.py +286 -0
- specify_cli/merge/__init__.py +63 -0
- specify_cli/merge/executor.py +653 -0
- specify_cli/merge/forecast.py +215 -0
- specify_cli/merge/ordering.py +126 -0
- specify_cli/merge/preflight.py +230 -0
- specify_cli/merge/state.py +185 -0
- specify_cli/merge/status_resolver.py +354 -0
- specify_cli/mission.py +654 -0
- specify_cli/missions/documentation/command-templates/implement.md +309 -0
- specify_cli/missions/documentation/command-templates/plan.md +275 -0
- specify_cli/missions/documentation/command-templates/review.md +344 -0
- specify_cli/missions/documentation/command-templates/specify.md +206 -0
- specify_cli/missions/documentation/command-templates/tasks.md +189 -0
- specify_cli/missions/documentation/mission.yaml +113 -0
- specify_cli/missions/documentation/templates/divio/explanation-template.md +192 -0
- specify_cli/missions/documentation/templates/divio/howto-template.md +168 -0
- specify_cli/missions/documentation/templates/divio/reference-template.md +179 -0
- specify_cli/missions/documentation/templates/divio/tutorial-template.md +146 -0
- specify_cli/missions/documentation/templates/generators/jsdoc.json.template +18 -0
- specify_cli/missions/documentation/templates/generators/sphinx-conf.py.template +36 -0
- specify_cli/missions/documentation/templates/plan-template.md +269 -0
- specify_cli/missions/documentation/templates/release-template.md +222 -0
- specify_cli/missions/documentation/templates/spec-template.md +172 -0
- specify_cli/missions/documentation/templates/task-prompt-template.md +140 -0
- specify_cli/missions/documentation/templates/tasks-template.md +159 -0
- specify_cli/missions/research/command-templates/merge.md +388 -0
- specify_cli/missions/research/command-templates/plan.md +125 -0
- specify_cli/missions/research/command-templates/review.md +144 -0
- specify_cli/missions/research/command-templates/tasks.md +225 -0
- specify_cli/missions/research/mission.yaml +115 -0
- specify_cli/missions/research/templates/data-model-template.md +33 -0
- specify_cli/missions/research/templates/plan-template.md +161 -0
- specify_cli/missions/research/templates/research/evidence-log.csv +18 -0
- specify_cli/missions/research/templates/research/source-register.csv +18 -0
- specify_cli/missions/research/templates/research-template.md +35 -0
- specify_cli/missions/research/templates/spec-template.md +64 -0
- specify_cli/missions/research/templates/task-prompt-template.md +148 -0
- specify_cli/missions/research/templates/tasks-template.md +114 -0
- specify_cli/missions/software-dev/command-templates/accept.md +75 -0
- specify_cli/missions/software-dev/command-templates/analyze.md +183 -0
- specify_cli/missions/software-dev/command-templates/checklist.md +286 -0
- specify_cli/missions/software-dev/command-templates/clarify.md +157 -0
- specify_cli/missions/software-dev/command-templates/constitution.md +432 -0
- specify_cli/missions/software-dev/command-templates/dashboard.md +101 -0
- specify_cli/missions/software-dev/command-templates/implement.md +41 -0
- specify_cli/missions/software-dev/command-templates/merge.md +383 -0
- specify_cli/missions/software-dev/command-templates/plan.md +171 -0
- specify_cli/missions/software-dev/command-templates/review.md +32 -0
- specify_cli/missions/software-dev/command-templates/specify.md +321 -0
- specify_cli/missions/software-dev/command-templates/tasks.md +566 -0
- specify_cli/missions/software-dev/mission.yaml +100 -0
- specify_cli/missions/software-dev/templates/plan-template.md +132 -0
- specify_cli/missions/software-dev/templates/spec-template.md +116 -0
- specify_cli/missions/software-dev/templates/task-prompt-template.md +140 -0
- specify_cli/missions/software-dev/templates/tasks-template.md +159 -0
- specify_cli/orchestrator/__init__.py +75 -0
- specify_cli/orchestrator/agent_config.py +224 -0
- specify_cli/orchestrator/agents/__init__.py +170 -0
- specify_cli/orchestrator/agents/augment.py +112 -0
- specify_cli/orchestrator/agents/base.py +243 -0
- specify_cli/orchestrator/agents/claude.py +112 -0
- specify_cli/orchestrator/agents/codex.py +106 -0
- specify_cli/orchestrator/agents/copilot.py +137 -0
- specify_cli/orchestrator/agents/cursor.py +139 -0
- specify_cli/orchestrator/agents/gemini.py +115 -0
- specify_cli/orchestrator/agents/kilocode.py +94 -0
- specify_cli/orchestrator/agents/opencode.py +132 -0
- specify_cli/orchestrator/agents/qwen.py +96 -0
- specify_cli/orchestrator/config.py +455 -0
- specify_cli/orchestrator/executor.py +642 -0
- specify_cli/orchestrator/integration.py +1230 -0
- specify_cli/orchestrator/monitor.py +898 -0
- specify_cli/orchestrator/scheduler.py +832 -0
- specify_cli/orchestrator/state.py +508 -0
- specify_cli/orchestrator/testing/__init__.py +122 -0
- specify_cli/orchestrator/testing/availability.py +346 -0
- specify_cli/orchestrator/testing/fixtures.py +684 -0
- specify_cli/orchestrator/testing/paths.py +218 -0
- specify_cli/plan_validation.py +107 -0
- specify_cli/scripts/debug-dashboard-scan.py +61 -0
- specify_cli/scripts/tasks/acceptance_support.py +695 -0
- specify_cli/scripts/tasks/task_helpers.py +506 -0
- specify_cli/scripts/tasks/tasks_cli.py +848 -0
- specify_cli/scripts/validate_encoding.py +180 -0
- specify_cli/task_metadata_validation.py +274 -0
- specify_cli/tasks_support.py +447 -0
- specify_cli/template/__init__.py +47 -0
- specify_cli/template/asset_generator.py +206 -0
- specify_cli/template/github_client.py +334 -0
- specify_cli/template/manager.py +193 -0
- specify_cli/template/renderer.py +99 -0
- specify_cli/templates/AGENTS.md +190 -0
- specify_cli/templates/POWERSHELL_SYNTAX.md +229 -0
- specify_cli/templates/agent-file-template.md +35 -0
- specify_cli/templates/checklist-template.md +42 -0
- specify_cli/templates/claudeignore-template +58 -0
- specify_cli/templates/command-templates/accept.md +141 -0
- specify_cli/templates/command-templates/analyze.md +253 -0
- specify_cli/templates/command-templates/checklist.md +352 -0
- specify_cli/templates/command-templates/clarify.md +224 -0
- specify_cli/templates/command-templates/constitution.md +432 -0
- specify_cli/templates/command-templates/dashboard.md +175 -0
- specify_cli/templates/command-templates/implement.md +190 -0
- specify_cli/templates/command-templates/merge.md +374 -0
- specify_cli/templates/command-templates/plan.md +171 -0
- specify_cli/templates/command-templates/research.md +88 -0
- specify_cli/templates/command-templates/review.md +510 -0
- specify_cli/templates/command-templates/specify.md +321 -0
- specify_cli/templates/command-templates/status.md +92 -0
- specify_cli/templates/command-templates/tasks.md +199 -0
- specify_cli/templates/git-hooks/pre-commit +22 -0
- specify_cli/templates/git-hooks/pre-commit-agent-check +37 -0
- specify_cli/templates/git-hooks/pre-commit-encoding-check +142 -0
- specify_cli/templates/plan-template.md +108 -0
- specify_cli/templates/spec-template.md +118 -0
- specify_cli/templates/task-prompt-template.md +165 -0
- specify_cli/templates/tasks-template.md +161 -0
- specify_cli/templates/vscode-settings.json +13 -0
- specify_cli/text_sanitization.py +225 -0
- specify_cli/upgrade/__init__.py +18 -0
- specify_cli/upgrade/detector.py +239 -0
- specify_cli/upgrade/metadata.py +182 -0
- specify_cli/upgrade/migrations/__init__.py +65 -0
- specify_cli/upgrade/migrations/base.py +80 -0
- specify_cli/upgrade/migrations/m_0_10_0_python_only.py +359 -0
- specify_cli/upgrade/migrations/m_0_10_12_constitution_cleanup.py +99 -0
- specify_cli/upgrade/migrations/m_0_10_14_update_implement_slash_command.py +176 -0
- specify_cli/upgrade/migrations/m_0_10_1_populate_slash_commands.py +174 -0
- specify_cli/upgrade/migrations/m_0_10_2_update_slash_commands.py +172 -0
- specify_cli/upgrade/migrations/m_0_10_6_workflow_simplification.py +174 -0
- specify_cli/upgrade/migrations/m_0_10_8_fix_memory_structure.py +252 -0
- specify_cli/upgrade/migrations/m_0_10_9_repair_templates.py +168 -0
- specify_cli/upgrade/migrations/m_0_11_0_workspace_per_wp.py +182 -0
- specify_cli/upgrade/migrations/m_0_11_1_improved_workflow_templates.py +173 -0
- specify_cli/upgrade/migrations/m_0_11_1_update_implement_slash_command.py +160 -0
- specify_cli/upgrade/migrations/m_0_11_2_improved_workflow_templates.py +173 -0
- specify_cli/upgrade/migrations/m_0_11_3_workflow_agent_flag.py +114 -0
- specify_cli/upgrade/migrations/m_0_12_0_documentation_mission.py +155 -0
- specify_cli/upgrade/migrations/m_0_12_1_remove_kitty_specs_from_gitignore.py +183 -0
- specify_cli/upgrade/migrations/m_0_2_0_specify_to_kittify.py +80 -0
- specify_cli/upgrade/migrations/m_0_4_8_gitignore_agents.py +118 -0
- specify_cli/upgrade/migrations/m_0_5_0_encoding_hooks.py +141 -0
- specify_cli/upgrade/migrations/m_0_6_5_commands_rename.py +169 -0
- specify_cli/upgrade/migrations/m_0_6_7_ensure_missions.py +228 -0
- specify_cli/upgrade/migrations/m_0_7_2_worktree_commands_dedup.py +89 -0
- specify_cli/upgrade/migrations/m_0_7_3_update_scripts.py +114 -0
- specify_cli/upgrade/migrations/m_0_8_0_remove_active_mission.py +82 -0
- specify_cli/upgrade/migrations/m_0_8_0_worktree_agents_symlink.py +148 -0
- specify_cli/upgrade/migrations/m_0_9_0_frontmatter_only_lanes.py +346 -0
- specify_cli/upgrade/migrations/m_0_9_1_complete_lane_migration.py +656 -0
- specify_cli/upgrade/migrations/m_0_9_2_research_mission_templates.py +221 -0
- specify_cli/upgrade/registry.py +121 -0
- specify_cli/upgrade/runner.py +284 -0
- specify_cli/validators/__init__.py +14 -0
- specify_cli/validators/paths.py +154 -0
- specify_cli/validators/research.py +428 -0
- specify_cli/verify_enhanced.py +270 -0
- specify_cli/workspace_context.py +224 -0
|
@@ -0,0 +1,653 @@
|
|
|
1
|
+
"""Core merge execution logic.
|
|
2
|
+
|
|
3
|
+
Provides the main entry point for merge operations, orchestrating
|
|
4
|
+
pre-flight validation, conflict forecasting, ordering, and execution.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import subprocess
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Callable
|
|
14
|
+
|
|
15
|
+
from specify_cli.cli import StepTracker
|
|
16
|
+
from specify_cli.cli.helpers import console
|
|
17
|
+
from specify_cli.core.git_ops import run_command
|
|
18
|
+
from specify_cli.merge.ordering import (
|
|
19
|
+
MergeOrderError,
|
|
20
|
+
display_merge_order,
|
|
21
|
+
get_merge_order,
|
|
22
|
+
)
|
|
23
|
+
from specify_cli.merge.preflight import (
|
|
24
|
+
PreflightResult,
|
|
25
|
+
display_preflight_result,
|
|
26
|
+
run_preflight,
|
|
27
|
+
)
|
|
28
|
+
from specify_cli.merge.status_resolver import get_conflicted_files, resolve_status_conflicts
|
|
29
|
+
from specify_cli.merge.forecast import (
|
|
30
|
+
display_conflict_forecast,
|
|
31
|
+
predict_conflicts,
|
|
32
|
+
)
|
|
33
|
+
from specify_cli.merge.state import (
|
|
34
|
+
MergeState,
|
|
35
|
+
clear_state,
|
|
36
|
+
save_state,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
"execute_merge",
|
|
41
|
+
"execute_legacy_merge",
|
|
42
|
+
"MergeResult",
|
|
43
|
+
"MergeExecutionError",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class MergeExecutionError(Exception):
|
|
48
|
+
"""Error during merge execution."""
|
|
49
|
+
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class MergeResult:
|
|
55
|
+
"""Result of merge execution."""
|
|
56
|
+
|
|
57
|
+
success: bool
|
|
58
|
+
merged_wps: list[str] = field(default_factory=list)
|
|
59
|
+
failed_wp: str | None = None
|
|
60
|
+
error: str | None = None
|
|
61
|
+
preflight_result: PreflightResult | None = None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def execute_merge(
|
|
65
|
+
wp_workspaces: list[tuple[Path, str, str]],
|
|
66
|
+
feature_slug: str,
|
|
67
|
+
feature_dir: Path | None,
|
|
68
|
+
target_branch: str,
|
|
69
|
+
strategy: str,
|
|
70
|
+
repo_root: Path,
|
|
71
|
+
merge_root: Path,
|
|
72
|
+
tracker: StepTracker,
|
|
73
|
+
delete_branch: bool = True,
|
|
74
|
+
remove_worktree: bool = True,
|
|
75
|
+
push: bool = False,
|
|
76
|
+
dry_run: bool = False,
|
|
77
|
+
on_wp_merged: Callable[[str], None] | None = None,
|
|
78
|
+
resume_state: MergeState | None = None,
|
|
79
|
+
) -> MergeResult:
|
|
80
|
+
"""Execute merge for all WPs with preflight and ordering.
|
|
81
|
+
|
|
82
|
+
This is the main entry point for workspace-per-WP merges, coordinating:
|
|
83
|
+
1. Pre-flight validation (all worktrees clean, target not diverged)
|
|
84
|
+
2. Dependency-based ordering (topological sort)
|
|
85
|
+
3. Sequential merge execution with state persistence
|
|
86
|
+
4. Cleanup (worktree removal, branch deletion)
|
|
87
|
+
5. State cleared on success
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
wp_workspaces: List of (worktree_path, wp_id, branch_name) tuples
|
|
91
|
+
feature_slug: Feature identifier (e.g., "010-feature-name")
|
|
92
|
+
feature_dir: Path to feature directory (for dependency info), or None
|
|
93
|
+
target_branch: Branch to merge into (e.g., "main")
|
|
94
|
+
strategy: "merge", "squash", or "rebase"
|
|
95
|
+
repo_root: Repository root path
|
|
96
|
+
merge_root: Directory to execute merge from (main repo)
|
|
97
|
+
tracker: StepTracker for progress display
|
|
98
|
+
delete_branch: Whether to delete branches after merge
|
|
99
|
+
remove_worktree: Whether to remove worktrees after merge
|
|
100
|
+
push: Whether to push to remote after merge
|
|
101
|
+
dry_run: If True, show what would be done without executing
|
|
102
|
+
on_wp_merged: Callback after each WP merges (for state updates)
|
|
103
|
+
resume_state: Existing MergeState to resume from (if --resume)
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
MergeResult with success status and details
|
|
107
|
+
"""
|
|
108
|
+
result = MergeResult(success=False)
|
|
109
|
+
|
|
110
|
+
if not wp_workspaces:
|
|
111
|
+
result.error = "No WP workspaces provided"
|
|
112
|
+
return result
|
|
113
|
+
|
|
114
|
+
# Step 1: Run preflight checks
|
|
115
|
+
tracker.start("preflight")
|
|
116
|
+
preflight_result = run_preflight(
|
|
117
|
+
feature_slug=feature_slug,
|
|
118
|
+
target_branch=target_branch,
|
|
119
|
+
repo_root=repo_root,
|
|
120
|
+
wp_workspaces=wp_workspaces,
|
|
121
|
+
)
|
|
122
|
+
result.preflight_result = preflight_result
|
|
123
|
+
display_preflight_result(preflight_result, console)
|
|
124
|
+
|
|
125
|
+
if not preflight_result.passed:
|
|
126
|
+
tracker.error("preflight", "validation failed")
|
|
127
|
+
result.error = "Pre-flight validation failed"
|
|
128
|
+
return result
|
|
129
|
+
tracker.complete("preflight", "all checks passed")
|
|
130
|
+
|
|
131
|
+
# Step 2: Determine merge order based on dependencies
|
|
132
|
+
if feature_dir and feature_dir.exists():
|
|
133
|
+
try:
|
|
134
|
+
ordered_workspaces = get_merge_order(wp_workspaces, feature_dir)
|
|
135
|
+
display_merge_order(ordered_workspaces, console)
|
|
136
|
+
except MergeOrderError as e:
|
|
137
|
+
tracker.error("preflight", f"ordering failed: {e}")
|
|
138
|
+
result.error = str(e)
|
|
139
|
+
return result
|
|
140
|
+
else:
|
|
141
|
+
# No feature dir - use as-is (already sorted by WP ID)
|
|
142
|
+
ordered_workspaces = sorted(wp_workspaces, key=lambda x: x[1])
|
|
143
|
+
console.print("\n[dim]Merge order: numerical (no dependency info)[/dim]")
|
|
144
|
+
|
|
145
|
+
# Step 3: Validate all WP workspaces are ready
|
|
146
|
+
tracker.start("verify")
|
|
147
|
+
errors = []
|
|
148
|
+
for wt_path, wp_id, branch in ordered_workspaces:
|
|
149
|
+
is_valid, error_msg = _validate_wp_ready(repo_root, wt_path, branch)
|
|
150
|
+
if not is_valid:
|
|
151
|
+
errors.append(f" - {wp_id}: {error_msg}")
|
|
152
|
+
|
|
153
|
+
if errors:
|
|
154
|
+
tracker.error("verify", "WP workspaces not ready")
|
|
155
|
+
result.error = "WP workspaces not ready:\n" + "\n".join(errors)
|
|
156
|
+
return result
|
|
157
|
+
|
|
158
|
+
tracker.complete("verify", f"validated {len(ordered_workspaces)} workspaces")
|
|
159
|
+
|
|
160
|
+
# Step 4: Dry run - show what would be done
|
|
161
|
+
if dry_run:
|
|
162
|
+
# Predict conflicts before showing dry-run steps
|
|
163
|
+
predictions = predict_conflicts(ordered_workspaces, target_branch, repo_root)
|
|
164
|
+
display_conflict_forecast(predictions, console)
|
|
165
|
+
|
|
166
|
+
_show_dry_run(
|
|
167
|
+
ordered_workspaces,
|
|
168
|
+
target_branch,
|
|
169
|
+
strategy,
|
|
170
|
+
feature_slug,
|
|
171
|
+
push,
|
|
172
|
+
remove_worktree,
|
|
173
|
+
delete_branch,
|
|
174
|
+
)
|
|
175
|
+
result.success = True
|
|
176
|
+
result.merged_wps = [wp_id for _, wp_id, _ in ordered_workspaces]
|
|
177
|
+
return result
|
|
178
|
+
|
|
179
|
+
# Initialize or use resume state
|
|
180
|
+
if resume_state:
|
|
181
|
+
state = resume_state
|
|
182
|
+
# Filter ordered_workspaces to only remaining WPs
|
|
183
|
+
remaining_set = set(state.remaining_wps)
|
|
184
|
+
ordered_workspaces = [
|
|
185
|
+
(wt_path, wp_id, branch)
|
|
186
|
+
for wt_path, wp_id, branch in ordered_workspaces
|
|
187
|
+
if wp_id in remaining_set
|
|
188
|
+
]
|
|
189
|
+
console.print(f"[cyan]Resuming from {state.completed_wps[-1] if state.completed_wps else 'start'}[/cyan]")
|
|
190
|
+
else:
|
|
191
|
+
state = MergeState(
|
|
192
|
+
feature_slug=feature_slug,
|
|
193
|
+
target_branch=target_branch,
|
|
194
|
+
wp_order=[wp_id for _, wp_id, _ in ordered_workspaces],
|
|
195
|
+
strategy=strategy,
|
|
196
|
+
)
|
|
197
|
+
save_state(state, repo_root)
|
|
198
|
+
|
|
199
|
+
# Step 5: Checkout and update target branch
|
|
200
|
+
tracker.start("checkout")
|
|
201
|
+
try:
|
|
202
|
+
os.chdir(merge_root)
|
|
203
|
+
_, target_status, _ = run_command(["git", "status", "--porcelain"], capture=True)
|
|
204
|
+
if target_status.strip():
|
|
205
|
+
raise MergeExecutionError(
|
|
206
|
+
f"Target repository at {merge_root} has uncommitted changes."
|
|
207
|
+
)
|
|
208
|
+
run_command(["git", "checkout", target_branch])
|
|
209
|
+
tracker.complete("checkout", f"using {merge_root}")
|
|
210
|
+
except Exception as exc:
|
|
211
|
+
tracker.error("checkout", str(exc))
|
|
212
|
+
result.error = f"Checkout failed: {exc}"
|
|
213
|
+
return result
|
|
214
|
+
|
|
215
|
+
tracker.start("pull")
|
|
216
|
+
try:
|
|
217
|
+
run_command(["git", "pull", "--ff-only"])
|
|
218
|
+
tracker.complete("pull")
|
|
219
|
+
except Exception as exc:
|
|
220
|
+
tracker.error("pull", str(exc))
|
|
221
|
+
result.error = f"Pull failed: {exc}. You may need to resolve conflicts manually."
|
|
222
|
+
return result
|
|
223
|
+
|
|
224
|
+
# Step 6: Merge all WP branches in dependency order
|
|
225
|
+
tracker.start("merge")
|
|
226
|
+
try:
|
|
227
|
+
for wt_path, wp_id, branch in ordered_workspaces:
|
|
228
|
+
# Set current WP and save state before merge
|
|
229
|
+
state.set_current_wp(wp_id)
|
|
230
|
+
save_state(state, repo_root)
|
|
231
|
+
|
|
232
|
+
console.print(f"[cyan]Merging {wp_id} ({branch})...[/cyan]")
|
|
233
|
+
|
|
234
|
+
if strategy == "squash":
|
|
235
|
+
merge_code, _, _ = run_command(
|
|
236
|
+
["git", "merge", "--squash", branch],
|
|
237
|
+
check_return=False,
|
|
238
|
+
capture=True,
|
|
239
|
+
)
|
|
240
|
+
conflict_error = _resolve_merge_conflicts(repo_root, wp_id)
|
|
241
|
+
if conflict_error:
|
|
242
|
+
state.set_pending_conflicts(True)
|
|
243
|
+
save_state(state, repo_root)
|
|
244
|
+
result.error = conflict_error
|
|
245
|
+
return result
|
|
246
|
+
run_command(
|
|
247
|
+
["git", "commit", "-m", f"Merge {wp_id} from {feature_slug}"]
|
|
248
|
+
)
|
|
249
|
+
elif strategy == "rebase":
|
|
250
|
+
result.error = "Rebase strategy not supported for workspace-per-WP."
|
|
251
|
+
tracker.skip("merge", "rebase not supported")
|
|
252
|
+
return result
|
|
253
|
+
else: # merge (default)
|
|
254
|
+
merge_code, _, _ = run_command(
|
|
255
|
+
[
|
|
256
|
+
"git",
|
|
257
|
+
"merge",
|
|
258
|
+
"--no-ff",
|
|
259
|
+
branch,
|
|
260
|
+
"-m",
|
|
261
|
+
f"Merge {wp_id} from {feature_slug}",
|
|
262
|
+
],
|
|
263
|
+
check_return=False,
|
|
264
|
+
capture=True,
|
|
265
|
+
)
|
|
266
|
+
conflict_error = _resolve_merge_conflicts(repo_root, wp_id)
|
|
267
|
+
if conflict_error:
|
|
268
|
+
state.set_pending_conflicts(True)
|
|
269
|
+
save_state(state, repo_root)
|
|
270
|
+
result.error = conflict_error
|
|
271
|
+
return result
|
|
272
|
+
if merge_code != 0:
|
|
273
|
+
run_command(
|
|
274
|
+
["git", "commit", "-m", f"Merge {wp_id} from {feature_slug}"]
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
# Mark WP complete and save state
|
|
278
|
+
state.mark_wp_complete(wp_id)
|
|
279
|
+
save_state(state, repo_root)
|
|
280
|
+
|
|
281
|
+
result.merged_wps.append(wp_id)
|
|
282
|
+
console.print(f"[green]\u2713[/green] {wp_id} merged")
|
|
283
|
+
|
|
284
|
+
if on_wp_merged:
|
|
285
|
+
on_wp_merged(wp_id)
|
|
286
|
+
|
|
287
|
+
tracker.complete("merge", f"merged {len(ordered_workspaces)} work packages")
|
|
288
|
+
except Exception as exc:
|
|
289
|
+
tracker.error("merge", str(exc))
|
|
290
|
+
result.failed_wp = wp_id if "wp_id" in dir() else None
|
|
291
|
+
result.error = f"Merge failed: {exc}"
|
|
292
|
+
# Save state on error for resume
|
|
293
|
+
state.set_pending_conflicts(True)
|
|
294
|
+
save_state(state, repo_root)
|
|
295
|
+
return result
|
|
296
|
+
|
|
297
|
+
# Step 7: Push if requested
|
|
298
|
+
if push:
|
|
299
|
+
tracker.start("push")
|
|
300
|
+
try:
|
|
301
|
+
run_command(["git", "push", "origin", target_branch])
|
|
302
|
+
tracker.complete("push")
|
|
303
|
+
except Exception as exc:
|
|
304
|
+
tracker.error("push", str(exc))
|
|
305
|
+
console.print(
|
|
306
|
+
f"\n[yellow]Warning:[/yellow] Merge succeeded but push failed."
|
|
307
|
+
)
|
|
308
|
+
console.print(f"Run manually: git push origin {target_branch}")
|
|
309
|
+
|
|
310
|
+
# Step 8: Remove worktrees
|
|
311
|
+
if remove_worktree:
|
|
312
|
+
tracker.start("worktree")
|
|
313
|
+
failed_removals = []
|
|
314
|
+
for wt_path, wp_id, branch in ordered_workspaces:
|
|
315
|
+
try:
|
|
316
|
+
run_command(["git", "worktree", "remove", str(wt_path), "--force"])
|
|
317
|
+
console.print(f"[green]\u2713[/green] Removed worktree: {wp_id}")
|
|
318
|
+
except Exception:
|
|
319
|
+
failed_removals.append((wp_id, wt_path))
|
|
320
|
+
|
|
321
|
+
if failed_removals:
|
|
322
|
+
tracker.error(
|
|
323
|
+
"worktree", f"could not remove {len(failed_removals)} worktrees"
|
|
324
|
+
)
|
|
325
|
+
console.print(
|
|
326
|
+
f"\n[yellow]Warning:[/yellow] Could not remove some worktrees:"
|
|
327
|
+
)
|
|
328
|
+
for wp_id, wt_path in failed_removals:
|
|
329
|
+
console.print(f" {wp_id}: git worktree remove {wt_path}")
|
|
330
|
+
else:
|
|
331
|
+
tracker.complete("worktree", f"removed {len(ordered_workspaces)} worktrees")
|
|
332
|
+
|
|
333
|
+
# Step 9: Delete branches
|
|
334
|
+
if delete_branch:
|
|
335
|
+
tracker.start("branch")
|
|
336
|
+
failed_deletions = []
|
|
337
|
+
for wt_path, wp_id, branch in ordered_workspaces:
|
|
338
|
+
try:
|
|
339
|
+
run_command(["git", "branch", "-d", branch])
|
|
340
|
+
console.print(f"[green]\u2713[/green] Deleted branch: {branch}")
|
|
341
|
+
except Exception:
|
|
342
|
+
# Try force delete
|
|
343
|
+
try:
|
|
344
|
+
run_command(["git", "branch", "-D", branch])
|
|
345
|
+
console.print(f"[green]\u2713[/green] Force deleted branch: {branch}")
|
|
346
|
+
except Exception:
|
|
347
|
+
failed_deletions.append((wp_id, branch))
|
|
348
|
+
|
|
349
|
+
if failed_deletions:
|
|
350
|
+
tracker.error(
|
|
351
|
+
"branch", f"could not delete {len(failed_deletions)} branches"
|
|
352
|
+
)
|
|
353
|
+
console.print(
|
|
354
|
+
f"\n[yellow]Warning:[/yellow] Could not delete some branches:"
|
|
355
|
+
)
|
|
356
|
+
for wp_id, branch in failed_deletions:
|
|
357
|
+
console.print(f" {wp_id}: git branch -D {branch}")
|
|
358
|
+
else:
|
|
359
|
+
tracker.complete("branch", f"deleted {len(ordered_workspaces)} branches")
|
|
360
|
+
|
|
361
|
+
# Clear state on successful completion
|
|
362
|
+
clear_state(repo_root)
|
|
363
|
+
|
|
364
|
+
result.success = True
|
|
365
|
+
return result
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def execute_legacy_merge(
|
|
369
|
+
current_branch: str,
|
|
370
|
+
target_branch: str,
|
|
371
|
+
strategy: str,
|
|
372
|
+
merge_root: Path,
|
|
373
|
+
feature_worktree_path: Path,
|
|
374
|
+
tracker: StepTracker,
|
|
375
|
+
push: bool = False,
|
|
376
|
+
remove_worktree: bool = True,
|
|
377
|
+
delete_branch: bool = True,
|
|
378
|
+
dry_run: bool = False,
|
|
379
|
+
in_worktree: bool = False,
|
|
380
|
+
) -> MergeResult:
|
|
381
|
+
"""Execute legacy single-worktree merge flow.
|
|
382
|
+
|
|
383
|
+
Args:
|
|
384
|
+
current_branch: Current feature branch name
|
|
385
|
+
target_branch: Branch to merge into
|
|
386
|
+
strategy: "merge", "squash", or "rebase"
|
|
387
|
+
merge_root: Repository root to run merge commands from
|
|
388
|
+
feature_worktree_path: Worktree path to remove (if applicable)
|
|
389
|
+
tracker: StepTracker for progress display
|
|
390
|
+
push: Whether to push to remote after merge
|
|
391
|
+
remove_worktree: Whether to remove worktree after merge
|
|
392
|
+
delete_branch: Whether to delete branch after merge
|
|
393
|
+
dry_run: If True, show what would be done without executing
|
|
394
|
+
in_worktree: Whether caller is in a worktree context
|
|
395
|
+
|
|
396
|
+
Returns:
|
|
397
|
+
MergeResult with success status and details
|
|
398
|
+
"""
|
|
399
|
+
result = MergeResult(success=False)
|
|
400
|
+
|
|
401
|
+
tracker.start("verify")
|
|
402
|
+
try:
|
|
403
|
+
_, status_output, _ = run_command(["git", "status", "--porcelain"], capture=True)
|
|
404
|
+
if status_output.strip():
|
|
405
|
+
tracker.error("verify", "uncommitted changes")
|
|
406
|
+
result.error = "Working directory has uncommitted changes."
|
|
407
|
+
return result
|
|
408
|
+
tracker.complete("verify", "clean working directory")
|
|
409
|
+
except Exception as exc:
|
|
410
|
+
tracker.error("verify", str(exc))
|
|
411
|
+
result.error = str(exc)
|
|
412
|
+
return result
|
|
413
|
+
|
|
414
|
+
if dry_run:
|
|
415
|
+
console.print(tracker.render())
|
|
416
|
+
console.print("\n[cyan]Dry run - would execute:[/cyan]")
|
|
417
|
+
checkout_prefix = f"(from {merge_root}) " if in_worktree else ""
|
|
418
|
+
steps = [
|
|
419
|
+
f"{checkout_prefix}git checkout {target_branch}",
|
|
420
|
+
"git pull --ff-only",
|
|
421
|
+
]
|
|
422
|
+
if strategy == "squash":
|
|
423
|
+
steps.extend(
|
|
424
|
+
[
|
|
425
|
+
f"git merge --squash {current_branch}",
|
|
426
|
+
f"git commit -m 'Merge feature {current_branch}'",
|
|
427
|
+
]
|
|
428
|
+
)
|
|
429
|
+
elif strategy == "rebase":
|
|
430
|
+
steps.append(f"git merge --ff-only {current_branch} (after rebase)")
|
|
431
|
+
else:
|
|
432
|
+
steps.append(f"git merge --no-ff {current_branch}")
|
|
433
|
+
if push:
|
|
434
|
+
steps.append(f"git push origin {target_branch}")
|
|
435
|
+
if in_worktree and remove_worktree:
|
|
436
|
+
steps.append(f"git worktree remove {feature_worktree_path}")
|
|
437
|
+
if delete_branch:
|
|
438
|
+
steps.append(f"git branch -d {current_branch}")
|
|
439
|
+
for idx, step in enumerate(steps, start=1):
|
|
440
|
+
console.print(f" {idx}. {step}")
|
|
441
|
+
result.success = True
|
|
442
|
+
return result
|
|
443
|
+
|
|
444
|
+
tracker.start("checkout")
|
|
445
|
+
try:
|
|
446
|
+
if in_worktree:
|
|
447
|
+
console.print(
|
|
448
|
+
f"[cyan]Detected worktree. Merge operations will run from {merge_root}[/cyan]"
|
|
449
|
+
)
|
|
450
|
+
os.chdir(merge_root)
|
|
451
|
+
_, target_status, _ = run_command(["git", "status", "--porcelain"], capture=True)
|
|
452
|
+
if target_status.strip():
|
|
453
|
+
raise MergeExecutionError(
|
|
454
|
+
f"Target repository at {merge_root} has uncommitted changes."
|
|
455
|
+
)
|
|
456
|
+
run_command(["git", "checkout", target_branch])
|
|
457
|
+
tracker.complete("checkout", f"using {merge_root}")
|
|
458
|
+
except Exception as exc:
|
|
459
|
+
tracker.error("checkout", str(exc))
|
|
460
|
+
result.error = f"Checkout failed: {exc}"
|
|
461
|
+
return result
|
|
462
|
+
|
|
463
|
+
tracker.start("pull")
|
|
464
|
+
try:
|
|
465
|
+
run_command(["git", "pull", "--ff-only"])
|
|
466
|
+
tracker.complete("pull")
|
|
467
|
+
except Exception as exc:
|
|
468
|
+
tracker.error("pull", str(exc))
|
|
469
|
+
result.error = (
|
|
470
|
+
f"Pull failed: {exc}. You may need to resolve conflicts manually."
|
|
471
|
+
)
|
|
472
|
+
return result
|
|
473
|
+
|
|
474
|
+
tracker.start("merge")
|
|
475
|
+
try:
|
|
476
|
+
if strategy == "squash":
|
|
477
|
+
run_command(["git", "merge", "--squash", current_branch])
|
|
478
|
+
run_command(["git", "commit", "-m", f"Merge feature {current_branch}"])
|
|
479
|
+
tracker.complete("merge", "squashed")
|
|
480
|
+
elif strategy == "rebase":
|
|
481
|
+
console.print(
|
|
482
|
+
"\n[yellow]Note:[/yellow] Rebase strategy requires manual intervention."
|
|
483
|
+
)
|
|
484
|
+
console.print(
|
|
485
|
+
f"Please run: git checkout {current_branch} && git rebase {target_branch}"
|
|
486
|
+
)
|
|
487
|
+
tracker.skip("merge", "requires manual rebase")
|
|
488
|
+
result.success = True
|
|
489
|
+
return result
|
|
490
|
+
else:
|
|
491
|
+
run_command(
|
|
492
|
+
["git", "merge", "--no-ff", current_branch, "-m", f"Merge feature {current_branch}"]
|
|
493
|
+
)
|
|
494
|
+
tracker.complete("merge", "merged with merge commit")
|
|
495
|
+
except Exception as exc:
|
|
496
|
+
tracker.error("merge", str(exc))
|
|
497
|
+
result.error = f"Merge failed: {exc}"
|
|
498
|
+
return result
|
|
499
|
+
|
|
500
|
+
if push:
|
|
501
|
+
tracker.start("push")
|
|
502
|
+
try:
|
|
503
|
+
run_command(["git", "push", "origin", target_branch])
|
|
504
|
+
tracker.complete("push")
|
|
505
|
+
except Exception as exc:
|
|
506
|
+
tracker.error("push", str(exc))
|
|
507
|
+
console.print(
|
|
508
|
+
f"\n[yellow]Warning:[/yellow] Merge succeeded but push failed."
|
|
509
|
+
)
|
|
510
|
+
console.print(f"Run manually: git push origin {target_branch}")
|
|
511
|
+
|
|
512
|
+
if in_worktree and remove_worktree:
|
|
513
|
+
tracker.start("worktree")
|
|
514
|
+
try:
|
|
515
|
+
run_command(
|
|
516
|
+
["git", "worktree", "remove", str(feature_worktree_path), "--force"]
|
|
517
|
+
)
|
|
518
|
+
tracker.complete("worktree", f"removed {feature_worktree_path}")
|
|
519
|
+
except Exception as exc:
|
|
520
|
+
tracker.error("worktree", str(exc))
|
|
521
|
+
console.print(
|
|
522
|
+
f"\n[yellow]Warning:[/yellow] Could not remove worktree."
|
|
523
|
+
)
|
|
524
|
+
console.print(f"Run manually: git worktree remove {feature_worktree_path}")
|
|
525
|
+
|
|
526
|
+
if delete_branch:
|
|
527
|
+
tracker.start("branch")
|
|
528
|
+
try:
|
|
529
|
+
run_command(["git", "branch", "-d", current_branch])
|
|
530
|
+
tracker.complete("branch", f"deleted {current_branch}")
|
|
531
|
+
except Exception as exc:
|
|
532
|
+
try:
|
|
533
|
+
run_command(["git", "branch", "-D", current_branch])
|
|
534
|
+
tracker.complete("branch", f"force deleted {current_branch}")
|
|
535
|
+
except Exception:
|
|
536
|
+
tracker.error("branch", str(exc))
|
|
537
|
+
console.print(tracker.render())
|
|
538
|
+
console.print(
|
|
539
|
+
f"\n[yellow]Warning:[/yellow] Could not delete branch {current_branch}."
|
|
540
|
+
)
|
|
541
|
+
console.print(f"Run manually: git branch -d {current_branch}")
|
|
542
|
+
|
|
543
|
+
console.print(tracker.render())
|
|
544
|
+
console.print(
|
|
545
|
+
f"\n[bold green]✓ Feature {current_branch} successfully merged into {target_branch}[/bold green]"
|
|
546
|
+
)
|
|
547
|
+
result.success = True
|
|
548
|
+
return result
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def _validate_wp_ready(
|
|
552
|
+
repo_root: Path, worktree_path: Path, branch_name: str
|
|
553
|
+
) -> tuple[bool, str]:
|
|
554
|
+
"""Validate WP workspace is ready to merge.
|
|
555
|
+
|
|
556
|
+
Args:
|
|
557
|
+
repo_root: Repository root
|
|
558
|
+
worktree_path: Path to worktree
|
|
559
|
+
branch_name: Branch name to verify
|
|
560
|
+
|
|
561
|
+
Returns:
|
|
562
|
+
Tuple of (is_valid, error_message)
|
|
563
|
+
"""
|
|
564
|
+
# Check 1: Branch exists in git
|
|
565
|
+
result = subprocess.run(
|
|
566
|
+
["git", "rev-parse", "--verify", branch_name],
|
|
567
|
+
cwd=str(repo_root),
|
|
568
|
+
capture_output=True,
|
|
569
|
+
check=False,
|
|
570
|
+
)
|
|
571
|
+
if result.returncode != 0:
|
|
572
|
+
return False, f"Branch {branch_name} does not exist"
|
|
573
|
+
|
|
574
|
+
# Check 2: No uncommitted changes in worktree
|
|
575
|
+
result = subprocess.run(
|
|
576
|
+
["git", "status", "--porcelain"],
|
|
577
|
+
cwd=str(worktree_path),
|
|
578
|
+
capture_output=True,
|
|
579
|
+
text=True,
|
|
580
|
+
)
|
|
581
|
+
if result.stdout.strip():
|
|
582
|
+
return False, f"Worktree {worktree_path.name} has uncommitted changes"
|
|
583
|
+
|
|
584
|
+
return True, ""
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
def _show_dry_run(
|
|
588
|
+
ordered_workspaces: list[tuple[Path, str, str]],
|
|
589
|
+
target_branch: str,
|
|
590
|
+
strategy: str,
|
|
591
|
+
feature_slug: str,
|
|
592
|
+
push: bool,
|
|
593
|
+
remove_worktree: bool,
|
|
594
|
+
delete_branch: bool,
|
|
595
|
+
) -> None:
|
|
596
|
+
"""Display dry run output showing what would be executed.
|
|
597
|
+
|
|
598
|
+
Args:
|
|
599
|
+
ordered_workspaces: Ordered list of (path, wp_id, branch) tuples
|
|
600
|
+
target_branch: Target branch name
|
|
601
|
+
strategy: Merge strategy
|
|
602
|
+
feature_slug: Feature identifier
|
|
603
|
+
push: Whether push is enabled
|
|
604
|
+
remove_worktree: Whether worktree removal is enabled
|
|
605
|
+
delete_branch: Whether branch deletion is enabled
|
|
606
|
+
"""
|
|
607
|
+
console.print("\n[cyan]Dry run - would execute:[/cyan]")
|
|
608
|
+
steps = [
|
|
609
|
+
f"git checkout {target_branch}",
|
|
610
|
+
"git pull --ff-only",
|
|
611
|
+
]
|
|
612
|
+
|
|
613
|
+
for wt_path, wp_id, branch in ordered_workspaces:
|
|
614
|
+
if strategy == "squash":
|
|
615
|
+
steps.extend(
|
|
616
|
+
[
|
|
617
|
+
f"git merge --squash {branch}",
|
|
618
|
+
f"git commit -m 'Merge {wp_id} from {feature_slug}'",
|
|
619
|
+
]
|
|
620
|
+
)
|
|
621
|
+
else:
|
|
622
|
+
steps.append(
|
|
623
|
+
f"git merge --no-ff {branch} -m 'Merge {wp_id} from {feature_slug}'"
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
if push:
|
|
627
|
+
steps.append(f"git push origin {target_branch}")
|
|
628
|
+
|
|
629
|
+
if remove_worktree:
|
|
630
|
+
for wt_path, wp_id, branch in ordered_workspaces:
|
|
631
|
+
steps.append(f"git worktree remove {wt_path}")
|
|
632
|
+
|
|
633
|
+
if delete_branch:
|
|
634
|
+
for wt_path, wp_id, branch in ordered_workspaces:
|
|
635
|
+
steps.append(f"git branch -d {branch}")
|
|
636
|
+
|
|
637
|
+
for idx, step in enumerate(steps, start=1):
|
|
638
|
+
console.print(f" {idx}. {step}")
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
def _resolve_merge_conflicts(repo_root: Path, wp_id: str) -> str | None:
|
|
642
|
+
"""Resolve status file conflicts and return error if any remain."""
|
|
643
|
+
conflicted = get_conflicted_files(repo_root)
|
|
644
|
+
if not conflicted:
|
|
645
|
+
return None
|
|
646
|
+
|
|
647
|
+
resolve_status_conflicts(repo_root)
|
|
648
|
+
remaining = get_conflicted_files(repo_root)
|
|
649
|
+
if not remaining:
|
|
650
|
+
return None
|
|
651
|
+
|
|
652
|
+
files = "\n".join(f" - {path.relative_to(repo_root)}" for path in remaining)
|
|
653
|
+
return f"Merge for {wp_id} has unresolved conflicts:\n{files}"
|