quantalogic 0.35.0__py3-none-any.whl → 0.40.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.
- quantalogic/__init__.py +0 -4
- quantalogic/agent.py +603 -363
- quantalogic/agent_config.py +233 -46
- quantalogic/agent_factory.py +34 -22
- quantalogic/coding_agent.py +16 -14
- quantalogic/config.py +2 -1
- quantalogic/console_print_events.py +4 -8
- quantalogic/console_print_token.py +2 -2
- quantalogic/docs_cli.py +15 -10
- quantalogic/event_emitter.py +258 -83
- quantalogic/flow/__init__.py +23 -0
- quantalogic/flow/flow.py +595 -0
- quantalogic/flow/flow_extractor.py +672 -0
- quantalogic/flow/flow_generator.py +89 -0
- quantalogic/flow/flow_manager.py +407 -0
- quantalogic/flow/flow_manager_schema.py +169 -0
- quantalogic/flow/flow_yaml.md +419 -0
- quantalogic/generative_model.py +109 -77
- quantalogic/get_model_info.py +5 -5
- quantalogic/interactive_text_editor.py +100 -73
- quantalogic/main.py +17 -21
- quantalogic/model_info_list.py +3 -3
- quantalogic/model_info_litellm.py +14 -14
- quantalogic/prompts.py +2 -1
- quantalogic/{llm.py → quantlitellm.py} +29 -39
- quantalogic/search_agent.py +4 -4
- quantalogic/server/models.py +4 -1
- quantalogic/task_file_reader.py +5 -5
- quantalogic/task_runner.py +20 -20
- quantalogic/tool_manager.py +10 -21
- quantalogic/tools/__init__.py +98 -68
- quantalogic/tools/composio/composio.py +416 -0
- quantalogic/tools/{generate_database_report_tool.py → database/generate_database_report_tool.py} +4 -9
- quantalogic/tools/database/sql_query_tool_advanced.py +261 -0
- quantalogic/tools/document_tools/markdown_to_docx_tool.py +620 -0
- quantalogic/tools/document_tools/markdown_to_epub_tool.py +438 -0
- quantalogic/tools/document_tools/markdown_to_html_tool.py +362 -0
- quantalogic/tools/document_tools/markdown_to_ipynb_tool.py +319 -0
- quantalogic/tools/document_tools/markdown_to_latex_tool.py +420 -0
- quantalogic/tools/document_tools/markdown_to_pdf_tool.py +623 -0
- quantalogic/tools/document_tools/markdown_to_pptx_tool.py +319 -0
- quantalogic/tools/duckduckgo_search_tool.py +2 -4
- quantalogic/tools/finance/alpha_vantage_tool.py +440 -0
- quantalogic/tools/finance/ccxt_tool.py +373 -0
- quantalogic/tools/finance/finance_llm_tool.py +387 -0
- quantalogic/tools/finance/google_finance.py +192 -0
- quantalogic/tools/finance/market_intelligence_tool.py +520 -0
- quantalogic/tools/finance/technical_analysis_tool.py +491 -0
- quantalogic/tools/finance/tradingview_tool.py +336 -0
- quantalogic/tools/finance/yahoo_finance.py +236 -0
- quantalogic/tools/git/bitbucket_clone_repo_tool.py +181 -0
- quantalogic/tools/git/bitbucket_operations_tool.py +326 -0
- quantalogic/tools/git/clone_repo_tool.py +189 -0
- quantalogic/tools/git/git_operations_tool.py +532 -0
- quantalogic/tools/google_packages/google_news_tool.py +480 -0
- quantalogic/tools/grep_app_tool.py +123 -186
- quantalogic/tools/{dalle_e.py → image_generation/dalle_e.py} +37 -27
- quantalogic/tools/jinja_tool.py +6 -10
- quantalogic/tools/language_handlers/__init__.py +22 -9
- quantalogic/tools/list_directory_tool.py +131 -42
- quantalogic/tools/llm_tool.py +45 -15
- quantalogic/tools/llm_vision_tool.py +59 -7
- quantalogic/tools/markitdown_tool.py +17 -5
- quantalogic/tools/nasa_packages/models.py +47 -0
- quantalogic/tools/nasa_packages/nasa_apod_tool.py +232 -0
- quantalogic/tools/nasa_packages/nasa_neows_tool.py +147 -0
- quantalogic/tools/nasa_packages/services.py +82 -0
- quantalogic/tools/presentation_tools/presentation_llm_tool.py +396 -0
- quantalogic/tools/product_hunt/product_hunt_tool.py +258 -0
- quantalogic/tools/product_hunt/services.py +63 -0
- quantalogic/tools/rag_tool/__init__.py +48 -0
- quantalogic/tools/rag_tool/document_metadata.py +15 -0
- quantalogic/tools/rag_tool/query_response.py +20 -0
- quantalogic/tools/rag_tool/rag_tool.py +566 -0
- quantalogic/tools/rag_tool/rag_tool_beta.py +264 -0
- quantalogic/tools/read_html_tool.py +24 -38
- quantalogic/tools/replace_in_file_tool.py +10 -10
- quantalogic/tools/safe_python_interpreter_tool.py +10 -24
- quantalogic/tools/search_definition_names.py +2 -2
- quantalogic/tools/sequence_tool.py +14 -23
- quantalogic/tools/sql_query_tool.py +17 -19
- quantalogic/tools/tool.py +39 -15
- quantalogic/tools/unified_diff_tool.py +1 -1
- quantalogic/tools/utilities/csv_processor_tool.py +234 -0
- quantalogic/tools/utilities/download_file_tool.py +179 -0
- quantalogic/tools/utilities/mermaid_validator_tool.py +661 -0
- quantalogic/tools/utils/__init__.py +1 -4
- quantalogic/tools/utils/create_sample_database.py +24 -38
- quantalogic/tools/utils/generate_database_report.py +74 -82
- quantalogic/tools/wikipedia_search_tool.py +17 -21
- quantalogic/utils/ask_user_validation.py +1 -1
- quantalogic/utils/async_utils.py +35 -0
- quantalogic/utils/check_version.py +3 -5
- quantalogic/utils/get_all_models.py +2 -1
- quantalogic/utils/git_ls.py +21 -7
- quantalogic/utils/lm_studio_model_info.py +9 -7
- quantalogic/utils/python_interpreter.py +113 -43
- quantalogic/utils/xml_utility.py +178 -0
- quantalogic/version_check.py +1 -1
- quantalogic/welcome_message.py +7 -7
- quantalogic/xml_parser.py +0 -1
- {quantalogic-0.35.0.dist-info → quantalogic-0.40.0.dist-info}/METADATA +41 -1
- quantalogic-0.40.0.dist-info/RECORD +148 -0
- quantalogic-0.35.0.dist-info/RECORD +0 -102
- {quantalogic-0.35.0.dist-info → quantalogic-0.40.0.dist-info}/LICENSE +0 -0
- {quantalogic-0.35.0.dist-info → quantalogic-0.40.0.dist-info}/WHEEL +0 -0
- {quantalogic-0.35.0.dist-info → quantalogic-0.40.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,532 @@
|
|
1
|
+
"""Tool for performing Git operations like creating branches and making commits."""
|
2
|
+
|
3
|
+
import os
|
4
|
+
import re
|
5
|
+
from typing import ClassVar, Dict
|
6
|
+
from urllib.parse import urlparse
|
7
|
+
|
8
|
+
from git import Repo
|
9
|
+
from git.exc import GitCommandError
|
10
|
+
from loguru import logger
|
11
|
+
from pydantic import Field, validator
|
12
|
+
|
13
|
+
from quantalogic.tools.tool import Tool, ToolArgument
|
14
|
+
|
15
|
+
|
16
|
+
class GitOperationsTool(Tool):
|
17
|
+
"""Tool for Git operations including branch creation and commits.
|
18
|
+
|
19
|
+
This tool provides a simple interface for common Git operations like creating branches,
|
20
|
+
making commits, pushing changes, and more. It handles both public and private repositories
|
21
|
+
through token-based authentication.
|
22
|
+
|
23
|
+
Examples:
|
24
|
+
Create a new branch and switch to it:
|
25
|
+
```python
|
26
|
+
tool = GitOperationsTool(auth_token="your_github_token")
|
27
|
+
tool.execute(
|
28
|
+
repo_path="/path/to/repo",
|
29
|
+
operation="create_branch",
|
30
|
+
branch_name="feature/new-feature"
|
31
|
+
)
|
32
|
+
```
|
33
|
+
|
34
|
+
Make a commit with specific files:
|
35
|
+
```python
|
36
|
+
tool.execute(
|
37
|
+
repo_path="/path/to/repo",
|
38
|
+
operation="commit",
|
39
|
+
commit_message="Add new feature implementation",
|
40
|
+
files_to_commit="file1.py,file2.py"
|
41
|
+
)
|
42
|
+
```
|
43
|
+
|
44
|
+
Push changes to remote:
|
45
|
+
```python
|
46
|
+
tool.execute(
|
47
|
+
repo_path="/path/to/repo",
|
48
|
+
operation="push"
|
49
|
+
)
|
50
|
+
```
|
51
|
+
|
52
|
+
Pull latest changes:
|
53
|
+
```python
|
54
|
+
tool.execute(
|
55
|
+
repo_path="/path/to/repo",
|
56
|
+
operation="pull"
|
57
|
+
)
|
58
|
+
```
|
59
|
+
|
60
|
+
Switch to existing branch:
|
61
|
+
```python
|
62
|
+
tool.execute(
|
63
|
+
repo_path="/path/to/repo",
|
64
|
+
operation="checkout",
|
65
|
+
branch_name="main"
|
66
|
+
)
|
67
|
+
```
|
68
|
+
"""
|
69
|
+
|
70
|
+
name: str = "git_operations_tool"
|
71
|
+
description: str = (
|
72
|
+
"Performs Git operations on a repository including creating branches, "
|
73
|
+
"making commits, pushing changes, pulling updates, and checking out branches. "
|
74
|
+
"Automatically handles authentication for private repositories using the provided token."
|
75
|
+
)
|
76
|
+
need_validation: bool = False
|
77
|
+
auth_token: str = Field(default=None, description="Authentication token for private repositories")
|
78
|
+
provider_urls: ClassVar[Dict[str, str]] = {
|
79
|
+
"github.com": "https://github.com",
|
80
|
+
"gitlab.com": "https://gitlab.com",
|
81
|
+
"bitbucket.org": "https://bitbucket.org",
|
82
|
+
"dev.azure.com": "https://dev.azure.com"
|
83
|
+
}
|
84
|
+
|
85
|
+
def __init__(self, auth_token: str = None, **data):
|
86
|
+
"""Initialize the tool with an optional auth token.
|
87
|
+
|
88
|
+
Args:
|
89
|
+
auth_token: Authentication token for private repositories
|
90
|
+
**data: Additional tool configuration data
|
91
|
+
"""
|
92
|
+
super().__init__(**data)
|
93
|
+
self.auth_token = auth_token
|
94
|
+
|
95
|
+
@validator('auth_token')
|
96
|
+
def validate_auth_token(cls, v):
|
97
|
+
"""Validate the authentication token format.
|
98
|
+
|
99
|
+
Args:
|
100
|
+
v: The token value to validate
|
101
|
+
|
102
|
+
Returns:
|
103
|
+
The validated token
|
104
|
+
|
105
|
+
Raises:
|
106
|
+
ValueError: If the token format is invalid
|
107
|
+
"""
|
108
|
+
if v is not None:
|
109
|
+
if not isinstance(v, str):
|
110
|
+
raise ValueError("Authentication token must be a string")
|
111
|
+
if len(v.strip()) < 8:
|
112
|
+
raise ValueError("Authentication token seems too short")
|
113
|
+
if not re.match(r'^[a-zA-Z0-9_\-]+$', v):
|
114
|
+
raise ValueError("Authentication token contains invalid characters")
|
115
|
+
return v
|
116
|
+
|
117
|
+
def _get_provider_from_url(self, url: str) -> str:
|
118
|
+
"""Determine the Git provider from the repository URL.
|
119
|
+
|
120
|
+
Args:
|
121
|
+
url: Repository URL
|
122
|
+
|
123
|
+
Returns:
|
124
|
+
str: Provider name or 'unknown'
|
125
|
+
"""
|
126
|
+
try:
|
127
|
+
parsed = urlparse(url)
|
128
|
+
domain = parsed.netloc.lower()
|
129
|
+
for provider, base_url in self.provider_urls.items():
|
130
|
+
if provider in domain:
|
131
|
+
return provider
|
132
|
+
return 'unknown'
|
133
|
+
except Exception:
|
134
|
+
return 'unknown'
|
135
|
+
|
136
|
+
def _setup_auth_for_remote(self, repo: Repo) -> tuple[str, bool]:
|
137
|
+
"""Setup authentication for remote operations if needed.
|
138
|
+
|
139
|
+
Args:
|
140
|
+
repo: Git repository instance
|
141
|
+
|
142
|
+
Returns:
|
143
|
+
tuple: Original remote URL and whether URL was modified
|
144
|
+
|
145
|
+
Raises:
|
146
|
+
ValueError: If remote URL is invalid or authentication setup fails
|
147
|
+
"""
|
148
|
+
try:
|
149
|
+
original_url = repo.remote().url
|
150
|
+
if not original_url:
|
151
|
+
raise ValueError("No remote URL found in repository")
|
152
|
+
|
153
|
+
# Only modify HTTPS URLs
|
154
|
+
if not self.auth_token or not original_url.startswith('https://'):
|
155
|
+
return original_url, False
|
156
|
+
|
157
|
+
provider = self._get_provider_from_url(original_url)
|
158
|
+
if provider == 'unknown':
|
159
|
+
logger.warning(f"Unknown Git provider for URL: {original_url}")
|
160
|
+
|
161
|
+
# Remove any existing credentials from URL
|
162
|
+
url_parts = original_url.split('@')
|
163
|
+
if len(url_parts) > 1:
|
164
|
+
base_url = url_parts[-1]
|
165
|
+
new_url = f"https://{self.auth_token}@{base_url}"
|
166
|
+
else:
|
167
|
+
new_url = original_url.replace("https://", f"https://{self.auth_token}@")
|
168
|
+
|
169
|
+
repo.remote().set_url(new_url)
|
170
|
+
logger.debug("Successfully configured authentication for remote operations")
|
171
|
+
return original_url, True
|
172
|
+
|
173
|
+
except Exception as e:
|
174
|
+
raise ValueError(f"Failed to setup authentication: {str(e)}")
|
175
|
+
|
176
|
+
arguments: list = [
|
177
|
+
ToolArgument(
|
178
|
+
name="repo_path",
|
179
|
+
arg_type="string",
|
180
|
+
description=(
|
181
|
+
"The local path to the Git repository. This should be an absolute path to "
|
182
|
+
"an existing Git repository on your system.\n"
|
183
|
+
"Examples:\n"
|
184
|
+
"- '/home/user/projects/my-repo'\n"
|
185
|
+
"- '/path/to/project'\n"
|
186
|
+
"- './current/directory/repo'"
|
187
|
+
),
|
188
|
+
required=True,
|
189
|
+
example="/path/to/repo",
|
190
|
+
),
|
191
|
+
ToolArgument(
|
192
|
+
name="operation",
|
193
|
+
arg_type="string",
|
194
|
+
description=(
|
195
|
+
"Git operation to perform. Available operations:\n"
|
196
|
+
"- 'create_branch': Create and checkout a new branch\n"
|
197
|
+
"- 'commit': Create a new commit with specified files\n"
|
198
|
+
"- 'push': Push local changes to remote repository\n"
|
199
|
+
"- 'pull': Pull latest changes from remote repository\n"
|
200
|
+
"- 'checkout': Switch to an existing branch\n\n"
|
201
|
+
"Usage examples:\n"
|
202
|
+
"- operation='create_branch' + branch_name='feature/new-feature'\n"
|
203
|
+
"- operation='commit' + commit_message='Add new feature' + files_to_commit='file1.py,file2.py'\n"
|
204
|
+
"- operation='push' + branch_name='feature/new-feature' (pushes specified branch)\n"
|
205
|
+
"- operation='checkout' + branch_name='main'"
|
206
|
+
),
|
207
|
+
required=True,
|
208
|
+
example="create_branch",
|
209
|
+
),
|
210
|
+
ToolArgument(
|
211
|
+
name="branch_name",
|
212
|
+
arg_type="string",
|
213
|
+
description=(
|
214
|
+
"Name of the branch to create or switch to. Required for 'create_branch' and 'checkout' operations.\n"
|
215
|
+
"Branch naming conventions:\n"
|
216
|
+
"- feature/[feature-name] for new features\n"
|
217
|
+
"- bugfix/[bug-name] for bug fixes\n"
|
218
|
+
"- hotfix/[fix-name] for urgent fixes\n"
|
219
|
+
"- release/[version] for release branches\n\n"
|
220
|
+
"Examples:\n"
|
221
|
+
"- 'feature/user-authentication'\n"
|
222
|
+
"- 'bugfix/login-error'\n"
|
223
|
+
"- 'main' or 'master' for main branch\n"
|
224
|
+
"- 'develop' for development branch"
|
225
|
+
),
|
226
|
+
required=False,
|
227
|
+
example="feature/new-feature",
|
228
|
+
),
|
229
|
+
ToolArgument(
|
230
|
+
name="commit_message",
|
231
|
+
arg_type="string",
|
232
|
+
description=(
|
233
|
+
"Commit message when operation is 'commit'. If not provided, a default message will be generated.\n"
|
234
|
+
"Commit message guidelines:\n"
|
235
|
+
"- Start with a verb (Add, Fix, Update, Refactor, etc.)\n"
|
236
|
+
"- Keep it concise but descriptive\n"
|
237
|
+
"- Include ticket/issue number if applicable\n\n"
|
238
|
+
"Examples:\n"
|
239
|
+
"- 'Add user authentication feature'\n"
|
240
|
+
"- 'Fix login validation bug #123'\n"
|
241
|
+
"- 'Update README with API documentation'\n"
|
242
|
+
"- 'Refactor database connection logic'"
|
243
|
+
),
|
244
|
+
required=False,
|
245
|
+
example="Add new feature implementation",
|
246
|
+
),
|
247
|
+
ToolArgument(
|
248
|
+
name="files_to_commit",
|
249
|
+
arg_type="string",
|
250
|
+
description=(
|
251
|
+
"Comma-separated list of files to commit, or '.' for all changes. Used with 'commit' operation.\n"
|
252
|
+
"File specification:\n"
|
253
|
+
"- Use '.' to commit all changes\n"
|
254
|
+
"- Use relative paths from repo root\n"
|
255
|
+
"- Separate multiple files with commas\n"
|
256
|
+
"- Supports wildcards (*.py, *.js)\n\n"
|
257
|
+
"Examples:\n"
|
258
|
+
"- '.' (all changes)\n"
|
259
|
+
"- 'src/main.py,tests/test_main.py'\n"
|
260
|
+
"- 'docs/*.md'\n"
|
261
|
+
"- 'feature/auth/*.py,feature/auth/*.js'"
|
262
|
+
),
|
263
|
+
required=False,
|
264
|
+
example="file1.py,file2.py",
|
265
|
+
default=".",
|
266
|
+
),
|
267
|
+
]
|
268
|
+
|
269
|
+
def execute(
|
270
|
+
self,
|
271
|
+
repo_path: str,
|
272
|
+
operation: str,
|
273
|
+
branch_name: str = None,
|
274
|
+
commit_message: str = None,
|
275
|
+
files_to_commit: str = ".",
|
276
|
+
) -> str:
|
277
|
+
"""Executes the specified Git operation.
|
278
|
+
|
279
|
+
Args:
|
280
|
+
repo_path: Path to the local Git repository
|
281
|
+
operation: Git operation to perform
|
282
|
+
branch_name: Name of the branch (for create_branch/checkout)
|
283
|
+
commit_message: Commit message (for commit)
|
284
|
+
files_to_commit: Files to commit (for commit)
|
285
|
+
|
286
|
+
Returns:
|
287
|
+
str: Result message
|
288
|
+
|
289
|
+
Raises:
|
290
|
+
GitCommandError: If there's an error during Git operations
|
291
|
+
ValueError: If the parameters are invalid
|
292
|
+
"""
|
293
|
+
try:
|
294
|
+
if not os.path.exists(repo_path):
|
295
|
+
raise ValueError(f"Repository path does not exist: {repo_path}")
|
296
|
+
if not os.path.isdir(os.path.join(repo_path, '.git')):
|
297
|
+
raise ValueError(f"Not a valid Git repository: {repo_path}")
|
298
|
+
|
299
|
+
repo = Repo(repo_path)
|
300
|
+
|
301
|
+
# Validate remote configuration for operations that need it
|
302
|
+
if operation in ['push', 'pull']:
|
303
|
+
if not repo.remotes:
|
304
|
+
raise ValueError("Repository has no configured remotes")
|
305
|
+
remote = repo.remote()
|
306
|
+
if not remote.url:
|
307
|
+
raise ValueError("Remote URL is not configured")
|
308
|
+
|
309
|
+
if operation == "create_branch":
|
310
|
+
if not branch_name:
|
311
|
+
raise ValueError("branch_name is required for create_branch operation")
|
312
|
+
|
313
|
+
# Check if branch already exists
|
314
|
+
if branch_name in repo.heads:
|
315
|
+
raise ValueError(f"Branch '{branch_name}' already exists")
|
316
|
+
|
317
|
+
# Create and checkout new branch
|
318
|
+
current = repo.create_head(branch_name)
|
319
|
+
current.checkout()
|
320
|
+
logger.info(f"Created and checked out branch: {branch_name}")
|
321
|
+
return f"Successfully created and checked out branch: {branch_name}"
|
322
|
+
|
323
|
+
elif operation == "commit":
|
324
|
+
# Validate repository state
|
325
|
+
if repo.is_dirty(untracked_files=True):
|
326
|
+
# Handle default commit behavior
|
327
|
+
if files_to_commit == ".":
|
328
|
+
# Stage all changes
|
329
|
+
repo.git.add(A=True)
|
330
|
+
else:
|
331
|
+
# Stage specific files
|
332
|
+
for file in files_to_commit.split(","):
|
333
|
+
file = file.strip()
|
334
|
+
file_path = os.path.join(repo_path, file)
|
335
|
+
if os.path.exists(file_path):
|
336
|
+
repo.git.add(file)
|
337
|
+
else:
|
338
|
+
logger.warning(f"File not found: {file}")
|
339
|
+
|
340
|
+
# Get list of staged files
|
341
|
+
staged_files = repo.index.diff("HEAD")
|
342
|
+
if not staged_files:
|
343
|
+
return "No changes to commit"
|
344
|
+
|
345
|
+
# Generate default commit message if none provided
|
346
|
+
if not commit_message:
|
347
|
+
status = repo.git.status('--porcelain')
|
348
|
+
changes = self._analyze_changes(status)
|
349
|
+
commit_message = self._generate_commit_message(changes)
|
350
|
+
|
351
|
+
# Create commit
|
352
|
+
commit = repo.index.commit(commit_message)
|
353
|
+
logger.info(f"Created commit: {commit.hexsha[:8]}")
|
354
|
+
return f"Successfully created commit: {commit.hexsha[:8]}\n{commit_message}"
|
355
|
+
else:
|
356
|
+
return "No changes to commit"
|
357
|
+
|
358
|
+
elif operation in ["push", "pull"]:
|
359
|
+
# Setup authentication if needed
|
360
|
+
original_url, url_modified = self._setup_auth_for_remote(repo)
|
361
|
+
|
362
|
+
try:
|
363
|
+
if operation == "push":
|
364
|
+
current_branch = repo.active_branch
|
365
|
+
try:
|
366
|
+
# Try to push with current configuration
|
367
|
+
repo.remote().push(current_branch)
|
368
|
+
except GitCommandError as e:
|
369
|
+
if "no upstream branch" in str(e).lower():
|
370
|
+
# Set upstream and push
|
371
|
+
repo.git.push('--set-upstream', 'origin', current_branch.name)
|
372
|
+
else:
|
373
|
+
raise
|
374
|
+
logger.info(f"Pushed changes to remote repository on branch: {current_branch.name}")
|
375
|
+
result = f"Successfully pushed changes to remote repository on branch: {current_branch.name}"
|
376
|
+
else: # pull
|
377
|
+
repo.remote().pull()
|
378
|
+
logger.info("Pulled latest changes from remote repository")
|
379
|
+
result = "Successfully pulled latest changes from remote repository"
|
380
|
+
finally:
|
381
|
+
# Reset URL if it was modified
|
382
|
+
if url_modified:
|
383
|
+
repo.remote().set_url(original_url)
|
384
|
+
|
385
|
+
return result
|
386
|
+
|
387
|
+
elif operation == "checkout":
|
388
|
+
if not branch_name:
|
389
|
+
raise ValueError("branch_name is required for checkout operation")
|
390
|
+
|
391
|
+
# Check if branch exists
|
392
|
+
if branch_name in repo.heads:
|
393
|
+
# Check if there are uncommitted changes
|
394
|
+
if repo.is_dirty():
|
395
|
+
raise ValueError("Cannot checkout branch: You have uncommitted changes")
|
396
|
+
# Checkout existing branch
|
397
|
+
repo.heads[branch_name].checkout()
|
398
|
+
logger.info(f"Checked out existing branch: {branch_name}")
|
399
|
+
return f"Successfully checked out branch: {branch_name}"
|
400
|
+
else:
|
401
|
+
raise ValueError(f"Branch '{branch_name}' does not exist. Use create_branch operation to create a new branch.")
|
402
|
+
|
403
|
+
else:
|
404
|
+
raise ValueError(f"Unsupported operation: {operation}")
|
405
|
+
|
406
|
+
except GitCommandError as e:
|
407
|
+
error_msg = str(e)
|
408
|
+
# Remove sensitive information from error message
|
409
|
+
if self.auth_token:
|
410
|
+
error_msg = error_msg.replace(self.auth_token, "***")
|
411
|
+
logger.error(f"Git operation failed: {error_msg}")
|
412
|
+
raise GitCommandError("Git operation failed", e.status)
|
413
|
+
|
414
|
+
except Exception as e:
|
415
|
+
logger.error(f"An error occurred during Git operation: {str(e)}")
|
416
|
+
raise ValueError(f"An error occurred during Git operation: {str(e)}")
|
417
|
+
|
418
|
+
def _analyze_changes(self, status: str) -> dict:
|
419
|
+
"""Analyze Git status output to categorize changes.
|
420
|
+
|
421
|
+
Args:
|
422
|
+
status: Git status porcelain output
|
423
|
+
|
424
|
+
Returns:
|
425
|
+
dict: Categorized changes
|
426
|
+
"""
|
427
|
+
changes = {
|
428
|
+
'added': [],
|
429
|
+
'modified': [],
|
430
|
+
'deleted': []
|
431
|
+
}
|
432
|
+
|
433
|
+
for line in status.split('\n'):
|
434
|
+
if line:
|
435
|
+
status_code = line[:2]
|
436
|
+
file_path = line[3:]
|
437
|
+
if status_code.startswith('A'):
|
438
|
+
changes['added'].append(file_path)
|
439
|
+
elif status_code.startswith('M'):
|
440
|
+
changes['modified'].append(file_path)
|
441
|
+
elif status_code.startswith('D'):
|
442
|
+
changes['deleted'].append(file_path)
|
443
|
+
|
444
|
+
return changes
|
445
|
+
|
446
|
+
def _generate_commit_message(self, changes: dict) -> str:
|
447
|
+
"""Generate a descriptive commit message from changes.
|
448
|
+
|
449
|
+
Args:
|
450
|
+
changes: Dictionary of categorized changes
|
451
|
+
|
452
|
+
Returns:
|
453
|
+
str: Generated commit message
|
454
|
+
"""
|
455
|
+
message_parts = []
|
456
|
+
if changes['added']:
|
457
|
+
message_parts.append(f"Add {len(changes['added'])} file(s)")
|
458
|
+
if changes['modified']:
|
459
|
+
message_parts.append(f"Update {len(changes['modified'])} file(s)")
|
460
|
+
if changes['deleted']:
|
461
|
+
message_parts.append(f"Remove {len(changes['deleted'])} file(s)")
|
462
|
+
|
463
|
+
commit_message = " & ".join(message_parts)
|
464
|
+
|
465
|
+
# Add file details
|
466
|
+
details = []
|
467
|
+
if changes['added']:
|
468
|
+
details.append("\nAdded files:\n- " + "\n- ".join(changes['added']))
|
469
|
+
if changes['modified']:
|
470
|
+
details.append("\nModified files:\n- " + "\n- ".join(changes['modified']))
|
471
|
+
if changes['deleted']:
|
472
|
+
details.append("\nDeleted files:\n- " + "\n- ".join(changes['deleted']))
|
473
|
+
|
474
|
+
return commit_message + "".join(details)
|
475
|
+
|
476
|
+
|
477
|
+
if __name__ == "__main__":
|
478
|
+
# Example usage of the GitOperationsTool
|
479
|
+
def run_example(repo_path: str):
|
480
|
+
"""Run example Git operations using the tool."""
|
481
|
+
tool = GitOperationsTool(auth_token="your_token_here")
|
482
|
+
|
483
|
+
try:
|
484
|
+
# 1. Create and switch to a new feature branch
|
485
|
+
logger.info("Creating new feature branch...")
|
486
|
+
tool.execute(
|
487
|
+
repo_path=repo_path,
|
488
|
+
operation="create_branch",
|
489
|
+
branch_name="feature/example-feature"
|
490
|
+
)
|
491
|
+
|
492
|
+
# 2. Make some changes and commit them
|
493
|
+
logger.info("Creating a commit...")
|
494
|
+
tool.execute(
|
495
|
+
repo_path=repo_path,
|
496
|
+
operation="commit",
|
497
|
+
commit_message="Add new example feature",
|
498
|
+
files_to_commit="." # Commit all changes
|
499
|
+
)
|
500
|
+
|
501
|
+
# 3. Push changes to remote
|
502
|
+
logger.info("Pushing changes to remote...")
|
503
|
+
tool.execute(
|
504
|
+
repo_path=repo_path,
|
505
|
+
operation="push"
|
506
|
+
)
|
507
|
+
|
508
|
+
# 4. Switch back to main branch
|
509
|
+
logger.info("Switching back to main branch...")
|
510
|
+
tool.execute(
|
511
|
+
repo_path=repo_path,
|
512
|
+
operation="checkout",
|
513
|
+
branch_name="main"
|
514
|
+
)
|
515
|
+
|
516
|
+
# 5. Pull latest changes
|
517
|
+
logger.info("Pulling latest changes...")
|
518
|
+
tool.execute(
|
519
|
+
repo_path=repo_path,
|
520
|
+
operation="pull"
|
521
|
+
)
|
522
|
+
|
523
|
+
except Exception as e:
|
524
|
+
logger.error(f"Example failed: {str(e)}")
|
525
|
+
raise
|
526
|
+
|
527
|
+
# To run the example, uncomment and modify the path:
|
528
|
+
# run_example("/path/to/your/repo")
|
529
|
+
|
530
|
+
# Print tool documentation
|
531
|
+
tool = GitOperationsTool(auth_token="your_token_here")
|
532
|
+
print(tool.to_markdown())
|