ai-coding-assistant 0.5.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.
- ai_coding_assistant-0.5.0.dist-info/METADATA +226 -0
- ai_coding_assistant-0.5.0.dist-info/RECORD +89 -0
- ai_coding_assistant-0.5.0.dist-info/WHEEL +4 -0
- ai_coding_assistant-0.5.0.dist-info/entry_points.txt +3 -0
- ai_coding_assistant-0.5.0.dist-info/licenses/LICENSE +21 -0
- coding_assistant/__init__.py +3 -0
- coding_assistant/__main__.py +19 -0
- coding_assistant/cli/__init__.py +1 -0
- coding_assistant/cli/app.py +158 -0
- coding_assistant/cli/commands/__init__.py +19 -0
- coding_assistant/cli/commands/ask.py +178 -0
- coding_assistant/cli/commands/config.py +438 -0
- coding_assistant/cli/commands/diagram.py +267 -0
- coding_assistant/cli/commands/document.py +410 -0
- coding_assistant/cli/commands/explain.py +192 -0
- coding_assistant/cli/commands/fix.py +249 -0
- coding_assistant/cli/commands/index.py +162 -0
- coding_assistant/cli/commands/refactor.py +245 -0
- coding_assistant/cli/commands/search.py +182 -0
- coding_assistant/cli/commands/serve_docs.py +128 -0
- coding_assistant/cli/repl.py +381 -0
- coding_assistant/cli/theme.py +90 -0
- coding_assistant/codebase/__init__.py +1 -0
- coding_assistant/codebase/crawler.py +93 -0
- coding_assistant/codebase/parser.py +266 -0
- coding_assistant/config/__init__.py +25 -0
- coding_assistant/config/config_manager.py +615 -0
- coding_assistant/config/settings.py +82 -0
- coding_assistant/context/__init__.py +19 -0
- coding_assistant/context/chunker.py +443 -0
- coding_assistant/context/enhanced_retriever.py +322 -0
- coding_assistant/context/hybrid_search.py +311 -0
- coding_assistant/context/ranker.py +355 -0
- coding_assistant/context/retriever.py +119 -0
- coding_assistant/context/window.py +362 -0
- coding_assistant/documentation/__init__.py +23 -0
- coding_assistant/documentation/agents/__init__.py +27 -0
- coding_assistant/documentation/agents/coordinator.py +510 -0
- coding_assistant/documentation/agents/module_documenter.py +111 -0
- coding_assistant/documentation/agents/synthesizer.py +139 -0
- coding_assistant/documentation/agents/task_delegator.py +100 -0
- coding_assistant/documentation/decomposition/__init__.py +21 -0
- coding_assistant/documentation/decomposition/context_preserver.py +477 -0
- coding_assistant/documentation/decomposition/module_detector.py +302 -0
- coding_assistant/documentation/decomposition/partitioner.py +621 -0
- coding_assistant/documentation/generators/__init__.py +14 -0
- coding_assistant/documentation/generators/dataflow_generator.py +440 -0
- coding_assistant/documentation/generators/diagram_generator.py +511 -0
- coding_assistant/documentation/graph/__init__.py +13 -0
- coding_assistant/documentation/graph/dependency_builder.py +468 -0
- coding_assistant/documentation/graph/module_analyzer.py +475 -0
- coding_assistant/documentation/writers/__init__.py +11 -0
- coding_assistant/documentation/writers/markdown_writer.py +322 -0
- coding_assistant/embeddings/__init__.py +0 -0
- coding_assistant/embeddings/generator.py +89 -0
- coding_assistant/embeddings/store.py +187 -0
- coding_assistant/exceptions/__init__.py +50 -0
- coding_assistant/exceptions/base.py +110 -0
- coding_assistant/exceptions/llm.py +249 -0
- coding_assistant/exceptions/recovery.py +263 -0
- coding_assistant/exceptions/storage.py +213 -0
- coding_assistant/exceptions/validation.py +230 -0
- coding_assistant/llm/__init__.py +1 -0
- coding_assistant/llm/client.py +277 -0
- coding_assistant/llm/gemini_client.py +181 -0
- coding_assistant/llm/groq_client.py +160 -0
- coding_assistant/llm/prompts.py +98 -0
- coding_assistant/llm/together_client.py +160 -0
- coding_assistant/operations/__init__.py +13 -0
- coding_assistant/operations/differ.py +369 -0
- coding_assistant/operations/generator.py +347 -0
- coding_assistant/operations/linter.py +430 -0
- coding_assistant/operations/validator.py +406 -0
- coding_assistant/storage/__init__.py +9 -0
- coding_assistant/storage/database.py +363 -0
- coding_assistant/storage/session.py +231 -0
- coding_assistant/utils/__init__.py +31 -0
- coding_assistant/utils/cache.py +477 -0
- coding_assistant/utils/hardware.py +132 -0
- coding_assistant/utils/keystore.py +206 -0
- coding_assistant/utils/logger.py +32 -0
- coding_assistant/utils/progress.py +311 -0
- coding_assistant/validation/__init__.py +13 -0
- coding_assistant/validation/files.py +305 -0
- coding_assistant/validation/inputs.py +335 -0
- coding_assistant/validation/params.py +280 -0
- coding_assistant/validation/sanitizers.py +243 -0
- coding_assistant/vcs/__init__.py +5 -0
- coding_assistant/vcs/git.py +269 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"""Git integration for context retrieval."""
|
|
2
|
+
|
|
3
|
+
from typing import List, Dict, Optional
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from datetime import datetime, timedelta
|
|
6
|
+
import git
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class GitIntegration:
|
|
10
|
+
"""Git repository integration for enhanced context."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, repo_path: str):
|
|
13
|
+
"""Initialize Git integration.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
repo_path: Path to git repository
|
|
17
|
+
|
|
18
|
+
Raises:
|
|
19
|
+
git.exc.InvalidGitRepositoryError: If not a git repository
|
|
20
|
+
"""
|
|
21
|
+
self.repo_path = Path(repo_path)
|
|
22
|
+
try:
|
|
23
|
+
self.repo = git.Repo(repo_path, search_parent_directories=True)
|
|
24
|
+
except git.exc.InvalidGitRepositoryError:
|
|
25
|
+
self.repo = None
|
|
26
|
+
|
|
27
|
+
def is_git_repo(self) -> bool:
|
|
28
|
+
"""Check if path is a git repository.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
True if git repo, False otherwise
|
|
32
|
+
"""
|
|
33
|
+
return self.repo is not None
|
|
34
|
+
|
|
35
|
+
def get_recent_changes(self, days: int = 7, max_files: int = 20) -> List[Dict]:
|
|
36
|
+
"""Get files changed in last N days.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
days: Number of days to look back
|
|
40
|
+
max_files: Maximum number of files to return
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
List of dicts with file change information
|
|
44
|
+
"""
|
|
45
|
+
if not self.is_git_repo():
|
|
46
|
+
return []
|
|
47
|
+
|
|
48
|
+
since = datetime.now() - timedelta(days=days)
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
# Get commits since date
|
|
52
|
+
commits = list(self.repo.iter_commits(
|
|
53
|
+
all=True,
|
|
54
|
+
since=since.isoformat()
|
|
55
|
+
))
|
|
56
|
+
|
|
57
|
+
# Track file changes
|
|
58
|
+
file_changes = {}
|
|
59
|
+
|
|
60
|
+
for commit in commits:
|
|
61
|
+
# Skip merge commits
|
|
62
|
+
if len(commit.parents) > 1:
|
|
63
|
+
continue
|
|
64
|
+
|
|
65
|
+
for file_path in commit.stats.files.keys():
|
|
66
|
+
if file_path not in file_changes:
|
|
67
|
+
file_changes[file_path] = {
|
|
68
|
+
'file': file_path,
|
|
69
|
+
'commits': [],
|
|
70
|
+
'total_changes': 0,
|
|
71
|
+
'last_modified': commit.committed_datetime,
|
|
72
|
+
'last_author': commit.author.name
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
stats = commit.stats.files[file_path]
|
|
76
|
+
file_changes[file_path]['commits'].append({
|
|
77
|
+
'sha': commit.hexsha[:7],
|
|
78
|
+
'message': commit.message.split('\n')[0], # First line only
|
|
79
|
+
'author': commit.author.name,
|
|
80
|
+
'date': commit.committed_datetime.isoformat(),
|
|
81
|
+
'insertions': stats.get('insertions', 0),
|
|
82
|
+
'deletions': stats.get('deletions', 0),
|
|
83
|
+
})
|
|
84
|
+
file_changes[file_path]['total_changes'] += (
|
|
85
|
+
stats.get('insertions', 0) + stats.get('deletions', 0)
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# Sort by total changes (most active files first)
|
|
89
|
+
sorted_files = sorted(
|
|
90
|
+
file_changes.values(),
|
|
91
|
+
key=lambda x: x['total_changes'],
|
|
92
|
+
reverse=True
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
return sorted_files[:max_files]
|
|
96
|
+
|
|
97
|
+
except Exception as e:
|
|
98
|
+
# Git operations can fail for various reasons
|
|
99
|
+
return []
|
|
100
|
+
|
|
101
|
+
def get_uncommitted_changes(self) -> List[str]:
|
|
102
|
+
"""Get list of files with uncommitted changes.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
List of file paths
|
|
106
|
+
"""
|
|
107
|
+
if not self.is_git_repo():
|
|
108
|
+
return []
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
# Get both staged and unstaged changes
|
|
112
|
+
changed_files = set()
|
|
113
|
+
|
|
114
|
+
# Unstaged changes
|
|
115
|
+
changed_files.update(item.a_path for item in self.repo.index.diff(None))
|
|
116
|
+
|
|
117
|
+
# Staged changes
|
|
118
|
+
changed_files.update(item.a_path for item in self.repo.index.diff('HEAD'))
|
|
119
|
+
|
|
120
|
+
# Untracked files
|
|
121
|
+
changed_files.update(self.repo.untracked_files)
|
|
122
|
+
|
|
123
|
+
return list(changed_files)
|
|
124
|
+
|
|
125
|
+
except Exception:
|
|
126
|
+
return []
|
|
127
|
+
|
|
128
|
+
def get_file_history(self, file_path: str, limit: int = 5) -> List[Dict]:
|
|
129
|
+
"""Get commit history for a specific file.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
file_path: Path to file (relative to repo root)
|
|
133
|
+
limit: Maximum number of commits
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
List of commit dicts
|
|
137
|
+
"""
|
|
138
|
+
if not self.is_git_repo():
|
|
139
|
+
return []
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
commits = list(self.repo.iter_commits(paths=file_path, max_count=limit))
|
|
143
|
+
|
|
144
|
+
history = []
|
|
145
|
+
for commit in commits:
|
|
146
|
+
history.append({
|
|
147
|
+
'sha': commit.hexsha[:7],
|
|
148
|
+
'message': commit.message.split('\n')[0],
|
|
149
|
+
'author': commit.author.name,
|
|
150
|
+
'date': commit.committed_datetime.isoformat(),
|
|
151
|
+
'email': commit.author.email
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
return history
|
|
155
|
+
|
|
156
|
+
except Exception:
|
|
157
|
+
return []
|
|
158
|
+
|
|
159
|
+
def get_current_branch(self) -> Optional[str]:
|
|
160
|
+
"""Get current branch name.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
Branch name or None
|
|
164
|
+
"""
|
|
165
|
+
if not self.is_git_repo():
|
|
166
|
+
return None
|
|
167
|
+
|
|
168
|
+
try:
|
|
169
|
+
return self.repo.active_branch.name
|
|
170
|
+
except Exception:
|
|
171
|
+
return None
|
|
172
|
+
|
|
173
|
+
def get_diff(self, file_path: Optional[str] = None) -> str:
|
|
174
|
+
"""Get diff for uncommitted changes.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
file_path: Optional specific file path
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
Diff string
|
|
181
|
+
"""
|
|
182
|
+
if not self.is_git_repo():
|
|
183
|
+
return ""
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
if file_path:
|
|
187
|
+
# Get diff for specific file
|
|
188
|
+
diff = self.repo.git.diff(file_path)
|
|
189
|
+
else:
|
|
190
|
+
# Get all diffs
|
|
191
|
+
diff = self.repo.git.diff()
|
|
192
|
+
|
|
193
|
+
return diff
|
|
194
|
+
|
|
195
|
+
except Exception:
|
|
196
|
+
return ""
|
|
197
|
+
|
|
198
|
+
def get_recent_commit_messages(self, count: int = 10) -> List[str]:
|
|
199
|
+
"""Get recent commit messages for context.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
count: Number of commits
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
List of commit messages
|
|
206
|
+
"""
|
|
207
|
+
if not self.is_git_repo():
|
|
208
|
+
return []
|
|
209
|
+
|
|
210
|
+
try:
|
|
211
|
+
commits = list(self.repo.iter_commits(max_count=count))
|
|
212
|
+
return [commit.message.split('\n')[0] for commit in commits]
|
|
213
|
+
except Exception:
|
|
214
|
+
return []
|
|
215
|
+
|
|
216
|
+
def get_contributors(self, limit: int = 10) -> List[Dict]:
|
|
217
|
+
"""Get repository contributors.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
limit: Maximum number of contributors
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
List of contributor dicts
|
|
224
|
+
"""
|
|
225
|
+
if not self.is_git_repo():
|
|
226
|
+
return []
|
|
227
|
+
|
|
228
|
+
try:
|
|
229
|
+
# Count commits per author
|
|
230
|
+
author_counts = {}
|
|
231
|
+
for commit in self.repo.iter_commits():
|
|
232
|
+
author = commit.author.name
|
|
233
|
+
if author not in author_counts:
|
|
234
|
+
author_counts[author] = {
|
|
235
|
+
'name': author,
|
|
236
|
+
'email': commit.author.email,
|
|
237
|
+
'commits': 0,
|
|
238
|
+
'last_commit': commit.committed_datetime
|
|
239
|
+
}
|
|
240
|
+
author_counts[author]['commits'] += 1
|
|
241
|
+
|
|
242
|
+
# Sort by commit count
|
|
243
|
+
sorted_contributors = sorted(
|
|
244
|
+
author_counts.values(),
|
|
245
|
+
key=lambda x: x['commits'],
|
|
246
|
+
reverse=True
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
return sorted_contributors[:limit]
|
|
250
|
+
|
|
251
|
+
except Exception:
|
|
252
|
+
return []
|
|
253
|
+
|
|
254
|
+
def is_ignored(self, file_path: str) -> bool:
|
|
255
|
+
"""Check if file is in .gitignore.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
file_path: Path to file
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
True if ignored, False otherwise
|
|
262
|
+
"""
|
|
263
|
+
if not self.is_git_repo():
|
|
264
|
+
return False
|
|
265
|
+
|
|
266
|
+
try:
|
|
267
|
+
return file_path in self.repo.ignored(file_path)
|
|
268
|
+
except Exception:
|
|
269
|
+
return False
|