shotgun-sh 0.2.29.dev2__py3-none-any.whl → 0.6.1.dev1__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 shotgun-sh might be problematic. Click here for more details.
- shotgun/agents/agent_manager.py +497 -30
- shotgun/agents/cancellation.py +103 -0
- shotgun/agents/common.py +90 -77
- shotgun/agents/config/README.md +0 -1
- shotgun/agents/config/manager.py +52 -8
- shotgun/agents/config/models.py +48 -45
- shotgun/agents/config/provider.py +44 -29
- shotgun/agents/conversation/history/file_content_deduplication.py +66 -43
- shotgun/agents/conversation/history/token_counting/base.py +51 -9
- shotgun/agents/export.py +12 -13
- shotgun/agents/file_read.py +176 -0
- shotgun/agents/messages.py +15 -3
- shotgun/agents/models.py +90 -2
- shotgun/agents/plan.py +12 -13
- shotgun/agents/research.py +13 -10
- shotgun/agents/router/__init__.py +47 -0
- shotgun/agents/router/models.py +384 -0
- shotgun/agents/router/router.py +185 -0
- shotgun/agents/router/tools/__init__.py +18 -0
- shotgun/agents/router/tools/delegation_tools.py +557 -0
- shotgun/agents/router/tools/plan_tools.py +403 -0
- shotgun/agents/runner.py +17 -2
- shotgun/agents/specify.py +12 -13
- shotgun/agents/tasks.py +12 -13
- shotgun/agents/tools/__init__.py +8 -0
- shotgun/agents/tools/codebase/directory_lister.py +27 -39
- shotgun/agents/tools/codebase/file_read.py +26 -35
- shotgun/agents/tools/codebase/query_graph.py +9 -0
- shotgun/agents/tools/codebase/retrieve_code.py +9 -0
- shotgun/agents/tools/file_management.py +81 -3
- shotgun/agents/tools/file_read_tools/__init__.py +7 -0
- shotgun/agents/tools/file_read_tools/multimodal_file_read.py +167 -0
- shotgun/agents/tools/markdown_tools/__init__.py +62 -0
- shotgun/agents/tools/markdown_tools/insert_section.py +148 -0
- shotgun/agents/tools/markdown_tools/models.py +86 -0
- shotgun/agents/tools/markdown_tools/remove_section.py +114 -0
- shotgun/agents/tools/markdown_tools/replace_section.py +119 -0
- shotgun/agents/tools/markdown_tools/utils.py +453 -0
- shotgun/agents/tools/registry.py +41 -0
- shotgun/agents/tools/web_search/__init__.py +1 -2
- shotgun/agents/tools/web_search/gemini.py +1 -3
- shotgun/agents/tools/web_search/openai.py +42 -23
- shotgun/attachments/__init__.py +41 -0
- shotgun/attachments/errors.py +60 -0
- shotgun/attachments/models.py +107 -0
- shotgun/attachments/parser.py +257 -0
- shotgun/attachments/processor.py +193 -0
- shotgun/cli/clear.py +2 -2
- shotgun/cli/codebase/commands.py +181 -65
- shotgun/cli/compact.py +2 -2
- shotgun/cli/context.py +2 -2
- shotgun/cli/run.py +90 -0
- shotgun/cli/spec/backup.py +2 -1
- shotgun/cli/spec/commands.py +2 -0
- shotgun/cli/spec/models.py +18 -0
- shotgun/cli/spec/pull_service.py +122 -68
- shotgun/codebase/__init__.py +2 -0
- shotgun/codebase/benchmarks/__init__.py +35 -0
- shotgun/codebase/benchmarks/benchmark_runner.py +309 -0
- shotgun/codebase/benchmarks/exporters.py +119 -0
- shotgun/codebase/benchmarks/formatters/__init__.py +49 -0
- shotgun/codebase/benchmarks/formatters/base.py +34 -0
- shotgun/codebase/benchmarks/formatters/json_formatter.py +106 -0
- shotgun/codebase/benchmarks/formatters/markdown.py +136 -0
- shotgun/codebase/benchmarks/models.py +129 -0
- shotgun/codebase/core/__init__.py +4 -0
- shotgun/codebase/core/call_resolution.py +91 -0
- shotgun/codebase/core/change_detector.py +11 -6
- shotgun/codebase/core/errors.py +159 -0
- shotgun/codebase/core/extractors/__init__.py +23 -0
- shotgun/codebase/core/extractors/base.py +138 -0
- shotgun/codebase/core/extractors/factory.py +63 -0
- shotgun/codebase/core/extractors/go/__init__.py +7 -0
- shotgun/codebase/core/extractors/go/extractor.py +122 -0
- shotgun/codebase/core/extractors/javascript/__init__.py +7 -0
- shotgun/codebase/core/extractors/javascript/extractor.py +132 -0
- shotgun/codebase/core/extractors/protocol.py +109 -0
- shotgun/codebase/core/extractors/python/__init__.py +7 -0
- shotgun/codebase/core/extractors/python/extractor.py +141 -0
- shotgun/codebase/core/extractors/rust/__init__.py +7 -0
- shotgun/codebase/core/extractors/rust/extractor.py +139 -0
- shotgun/codebase/core/extractors/types.py +15 -0
- shotgun/codebase/core/extractors/typescript/__init__.py +7 -0
- shotgun/codebase/core/extractors/typescript/extractor.py +92 -0
- shotgun/codebase/core/gitignore.py +252 -0
- shotgun/codebase/core/ingestor.py +644 -354
- shotgun/codebase/core/kuzu_compat.py +119 -0
- shotgun/codebase/core/language_config.py +239 -0
- shotgun/codebase/core/manager.py +256 -46
- shotgun/codebase/core/metrics_collector.py +310 -0
- shotgun/codebase/core/metrics_types.py +347 -0
- shotgun/codebase/core/parallel_executor.py +424 -0
- shotgun/codebase/core/work_distributor.py +254 -0
- shotgun/codebase/core/worker.py +768 -0
- shotgun/codebase/indexing_state.py +86 -0
- shotgun/codebase/models.py +94 -0
- shotgun/codebase/service.py +13 -0
- shotgun/exceptions.py +1 -1
- shotgun/main.py +2 -10
- shotgun/prompts/agents/export.j2 +2 -0
- shotgun/prompts/agents/file_read.j2 +48 -0
- shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +20 -28
- shotgun/prompts/agents/partials/content_formatting.j2 +12 -33
- shotgun/prompts/agents/partials/interactive_mode.j2 +9 -32
- shotgun/prompts/agents/partials/router_delegation_mode.j2 +35 -0
- shotgun/prompts/agents/plan.j2 +43 -1
- shotgun/prompts/agents/research.j2 +75 -20
- shotgun/prompts/agents/router.j2 +713 -0
- shotgun/prompts/agents/specify.j2 +94 -4
- shotgun/prompts/agents/state/codebase/codebase_graphs_available.j2 +14 -1
- shotgun/prompts/agents/state/system_state.j2 +24 -15
- shotgun/prompts/agents/tasks.j2 +77 -23
- shotgun/settings.py +44 -0
- shotgun/shotgun_web/shared_specs/upload_pipeline.py +38 -0
- shotgun/tui/app.py +90 -23
- shotgun/tui/commands/__init__.py +9 -1
- shotgun/tui/components/attachment_bar.py +87 -0
- shotgun/tui/components/mode_indicator.py +120 -25
- shotgun/tui/components/prompt_input.py +23 -28
- shotgun/tui/components/status_bar.py +5 -4
- shotgun/tui/dependencies.py +58 -8
- shotgun/tui/protocols.py +37 -0
- shotgun/tui/screens/chat/chat.tcss +24 -1
- shotgun/tui/screens/chat/chat_screen.py +1374 -211
- shotgun/tui/screens/chat/codebase_index_prompt_screen.py +8 -4
- shotgun/tui/screens/chat_screen/attachment_hint.py +40 -0
- shotgun/tui/screens/chat_screen/command_providers.py +0 -97
- shotgun/tui/screens/chat_screen/history/agent_response.py +7 -3
- shotgun/tui/screens/chat_screen/history/chat_history.py +49 -6
- shotgun/tui/screens/chat_screen/history/formatters.py +75 -15
- shotgun/tui/screens/chat_screen/history/partial_response.py +11 -1
- shotgun/tui/screens/chat_screen/history/user_question.py +25 -3
- shotgun/tui/screens/chat_screen/messages.py +219 -0
- shotgun/tui/screens/database_locked_dialog.py +219 -0
- shotgun/tui/screens/database_timeout_dialog.py +158 -0
- shotgun/tui/screens/kuzu_error_dialog.py +135 -0
- shotgun/tui/screens/model_picker.py +14 -9
- shotgun/tui/screens/models.py +11 -0
- shotgun/tui/screens/shotgun_auth.py +50 -0
- shotgun/tui/screens/spec_pull.py +2 -0
- shotgun/tui/state/processing_state.py +19 -0
- shotgun/tui/utils/mode_progress.py +20 -86
- shotgun/tui/widgets/__init__.py +2 -1
- shotgun/tui/widgets/approval_widget.py +152 -0
- shotgun/tui/widgets/cascade_confirmation_widget.py +203 -0
- shotgun/tui/widgets/plan_panel.py +129 -0
- shotgun/tui/widgets/step_checkpoint_widget.py +180 -0
- shotgun/tui/widgets/widget_coordinator.py +18 -0
- shotgun/utils/file_system_utils.py +4 -1
- {shotgun_sh-0.2.29.dev2.dist-info → shotgun_sh-0.6.1.dev1.dist-info}/METADATA +88 -34
- shotgun_sh-0.6.1.dev1.dist-info/RECORD +292 -0
- shotgun/cli/export.py +0 -81
- shotgun/cli/plan.py +0 -73
- shotgun/cli/research.py +0 -93
- shotgun/cli/specify.py +0 -70
- shotgun/cli/tasks.py +0 -78
- shotgun/tui/screens/onboarding.py +0 -580
- shotgun_sh-0.2.29.dev2.dist-info/RECORD +0 -229
- {shotgun_sh-0.2.29.dev2.dist-info → shotgun_sh-0.6.1.dev1.dist-info}/WHEEL +0 -0
- {shotgun_sh-0.2.29.dev2.dist-info → shotgun_sh-0.6.1.dev1.dist-info}/entry_points.txt +0 -0
- {shotgun_sh-0.2.29.dev2.dist-info → shotgun_sh-0.6.1.dev1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""Gitignore pattern matching for codebase indexing.
|
|
2
|
+
|
|
3
|
+
This module provides functionality to read and apply .gitignore rules
|
|
4
|
+
to filter files during indexing, significantly improving performance
|
|
5
|
+
for large codebases that may contain ignored directories like venv,
|
|
6
|
+
node_modules, build artifacts, etc.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
13
|
+
|
|
14
|
+
import pathspec
|
|
15
|
+
|
|
16
|
+
from shotgun.codebase.models import GitignoreStats
|
|
17
|
+
from shotgun.logging_config import get_logger
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from pathspec import PathSpec
|
|
21
|
+
|
|
22
|
+
logger = get_logger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class GitignoreManager:
|
|
26
|
+
"""Manages gitignore patterns for a repository.
|
|
27
|
+
|
|
28
|
+
This class loads and caches gitignore patterns from:
|
|
29
|
+
1. The repository's .gitignore file
|
|
30
|
+
2. Any .gitignore files in subdirectories (hierarchical gitignore)
|
|
31
|
+
3. Global gitignore patterns (optional)
|
|
32
|
+
|
|
33
|
+
Usage:
|
|
34
|
+
manager = GitignoreManager(repo_path)
|
|
35
|
+
if manager.is_ignored("path/to/file.py"):
|
|
36
|
+
# Skip this file
|
|
37
|
+
pass
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
repo_path: Path,
|
|
43
|
+
load_nested: bool = True,
|
|
44
|
+
respect_gitignore: bool = True,
|
|
45
|
+
):
|
|
46
|
+
"""Initialize the gitignore manager.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
repo_path: Root path of the repository
|
|
50
|
+
load_nested: Whether to load .gitignore files from subdirectories
|
|
51
|
+
respect_gitignore: Whether to respect .gitignore at all (if False, nothing is ignored)
|
|
52
|
+
"""
|
|
53
|
+
self.repo_path = repo_path.resolve()
|
|
54
|
+
self.load_nested = load_nested
|
|
55
|
+
self.respect_gitignore = respect_gitignore
|
|
56
|
+
|
|
57
|
+
# Cache for PathSpec objects by directory
|
|
58
|
+
self._specs: dict[Path, PathSpec] = {}
|
|
59
|
+
|
|
60
|
+
# Combined spec for the root gitignore
|
|
61
|
+
self._root_spec: PathSpec | None = None
|
|
62
|
+
|
|
63
|
+
# Statistics for debugging
|
|
64
|
+
self.stats = GitignoreStats()
|
|
65
|
+
|
|
66
|
+
if respect_gitignore:
|
|
67
|
+
self._load_gitignore_files()
|
|
68
|
+
|
|
69
|
+
def _load_gitignore_files(self) -> None:
|
|
70
|
+
"""Load all gitignore files in the repository."""
|
|
71
|
+
root_gitignore = self.repo_path / ".gitignore"
|
|
72
|
+
|
|
73
|
+
if root_gitignore.exists():
|
|
74
|
+
self._root_spec = self._load_gitignore_file(root_gitignore)
|
|
75
|
+
self.stats.gitignore_files_loaded += 1
|
|
76
|
+
logger.debug(
|
|
77
|
+
f"Loaded root .gitignore with patterns - "
|
|
78
|
+
f"path: {root_gitignore}, patterns: {self.stats.patterns_loaded}"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
if self.load_nested:
|
|
82
|
+
# Find all nested .gitignore files
|
|
83
|
+
for gitignore_path in self.repo_path.rglob(".gitignore"):
|
|
84
|
+
if gitignore_path.parent == self.repo_path:
|
|
85
|
+
continue # Skip root, already loaded
|
|
86
|
+
|
|
87
|
+
spec = self._load_gitignore_file(gitignore_path)
|
|
88
|
+
if spec:
|
|
89
|
+
self._specs[gitignore_path.parent] = spec
|
|
90
|
+
self.stats.gitignore_files_loaded += 1
|
|
91
|
+
|
|
92
|
+
if self._specs:
|
|
93
|
+
logger.debug(f"Loaded {len(self._specs)} nested .gitignore files")
|
|
94
|
+
|
|
95
|
+
def _load_gitignore_file(self, gitignore_path: Path) -> PathSpec | None:
|
|
96
|
+
"""Load patterns from a single gitignore file.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
gitignore_path: Path to the .gitignore file
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
PathSpec object or None if file couldn't be loaded
|
|
103
|
+
"""
|
|
104
|
+
try:
|
|
105
|
+
with open(gitignore_path, encoding="utf-8", errors="ignore") as f:
|
|
106
|
+
patterns = f.read().splitlines()
|
|
107
|
+
|
|
108
|
+
# Filter out empty lines and comments
|
|
109
|
+
valid_patterns = [
|
|
110
|
+
p.strip()
|
|
111
|
+
for p in patterns
|
|
112
|
+
if p.strip() and not p.strip().startswith("#")
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
if not valid_patterns:
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
self.stats.patterns_loaded += len(valid_patterns)
|
|
119
|
+
|
|
120
|
+
return pathspec.PathSpec.from_lines("gitwildmatch", valid_patterns)
|
|
121
|
+
except Exception as e:
|
|
122
|
+
logger.warning(f"Failed to load gitignore: {gitignore_path}, error: {e}")
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
def is_ignored(self, path: str | Path) -> bool:
|
|
126
|
+
"""Check if a path should be ignored based on gitignore rules.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
path: Path to check (relative to repo root or absolute)
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
True if the path should be ignored
|
|
133
|
+
"""
|
|
134
|
+
if not self.respect_gitignore:
|
|
135
|
+
return False
|
|
136
|
+
|
|
137
|
+
self.stats.files_checked += 1
|
|
138
|
+
|
|
139
|
+
# Convert to Path and make relative to repo root
|
|
140
|
+
path_obj = Path(path) if isinstance(path, str) else path
|
|
141
|
+
|
|
142
|
+
if path_obj.is_absolute():
|
|
143
|
+
# Resolve to handle symlinks (e.g., /var -> /private/var on macOS)
|
|
144
|
+
try:
|
|
145
|
+
resolved_path = path_obj.resolve()
|
|
146
|
+
path_obj = resolved_path.relative_to(self.repo_path)
|
|
147
|
+
except ValueError:
|
|
148
|
+
# Path is not under repo_path
|
|
149
|
+
return False
|
|
150
|
+
|
|
151
|
+
# Convert to string with forward slashes for consistency
|
|
152
|
+
path_str = str(path_obj).replace("\\", "/")
|
|
153
|
+
|
|
154
|
+
# Check root gitignore first
|
|
155
|
+
if self._root_spec and self._root_spec.match_file(path_str):
|
|
156
|
+
self.stats.files_ignored += 1
|
|
157
|
+
return True
|
|
158
|
+
|
|
159
|
+
# Check nested gitignore files
|
|
160
|
+
if self.load_nested:
|
|
161
|
+
# Walk up the directory tree to find applicable gitignore files
|
|
162
|
+
current_dir = (self.repo_path / path_obj).parent
|
|
163
|
+
while current_dir >= self.repo_path:
|
|
164
|
+
if current_dir in self._specs:
|
|
165
|
+
# Make path relative to this gitignore's directory
|
|
166
|
+
try:
|
|
167
|
+
rel_path = path_obj.relative_to(
|
|
168
|
+
current_dir.relative_to(self.repo_path)
|
|
169
|
+
)
|
|
170
|
+
rel_path_str = str(rel_path).replace("\\", "/")
|
|
171
|
+
if self._specs[current_dir].match_file(rel_path_str):
|
|
172
|
+
self.stats.files_ignored += 1
|
|
173
|
+
return True
|
|
174
|
+
except ValueError:
|
|
175
|
+
pass
|
|
176
|
+
current_dir = current_dir.parent
|
|
177
|
+
|
|
178
|
+
return False
|
|
179
|
+
|
|
180
|
+
def is_directory_ignored(self, path: str | Path) -> bool:
|
|
181
|
+
"""Check if a directory should be ignored.
|
|
182
|
+
|
|
183
|
+
For directories, we add a trailing slash to match gitignore semantics.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
path: Directory path to check
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
True if the directory should be ignored
|
|
190
|
+
"""
|
|
191
|
+
if not self.respect_gitignore:
|
|
192
|
+
return False
|
|
193
|
+
|
|
194
|
+
# Convert to Path and make relative to repo root
|
|
195
|
+
path_obj = Path(path) if isinstance(path, str) else path
|
|
196
|
+
|
|
197
|
+
if path_obj.is_absolute():
|
|
198
|
+
try:
|
|
199
|
+
path_obj = path_obj.relative_to(self.repo_path)
|
|
200
|
+
except ValueError:
|
|
201
|
+
return False
|
|
202
|
+
|
|
203
|
+
# Check both with and without trailing slash
|
|
204
|
+
path_str = str(path_obj).replace("\\", "/")
|
|
205
|
+
path_str_dir = path_str.rstrip("/") + "/"
|
|
206
|
+
|
|
207
|
+
# Check root gitignore
|
|
208
|
+
if self._root_spec:
|
|
209
|
+
if self._root_spec.match_file(path_str) or self._root_spec.match_file(
|
|
210
|
+
path_str_dir
|
|
211
|
+
):
|
|
212
|
+
logger.debug(f"Directory ignored by root .gitignore: {path_str}")
|
|
213
|
+
return True
|
|
214
|
+
|
|
215
|
+
return False
|
|
216
|
+
|
|
217
|
+
def filter_paths(self, paths: list[Path]) -> list[Path]:
|
|
218
|
+
"""Filter a list of paths, removing ignored ones.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
paths: List of paths to filter
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
List of paths that are not ignored
|
|
225
|
+
"""
|
|
226
|
+
return [p for p in paths if not self.is_ignored(p)]
|
|
227
|
+
|
|
228
|
+
def get_stats_summary(self) -> str:
|
|
229
|
+
"""Get a summary of gitignore statistics.
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
Human-readable statistics string
|
|
233
|
+
"""
|
|
234
|
+
return (
|
|
235
|
+
f"Gitignore stats: "
|
|
236
|
+
f"{self.stats.gitignore_files_loaded} files loaded, "
|
|
237
|
+
f"{self.stats.patterns_loaded} patterns, "
|
|
238
|
+
f"{self.stats.files_checked} paths checked, "
|
|
239
|
+
f"{self.stats.files_ignored} ignored"
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def load_gitignore_for_repo(repo_path: Path | str) -> GitignoreManager:
|
|
244
|
+
"""Convenience function to create a GitignoreManager for a repository.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
repo_path: Path to the repository root
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
Configured GitignoreManager
|
|
251
|
+
"""
|
|
252
|
+
return GitignoreManager(Path(repo_path))
|