shotgun-sh 0.1.0.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/__init__.py +3 -0
- shotgun/agents/__init__.py +1 -0
- shotgun/agents/agent_manager.py +196 -0
- shotgun/agents/common.py +295 -0
- shotgun/agents/config/__init__.py +13 -0
- shotgun/agents/config/manager.py +215 -0
- shotgun/agents/config/models.py +120 -0
- shotgun/agents/config/provider.py +91 -0
- shotgun/agents/history/__init__.py +5 -0
- shotgun/agents/history/history_processors.py +213 -0
- shotgun/agents/models.py +94 -0
- shotgun/agents/plan.py +119 -0
- shotgun/agents/research.py +131 -0
- shotgun/agents/tasks.py +122 -0
- shotgun/agents/tools/__init__.py +26 -0
- shotgun/agents/tools/codebase/__init__.py +28 -0
- shotgun/agents/tools/codebase/codebase_shell.py +256 -0
- shotgun/agents/tools/codebase/directory_lister.py +141 -0
- shotgun/agents/tools/codebase/file_read.py +144 -0
- shotgun/agents/tools/codebase/models.py +252 -0
- shotgun/agents/tools/codebase/query_graph.py +67 -0
- shotgun/agents/tools/codebase/retrieve_code.py +81 -0
- shotgun/agents/tools/file_management.py +130 -0
- shotgun/agents/tools/user_interaction.py +36 -0
- shotgun/agents/tools/web_search.py +69 -0
- shotgun/cli/__init__.py +1 -0
- shotgun/cli/codebase/__init__.py +5 -0
- shotgun/cli/codebase/commands.py +202 -0
- shotgun/cli/codebase/models.py +21 -0
- shotgun/cli/config.py +261 -0
- shotgun/cli/models.py +10 -0
- shotgun/cli/plan.py +65 -0
- shotgun/cli/research.py +78 -0
- shotgun/cli/tasks.py +71 -0
- shotgun/cli/utils.py +25 -0
- shotgun/codebase/__init__.py +12 -0
- shotgun/codebase/core/__init__.py +46 -0
- shotgun/codebase/core/change_detector.py +358 -0
- shotgun/codebase/core/code_retrieval.py +243 -0
- shotgun/codebase/core/ingestor.py +1497 -0
- shotgun/codebase/core/language_config.py +297 -0
- shotgun/codebase/core/manager.py +1554 -0
- shotgun/codebase/core/nl_query.py +327 -0
- shotgun/codebase/core/parser_loader.py +152 -0
- shotgun/codebase/models.py +107 -0
- shotgun/codebase/service.py +148 -0
- shotgun/logging_config.py +172 -0
- shotgun/main.py +73 -0
- shotgun/prompts/__init__.py +5 -0
- shotgun/prompts/agents/__init__.py +1 -0
- shotgun/prompts/agents/partials/codebase_understanding.j2 +79 -0
- shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +10 -0
- shotgun/prompts/agents/partials/interactive_mode.j2 +8 -0
- shotgun/prompts/agents/plan.j2 +57 -0
- shotgun/prompts/agents/research.j2 +38 -0
- shotgun/prompts/agents/state/codebase/codebase_graphs_available.j2 +13 -0
- shotgun/prompts/agents/state/system_state.j2 +1 -0
- shotgun/prompts/agents/tasks.j2 +67 -0
- shotgun/prompts/codebase/__init__.py +1 -0
- shotgun/prompts/codebase/cypher_query_patterns.j2 +221 -0
- shotgun/prompts/codebase/cypher_system.j2 +28 -0
- shotgun/prompts/codebase/enhanced_query_context.j2 +10 -0
- shotgun/prompts/codebase/partials/cypher_rules.j2 +24 -0
- shotgun/prompts/codebase/partials/graph_schema.j2 +28 -0
- shotgun/prompts/codebase/partials/temporal_context.j2 +21 -0
- shotgun/prompts/history/__init__.py +1 -0
- shotgun/prompts/history/summarization.j2 +46 -0
- shotgun/prompts/loader.py +140 -0
- shotgun/prompts/user/research.j2 +5 -0
- shotgun/py.typed +0 -0
- shotgun/sdk/__init__.py +13 -0
- shotgun/sdk/codebase.py +195 -0
- shotgun/sdk/exceptions.py +17 -0
- shotgun/sdk/models.py +189 -0
- shotgun/sdk/services.py +23 -0
- shotgun/telemetry.py +68 -0
- shotgun/tui/__init__.py +0 -0
- shotgun/tui/app.py +49 -0
- shotgun/tui/components/prompt_input.py +69 -0
- shotgun/tui/components/spinner.py +86 -0
- shotgun/tui/components/splash.py +25 -0
- shotgun/tui/components/vertical_tail.py +28 -0
- shotgun/tui/screens/chat.py +415 -0
- shotgun/tui/screens/chat.tcss +28 -0
- shotgun/tui/screens/provider_config.py +221 -0
- shotgun/tui/screens/splash.py +31 -0
- shotgun/tui/styles.tcss +10 -0
- shotgun/utils/__init__.py +5 -0
- shotgun/utils/file_system_utils.py +31 -0
- shotgun_sh-0.1.0.dev1.dist-info/METADATA +318 -0
- shotgun_sh-0.1.0.dev1.dist-info/RECORD +94 -0
- shotgun_sh-0.1.0.dev1.dist-info/WHEEL +4 -0
- shotgun_sh-0.1.0.dev1.dist-info/entry_points.txt +3 -0
- shotgun_sh-0.1.0.dev1.dist-info/licenses/LICENSE +21 -0
shotgun/cli/tasks.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Tasks command for shotgun CLI."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import Annotated
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from shotgun.agents.config import ProviderType
|
|
9
|
+
from shotgun.agents.models import AgentRuntimeOptions
|
|
10
|
+
from shotgun.agents.tasks import (
|
|
11
|
+
create_tasks_agent,
|
|
12
|
+
get_tasks_history,
|
|
13
|
+
run_tasks_agent,
|
|
14
|
+
)
|
|
15
|
+
from shotgun.logging_config import get_logger
|
|
16
|
+
|
|
17
|
+
app = typer.Typer(name="tasks", help="Generate task lists with agentic approach")
|
|
18
|
+
logger = get_logger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@app.callback(invoke_without_command=True)
|
|
22
|
+
def tasks(
|
|
23
|
+
instruction: Annotated[
|
|
24
|
+
str, typer.Argument(help="Task creation instruction or project description")
|
|
25
|
+
],
|
|
26
|
+
non_interactive: Annotated[
|
|
27
|
+
bool,
|
|
28
|
+
typer.Option(
|
|
29
|
+
"--non-interactive", "-n", help="Disable user interaction (for CI/CD)"
|
|
30
|
+
),
|
|
31
|
+
] = False,
|
|
32
|
+
provider: Annotated[
|
|
33
|
+
ProviderType | None,
|
|
34
|
+
typer.Option("--provider", "-p", help="AI provider to use (overrides default)"),
|
|
35
|
+
] = None,
|
|
36
|
+
) -> None:
|
|
37
|
+
"""Generate actionable task lists based on existing research and plans.
|
|
38
|
+
|
|
39
|
+
This command creates detailed task breakdowns using AI agents that analyze
|
|
40
|
+
your research and plans to generate prioritized, actionable tasks with
|
|
41
|
+
acceptance criteria and effort estimates.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
logger.info("📋 Task Creation Instruction: %s", instruction)
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
# Create agent dependencies
|
|
48
|
+
agent_runtime_options = AgentRuntimeOptions(
|
|
49
|
+
interactive_mode=not non_interactive
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# Create the tasks agent with deps and provider
|
|
53
|
+
agent, deps = create_tasks_agent(agent_runtime_options, provider)
|
|
54
|
+
|
|
55
|
+
# Start task creation process
|
|
56
|
+
logger.info("🎯 Starting task creation...")
|
|
57
|
+
result = asyncio.run(run_tasks_agent(agent, instruction, deps))
|
|
58
|
+
|
|
59
|
+
# Display results
|
|
60
|
+
logger.info("✅ Task Creation Complete!")
|
|
61
|
+
logger.info("📋 Results:")
|
|
62
|
+
logger.info("%s", result.output)
|
|
63
|
+
logger.info("📄 Tasks saved to: .shotgun/tasks.md")
|
|
64
|
+
logger.debug("📚 Current tasks:")
|
|
65
|
+
logger.debug("%s", get_tasks_history())
|
|
66
|
+
|
|
67
|
+
except Exception as e:
|
|
68
|
+
logger.error("❌ Error during task creation: %s", str(e))
|
|
69
|
+
import traceback
|
|
70
|
+
|
|
71
|
+
logger.debug("Full traceback:\n%s", traceback.format_exc())
|
shotgun/cli/utils.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Common utilities for CLI commands."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
from .models import OutputFormat
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def format_result_json(result: Any) -> str:
|
|
12
|
+
"""Format result object as JSON using Pydantic serialization."""
|
|
13
|
+
if isinstance(result, BaseModel):
|
|
14
|
+
return result.model_dump_json(indent=2)
|
|
15
|
+
else:
|
|
16
|
+
# Fallback for non-Pydantic objects
|
|
17
|
+
return json.dumps({"result": str(result)}, indent=2)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def output_result(result: Any, format_type: OutputFormat = OutputFormat.TEXT) -> None:
|
|
21
|
+
"""Output result in specified format."""
|
|
22
|
+
if format_type == OutputFormat.JSON:
|
|
23
|
+
print(format_result_json(result))
|
|
24
|
+
else: # Default to text
|
|
25
|
+
print(str(result))
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Shotgun codebase analysis and graph management."""
|
|
2
|
+
|
|
3
|
+
from shotgun.codebase.models import CodebaseGraph, GraphStatus, QueryResult, QueryType
|
|
4
|
+
from shotgun.codebase.service import CodebaseService
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"CodebaseService",
|
|
8
|
+
"CodebaseGraph",
|
|
9
|
+
"GraphStatus",
|
|
10
|
+
"QueryResult",
|
|
11
|
+
"QueryType",
|
|
12
|
+
]
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Core components for codebase understanding."""
|
|
2
|
+
|
|
3
|
+
from shotgun.codebase.core.code_retrieval import (
|
|
4
|
+
CodeSnippet,
|
|
5
|
+
retrieve_code_by_cypher,
|
|
6
|
+
retrieve_code_by_qualified_name,
|
|
7
|
+
)
|
|
8
|
+
from shotgun.codebase.core.ingestor import (
|
|
9
|
+
CodebaseIngestor,
|
|
10
|
+
Ingestor,
|
|
11
|
+
SimpleGraphBuilder,
|
|
12
|
+
)
|
|
13
|
+
from shotgun.codebase.core.language_config import (
|
|
14
|
+
LANGUAGE_CONFIGS,
|
|
15
|
+
LanguageConfig,
|
|
16
|
+
get_language_config,
|
|
17
|
+
)
|
|
18
|
+
from shotgun.codebase.core.manager import CodebaseGraphManager
|
|
19
|
+
from shotgun.codebase.core.nl_query import (
|
|
20
|
+
clean_cypher_response,
|
|
21
|
+
generate_cypher,
|
|
22
|
+
generate_cypher_openai_async,
|
|
23
|
+
)
|
|
24
|
+
from shotgun.codebase.core.parser_loader import load_parsers
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
# Ingestor classes
|
|
28
|
+
"CodebaseIngestor",
|
|
29
|
+
"Ingestor",
|
|
30
|
+
"SimpleGraphBuilder",
|
|
31
|
+
"CodebaseGraphManager",
|
|
32
|
+
# Language configuration
|
|
33
|
+
"LanguageConfig",
|
|
34
|
+
"LANGUAGE_CONFIGS",
|
|
35
|
+
"get_language_config",
|
|
36
|
+
# Parser loading
|
|
37
|
+
"load_parsers",
|
|
38
|
+
# Natural language query
|
|
39
|
+
"generate_cypher",
|
|
40
|
+
"generate_cypher_openai_async",
|
|
41
|
+
"clean_cypher_response",
|
|
42
|
+
# Code retrieval
|
|
43
|
+
"CodeSnippet",
|
|
44
|
+
"retrieve_code_by_qualified_name",
|
|
45
|
+
"retrieve_code_by_cypher",
|
|
46
|
+
]
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
"""Change detection for incremental graph updates."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import os
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, cast
|
|
8
|
+
|
|
9
|
+
import kuzu
|
|
10
|
+
|
|
11
|
+
from shotgun.logging_config import get_logger
|
|
12
|
+
|
|
13
|
+
logger = get_logger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ChangeType(Enum):
|
|
17
|
+
"""Types of file changes."""
|
|
18
|
+
|
|
19
|
+
ADDED = "added"
|
|
20
|
+
MODIFIED = "modified"
|
|
21
|
+
DELETED = "deleted"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ChangeDetector:
|
|
25
|
+
"""Detects changes in the codebase by comparing with FileMetadata nodes."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, connection: kuzu.Connection, repo_path: Path):
|
|
28
|
+
"""Initialize change detector.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
connection: Kuzu database connection
|
|
32
|
+
repo_path: Root path of the repository
|
|
33
|
+
"""
|
|
34
|
+
self.conn = connection
|
|
35
|
+
self.repo_path = Path(repo_path).resolve()
|
|
36
|
+
|
|
37
|
+
# Validate that repo path exists
|
|
38
|
+
if not self.repo_path.exists():
|
|
39
|
+
raise ValueError(f"Repository path does not exist: {self.repo_path}")
|
|
40
|
+
if not self.repo_path.is_dir():
|
|
41
|
+
raise ValueError(f"Repository path is not a directory: {self.repo_path}")
|
|
42
|
+
|
|
43
|
+
logger.info(f"ChangeDetector initialized with repo_path: {self.repo_path}")
|
|
44
|
+
|
|
45
|
+
def detect_changes(
|
|
46
|
+
self,
|
|
47
|
+
languages: list[str] | None = None,
|
|
48
|
+
exclude_patterns: list[str] | None = None,
|
|
49
|
+
) -> dict[str, ChangeType]:
|
|
50
|
+
"""Detect all changes since last update.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
languages: Optional list of languages to include
|
|
54
|
+
exclude_patterns: Optional patterns to exclude
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Dictionary mapping file paths to change types
|
|
58
|
+
"""
|
|
59
|
+
changes = {}
|
|
60
|
+
current_files = set()
|
|
61
|
+
|
|
62
|
+
# Get supported file extensions
|
|
63
|
+
from shotgun.codebase.core.language_config import get_language_config
|
|
64
|
+
|
|
65
|
+
supported_extensions = set()
|
|
66
|
+
if languages:
|
|
67
|
+
# Map language names to their primary extensions
|
|
68
|
+
lang_to_ext = {
|
|
69
|
+
"python": ".py",
|
|
70
|
+
"javascript": ".js",
|
|
71
|
+
"typescript": ".ts",
|
|
72
|
+
"java": ".java",
|
|
73
|
+
"cpp": ".cpp",
|
|
74
|
+
"c": ".c",
|
|
75
|
+
"csharp": ".cs",
|
|
76
|
+
"go": ".go",
|
|
77
|
+
"rust": ".rs",
|
|
78
|
+
"ruby": ".rb",
|
|
79
|
+
"php": ".php",
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
for lang in languages:
|
|
83
|
+
# Try to get config using the language's primary extension
|
|
84
|
+
primary_ext = lang_to_ext.get(lang.lower())
|
|
85
|
+
if primary_ext:
|
|
86
|
+
config = get_language_config(primary_ext)
|
|
87
|
+
if config:
|
|
88
|
+
supported_extensions.update(config.file_extensions)
|
|
89
|
+
else:
|
|
90
|
+
# Get all supported extensions
|
|
91
|
+
for ext in [
|
|
92
|
+
".py",
|
|
93
|
+
".js",
|
|
94
|
+
".ts",
|
|
95
|
+
".tsx",
|
|
96
|
+
".jsx",
|
|
97
|
+
".java",
|
|
98
|
+
".cpp",
|
|
99
|
+
".c",
|
|
100
|
+
".hpp",
|
|
101
|
+
".h",
|
|
102
|
+
".cs",
|
|
103
|
+
".go",
|
|
104
|
+
".rs",
|
|
105
|
+
".rb",
|
|
106
|
+
".php",
|
|
107
|
+
]:
|
|
108
|
+
if get_language_config(ext):
|
|
109
|
+
supported_extensions.add(ext)
|
|
110
|
+
|
|
111
|
+
# Walk through all source files
|
|
112
|
+
logger.debug(
|
|
113
|
+
f"Walking source files in {self.repo_path} with extensions {supported_extensions}"
|
|
114
|
+
)
|
|
115
|
+
for filepath in self._walk_source_files(supported_extensions, exclude_patterns):
|
|
116
|
+
# Normalize the relative path to use forward slashes consistently
|
|
117
|
+
relative_path = str(filepath.relative_to(self.repo_path)).replace(
|
|
118
|
+
os.sep, "/"
|
|
119
|
+
)
|
|
120
|
+
current_files.add(relative_path)
|
|
121
|
+
|
|
122
|
+
file_info = self._get_file_info(relative_path)
|
|
123
|
+
if not file_info:
|
|
124
|
+
# New file
|
|
125
|
+
changes[relative_path] = ChangeType.ADDED
|
|
126
|
+
logger.debug(f"Detected new file: {relative_path}")
|
|
127
|
+
else:
|
|
128
|
+
# Check if modified
|
|
129
|
+
mtime = int(filepath.stat().st_mtime)
|
|
130
|
+
if mtime > file_info["mtime"]:
|
|
131
|
+
# Check hash to confirm actual change
|
|
132
|
+
current_hash = self._calculate_file_hash(filepath)
|
|
133
|
+
if current_hash != file_info.get("hash", ""):
|
|
134
|
+
changes[relative_path] = ChangeType.MODIFIED
|
|
135
|
+
logger.debug(f"Detected modified file: {relative_path}")
|
|
136
|
+
|
|
137
|
+
# Check for deleted files
|
|
138
|
+
tracked_files = self._get_tracked_files()
|
|
139
|
+
logger.debug(
|
|
140
|
+
f"Found {len(tracked_files)} tracked files, {len(current_files)} current files"
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# Log sample of files for debugging
|
|
144
|
+
if tracked_files and current_files:
|
|
145
|
+
logger.debug(f"Sample tracked files: {list(tracked_files)[:5]}")
|
|
146
|
+
logger.debug(f"Sample current files: {list(current_files)[:5]}")
|
|
147
|
+
|
|
148
|
+
for tracked_file in tracked_files:
|
|
149
|
+
if tracked_file not in current_files:
|
|
150
|
+
changes[tracked_file] = ChangeType.DELETED
|
|
151
|
+
logger.debug(f"Detected deleted file: {tracked_file}")
|
|
152
|
+
|
|
153
|
+
# Warn if detecting large number of deletions
|
|
154
|
+
deleted_count = sum(1 for c in changes.values() if c == ChangeType.DELETED)
|
|
155
|
+
if deleted_count > 100:
|
|
156
|
+
logger.warning(
|
|
157
|
+
f"Detected {deleted_count} file deletions - this may indicate a path resolution issue"
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
logger.info(
|
|
161
|
+
f"Detected {len(changes)} file changes: "
|
|
162
|
+
f"{sum(1 for c in changes.values() if c == ChangeType.ADDED)} added, "
|
|
163
|
+
f"{sum(1 for c in changes.values() if c == ChangeType.MODIFIED)} modified, "
|
|
164
|
+
f"{sum(1 for c in changes.values() if c == ChangeType.DELETED)} deleted"
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
return changes
|
|
168
|
+
|
|
169
|
+
def _get_file_info(self, filepath: str) -> dict[str, Any] | None:
|
|
170
|
+
"""Get FileMetadata info for a file.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
filepath: Relative file path
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
Dictionary with file metadata or None if not found
|
|
177
|
+
"""
|
|
178
|
+
try:
|
|
179
|
+
result = self.conn.execute(
|
|
180
|
+
"MATCH (f:FileMetadata {filepath: $path}) RETURN f.mtime, f.hash",
|
|
181
|
+
{"path": filepath},
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# Handle the QueryResult properly - cast to proper type
|
|
185
|
+
if hasattr(result, "has_next"):
|
|
186
|
+
query_result = cast(Any, result)
|
|
187
|
+
if query_result.has_next():
|
|
188
|
+
row = query_result.get_next()
|
|
189
|
+
if isinstance(row, list | tuple) and len(row) >= 2:
|
|
190
|
+
return {"mtime": row[0], "hash": row[1]}
|
|
191
|
+
except Exception as e:
|
|
192
|
+
logger.error(f"Failed to get file info for {filepath}: {e}")
|
|
193
|
+
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
def _get_tracked_files(self) -> list[str]:
|
|
197
|
+
"""Get all tracked file paths from database.
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
List of relative file paths
|
|
201
|
+
"""
|
|
202
|
+
files = []
|
|
203
|
+
try:
|
|
204
|
+
result = self.conn.execute("MATCH (f:FileMetadata) RETURN f.filepath")
|
|
205
|
+
# Handle the QueryResult properly - cast to proper type
|
|
206
|
+
if hasattr(result, "has_next"):
|
|
207
|
+
query_result = cast(Any, result)
|
|
208
|
+
while query_result.has_next():
|
|
209
|
+
# Normalize path separators to forward slashes
|
|
210
|
+
row = query_result.get_next()
|
|
211
|
+
if isinstance(row, list | tuple) and len(row) > 0:
|
|
212
|
+
filepath = row[0]
|
|
213
|
+
if filepath:
|
|
214
|
+
normalized_path = filepath.replace(os.sep, "/")
|
|
215
|
+
files.append(normalized_path)
|
|
216
|
+
except Exception as e:
|
|
217
|
+
logger.error(f"Failed to get tracked files: {e}")
|
|
218
|
+
return files
|
|
219
|
+
|
|
220
|
+
def _walk_source_files(
|
|
221
|
+
self, supported_extensions: set[str], exclude_patterns: list[str] | None = None
|
|
222
|
+
) -> list[Path]:
|
|
223
|
+
"""Walk repository and find all source files.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
supported_extensions: Set of file extensions to include
|
|
227
|
+
exclude_patterns: Optional patterns to exclude
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
List of absolute file paths
|
|
231
|
+
"""
|
|
232
|
+
source_files = []
|
|
233
|
+
ignore_dirs = {
|
|
234
|
+
".git",
|
|
235
|
+
"__pycache__",
|
|
236
|
+
"node_modules",
|
|
237
|
+
".venv",
|
|
238
|
+
"venv",
|
|
239
|
+
".env",
|
|
240
|
+
"dist",
|
|
241
|
+
"build",
|
|
242
|
+
".pytest_cache",
|
|
243
|
+
".mypy_cache",
|
|
244
|
+
".tox",
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
logger.debug(f"Walking files from: {self.repo_path}")
|
|
248
|
+
logger.debug(f"Current working directory: {Path.cwd()}")
|
|
249
|
+
|
|
250
|
+
# Add custom exclude patterns
|
|
251
|
+
if exclude_patterns:
|
|
252
|
+
for pattern in exclude_patterns:
|
|
253
|
+
if pattern.startswith("*/"):
|
|
254
|
+
ignore_dirs.add(pattern[2:])
|
|
255
|
+
|
|
256
|
+
for root, dirs, files in os.walk(self.repo_path):
|
|
257
|
+
root_path = Path(root)
|
|
258
|
+
|
|
259
|
+
# Filter out ignored directories
|
|
260
|
+
dirs[:] = [d for d in dirs if d not in ignore_dirs]
|
|
261
|
+
|
|
262
|
+
# Skip if any parent directory should be ignored
|
|
263
|
+
if any(part in ignore_dirs for part in root_path.parts):
|
|
264
|
+
continue
|
|
265
|
+
|
|
266
|
+
for file in files:
|
|
267
|
+
filepath = root_path / file
|
|
268
|
+
|
|
269
|
+
# Check if it's a supported source file
|
|
270
|
+
if filepath.suffix in supported_extensions:
|
|
271
|
+
# Check exclude patterns
|
|
272
|
+
if exclude_patterns:
|
|
273
|
+
relative_path = str(filepath.relative_to(self.repo_path))
|
|
274
|
+
if any(
|
|
275
|
+
self._matches_pattern(relative_path, pattern)
|
|
276
|
+
for pattern in exclude_patterns
|
|
277
|
+
):
|
|
278
|
+
continue
|
|
279
|
+
|
|
280
|
+
source_files.append(filepath)
|
|
281
|
+
|
|
282
|
+
return source_files
|
|
283
|
+
|
|
284
|
+
def _matches_pattern(self, filepath: str, pattern: str) -> bool:
|
|
285
|
+
"""Check if filepath matches an exclude pattern.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
filepath: File path to check
|
|
289
|
+
pattern: Pattern to match against
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
True if matches
|
|
293
|
+
"""
|
|
294
|
+
# Simple pattern matching
|
|
295
|
+
if "*" in pattern:
|
|
296
|
+
# Convert simple glob to regex-like matching
|
|
297
|
+
import fnmatch
|
|
298
|
+
|
|
299
|
+
return fnmatch.fnmatch(filepath, pattern)
|
|
300
|
+
else:
|
|
301
|
+
# Direct substring match
|
|
302
|
+
return pattern in filepath
|
|
303
|
+
|
|
304
|
+
def _calculate_file_hash(self, filepath: Path) -> str:
|
|
305
|
+
"""Calculate hash of file contents.
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
filepath: Path to file
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
SHA256 hash of file contents
|
|
312
|
+
"""
|
|
313
|
+
try:
|
|
314
|
+
with open(filepath, "rb") as f:
|
|
315
|
+
return hashlib.sha256(f.read()).hexdigest()
|
|
316
|
+
except Exception as e:
|
|
317
|
+
logger.error(f"Failed to calculate hash for {filepath}: {e}")
|
|
318
|
+
return ""
|
|
319
|
+
|
|
320
|
+
def get_file_nodes(self, filepath: str) -> set[str]:
|
|
321
|
+
"""Get all nodes tracked by a FileMetadata.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
filepath: Relative file path
|
|
325
|
+
|
|
326
|
+
Returns:
|
|
327
|
+
Set of qualified names of nodes in the file
|
|
328
|
+
"""
|
|
329
|
+
nodes = set()
|
|
330
|
+
|
|
331
|
+
# Query each TRACKS relationship type
|
|
332
|
+
for node_type, rel_type in [
|
|
333
|
+
("Module", "TRACKS_Module"),
|
|
334
|
+
("Class", "TRACKS_Class"),
|
|
335
|
+
("Function", "TRACKS_Function"),
|
|
336
|
+
("Method", "TRACKS_Method"),
|
|
337
|
+
]:
|
|
338
|
+
try:
|
|
339
|
+
result = self.conn.execute(
|
|
340
|
+
f"""
|
|
341
|
+
MATCH (f:FileMetadata {{filepath: $path}})-[:{rel_type}]->(n:{node_type})
|
|
342
|
+
RETURN n.qualified_name
|
|
343
|
+
""",
|
|
344
|
+
{"path": filepath},
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
# Handle the QueryResult properly - cast to proper type
|
|
348
|
+
if hasattr(result, "has_next"):
|
|
349
|
+
query_result = cast(Any, result)
|
|
350
|
+
while query_result.has_next():
|
|
351
|
+
row = query_result.get_next()
|
|
352
|
+
if isinstance(row, list | tuple) and len(row) > 0:
|
|
353
|
+
nodes.add(row[0])
|
|
354
|
+
except Exception as e:
|
|
355
|
+
# Ignore if relationship doesn't exist - this is expected when tables aren't created yet
|
|
356
|
+
logger.debug(f"No {rel_type} relationship found for {filepath}: {e}")
|
|
357
|
+
|
|
358
|
+
return nodes
|