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.

Files changed (159) hide show
  1. claude_mpm/__init__.py +17 -0
  2. claude_mpm/__main__.py +14 -0
  3. claude_mpm/_version.py +32 -0
  4. claude_mpm/agents/BASE_AGENT_TEMPLATE.md +88 -0
  5. claude_mpm/agents/INSTRUCTIONS.md +375 -0
  6. claude_mpm/agents/__init__.py +118 -0
  7. claude_mpm/agents/agent_loader.py +621 -0
  8. claude_mpm/agents/agent_loader_integration.py +229 -0
  9. claude_mpm/agents/agents_metadata.py +204 -0
  10. claude_mpm/agents/base_agent.json +27 -0
  11. claude_mpm/agents/base_agent_loader.py +519 -0
  12. claude_mpm/agents/schema/agent_schema.json +160 -0
  13. claude_mpm/agents/system_agent_config.py +587 -0
  14. claude_mpm/agents/templates/__init__.py +101 -0
  15. claude_mpm/agents/templates/data_engineer_agent.json +46 -0
  16. claude_mpm/agents/templates/documentation_agent.json +45 -0
  17. claude_mpm/agents/templates/engineer_agent.json +49 -0
  18. claude_mpm/agents/templates/ops_agent.json +46 -0
  19. claude_mpm/agents/templates/qa_agent.json +45 -0
  20. claude_mpm/agents/templates/research_agent.json +49 -0
  21. claude_mpm/agents/templates/security_agent.json +46 -0
  22. claude_mpm/agents/templates/update-optimized-specialized-agents.json +374 -0
  23. claude_mpm/agents/templates/version_control_agent.json +46 -0
  24. claude_mpm/agents/test_fix_deployment/.claude-pm/config/project.json +6 -0
  25. claude_mpm/cli.py +655 -0
  26. claude_mpm/cli_main.py +13 -0
  27. claude_mpm/cli_module/__init__.py +15 -0
  28. claude_mpm/cli_module/args.py +222 -0
  29. claude_mpm/cli_module/commands.py +203 -0
  30. claude_mpm/cli_module/migration_example.py +183 -0
  31. claude_mpm/cli_module/refactoring_guide.md +253 -0
  32. claude_mpm/cli_old/__init__.py +1 -0
  33. claude_mpm/cli_old/ticket_cli.py +102 -0
  34. claude_mpm/config/__init__.py +5 -0
  35. claude_mpm/config/hook_config.py +42 -0
  36. claude_mpm/constants.py +150 -0
  37. claude_mpm/core/__init__.py +45 -0
  38. claude_mpm/core/agent_name_normalizer.py +248 -0
  39. claude_mpm/core/agent_registry.py +627 -0
  40. claude_mpm/core/agent_registry.py.bak +312 -0
  41. claude_mpm/core/agent_session_manager.py +273 -0
  42. claude_mpm/core/base_service.py +747 -0
  43. claude_mpm/core/base_service.py.bak +406 -0
  44. claude_mpm/core/config.py +334 -0
  45. claude_mpm/core/config_aliases.py +292 -0
  46. claude_mpm/core/container.py +347 -0
  47. claude_mpm/core/factories.py +281 -0
  48. claude_mpm/core/framework_loader.py +472 -0
  49. claude_mpm/core/injectable_service.py +206 -0
  50. claude_mpm/core/interfaces.py +539 -0
  51. claude_mpm/core/logger.py +468 -0
  52. claude_mpm/core/minimal_framework_loader.py +107 -0
  53. claude_mpm/core/mixins.py +150 -0
  54. claude_mpm/core/service_registry.py +299 -0
  55. claude_mpm/core/session_manager.py +190 -0
  56. claude_mpm/core/simple_runner.py +511 -0
  57. claude_mpm/core/tool_access_control.py +173 -0
  58. claude_mpm/hooks/README.md +243 -0
  59. claude_mpm/hooks/__init__.py +5 -0
  60. claude_mpm/hooks/base_hook.py +154 -0
  61. claude_mpm/hooks/builtin/__init__.py +1 -0
  62. claude_mpm/hooks/builtin/logging_hook_example.py +165 -0
  63. claude_mpm/hooks/builtin/post_delegation_hook_example.py +124 -0
  64. claude_mpm/hooks/builtin/pre_delegation_hook_example.py +125 -0
  65. claude_mpm/hooks/builtin/submit_hook_example.py +100 -0
  66. claude_mpm/hooks/builtin/ticket_extraction_hook_example.py +237 -0
  67. claude_mpm/hooks/builtin/todo_agent_prefix_hook.py +239 -0
  68. claude_mpm/hooks/builtin/workflow_start_hook.py +181 -0
  69. claude_mpm/hooks/hook_client.py +264 -0
  70. claude_mpm/hooks/hook_runner.py +370 -0
  71. claude_mpm/hooks/json_rpc_executor.py +259 -0
  72. claude_mpm/hooks/json_rpc_hook_client.py +319 -0
  73. claude_mpm/hooks/tool_call_interceptor.py +204 -0
  74. claude_mpm/init.py +246 -0
  75. claude_mpm/orchestration/SUBPROCESS_DESIGN.md +66 -0
  76. claude_mpm/orchestration/__init__.py +6 -0
  77. claude_mpm/orchestration/archive/direct_orchestrator.py +195 -0
  78. claude_mpm/orchestration/archive/factory.py +215 -0
  79. claude_mpm/orchestration/archive/hook_enabled_orchestrator.py +188 -0
  80. claude_mpm/orchestration/archive/hook_integration_example.py +178 -0
  81. claude_mpm/orchestration/archive/interactive_subprocess_orchestrator.py +826 -0
  82. claude_mpm/orchestration/archive/orchestrator.py +501 -0
  83. claude_mpm/orchestration/archive/pexpect_orchestrator.py +252 -0
  84. claude_mpm/orchestration/archive/pty_orchestrator.py +270 -0
  85. claude_mpm/orchestration/archive/simple_orchestrator.py +82 -0
  86. claude_mpm/orchestration/archive/subprocess_orchestrator.py +801 -0
  87. claude_mpm/orchestration/archive/system_prompt_orchestrator.py +278 -0
  88. claude_mpm/orchestration/archive/wrapper_orchestrator.py +187 -0
  89. claude_mpm/scripts/__init__.py +1 -0
  90. claude_mpm/scripts/ticket.py +269 -0
  91. claude_mpm/services/__init__.py +10 -0
  92. claude_mpm/services/agent_deployment.py +955 -0
  93. claude_mpm/services/agent_lifecycle_manager.py +948 -0
  94. claude_mpm/services/agent_management_service.py +596 -0
  95. claude_mpm/services/agent_modification_tracker.py +841 -0
  96. claude_mpm/services/agent_profile_loader.py +606 -0
  97. claude_mpm/services/agent_registry.py +677 -0
  98. claude_mpm/services/base_agent_manager.py +380 -0
  99. claude_mpm/services/framework_agent_loader.py +337 -0
  100. claude_mpm/services/framework_claude_md_generator/README.md +92 -0
  101. claude_mpm/services/framework_claude_md_generator/__init__.py +206 -0
  102. claude_mpm/services/framework_claude_md_generator/content_assembler.py +151 -0
  103. claude_mpm/services/framework_claude_md_generator/content_validator.py +126 -0
  104. claude_mpm/services/framework_claude_md_generator/deployment_manager.py +137 -0
  105. claude_mpm/services/framework_claude_md_generator/section_generators/__init__.py +106 -0
  106. claude_mpm/services/framework_claude_md_generator/section_generators/agents.py +582 -0
  107. claude_mpm/services/framework_claude_md_generator/section_generators/claude_pm_init.py +97 -0
  108. claude_mpm/services/framework_claude_md_generator/section_generators/core_responsibilities.py +27 -0
  109. claude_mpm/services/framework_claude_md_generator/section_generators/delegation_constraints.py +23 -0
  110. claude_mpm/services/framework_claude_md_generator/section_generators/environment_config.py +23 -0
  111. claude_mpm/services/framework_claude_md_generator/section_generators/footer.py +20 -0
  112. claude_mpm/services/framework_claude_md_generator/section_generators/header.py +26 -0
  113. claude_mpm/services/framework_claude_md_generator/section_generators/orchestration_principles.py +30 -0
  114. claude_mpm/services/framework_claude_md_generator/section_generators/role_designation.py +37 -0
  115. claude_mpm/services/framework_claude_md_generator/section_generators/subprocess_validation.py +111 -0
  116. claude_mpm/services/framework_claude_md_generator/section_generators/todo_task_tools.py +89 -0
  117. claude_mpm/services/framework_claude_md_generator/section_generators/troubleshooting.py +39 -0
  118. claude_mpm/services/framework_claude_md_generator/section_manager.py +106 -0
  119. claude_mpm/services/framework_claude_md_generator/version_manager.py +121 -0
  120. claude_mpm/services/framework_claude_md_generator.py +621 -0
  121. claude_mpm/services/hook_service.py +388 -0
  122. claude_mpm/services/hook_service_manager.py +223 -0
  123. claude_mpm/services/json_rpc_hook_manager.py +92 -0
  124. claude_mpm/services/parent_directory_manager/README.md +83 -0
  125. claude_mpm/services/parent_directory_manager/__init__.py +577 -0
  126. claude_mpm/services/parent_directory_manager/backup_manager.py +258 -0
  127. claude_mpm/services/parent_directory_manager/config_manager.py +210 -0
  128. claude_mpm/services/parent_directory_manager/deduplication_manager.py +279 -0
  129. claude_mpm/services/parent_directory_manager/framework_protector.py +143 -0
  130. claude_mpm/services/parent_directory_manager/operations.py +186 -0
  131. claude_mpm/services/parent_directory_manager/state_manager.py +624 -0
  132. claude_mpm/services/parent_directory_manager/template_deployer.py +579 -0
  133. claude_mpm/services/parent_directory_manager/validation_manager.py +378 -0
  134. claude_mpm/services/parent_directory_manager/version_control_helper.py +339 -0
  135. claude_mpm/services/parent_directory_manager/version_manager.py +222 -0
  136. claude_mpm/services/shared_prompt_cache.py +819 -0
  137. claude_mpm/services/ticket_manager.py +213 -0
  138. claude_mpm/services/ticket_manager_di.py +318 -0
  139. claude_mpm/services/ticketing_service_original.py +508 -0
  140. claude_mpm/services/version_control/VERSION +1 -0
  141. claude_mpm/services/version_control/__init__.py +70 -0
  142. claude_mpm/services/version_control/branch_strategy.py +670 -0
  143. claude_mpm/services/version_control/conflict_resolution.py +744 -0
  144. claude_mpm/services/version_control/git_operations.py +784 -0
  145. claude_mpm/services/version_control/semantic_versioning.py +703 -0
  146. claude_mpm/ui/__init__.py +1 -0
  147. claude_mpm/ui/rich_terminal_ui.py +295 -0
  148. claude_mpm/ui/terminal_ui.py +328 -0
  149. claude_mpm/utils/__init__.py +16 -0
  150. claude_mpm/utils/config_manager.py +468 -0
  151. claude_mpm/utils/import_migration_example.py +80 -0
  152. claude_mpm/utils/imports.py +182 -0
  153. claude_mpm/utils/path_operations.py +357 -0
  154. claude_mpm/utils/paths.py +289 -0
  155. claude_mpm-0.3.0.dist-info/METADATA +290 -0
  156. claude_mpm-0.3.0.dist-info/RECORD +159 -0
  157. claude_mpm-0.3.0.dist-info/WHEEL +5 -0
  158. claude_mpm-0.3.0.dist-info/entry_points.txt +4 -0
  159. 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
+ }