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,784 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Git Operations Manager - Core Git operation automation for Version Control Agent.
|
|
3
|
+
|
|
4
|
+
This module provides comprehensive Git operation management including:
|
|
5
|
+
1. Branch creation and management
|
|
6
|
+
2. Merge operations and conflict resolution
|
|
7
|
+
3. Remote operations and synchronization
|
|
8
|
+
4. Quality gate integration
|
|
9
|
+
5. Automatic branch lifecycle management
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import subprocess
|
|
14
|
+
import re
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Dict, List, Optional, Any, Tuple
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
import logging
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class GitBranchInfo:
|
|
24
|
+
"""Information about a Git branch."""
|
|
25
|
+
|
|
26
|
+
name: str
|
|
27
|
+
current: bool
|
|
28
|
+
remote: Optional[str] = None
|
|
29
|
+
upstream: Optional[str] = None
|
|
30
|
+
last_commit: Optional[str] = None
|
|
31
|
+
last_commit_message: Optional[str] = None
|
|
32
|
+
ahead: int = 0
|
|
33
|
+
behind: int = 0
|
|
34
|
+
modified_files: List[str] = field(default_factory=list)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class GitOperationResult:
|
|
39
|
+
"""Result of a Git operation."""
|
|
40
|
+
|
|
41
|
+
success: bool
|
|
42
|
+
operation: str
|
|
43
|
+
message: str
|
|
44
|
+
output: str = ""
|
|
45
|
+
error: str = ""
|
|
46
|
+
branch_before: Optional[str] = None
|
|
47
|
+
branch_after: Optional[str] = None
|
|
48
|
+
files_changed: List[str] = field(default_factory=list)
|
|
49
|
+
execution_time: float = 0.0
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class GitOperationError(Exception):
|
|
53
|
+
"""Exception raised for Git operation errors."""
|
|
54
|
+
|
|
55
|
+
def __init__(self, message: str, command: str = "", output: str = "", error: str = ""):
|
|
56
|
+
super().__init__(message)
|
|
57
|
+
self.command = command
|
|
58
|
+
self.output = output
|
|
59
|
+
self.error = error
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class GitOperationsManager:
|
|
63
|
+
"""
|
|
64
|
+
Manages Git operations for the Version Control Agent.
|
|
65
|
+
|
|
66
|
+
Provides comprehensive Git operation automation including branch management,
|
|
67
|
+
merging, remote operations, and integration with quality gates.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
def __init__(self, project_root: str, logger: logging.Logger):
|
|
71
|
+
"""
|
|
72
|
+
Initialize Git Operations Manager.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
project_root: Root directory of the Git repository
|
|
76
|
+
logger: Logger instance
|
|
77
|
+
"""
|
|
78
|
+
self.project_root = Path(project_root)
|
|
79
|
+
self.logger = logger
|
|
80
|
+
self.git_dir = self.project_root / ".git"
|
|
81
|
+
|
|
82
|
+
# Branch naming conventions
|
|
83
|
+
self.branch_prefixes = {
|
|
84
|
+
"issue": "issue/",
|
|
85
|
+
"feature": "feature/",
|
|
86
|
+
"enhancement": "enhancement/",
|
|
87
|
+
"hotfix": "hotfix/",
|
|
88
|
+
"epic": "epic/",
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# Merge strategies
|
|
92
|
+
self.merge_strategies = {"merge": "--no-ff", "squash": "--squash", "rebase": "--rebase"}
|
|
93
|
+
|
|
94
|
+
# Validate Git repository
|
|
95
|
+
if not self._is_git_repository():
|
|
96
|
+
raise GitOperationError(f"Not a Git repository: {project_root}")
|
|
97
|
+
|
|
98
|
+
def _is_git_repository(self) -> bool:
|
|
99
|
+
"""Check if the directory is a Git repository."""
|
|
100
|
+
return self.git_dir.exists() and self.git_dir.is_dir()
|
|
101
|
+
|
|
102
|
+
def _run_git_command(
|
|
103
|
+
self,
|
|
104
|
+
args: List[str],
|
|
105
|
+
check: bool = True,
|
|
106
|
+
capture_output: bool = True,
|
|
107
|
+
cwd: Optional[str] = None,
|
|
108
|
+
) -> subprocess.CompletedProcess:
|
|
109
|
+
"""
|
|
110
|
+
Run a Git command and return the result.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
args: Git command arguments
|
|
114
|
+
check: Whether to raise exception on non-zero exit
|
|
115
|
+
capture_output: Whether to capture stdout/stderr
|
|
116
|
+
cwd: Working directory (defaults to project_root)
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
CompletedProcess result
|
|
120
|
+
|
|
121
|
+
Raises:
|
|
122
|
+
GitOperationError: If command fails and check=True
|
|
123
|
+
"""
|
|
124
|
+
cmd = ["git"] + args
|
|
125
|
+
cwd = cwd or str(self.project_root)
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
self.logger.debug(f"Running Git command: {' '.join(cmd)}")
|
|
129
|
+
|
|
130
|
+
result = subprocess.run(
|
|
131
|
+
cmd, cwd=cwd, capture_output=capture_output, text=True, check=False
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
if check and result.returncode != 0:
|
|
135
|
+
raise GitOperationError(
|
|
136
|
+
f"Git command failed: {' '.join(cmd)}",
|
|
137
|
+
command=" ".join(cmd),
|
|
138
|
+
output=result.stdout,
|
|
139
|
+
error=result.stderr,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
return result
|
|
143
|
+
|
|
144
|
+
except FileNotFoundError:
|
|
145
|
+
raise GitOperationError("Git is not installed or not in PATH")
|
|
146
|
+
except Exception as e:
|
|
147
|
+
raise GitOperationError(f"Error running Git command: {e}")
|
|
148
|
+
|
|
149
|
+
def get_current_branch(self) -> str:
|
|
150
|
+
"""Get the current Git branch name."""
|
|
151
|
+
try:
|
|
152
|
+
result = self._run_git_command(["branch", "--show-current"])
|
|
153
|
+
return result.stdout.strip()
|
|
154
|
+
except GitOperationError:
|
|
155
|
+
# Fallback for older Git versions
|
|
156
|
+
result = self._run_git_command(["rev-parse", "--abbrev-ref", "HEAD"])
|
|
157
|
+
return result.stdout.strip()
|
|
158
|
+
|
|
159
|
+
def get_branch_info(self, branch_name: Optional[str] = None) -> GitBranchInfo:
|
|
160
|
+
"""
|
|
161
|
+
Get detailed information about a branch.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
branch_name: Branch name (defaults to current branch)
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
GitBranchInfo object with branch details
|
|
168
|
+
"""
|
|
169
|
+
if not branch_name:
|
|
170
|
+
branch_name = self.get_current_branch()
|
|
171
|
+
|
|
172
|
+
# Get basic branch info
|
|
173
|
+
current_branch = self.get_current_branch()
|
|
174
|
+
is_current = branch_name == current_branch
|
|
175
|
+
|
|
176
|
+
# Get remote tracking info
|
|
177
|
+
remote = None
|
|
178
|
+
upstream = None
|
|
179
|
+
ahead = 0
|
|
180
|
+
behind = 0
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
# Get remote tracking branch
|
|
184
|
+
result = self._run_git_command(
|
|
185
|
+
["rev-parse", "--abbrev-ref", f"{branch_name}@{{upstream}}"], check=False
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
if result.returncode == 0:
|
|
189
|
+
upstream = result.stdout.strip()
|
|
190
|
+
remote = upstream.split("/")[0] if "/" in upstream else None
|
|
191
|
+
|
|
192
|
+
# Get ahead/behind info
|
|
193
|
+
result = self._run_git_command(
|
|
194
|
+
["rev-list", "--left-right", "--count", f"{upstream}...{branch_name}"],
|
|
195
|
+
check=False,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
if result.returncode == 0:
|
|
199
|
+
parts = result.stdout.strip().split()
|
|
200
|
+
if len(parts) == 2:
|
|
201
|
+
behind, ahead = map(int, parts)
|
|
202
|
+
|
|
203
|
+
except GitOperationError:
|
|
204
|
+
pass
|
|
205
|
+
|
|
206
|
+
# Get last commit info
|
|
207
|
+
last_commit = None
|
|
208
|
+
last_commit_message = None
|
|
209
|
+
|
|
210
|
+
try:
|
|
211
|
+
result = self._run_git_command(["log", "-1", "--format=%H", branch_name], check=False)
|
|
212
|
+
|
|
213
|
+
if result.returncode == 0:
|
|
214
|
+
last_commit = result.stdout.strip()
|
|
215
|
+
|
|
216
|
+
result = self._run_git_command(
|
|
217
|
+
["log", "-1", "--format=%s", branch_name], check=False
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
if result.returncode == 0:
|
|
221
|
+
last_commit_message = result.stdout.strip()
|
|
222
|
+
|
|
223
|
+
except GitOperationError:
|
|
224
|
+
pass
|
|
225
|
+
|
|
226
|
+
# Get modified files if current branch
|
|
227
|
+
modified_files = []
|
|
228
|
+
if is_current:
|
|
229
|
+
modified_files = self.get_modified_files()
|
|
230
|
+
|
|
231
|
+
return GitBranchInfo(
|
|
232
|
+
name=branch_name,
|
|
233
|
+
current=is_current,
|
|
234
|
+
remote=remote,
|
|
235
|
+
upstream=upstream,
|
|
236
|
+
last_commit=last_commit,
|
|
237
|
+
last_commit_message=last_commit_message,
|
|
238
|
+
ahead=ahead,
|
|
239
|
+
behind=behind,
|
|
240
|
+
modified_files=modified_files,
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
def get_all_branches(self, include_remotes: bool = False) -> List[GitBranchInfo]:
|
|
244
|
+
"""
|
|
245
|
+
Get information about all branches.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
include_remotes: Whether to include remote branches
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
List of GitBranchInfo objects
|
|
252
|
+
"""
|
|
253
|
+
branches = []
|
|
254
|
+
|
|
255
|
+
# Get local branches
|
|
256
|
+
result = self._run_git_command(["branch", "--list"])
|
|
257
|
+
for line in result.stdout.strip().split("\n"):
|
|
258
|
+
if line.strip():
|
|
259
|
+
branch_name = line.strip().lstrip("* ").strip()
|
|
260
|
+
if branch_name:
|
|
261
|
+
branches.append(self.get_branch_info(branch_name))
|
|
262
|
+
|
|
263
|
+
# Get remote branches if requested
|
|
264
|
+
if include_remotes:
|
|
265
|
+
result = self._run_git_command(["branch", "-r"], check=False)
|
|
266
|
+
if result.returncode == 0:
|
|
267
|
+
for line in result.stdout.strip().split("\n"):
|
|
268
|
+
if line.strip() and not line.strip().endswith("/HEAD"):
|
|
269
|
+
remote_branch = line.strip()
|
|
270
|
+
# Create simplified info for remote branches
|
|
271
|
+
branches.append(
|
|
272
|
+
GitBranchInfo(
|
|
273
|
+
name=remote_branch,
|
|
274
|
+
current=False,
|
|
275
|
+
remote=(
|
|
276
|
+
remote_branch.split("/")[0] if "/" in remote_branch else None
|
|
277
|
+
),
|
|
278
|
+
)
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
return branches
|
|
282
|
+
|
|
283
|
+
def get_modified_files(self) -> List[str]:
|
|
284
|
+
"""Get list of modified files in working directory."""
|
|
285
|
+
try:
|
|
286
|
+
result = self._run_git_command(["status", "--porcelain"])
|
|
287
|
+
modified_files = []
|
|
288
|
+
|
|
289
|
+
for line in result.stdout.strip().split("\n"):
|
|
290
|
+
if line.strip():
|
|
291
|
+
# Extract filename from git status output
|
|
292
|
+
filename = line[3:].strip()
|
|
293
|
+
modified_files.append(filename)
|
|
294
|
+
|
|
295
|
+
return modified_files
|
|
296
|
+
|
|
297
|
+
except GitOperationError:
|
|
298
|
+
return []
|
|
299
|
+
|
|
300
|
+
def is_working_directory_clean(self) -> bool:
|
|
301
|
+
"""Check if working directory is clean (no uncommitted changes)."""
|
|
302
|
+
try:
|
|
303
|
+
result = self._run_git_command(["status", "--porcelain"])
|
|
304
|
+
return not result.stdout.strip()
|
|
305
|
+
except GitOperationError:
|
|
306
|
+
return False
|
|
307
|
+
|
|
308
|
+
def create_branch(
|
|
309
|
+
self,
|
|
310
|
+
branch_name: str,
|
|
311
|
+
branch_type: str = "issue",
|
|
312
|
+
base_branch: str = "main",
|
|
313
|
+
switch_to_branch: bool = True,
|
|
314
|
+
) -> GitOperationResult:
|
|
315
|
+
"""
|
|
316
|
+
Create a new Git branch with proper naming conventions.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
branch_name: Base name for the branch
|
|
320
|
+
branch_type: Type of branch (issue, feature, enhancement, hotfix, epic)
|
|
321
|
+
base_branch: Base branch to create from
|
|
322
|
+
switch_to_branch: Whether to switch to the new branch
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
GitOperationResult with operation details
|
|
326
|
+
"""
|
|
327
|
+
start_time = datetime.now()
|
|
328
|
+
current_branch = self.get_current_branch()
|
|
329
|
+
|
|
330
|
+
# Generate full branch name with prefix
|
|
331
|
+
prefix = self.branch_prefixes.get(branch_type, "")
|
|
332
|
+
full_branch_name = f"{prefix}{branch_name}"
|
|
333
|
+
|
|
334
|
+
try:
|
|
335
|
+
# Ensure we're on the base branch
|
|
336
|
+
if current_branch != base_branch:
|
|
337
|
+
self._run_git_command(["checkout", base_branch])
|
|
338
|
+
|
|
339
|
+
# Pull latest changes from base branch
|
|
340
|
+
self._run_git_command(["pull", "origin", base_branch], check=False)
|
|
341
|
+
|
|
342
|
+
# Create the new branch
|
|
343
|
+
create_args = ["checkout", "-b", full_branch_name]
|
|
344
|
+
if not switch_to_branch:
|
|
345
|
+
create_args = ["branch", full_branch_name]
|
|
346
|
+
|
|
347
|
+
result = self._run_git_command(create_args)
|
|
348
|
+
|
|
349
|
+
# Set up remote tracking if creating a new branch
|
|
350
|
+
if switch_to_branch:
|
|
351
|
+
try:
|
|
352
|
+
self._run_git_command(["push", "-u", "origin", full_branch_name], check=False)
|
|
353
|
+
except GitOperationError:
|
|
354
|
+
# Remote push failed, continue without remote tracking
|
|
355
|
+
pass
|
|
356
|
+
|
|
357
|
+
execution_time = (datetime.now() - start_time).total_seconds()
|
|
358
|
+
|
|
359
|
+
return GitOperationResult(
|
|
360
|
+
success=True,
|
|
361
|
+
operation="create_branch",
|
|
362
|
+
message=f"Successfully created branch: {full_branch_name}",
|
|
363
|
+
output=result.stdout,
|
|
364
|
+
branch_before=current_branch,
|
|
365
|
+
branch_after=full_branch_name if switch_to_branch else current_branch,
|
|
366
|
+
execution_time=execution_time,
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
except GitOperationError as e:
|
|
370
|
+
execution_time = (datetime.now() - start_time).total_seconds()
|
|
371
|
+
|
|
372
|
+
return GitOperationResult(
|
|
373
|
+
success=False,
|
|
374
|
+
operation="create_branch",
|
|
375
|
+
message=f"Failed to create branch: {full_branch_name}",
|
|
376
|
+
error=str(e),
|
|
377
|
+
branch_before=current_branch,
|
|
378
|
+
branch_after=current_branch,
|
|
379
|
+
execution_time=execution_time,
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
def switch_branch(self, branch_name: str) -> GitOperationResult:
|
|
383
|
+
"""
|
|
384
|
+
Switch to an existing branch.
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
branch_name: Name of branch to switch to
|
|
388
|
+
|
|
389
|
+
Returns:
|
|
390
|
+
GitOperationResult with operation details
|
|
391
|
+
"""
|
|
392
|
+
start_time = datetime.now()
|
|
393
|
+
current_branch = self.get_current_branch()
|
|
394
|
+
|
|
395
|
+
try:
|
|
396
|
+
# Check if working directory is clean
|
|
397
|
+
if not self.is_working_directory_clean():
|
|
398
|
+
modified_files = self.get_modified_files()
|
|
399
|
+
return GitOperationResult(
|
|
400
|
+
success=False,
|
|
401
|
+
operation="switch_branch",
|
|
402
|
+
message=f"Cannot switch branch: uncommitted changes exist",
|
|
403
|
+
error=f"Modified files: {', '.join(modified_files)}",
|
|
404
|
+
branch_before=current_branch,
|
|
405
|
+
branch_after=current_branch,
|
|
406
|
+
files_changed=modified_files,
|
|
407
|
+
execution_time=(datetime.now() - start_time).total_seconds(),
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
# Switch to the branch
|
|
411
|
+
result = self._run_git_command(["checkout", branch_name])
|
|
412
|
+
|
|
413
|
+
execution_time = (datetime.now() - start_time).total_seconds()
|
|
414
|
+
|
|
415
|
+
return GitOperationResult(
|
|
416
|
+
success=True,
|
|
417
|
+
operation="switch_branch",
|
|
418
|
+
message=f"Successfully switched to branch: {branch_name}",
|
|
419
|
+
output=result.stdout,
|
|
420
|
+
branch_before=current_branch,
|
|
421
|
+
branch_after=branch_name,
|
|
422
|
+
execution_time=execution_time,
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
except GitOperationError as e:
|
|
426
|
+
execution_time = (datetime.now() - start_time).total_seconds()
|
|
427
|
+
|
|
428
|
+
return GitOperationResult(
|
|
429
|
+
success=False,
|
|
430
|
+
operation="switch_branch",
|
|
431
|
+
message=f"Failed to switch to branch: {branch_name}",
|
|
432
|
+
error=str(e),
|
|
433
|
+
branch_before=current_branch,
|
|
434
|
+
branch_after=current_branch,
|
|
435
|
+
execution_time=execution_time,
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
def merge_branch(
|
|
439
|
+
self,
|
|
440
|
+
source_branch: str,
|
|
441
|
+
target_branch: str = "main",
|
|
442
|
+
merge_strategy: str = "merge",
|
|
443
|
+
delete_source: bool = True,
|
|
444
|
+
) -> GitOperationResult:
|
|
445
|
+
"""
|
|
446
|
+
Merge a source branch into target branch.
|
|
447
|
+
|
|
448
|
+
Args:
|
|
449
|
+
source_branch: Branch to merge from
|
|
450
|
+
target_branch: Branch to merge into
|
|
451
|
+
merge_strategy: Merge strategy (merge, squash, rebase)
|
|
452
|
+
delete_source: Whether to delete source branch after merge
|
|
453
|
+
|
|
454
|
+
Returns:
|
|
455
|
+
GitOperationResult with operation details
|
|
456
|
+
"""
|
|
457
|
+
start_time = datetime.now()
|
|
458
|
+
current_branch = self.get_current_branch()
|
|
459
|
+
|
|
460
|
+
try:
|
|
461
|
+
# Switch to target branch
|
|
462
|
+
if current_branch != target_branch:
|
|
463
|
+
switch_result = self.switch_branch(target_branch)
|
|
464
|
+
if not switch_result.success:
|
|
465
|
+
return switch_result
|
|
466
|
+
|
|
467
|
+
# Pull latest changes
|
|
468
|
+
self._run_git_command(["pull", "origin", target_branch], check=False)
|
|
469
|
+
|
|
470
|
+
# Perform merge based on strategy
|
|
471
|
+
strategy_arg = self.merge_strategies.get(merge_strategy, "--no-ff")
|
|
472
|
+
|
|
473
|
+
if merge_strategy == "rebase":
|
|
474
|
+
# Rebase strategy
|
|
475
|
+
result = self._run_git_command(["rebase", source_branch])
|
|
476
|
+
else:
|
|
477
|
+
# Merge strategy
|
|
478
|
+
merge_args = ["merge", strategy_arg, source_branch]
|
|
479
|
+
if merge_strategy == "merge":
|
|
480
|
+
# Add merge commit message
|
|
481
|
+
merge_args.extend(["-m", f"Merge {source_branch} into {target_branch}"])
|
|
482
|
+
|
|
483
|
+
result = self._run_git_command(merge_args)
|
|
484
|
+
|
|
485
|
+
# Delete source branch if requested
|
|
486
|
+
if delete_source and source_branch != target_branch:
|
|
487
|
+
try:
|
|
488
|
+
# Delete local branch
|
|
489
|
+
self._run_git_command(["branch", "-d", source_branch], check=False)
|
|
490
|
+
|
|
491
|
+
# Delete remote branch
|
|
492
|
+
self._run_git_command(
|
|
493
|
+
["push", "origin", "--delete", source_branch], check=False
|
|
494
|
+
)
|
|
495
|
+
except GitOperationError:
|
|
496
|
+
# Branch deletion failed, but merge was successful
|
|
497
|
+
pass
|
|
498
|
+
|
|
499
|
+
execution_time = (datetime.now() - start_time).total_seconds()
|
|
500
|
+
|
|
501
|
+
return GitOperationResult(
|
|
502
|
+
success=True,
|
|
503
|
+
operation="merge_branch",
|
|
504
|
+
message=f"Successfully merged {source_branch} into {target_branch}",
|
|
505
|
+
output=result.stdout,
|
|
506
|
+
branch_before=current_branch,
|
|
507
|
+
branch_after=target_branch,
|
|
508
|
+
execution_time=execution_time,
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
except GitOperationError as e:
|
|
512
|
+
execution_time = (datetime.now() - start_time).total_seconds()
|
|
513
|
+
|
|
514
|
+
return GitOperationResult(
|
|
515
|
+
success=False,
|
|
516
|
+
operation="merge_branch",
|
|
517
|
+
message=f"Failed to merge {source_branch} into {target_branch}",
|
|
518
|
+
error=str(e),
|
|
519
|
+
branch_before=current_branch,
|
|
520
|
+
branch_after=current_branch,
|
|
521
|
+
execution_time=execution_time,
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
def detect_merge_conflicts(self, source_branch: str, target_branch: str) -> Dict[str, Any]:
|
|
525
|
+
"""
|
|
526
|
+
Detect potential merge conflicts between branches.
|
|
527
|
+
|
|
528
|
+
Args:
|
|
529
|
+
source_branch: Source branch
|
|
530
|
+
target_branch: Target branch
|
|
531
|
+
|
|
532
|
+
Returns:
|
|
533
|
+
Dictionary with conflict information
|
|
534
|
+
"""
|
|
535
|
+
try:
|
|
536
|
+
# Perform a dry-run merge to detect conflicts
|
|
537
|
+
result = self._run_git_command(
|
|
538
|
+
[
|
|
539
|
+
"merge-tree",
|
|
540
|
+
f"$(git merge-base {target_branch} {source_branch})",
|
|
541
|
+
target_branch,
|
|
542
|
+
source_branch,
|
|
543
|
+
],
|
|
544
|
+
check=False,
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
has_conflicts = result.returncode != 0 or "<<<<<<< " in result.stdout
|
|
548
|
+
|
|
549
|
+
# Parse conflicted files
|
|
550
|
+
conflicted_files = []
|
|
551
|
+
if has_conflicts:
|
|
552
|
+
lines = result.stdout.split("\n")
|
|
553
|
+
current_file = None
|
|
554
|
+
|
|
555
|
+
for line in lines:
|
|
556
|
+
if line.startswith("+++") or line.startswith("---"):
|
|
557
|
+
# Extract filename
|
|
558
|
+
parts = line.split("\t")
|
|
559
|
+
if len(parts) > 1:
|
|
560
|
+
filename = parts[1].strip()
|
|
561
|
+
if filename != "/dev/null" and filename not in conflicted_files:
|
|
562
|
+
conflicted_files.append(filename)
|
|
563
|
+
|
|
564
|
+
return {
|
|
565
|
+
"has_conflicts": has_conflicts,
|
|
566
|
+
"conflicted_files": conflicted_files,
|
|
567
|
+
"can_auto_merge": not has_conflicts,
|
|
568
|
+
"merge_base": self._get_merge_base(source_branch, target_branch),
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
except GitOperationError:
|
|
572
|
+
return {
|
|
573
|
+
"has_conflicts": True,
|
|
574
|
+
"conflicted_files": [],
|
|
575
|
+
"can_auto_merge": False,
|
|
576
|
+
"error": "Could not detect conflicts",
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
def _get_merge_base(self, branch1: str, branch2: str) -> Optional[str]:
|
|
580
|
+
"""Get the merge base commit between two branches."""
|
|
581
|
+
try:
|
|
582
|
+
result = self._run_git_command(["merge-base", branch1, branch2])
|
|
583
|
+
return result.stdout.strip()
|
|
584
|
+
except GitOperationError:
|
|
585
|
+
return None
|
|
586
|
+
|
|
587
|
+
def push_to_remote(
|
|
588
|
+
self, branch_name: Optional[str] = None, remote: str = "origin", set_upstream: bool = False
|
|
589
|
+
) -> GitOperationResult:
|
|
590
|
+
"""
|
|
591
|
+
Push branch to remote repository.
|
|
592
|
+
|
|
593
|
+
Args:
|
|
594
|
+
branch_name: Branch to push (defaults to current branch)
|
|
595
|
+
remote: Remote name
|
|
596
|
+
set_upstream: Whether to set upstream tracking
|
|
597
|
+
|
|
598
|
+
Returns:
|
|
599
|
+
GitOperationResult with operation details
|
|
600
|
+
"""
|
|
601
|
+
start_time = datetime.now()
|
|
602
|
+
current_branch = self.get_current_branch()
|
|
603
|
+
|
|
604
|
+
if not branch_name:
|
|
605
|
+
branch_name = current_branch
|
|
606
|
+
|
|
607
|
+
try:
|
|
608
|
+
# Build push command
|
|
609
|
+
push_args = ["push"]
|
|
610
|
+
|
|
611
|
+
if set_upstream:
|
|
612
|
+
push_args.extend(["-u", remote, branch_name])
|
|
613
|
+
else:
|
|
614
|
+
push_args.extend([remote, branch_name])
|
|
615
|
+
|
|
616
|
+
result = self._run_git_command(push_args)
|
|
617
|
+
|
|
618
|
+
execution_time = (datetime.now() - start_time).total_seconds()
|
|
619
|
+
|
|
620
|
+
return GitOperationResult(
|
|
621
|
+
success=True,
|
|
622
|
+
operation="push_to_remote",
|
|
623
|
+
message=f"Successfully pushed {branch_name} to {remote}",
|
|
624
|
+
output=result.stdout,
|
|
625
|
+
branch_before=current_branch,
|
|
626
|
+
branch_after=current_branch,
|
|
627
|
+
execution_time=execution_time,
|
|
628
|
+
)
|
|
629
|
+
|
|
630
|
+
except GitOperationError as e:
|
|
631
|
+
execution_time = (datetime.now() - start_time).total_seconds()
|
|
632
|
+
|
|
633
|
+
return GitOperationResult(
|
|
634
|
+
success=False,
|
|
635
|
+
operation="push_to_remote",
|
|
636
|
+
message=f"Failed to push {branch_name} to {remote}",
|
|
637
|
+
error=str(e),
|
|
638
|
+
branch_before=current_branch,
|
|
639
|
+
branch_after=current_branch,
|
|
640
|
+
execution_time=execution_time,
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
def sync_with_remote(
|
|
644
|
+
self, branch_name: Optional[str] = None, remote: str = "origin"
|
|
645
|
+
) -> GitOperationResult:
|
|
646
|
+
"""
|
|
647
|
+
Sync local branch with remote (fetch + pull).
|
|
648
|
+
|
|
649
|
+
Args:
|
|
650
|
+
branch_name: Branch to sync (defaults to current branch)
|
|
651
|
+
remote: Remote name
|
|
652
|
+
|
|
653
|
+
Returns:
|
|
654
|
+
GitOperationResult with operation details
|
|
655
|
+
"""
|
|
656
|
+
start_time = datetime.now()
|
|
657
|
+
current_branch = self.get_current_branch()
|
|
658
|
+
|
|
659
|
+
if not branch_name:
|
|
660
|
+
branch_name = current_branch
|
|
661
|
+
|
|
662
|
+
try:
|
|
663
|
+
# Fetch latest changes
|
|
664
|
+
self._run_git_command(["fetch", remote])
|
|
665
|
+
|
|
666
|
+
# Pull changes if on the target branch
|
|
667
|
+
if current_branch == branch_name:
|
|
668
|
+
result = self._run_git_command(["pull", remote, branch_name])
|
|
669
|
+
else:
|
|
670
|
+
# Switch to branch, pull, then switch back
|
|
671
|
+
self._run_git_command(["checkout", branch_name])
|
|
672
|
+
result = self._run_git_command(["pull", remote, branch_name])
|
|
673
|
+
self._run_git_command(["checkout", current_branch])
|
|
674
|
+
|
|
675
|
+
execution_time = (datetime.now() - start_time).total_seconds()
|
|
676
|
+
|
|
677
|
+
return GitOperationResult(
|
|
678
|
+
success=True,
|
|
679
|
+
operation="sync_with_remote",
|
|
680
|
+
message=f"Successfully synced {branch_name} with {remote}",
|
|
681
|
+
output=result.stdout,
|
|
682
|
+
branch_before=current_branch,
|
|
683
|
+
branch_after=current_branch,
|
|
684
|
+
execution_time=execution_time,
|
|
685
|
+
)
|
|
686
|
+
|
|
687
|
+
except GitOperationError as e:
|
|
688
|
+
execution_time = (datetime.now() - start_time).total_seconds()
|
|
689
|
+
|
|
690
|
+
return GitOperationResult(
|
|
691
|
+
success=False,
|
|
692
|
+
operation="sync_with_remote",
|
|
693
|
+
message=f"Failed to sync {branch_name} with {remote}",
|
|
694
|
+
error=str(e),
|
|
695
|
+
branch_before=current_branch,
|
|
696
|
+
branch_after=current_branch,
|
|
697
|
+
execution_time=execution_time,
|
|
698
|
+
)
|
|
699
|
+
|
|
700
|
+
def cleanup_merged_branches(self, target_branch: str = "main") -> GitOperationResult:
|
|
701
|
+
"""
|
|
702
|
+
Clean up branches that have been merged into target branch.
|
|
703
|
+
|
|
704
|
+
Args:
|
|
705
|
+
target_branch: Target branch to check for merged branches
|
|
706
|
+
|
|
707
|
+
Returns:
|
|
708
|
+
GitOperationResult with cleanup details
|
|
709
|
+
"""
|
|
710
|
+
start_time = datetime.now()
|
|
711
|
+
current_branch = self.get_current_branch()
|
|
712
|
+
|
|
713
|
+
try:
|
|
714
|
+
# Get merged branches
|
|
715
|
+
result = self._run_git_command(["branch", "--merged", target_branch])
|
|
716
|
+
|
|
717
|
+
merged_branches = []
|
|
718
|
+
for line in result.stdout.strip().split("\n"):
|
|
719
|
+
branch = line.strip().lstrip("* ").strip()
|
|
720
|
+
# Skip target branch and current branch
|
|
721
|
+
if branch and branch != target_branch and branch != current_branch:
|
|
722
|
+
merged_branches.append(branch)
|
|
723
|
+
|
|
724
|
+
# Delete merged branches
|
|
725
|
+
deleted_branches = []
|
|
726
|
+
for branch in merged_branches:
|
|
727
|
+
try:
|
|
728
|
+
self._run_git_command(["branch", "-d", branch])
|
|
729
|
+
deleted_branches.append(branch)
|
|
730
|
+
except GitOperationError:
|
|
731
|
+
# Skip branches that can't be deleted
|
|
732
|
+
pass
|
|
733
|
+
|
|
734
|
+
# Clean up remote tracking branches
|
|
735
|
+
self._run_git_command(["remote", "prune", "origin"], check=False)
|
|
736
|
+
|
|
737
|
+
execution_time = (datetime.now() - start_time).total_seconds()
|
|
738
|
+
|
|
739
|
+
return GitOperationResult(
|
|
740
|
+
success=True,
|
|
741
|
+
operation="cleanup_merged_branches",
|
|
742
|
+
message=f"Cleaned up {len(deleted_branches)} merged branches",
|
|
743
|
+
output=f"Deleted branches: {', '.join(deleted_branches)}",
|
|
744
|
+
branch_before=current_branch,
|
|
745
|
+
branch_after=current_branch,
|
|
746
|
+
execution_time=execution_time,
|
|
747
|
+
)
|
|
748
|
+
|
|
749
|
+
except GitOperationError as e:
|
|
750
|
+
execution_time = (datetime.now() - start_time).total_seconds()
|
|
751
|
+
|
|
752
|
+
return GitOperationResult(
|
|
753
|
+
success=False,
|
|
754
|
+
operation="cleanup_merged_branches",
|
|
755
|
+
message="Failed to cleanup merged branches",
|
|
756
|
+
error=str(e),
|
|
757
|
+
branch_before=current_branch,
|
|
758
|
+
branch_after=current_branch,
|
|
759
|
+
execution_time=execution_time,
|
|
760
|
+
)
|
|
761
|
+
|
|
762
|
+
def get_repository_status(self) -> Dict[str, Any]:
|
|
763
|
+
"""Get comprehensive repository status."""
|
|
764
|
+
try:
|
|
765
|
+
current_branch = self.get_current_branch()
|
|
766
|
+
branch_info = self.get_branch_info(current_branch)
|
|
767
|
+
all_branches = self.get_all_branches()
|
|
768
|
+
|
|
769
|
+
return {
|
|
770
|
+
"current_branch": current_branch,
|
|
771
|
+
"branch_info": branch_info,
|
|
772
|
+
"total_branches": len(all_branches),
|
|
773
|
+
"working_directory_clean": self.is_working_directory_clean(),
|
|
774
|
+
"modified_files": self.get_modified_files(),
|
|
775
|
+
"repository_root": str(self.project_root),
|
|
776
|
+
"is_git_repository": self._is_git_repository(),
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
except Exception as e:
|
|
780
|
+
return {
|
|
781
|
+
"error": str(e),
|
|
782
|
+
"repository_root": str(self.project_root),
|
|
783
|
+
"is_git_repository": self._is_git_repository(),
|
|
784
|
+
}
|