claude-mpm 0.3.0__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.
Potentially problematic release.
This version of claude-mpm might be problematic. Click here for more details.
- claude_mpm/__init__.py +17 -0
- claude_mpm/__main__.py +14 -0
- claude_mpm/_version.py +32 -0
- claude_mpm/agents/BASE_AGENT_TEMPLATE.md +88 -0
- claude_mpm/agents/INSTRUCTIONS.md +375 -0
- claude_mpm/agents/__init__.py +118 -0
- claude_mpm/agents/agent_loader.py +621 -0
- claude_mpm/agents/agent_loader_integration.py +229 -0
- claude_mpm/agents/agents_metadata.py +204 -0
- claude_mpm/agents/base_agent.json +27 -0
- claude_mpm/agents/base_agent_loader.py +519 -0
- claude_mpm/agents/schema/agent_schema.json +160 -0
- claude_mpm/agents/system_agent_config.py +587 -0
- claude_mpm/agents/templates/__init__.py +101 -0
- claude_mpm/agents/templates/data_engineer_agent.json +46 -0
- claude_mpm/agents/templates/documentation_agent.json +45 -0
- claude_mpm/agents/templates/engineer_agent.json +49 -0
- claude_mpm/agents/templates/ops_agent.json +46 -0
- claude_mpm/agents/templates/qa_agent.json +45 -0
- claude_mpm/agents/templates/research_agent.json +49 -0
- claude_mpm/agents/templates/security_agent.json +46 -0
- claude_mpm/agents/templates/update-optimized-specialized-agents.json +374 -0
- claude_mpm/agents/templates/version_control_agent.json +46 -0
- claude_mpm/agents/test_fix_deployment/.claude-pm/config/project.json +6 -0
- claude_mpm/cli.py +655 -0
- claude_mpm/cli_main.py +13 -0
- claude_mpm/cli_module/__init__.py +15 -0
- claude_mpm/cli_module/args.py +222 -0
- claude_mpm/cli_module/commands.py +203 -0
- claude_mpm/cli_module/migration_example.py +183 -0
- claude_mpm/cli_module/refactoring_guide.md +253 -0
- claude_mpm/cli_old/__init__.py +1 -0
- claude_mpm/cli_old/ticket_cli.py +102 -0
- claude_mpm/config/__init__.py +5 -0
- claude_mpm/config/hook_config.py +42 -0
- claude_mpm/constants.py +150 -0
- claude_mpm/core/__init__.py +45 -0
- claude_mpm/core/agent_name_normalizer.py +248 -0
- claude_mpm/core/agent_registry.py +627 -0
- claude_mpm/core/agent_registry.py.bak +312 -0
- claude_mpm/core/agent_session_manager.py +273 -0
- claude_mpm/core/base_service.py +747 -0
- claude_mpm/core/base_service.py.bak +406 -0
- claude_mpm/core/config.py +334 -0
- claude_mpm/core/config_aliases.py +292 -0
- claude_mpm/core/container.py +347 -0
- claude_mpm/core/factories.py +281 -0
- claude_mpm/core/framework_loader.py +472 -0
- claude_mpm/core/injectable_service.py +206 -0
- claude_mpm/core/interfaces.py +539 -0
- claude_mpm/core/logger.py +468 -0
- claude_mpm/core/minimal_framework_loader.py +107 -0
- claude_mpm/core/mixins.py +150 -0
- claude_mpm/core/service_registry.py +299 -0
- claude_mpm/core/session_manager.py +190 -0
- claude_mpm/core/simple_runner.py +511 -0
- claude_mpm/core/tool_access_control.py +173 -0
- claude_mpm/hooks/README.md +243 -0
- claude_mpm/hooks/__init__.py +5 -0
- claude_mpm/hooks/base_hook.py +154 -0
- claude_mpm/hooks/builtin/__init__.py +1 -0
- claude_mpm/hooks/builtin/logging_hook_example.py +165 -0
- claude_mpm/hooks/builtin/post_delegation_hook_example.py +124 -0
- claude_mpm/hooks/builtin/pre_delegation_hook_example.py +125 -0
- claude_mpm/hooks/builtin/submit_hook_example.py +100 -0
- claude_mpm/hooks/builtin/ticket_extraction_hook_example.py +237 -0
- claude_mpm/hooks/builtin/todo_agent_prefix_hook.py +239 -0
- claude_mpm/hooks/builtin/workflow_start_hook.py +181 -0
- claude_mpm/hooks/hook_client.py +264 -0
- claude_mpm/hooks/hook_runner.py +370 -0
- claude_mpm/hooks/json_rpc_executor.py +259 -0
- claude_mpm/hooks/json_rpc_hook_client.py +319 -0
- claude_mpm/hooks/tool_call_interceptor.py +204 -0
- claude_mpm/init.py +246 -0
- claude_mpm/orchestration/SUBPROCESS_DESIGN.md +66 -0
- claude_mpm/orchestration/__init__.py +6 -0
- claude_mpm/orchestration/archive/direct_orchestrator.py +195 -0
- claude_mpm/orchestration/archive/factory.py +215 -0
- claude_mpm/orchestration/archive/hook_enabled_orchestrator.py +188 -0
- claude_mpm/orchestration/archive/hook_integration_example.py +178 -0
- claude_mpm/orchestration/archive/interactive_subprocess_orchestrator.py +826 -0
- claude_mpm/orchestration/archive/orchestrator.py +501 -0
- claude_mpm/orchestration/archive/pexpect_orchestrator.py +252 -0
- claude_mpm/orchestration/archive/pty_orchestrator.py +270 -0
- claude_mpm/orchestration/archive/simple_orchestrator.py +82 -0
- claude_mpm/orchestration/archive/subprocess_orchestrator.py +801 -0
- claude_mpm/orchestration/archive/system_prompt_orchestrator.py +278 -0
- claude_mpm/orchestration/archive/wrapper_orchestrator.py +187 -0
- claude_mpm/scripts/__init__.py +1 -0
- claude_mpm/scripts/ticket.py +269 -0
- claude_mpm/services/__init__.py +10 -0
- claude_mpm/services/agent_deployment.py +955 -0
- claude_mpm/services/agent_lifecycle_manager.py +948 -0
- claude_mpm/services/agent_management_service.py +596 -0
- claude_mpm/services/agent_modification_tracker.py +841 -0
- claude_mpm/services/agent_profile_loader.py +606 -0
- claude_mpm/services/agent_registry.py +677 -0
- claude_mpm/services/base_agent_manager.py +380 -0
- claude_mpm/services/framework_agent_loader.py +337 -0
- claude_mpm/services/framework_claude_md_generator/README.md +92 -0
- claude_mpm/services/framework_claude_md_generator/__init__.py +206 -0
- claude_mpm/services/framework_claude_md_generator/content_assembler.py +151 -0
- claude_mpm/services/framework_claude_md_generator/content_validator.py +126 -0
- claude_mpm/services/framework_claude_md_generator/deployment_manager.py +137 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/__init__.py +106 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/agents.py +582 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/claude_pm_init.py +97 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/core_responsibilities.py +27 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/delegation_constraints.py +23 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/environment_config.py +23 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/footer.py +20 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/header.py +26 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/orchestration_principles.py +30 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/role_designation.py +37 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/subprocess_validation.py +111 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/todo_task_tools.py +89 -0
- claude_mpm/services/framework_claude_md_generator/section_generators/troubleshooting.py +39 -0
- claude_mpm/services/framework_claude_md_generator/section_manager.py +106 -0
- claude_mpm/services/framework_claude_md_generator/version_manager.py +121 -0
- claude_mpm/services/framework_claude_md_generator.py +621 -0
- claude_mpm/services/hook_service.py +388 -0
- claude_mpm/services/hook_service_manager.py +223 -0
- claude_mpm/services/json_rpc_hook_manager.py +92 -0
- claude_mpm/services/parent_directory_manager/README.md +83 -0
- claude_mpm/services/parent_directory_manager/__init__.py +577 -0
- claude_mpm/services/parent_directory_manager/backup_manager.py +258 -0
- claude_mpm/services/parent_directory_manager/config_manager.py +210 -0
- claude_mpm/services/parent_directory_manager/deduplication_manager.py +279 -0
- claude_mpm/services/parent_directory_manager/framework_protector.py +143 -0
- claude_mpm/services/parent_directory_manager/operations.py +186 -0
- claude_mpm/services/parent_directory_manager/state_manager.py +624 -0
- claude_mpm/services/parent_directory_manager/template_deployer.py +579 -0
- claude_mpm/services/parent_directory_manager/validation_manager.py +378 -0
- claude_mpm/services/parent_directory_manager/version_control_helper.py +339 -0
- claude_mpm/services/parent_directory_manager/version_manager.py +222 -0
- claude_mpm/services/shared_prompt_cache.py +819 -0
- claude_mpm/services/ticket_manager.py +213 -0
- claude_mpm/services/ticket_manager_di.py +318 -0
- claude_mpm/services/ticketing_service_original.py +508 -0
- claude_mpm/services/version_control/VERSION +1 -0
- claude_mpm/services/version_control/__init__.py +70 -0
- claude_mpm/services/version_control/branch_strategy.py +670 -0
- claude_mpm/services/version_control/conflict_resolution.py +744 -0
- claude_mpm/services/version_control/git_operations.py +784 -0
- claude_mpm/services/version_control/semantic_versioning.py +703 -0
- claude_mpm/ui/__init__.py +1 -0
- claude_mpm/ui/rich_terminal_ui.py +295 -0
- claude_mpm/ui/terminal_ui.py +328 -0
- claude_mpm/utils/__init__.py +16 -0
- claude_mpm/utils/config_manager.py +468 -0
- claude_mpm/utils/import_migration_example.py +80 -0
- claude_mpm/utils/imports.py +182 -0
- claude_mpm/utils/path_operations.py +357 -0
- claude_mpm/utils/paths.py +289 -0
- claude_mpm-0.3.0.dist-info/METADATA +290 -0
- claude_mpm-0.3.0.dist-info/RECORD +159 -0
- claude_mpm-0.3.0.dist-info/WHEEL +5 -0
- claude_mpm-0.3.0.dist-info/entry_points.txt +4 -0
- claude_mpm-0.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,744 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Conflict Resolution Manager - Basic conflict resolution for Version Control Agent.
|
|
3
|
+
|
|
4
|
+
This module provides conflict resolution management including:
|
|
5
|
+
1. Conflict detection and analysis
|
|
6
|
+
2. Automatic resolution strategies
|
|
7
|
+
3. Manual resolution guidance
|
|
8
|
+
4. Conflict prevention
|
|
9
|
+
5. Resolution validation
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
import difflib
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Dict, List, Optional, Any, Tuple, Union
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from enum import Enum
|
|
19
|
+
import logging
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ConflictType(Enum):
|
|
23
|
+
"""Types of merge conflicts."""
|
|
24
|
+
|
|
25
|
+
CONTENT = "content"
|
|
26
|
+
BINARY = "binary"
|
|
27
|
+
DELETE_MODIFY = "delete_modify"
|
|
28
|
+
ADD_ADD = "add_add"
|
|
29
|
+
RENAME_RENAME = "rename_rename"
|
|
30
|
+
SUBMODULE = "submodule"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ResolutionStrategy(Enum):
|
|
34
|
+
"""Strategies for conflict resolution."""
|
|
35
|
+
|
|
36
|
+
MANUAL = "manual"
|
|
37
|
+
OURS = "ours"
|
|
38
|
+
THEIRS = "theirs"
|
|
39
|
+
AUTO_MERGE = "auto_merge"
|
|
40
|
+
SMART_MERGE = "smart_merge"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class ConflictMarker:
|
|
45
|
+
"""Represents a conflict marker in a file."""
|
|
46
|
+
|
|
47
|
+
start_line: int
|
|
48
|
+
conflict_marker: int
|
|
49
|
+
end_line: int
|
|
50
|
+
ours_content: List[str]
|
|
51
|
+
theirs_content: List[str]
|
|
52
|
+
base_content: Optional[List[str]] = None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class FileConflict:
|
|
57
|
+
"""Represents a conflict in a single file."""
|
|
58
|
+
|
|
59
|
+
file_path: str
|
|
60
|
+
conflict_type: ConflictType
|
|
61
|
+
markers: List[ConflictMarker] = field(default_factory=list)
|
|
62
|
+
our_version: Optional[str] = None
|
|
63
|
+
their_version: Optional[str] = None
|
|
64
|
+
base_version: Optional[str] = None
|
|
65
|
+
binary_conflict: bool = False
|
|
66
|
+
resolution_suggestion: Optional[str] = None
|
|
67
|
+
auto_resolvable: bool = False
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class ConflictResolution:
|
|
72
|
+
"""Represents a conflict resolution."""
|
|
73
|
+
|
|
74
|
+
file_path: str
|
|
75
|
+
strategy: ResolutionStrategy
|
|
76
|
+
resolved_content: Optional[str] = None
|
|
77
|
+
success: bool = False
|
|
78
|
+
message: str = ""
|
|
79
|
+
manual_intervention_required: bool = False
|
|
80
|
+
backup_created: bool = False
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass
|
|
84
|
+
class ConflictAnalysis:
|
|
85
|
+
"""Analysis of merge conflicts."""
|
|
86
|
+
|
|
87
|
+
total_conflicts: int
|
|
88
|
+
conflicted_files: List[str]
|
|
89
|
+
file_conflicts: List[FileConflict]
|
|
90
|
+
auto_resolvable_count: int
|
|
91
|
+
manual_resolution_count: int
|
|
92
|
+
conflict_severity: str # low, medium, high
|
|
93
|
+
resolution_complexity: str # simple, moderate, complex
|
|
94
|
+
estimated_resolution_time: int # minutes
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class ConflictResolutionManager:
|
|
98
|
+
"""
|
|
99
|
+
Manages conflict resolution for the Version Control Agent.
|
|
100
|
+
|
|
101
|
+
Provides conflict detection, analysis, and resolution capabilities
|
|
102
|
+
with both automatic and manual resolution strategies.
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
def __init__(self, project_root: str, logger: logging.Logger):
|
|
106
|
+
"""
|
|
107
|
+
Initialize Conflict Resolution Manager.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
project_root: Root directory of the project
|
|
111
|
+
logger: Logger instance
|
|
112
|
+
"""
|
|
113
|
+
self.project_root = Path(project_root)
|
|
114
|
+
self.logger = logger
|
|
115
|
+
|
|
116
|
+
# Conflict markers
|
|
117
|
+
self.conflict_markers = {"start": r"^<{7} ", "separator": r"^={7}$", "end": r"^>{7} "}
|
|
118
|
+
|
|
119
|
+
# Auto-resolution patterns
|
|
120
|
+
self.auto_resolution_patterns = {
|
|
121
|
+
"whitespace_only": r"^\s*$",
|
|
122
|
+
"comment_only": r"^\s*[#//]",
|
|
123
|
+
"import_statement": r"^\s*(import|from|#include)",
|
|
124
|
+
"log_statement": r"(console\.log|print\(|logger\.|log\.)",
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
# File types that can be auto-resolved
|
|
128
|
+
self.auto_resolvable_extensions = {".md", ".txt", ".json", ".yml", ".yaml", ".xml"}
|
|
129
|
+
|
|
130
|
+
# Binary file extensions
|
|
131
|
+
self.binary_extensions = {
|
|
132
|
+
".jpg",
|
|
133
|
+
".jpeg",
|
|
134
|
+
".png",
|
|
135
|
+
".gif",
|
|
136
|
+
".bmp",
|
|
137
|
+
".ico",
|
|
138
|
+
".pdf",
|
|
139
|
+
".doc",
|
|
140
|
+
".docx",
|
|
141
|
+
".xls",
|
|
142
|
+
".xlsx",
|
|
143
|
+
".ppt",
|
|
144
|
+
".pptx",
|
|
145
|
+
".zip",
|
|
146
|
+
".tar",
|
|
147
|
+
".gz",
|
|
148
|
+
".rar",
|
|
149
|
+
".7z",
|
|
150
|
+
".exe",
|
|
151
|
+
".dll",
|
|
152
|
+
".so",
|
|
153
|
+
".dylib",
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
def detect_conflicts(self) -> ConflictAnalysis:
|
|
157
|
+
"""
|
|
158
|
+
Detect merge conflicts in the repository.
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
ConflictAnalysis with detailed conflict information
|
|
162
|
+
"""
|
|
163
|
+
try:
|
|
164
|
+
# Get list of conflicted files from git status
|
|
165
|
+
conflicted_files = self._get_conflicted_files()
|
|
166
|
+
|
|
167
|
+
if not conflicted_files:
|
|
168
|
+
return ConflictAnalysis(
|
|
169
|
+
total_conflicts=0,
|
|
170
|
+
conflicted_files=[],
|
|
171
|
+
file_conflicts=[],
|
|
172
|
+
auto_resolvable_count=0,
|
|
173
|
+
manual_resolution_count=0,
|
|
174
|
+
conflict_severity="none",
|
|
175
|
+
resolution_complexity="none",
|
|
176
|
+
estimated_resolution_time=0,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# Analyze each conflicted file
|
|
180
|
+
file_conflicts = []
|
|
181
|
+
auto_resolvable_count = 0
|
|
182
|
+
|
|
183
|
+
for file_path in conflicted_files:
|
|
184
|
+
conflict = self._analyze_file_conflict(file_path)
|
|
185
|
+
file_conflicts.append(conflict)
|
|
186
|
+
|
|
187
|
+
if conflict.auto_resolvable:
|
|
188
|
+
auto_resolvable_count += 1
|
|
189
|
+
|
|
190
|
+
# Calculate analysis metrics
|
|
191
|
+
total_conflicts = len(file_conflicts)
|
|
192
|
+
manual_resolution_count = total_conflicts - auto_resolvable_count
|
|
193
|
+
|
|
194
|
+
# Determine severity and complexity
|
|
195
|
+
severity = self._calculate_severity(file_conflicts)
|
|
196
|
+
complexity = self._calculate_complexity(file_conflicts)
|
|
197
|
+
estimated_time = self._estimate_resolution_time(file_conflicts)
|
|
198
|
+
|
|
199
|
+
return ConflictAnalysis(
|
|
200
|
+
total_conflicts=total_conflicts,
|
|
201
|
+
conflicted_files=conflicted_files,
|
|
202
|
+
file_conflicts=file_conflicts,
|
|
203
|
+
auto_resolvable_count=auto_resolvable_count,
|
|
204
|
+
manual_resolution_count=manual_resolution_count,
|
|
205
|
+
conflict_severity=severity,
|
|
206
|
+
resolution_complexity=complexity,
|
|
207
|
+
estimated_resolution_time=estimated_time,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
except Exception as e:
|
|
211
|
+
self.logger.error(f"Error detecting conflicts: {e}")
|
|
212
|
+
return ConflictAnalysis(
|
|
213
|
+
total_conflicts=0,
|
|
214
|
+
conflicted_files=[],
|
|
215
|
+
file_conflicts=[],
|
|
216
|
+
auto_resolvable_count=0,
|
|
217
|
+
manual_resolution_count=0,
|
|
218
|
+
conflict_severity="unknown",
|
|
219
|
+
resolution_complexity="unknown",
|
|
220
|
+
estimated_resolution_time=0,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
def _get_conflicted_files(self) -> List[str]:
|
|
224
|
+
"""Get list of files with merge conflicts."""
|
|
225
|
+
import subprocess
|
|
226
|
+
|
|
227
|
+
try:
|
|
228
|
+
# Use git status to find conflicted files
|
|
229
|
+
result = subprocess.run(
|
|
230
|
+
["git", "status", "--porcelain"],
|
|
231
|
+
cwd=self.project_root,
|
|
232
|
+
capture_output=True,
|
|
233
|
+
text=True,
|
|
234
|
+
check=True,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
conflicted_files = []
|
|
238
|
+
for line in result.stdout.strip().split("\n"):
|
|
239
|
+
if line.strip() and (
|
|
240
|
+
line.startswith("UU") or line.startswith("AA") or line.startswith("DD")
|
|
241
|
+
):
|
|
242
|
+
# Extract filename
|
|
243
|
+
filename = line[3:].strip()
|
|
244
|
+
conflicted_files.append(filename)
|
|
245
|
+
|
|
246
|
+
return conflicted_files
|
|
247
|
+
|
|
248
|
+
except subprocess.CalledProcessError:
|
|
249
|
+
return []
|
|
250
|
+
except Exception as e:
|
|
251
|
+
self.logger.error(f"Error getting conflicted files: {e}")
|
|
252
|
+
return []
|
|
253
|
+
|
|
254
|
+
def _analyze_file_conflict(self, file_path: str) -> FileConflict:
|
|
255
|
+
"""Analyze conflict in a single file."""
|
|
256
|
+
full_path = self.project_root / file_path
|
|
257
|
+
|
|
258
|
+
# Check if file is binary
|
|
259
|
+
if self._is_binary_file(full_path):
|
|
260
|
+
return FileConflict(
|
|
261
|
+
file_path=file_path,
|
|
262
|
+
conflict_type=ConflictType.BINARY,
|
|
263
|
+
binary_conflict=True,
|
|
264
|
+
auto_resolvable=False,
|
|
265
|
+
resolution_suggestion="Manual resolution required for binary file",
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
try:
|
|
269
|
+
with open(full_path, "r", encoding="utf-8") as f:
|
|
270
|
+
content = f.read()
|
|
271
|
+
|
|
272
|
+
# Parse conflict markers
|
|
273
|
+
markers = self._parse_conflict_markers(content)
|
|
274
|
+
|
|
275
|
+
if not markers:
|
|
276
|
+
# No conflict markers, might be add/add or delete/modify conflict
|
|
277
|
+
conflict_type = self._determine_conflict_type(file_path)
|
|
278
|
+
return FileConflict(
|
|
279
|
+
file_path=file_path,
|
|
280
|
+
conflict_type=conflict_type,
|
|
281
|
+
auto_resolvable=False,
|
|
282
|
+
resolution_suggestion="Check git status for conflict details",
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
# Determine if auto-resolvable
|
|
286
|
+
auto_resolvable = self._is_auto_resolvable(file_path, markers)
|
|
287
|
+
|
|
288
|
+
# Generate resolution suggestion
|
|
289
|
+
suggestion = self._generate_resolution_suggestion(file_path, markers)
|
|
290
|
+
|
|
291
|
+
return FileConflict(
|
|
292
|
+
file_path=file_path,
|
|
293
|
+
conflict_type=ConflictType.CONTENT,
|
|
294
|
+
markers=markers,
|
|
295
|
+
auto_resolvable=auto_resolvable,
|
|
296
|
+
resolution_suggestion=suggestion,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
except Exception as e:
|
|
300
|
+
self.logger.error(f"Error analyzing conflict in {file_path}: {e}")
|
|
301
|
+
return FileConflict(
|
|
302
|
+
file_path=file_path,
|
|
303
|
+
conflict_type=ConflictType.CONTENT,
|
|
304
|
+
auto_resolvable=False,
|
|
305
|
+
resolution_suggestion=f"Error analyzing file: {e}",
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
def _is_binary_file(self, file_path: Path) -> bool:
|
|
309
|
+
"""Check if file is binary."""
|
|
310
|
+
# Check extension
|
|
311
|
+
if file_path.suffix.lower() in self.binary_extensions:
|
|
312
|
+
return True
|
|
313
|
+
|
|
314
|
+
# Check content (simple heuristic)
|
|
315
|
+
try:
|
|
316
|
+
with open(file_path, "rb") as f:
|
|
317
|
+
chunk = f.read(1024)
|
|
318
|
+
if b"\0" in chunk:
|
|
319
|
+
return True
|
|
320
|
+
except Exception:
|
|
321
|
+
pass
|
|
322
|
+
|
|
323
|
+
return False
|
|
324
|
+
|
|
325
|
+
def _parse_conflict_markers(self, content: str) -> List[ConflictMarker]:
|
|
326
|
+
"""Parse conflict markers from file content."""
|
|
327
|
+
lines = content.split("\n")
|
|
328
|
+
markers = []
|
|
329
|
+
i = 0
|
|
330
|
+
|
|
331
|
+
while i < len(lines):
|
|
332
|
+
line = lines[i]
|
|
333
|
+
|
|
334
|
+
# Look for conflict start marker
|
|
335
|
+
if re.match(self.conflict_markers["start"], line):
|
|
336
|
+
start_line = i
|
|
337
|
+
ours_content = []
|
|
338
|
+
theirs_content = []
|
|
339
|
+
conflict_marker = -1
|
|
340
|
+
end_line = -1
|
|
341
|
+
|
|
342
|
+
# Find separator and end markers
|
|
343
|
+
i += 1
|
|
344
|
+
while i < len(lines):
|
|
345
|
+
current_line = lines[i]
|
|
346
|
+
|
|
347
|
+
if re.match(self.conflict_markers["separator"], current_line):
|
|
348
|
+
conflict_marker = i
|
|
349
|
+
elif re.match(self.conflict_markers["end"], current_line):
|
|
350
|
+
end_line = i
|
|
351
|
+
break
|
|
352
|
+
elif conflict_marker == -1:
|
|
353
|
+
# We're in the "ours" section
|
|
354
|
+
ours_content.append(current_line)
|
|
355
|
+
else:
|
|
356
|
+
# We're in the "theirs" section
|
|
357
|
+
theirs_content.append(current_line)
|
|
358
|
+
|
|
359
|
+
i += 1
|
|
360
|
+
|
|
361
|
+
if conflict_marker != -1 and end_line != -1:
|
|
362
|
+
markers.append(
|
|
363
|
+
ConflictMarker(
|
|
364
|
+
start_line=start_line,
|
|
365
|
+
conflict_marker=conflict_marker,
|
|
366
|
+
end_line=end_line,
|
|
367
|
+
ours_content=ours_content,
|
|
368
|
+
theirs_content=theirs_content,
|
|
369
|
+
)
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
i += 1
|
|
373
|
+
|
|
374
|
+
return markers
|
|
375
|
+
|
|
376
|
+
def _determine_conflict_type(self, file_path: str) -> ConflictType:
|
|
377
|
+
"""Determine the type of conflict for a file."""
|
|
378
|
+
# This would involve checking git status more carefully
|
|
379
|
+
# For now, default to content conflict
|
|
380
|
+
return ConflictType.CONTENT
|
|
381
|
+
|
|
382
|
+
def _is_auto_resolvable(self, file_path: str, markers: List[ConflictMarker]) -> bool:
|
|
383
|
+
"""Determine if conflict can be automatically resolved."""
|
|
384
|
+
file_ext = Path(file_path).suffix.lower()
|
|
385
|
+
|
|
386
|
+
# Check if file type is auto-resolvable
|
|
387
|
+
if file_ext not in self.auto_resolvable_extensions:
|
|
388
|
+
return False
|
|
389
|
+
|
|
390
|
+
# Check if all conflicts are simple
|
|
391
|
+
for marker in markers:
|
|
392
|
+
if not self._is_simple_conflict(marker):
|
|
393
|
+
return False
|
|
394
|
+
|
|
395
|
+
return True
|
|
396
|
+
|
|
397
|
+
def _is_simple_conflict(self, marker: ConflictMarker) -> bool:
|
|
398
|
+
"""Check if a conflict marker represents a simple conflict."""
|
|
399
|
+
ours_lines = marker.ours_content
|
|
400
|
+
theirs_lines = marker.theirs_content
|
|
401
|
+
|
|
402
|
+
# Check if one side is empty (addition vs no-change)
|
|
403
|
+
if not ours_lines or not theirs_lines:
|
|
404
|
+
return True
|
|
405
|
+
|
|
406
|
+
# Check if differences are only whitespace
|
|
407
|
+
if self._only_whitespace_differences(ours_lines, theirs_lines):
|
|
408
|
+
return True
|
|
409
|
+
|
|
410
|
+
# Check if differences are only comments
|
|
411
|
+
if self._only_comment_differences(ours_lines, theirs_lines):
|
|
412
|
+
return True
|
|
413
|
+
|
|
414
|
+
# Check if differences are only imports
|
|
415
|
+
if self._only_import_differences(ours_lines, theirs_lines):
|
|
416
|
+
return True
|
|
417
|
+
|
|
418
|
+
return False
|
|
419
|
+
|
|
420
|
+
def _only_whitespace_differences(self, ours: List[str], theirs: List[str]) -> bool:
|
|
421
|
+
"""Check if differences are only whitespace."""
|
|
422
|
+
ours_stripped = [line.strip() for line in ours]
|
|
423
|
+
theirs_stripped = [line.strip() for line in theirs]
|
|
424
|
+
return ours_stripped == theirs_stripped
|
|
425
|
+
|
|
426
|
+
def _only_comment_differences(self, ours: List[str], theirs: List[str]) -> bool:
|
|
427
|
+
"""Check if differences are only in comments."""
|
|
428
|
+
# This is a simplified check
|
|
429
|
+
for line in ours + theirs:
|
|
430
|
+
stripped = line.strip()
|
|
431
|
+
if stripped and not (stripped.startswith("#") or stripped.startswith("//")):
|
|
432
|
+
return False
|
|
433
|
+
return True
|
|
434
|
+
|
|
435
|
+
def _only_import_differences(self, ours: List[str], theirs: List[str]) -> bool:
|
|
436
|
+
"""Check if differences are only in import statements."""
|
|
437
|
+
import_pattern = re.compile(r"^\s*(import|from|#include)")
|
|
438
|
+
|
|
439
|
+
for line in ours + theirs:
|
|
440
|
+
stripped = line.strip()
|
|
441
|
+
if stripped and not import_pattern.match(line):
|
|
442
|
+
return False
|
|
443
|
+
return True
|
|
444
|
+
|
|
445
|
+
def _generate_resolution_suggestion(self, file_path: str, markers: List[ConflictMarker]) -> str:
|
|
446
|
+
"""Generate resolution suggestion for a file."""
|
|
447
|
+
if not markers:
|
|
448
|
+
return "No conflict markers found"
|
|
449
|
+
|
|
450
|
+
suggestions = []
|
|
451
|
+
|
|
452
|
+
for i, marker in enumerate(markers):
|
|
453
|
+
if not marker.ours_content and marker.theirs_content:
|
|
454
|
+
suggestions.append(f"Conflict {i+1}: Accept incoming changes (theirs)")
|
|
455
|
+
elif marker.ours_content and not marker.theirs_content:
|
|
456
|
+
suggestions.append(f"Conflict {i+1}: Keep current changes (ours)")
|
|
457
|
+
elif self._only_whitespace_differences(marker.ours_content, marker.theirs_content):
|
|
458
|
+
suggestions.append(
|
|
459
|
+
f"Conflict {i+1}: Whitespace differences only - can auto-resolve"
|
|
460
|
+
)
|
|
461
|
+
else:
|
|
462
|
+
suggestions.append(f"Conflict {i+1}: Manual merge required")
|
|
463
|
+
|
|
464
|
+
return "; ".join(suggestions)
|
|
465
|
+
|
|
466
|
+
def _calculate_severity(self, file_conflicts: List[FileConflict]) -> str:
|
|
467
|
+
"""Calculate conflict severity."""
|
|
468
|
+
if not file_conflicts:
|
|
469
|
+
return "none"
|
|
470
|
+
|
|
471
|
+
binary_conflicts = sum(1 for fc in file_conflicts if fc.binary_conflict)
|
|
472
|
+
total_markers = sum(len(fc.markers) for fc in file_conflicts)
|
|
473
|
+
auto_resolvable = sum(1 for fc in file_conflicts if fc.auto_resolvable)
|
|
474
|
+
|
|
475
|
+
# High severity: binary conflicts or many complex conflicts
|
|
476
|
+
if binary_conflicts > 0 or (
|
|
477
|
+
total_markers > 10 and auto_resolvable < len(file_conflicts) * 0.5
|
|
478
|
+
):
|
|
479
|
+
return "high"
|
|
480
|
+
|
|
481
|
+
# Medium severity: some conflicts require manual resolution
|
|
482
|
+
if total_markers > 5 or auto_resolvable < len(file_conflicts) * 0.8:
|
|
483
|
+
return "medium"
|
|
484
|
+
|
|
485
|
+
# Low severity: mostly auto-resolvable
|
|
486
|
+
return "low"
|
|
487
|
+
|
|
488
|
+
def _calculate_complexity(self, file_conflicts: List[FileConflict]) -> str:
|
|
489
|
+
"""Calculate resolution complexity."""
|
|
490
|
+
if not file_conflicts:
|
|
491
|
+
return "none"
|
|
492
|
+
|
|
493
|
+
total_conflicts = len(file_conflicts)
|
|
494
|
+
auto_resolvable = sum(1 for fc in file_conflicts if fc.auto_resolvable)
|
|
495
|
+
auto_ratio = auto_resolvable / total_conflicts
|
|
496
|
+
|
|
497
|
+
if auto_ratio > 0.8:
|
|
498
|
+
return "simple"
|
|
499
|
+
elif auto_ratio > 0.5:
|
|
500
|
+
return "moderate"
|
|
501
|
+
else:
|
|
502
|
+
return "complex"
|
|
503
|
+
|
|
504
|
+
def _estimate_resolution_time(self, file_conflicts: List[FileConflict]) -> int:
|
|
505
|
+
"""Estimate resolution time in minutes."""
|
|
506
|
+
if not file_conflicts:
|
|
507
|
+
return 0
|
|
508
|
+
|
|
509
|
+
total_time = 0
|
|
510
|
+
|
|
511
|
+
for conflict in file_conflicts:
|
|
512
|
+
if conflict.auto_resolvable:
|
|
513
|
+
total_time += 1 # 1 minute for auto-resolution
|
|
514
|
+
elif conflict.binary_conflict:
|
|
515
|
+
total_time += 15 # 15 minutes for binary conflicts
|
|
516
|
+
else:
|
|
517
|
+
# Time based on number of conflict markers
|
|
518
|
+
markers_count = len(conflict.markers)
|
|
519
|
+
total_time += min(5 + markers_count * 2, 30) # 5-30 minutes per file
|
|
520
|
+
|
|
521
|
+
return total_time
|
|
522
|
+
|
|
523
|
+
def resolve_conflicts_automatically(
|
|
524
|
+
self,
|
|
525
|
+
file_conflicts: List[FileConflict],
|
|
526
|
+
strategy: ResolutionStrategy = ResolutionStrategy.AUTO_MERGE,
|
|
527
|
+
) -> List[ConflictResolution]:
|
|
528
|
+
"""
|
|
529
|
+
Automatically resolve conflicts where possible.
|
|
530
|
+
|
|
531
|
+
Args:
|
|
532
|
+
file_conflicts: List of file conflicts to resolve
|
|
533
|
+
strategy: Resolution strategy to use
|
|
534
|
+
|
|
535
|
+
Returns:
|
|
536
|
+
List of resolution results
|
|
537
|
+
"""
|
|
538
|
+
resolutions = []
|
|
539
|
+
|
|
540
|
+
for conflict in file_conflicts:
|
|
541
|
+
if not conflict.auto_resolvable:
|
|
542
|
+
resolutions.append(
|
|
543
|
+
ConflictResolution(
|
|
544
|
+
file_path=conflict.file_path,
|
|
545
|
+
strategy=ResolutionStrategy.MANUAL,
|
|
546
|
+
success=False,
|
|
547
|
+
message="Requires manual resolution",
|
|
548
|
+
manual_intervention_required=True,
|
|
549
|
+
)
|
|
550
|
+
)
|
|
551
|
+
continue
|
|
552
|
+
|
|
553
|
+
try:
|
|
554
|
+
resolution = self._resolve_file_conflict(conflict, strategy)
|
|
555
|
+
resolutions.append(resolution)
|
|
556
|
+
|
|
557
|
+
except Exception as e:
|
|
558
|
+
self.logger.error(f"Error resolving conflict in {conflict.file_path}: {e}")
|
|
559
|
+
resolutions.append(
|
|
560
|
+
ConflictResolution(
|
|
561
|
+
file_path=conflict.file_path,
|
|
562
|
+
strategy=strategy,
|
|
563
|
+
success=False,
|
|
564
|
+
message=f"Auto-resolution failed: {e}",
|
|
565
|
+
manual_intervention_required=True,
|
|
566
|
+
)
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
return resolutions
|
|
570
|
+
|
|
571
|
+
def _resolve_file_conflict(
|
|
572
|
+
self, conflict: FileConflict, strategy: ResolutionStrategy
|
|
573
|
+
) -> ConflictResolution:
|
|
574
|
+
"""Resolve conflict in a single file."""
|
|
575
|
+
file_path = self.project_root / conflict.file_path
|
|
576
|
+
|
|
577
|
+
# Create backup
|
|
578
|
+
backup_created = self._create_backup(file_path)
|
|
579
|
+
|
|
580
|
+
try:
|
|
581
|
+
# Read current content
|
|
582
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
583
|
+
content = f.read()
|
|
584
|
+
|
|
585
|
+
# Apply resolution strategy
|
|
586
|
+
resolved_content = self._apply_resolution_strategy(content, conflict.markers, strategy)
|
|
587
|
+
|
|
588
|
+
# Write resolved content
|
|
589
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
|
590
|
+
f.write(resolved_content)
|
|
591
|
+
|
|
592
|
+
return ConflictResolution(
|
|
593
|
+
file_path=conflict.file_path,
|
|
594
|
+
strategy=strategy,
|
|
595
|
+
resolved_content=resolved_content,
|
|
596
|
+
success=True,
|
|
597
|
+
message="Successfully auto-resolved",
|
|
598
|
+
backup_created=backup_created,
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
except Exception as e:
|
|
602
|
+
return ConflictResolution(
|
|
603
|
+
file_path=conflict.file_path,
|
|
604
|
+
strategy=strategy,
|
|
605
|
+
success=False,
|
|
606
|
+
message=f"Resolution failed: {e}",
|
|
607
|
+
manual_intervention_required=True,
|
|
608
|
+
backup_created=backup_created,
|
|
609
|
+
)
|
|
610
|
+
|
|
611
|
+
def _create_backup(self, file_path: Path) -> bool:
|
|
612
|
+
"""Create backup of file before resolution."""
|
|
613
|
+
try:
|
|
614
|
+
backup_path = file_path.with_suffix(f"{file_path.suffix}.conflict_backup")
|
|
615
|
+
backup_path.write_text(file_path.read_text())
|
|
616
|
+
return True
|
|
617
|
+
except Exception as e:
|
|
618
|
+
self.logger.warning(f"Could not create backup for {file_path}: {e}")
|
|
619
|
+
return False
|
|
620
|
+
|
|
621
|
+
def _apply_resolution_strategy(
|
|
622
|
+
self, content: str, markers: List[ConflictMarker], strategy: ResolutionStrategy
|
|
623
|
+
) -> str:
|
|
624
|
+
"""Apply resolution strategy to content."""
|
|
625
|
+
lines = content.split("\n")
|
|
626
|
+
|
|
627
|
+
# Process markers in reverse order to preserve line numbers
|
|
628
|
+
for marker in reversed(markers):
|
|
629
|
+
if strategy == ResolutionStrategy.OURS:
|
|
630
|
+
# Keep our version
|
|
631
|
+
replacement = marker.ours_content
|
|
632
|
+
elif strategy == ResolutionStrategy.THEIRS:
|
|
633
|
+
# Keep their version
|
|
634
|
+
replacement = marker.theirs_content
|
|
635
|
+
elif strategy == ResolutionStrategy.AUTO_MERGE:
|
|
636
|
+
# Smart merge
|
|
637
|
+
replacement = self._smart_merge(marker)
|
|
638
|
+
else:
|
|
639
|
+
# Default to ours
|
|
640
|
+
replacement = marker.ours_content
|
|
641
|
+
|
|
642
|
+
# Replace the conflict section
|
|
643
|
+
lines[marker.start_line : marker.end_line + 1] = replacement
|
|
644
|
+
|
|
645
|
+
return "\n".join(lines)
|
|
646
|
+
|
|
647
|
+
def _smart_merge(self, marker: ConflictMarker) -> List[str]:
|
|
648
|
+
"""Perform smart merge of conflict marker."""
|
|
649
|
+
ours = marker.ours_content
|
|
650
|
+
theirs = marker.theirs_content
|
|
651
|
+
|
|
652
|
+
# If one side is empty, use the other
|
|
653
|
+
if not ours:
|
|
654
|
+
return theirs
|
|
655
|
+
if not theirs:
|
|
656
|
+
return ours
|
|
657
|
+
|
|
658
|
+
# If only whitespace differences, use ours but with their spacing
|
|
659
|
+
if self._only_whitespace_differences(ours, theirs):
|
|
660
|
+
return theirs # Keep their formatting
|
|
661
|
+
|
|
662
|
+
# If only comment differences, merge both
|
|
663
|
+
if self._only_comment_differences(ours, theirs):
|
|
664
|
+
return ours + theirs
|
|
665
|
+
|
|
666
|
+
# If only import differences, merge and sort
|
|
667
|
+
if self._only_import_differences(ours, theirs):
|
|
668
|
+
combined = list(set(ours + theirs))
|
|
669
|
+
return sorted(combined)
|
|
670
|
+
|
|
671
|
+
# Default: keep ours
|
|
672
|
+
return ours
|
|
673
|
+
|
|
674
|
+
def generate_resolution_report(self, analysis: ConflictAnalysis) -> str:
|
|
675
|
+
"""Generate a human-readable conflict resolution report."""
|
|
676
|
+
if analysis.total_conflicts == 0:
|
|
677
|
+
return "No merge conflicts detected."
|
|
678
|
+
|
|
679
|
+
report_lines = [
|
|
680
|
+
"Merge Conflict Analysis Report",
|
|
681
|
+
"=" * 40,
|
|
682
|
+
"",
|
|
683
|
+
f"Total conflicts: {analysis.total_conflicts}",
|
|
684
|
+
f"Conflicted files: {len(analysis.conflicted_files)}",
|
|
685
|
+
f"Auto-resolvable: {analysis.auto_resolvable_count}",
|
|
686
|
+
f"Manual resolution required: {analysis.manual_resolution_count}",
|
|
687
|
+
f"Severity: {analysis.conflict_severity}",
|
|
688
|
+
f"Complexity: {analysis.resolution_complexity}",
|
|
689
|
+
f"Estimated resolution time: {analysis.estimated_resolution_time} minutes",
|
|
690
|
+
"",
|
|
691
|
+
"Conflicted Files:",
|
|
692
|
+
"-" * 20,
|
|
693
|
+
]
|
|
694
|
+
|
|
695
|
+
for conflict in analysis.file_conflicts:
|
|
696
|
+
report_lines.append(f"• {conflict.file_path}")
|
|
697
|
+
report_lines.append(f" Type: {conflict.conflict_type.value}")
|
|
698
|
+
if conflict.auto_resolvable:
|
|
699
|
+
report_lines.append(" Status: Auto-resolvable")
|
|
700
|
+
else:
|
|
701
|
+
report_lines.append(" Status: Manual resolution required")
|
|
702
|
+
|
|
703
|
+
if conflict.resolution_suggestion:
|
|
704
|
+
report_lines.append(f" Suggestion: {conflict.resolution_suggestion}")
|
|
705
|
+
|
|
706
|
+
report_lines.append("")
|
|
707
|
+
|
|
708
|
+
return "\n".join(report_lines)
|
|
709
|
+
|
|
710
|
+
def get_resolution_guidance(self, file_path: str) -> Dict[str, Any]:
|
|
711
|
+
"""Get detailed resolution guidance for a specific file."""
|
|
712
|
+
full_path = self.project_root / file_path
|
|
713
|
+
|
|
714
|
+
if not full_path.exists():
|
|
715
|
+
return {"error": f"File not found: {file_path}"}
|
|
716
|
+
|
|
717
|
+
try:
|
|
718
|
+
conflict = self._analyze_file_conflict(file_path)
|
|
719
|
+
|
|
720
|
+
guidance = {
|
|
721
|
+
"file_path": file_path,
|
|
722
|
+
"conflict_type": conflict.conflict_type.value,
|
|
723
|
+
"auto_resolvable": conflict.auto_resolvable,
|
|
724
|
+
"resolution_suggestion": conflict.resolution_suggestion,
|
|
725
|
+
"markers": [],
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
for i, marker in enumerate(conflict.markers):
|
|
729
|
+
marker_info = {
|
|
730
|
+
"conflict_number": i + 1,
|
|
731
|
+
"lines": f"{marker.start_line + 1}-{marker.end_line + 1}",
|
|
732
|
+
"ours_lines": len(marker.ours_content),
|
|
733
|
+
"theirs_lines": len(marker.theirs_content),
|
|
734
|
+
"preview": {
|
|
735
|
+
"ours": marker.ours_content[:3], # First 3 lines
|
|
736
|
+
"theirs": marker.theirs_content[:3],
|
|
737
|
+
},
|
|
738
|
+
}
|
|
739
|
+
guidance["markers"].append(marker_info)
|
|
740
|
+
|
|
741
|
+
return guidance
|
|
742
|
+
|
|
743
|
+
except Exception as e:
|
|
744
|
+
return {"error": f"Error analyzing file: {e}"}
|