smart-commit-cli 0.1.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.
- smart_commit/__init__.py +6 -0
- smart_commit/ai/__init__.py +7 -0
- smart_commit/ai/client.py +123 -0
- smart_commit/ai/reviewer.py +91 -0
- smart_commit/ai.py +136 -0
- smart_commit/analysis/__init__.py +7 -0
- smart_commit/analysis/ast_parser.py +173 -0
- smart_commit/analysis/dependency_graph.py +114 -0
- smart_commit/analysis/diff_parser.py +172 -0
- smart_commit/analysis/summarizer.py +101 -0
- smart_commit/analysis/symbol_extractor.py +88 -0
- smart_commit/benchmark.py +88 -0
- smart_commit/cache.py +65 -0
- smart_commit/cli.py +391 -0
- smart_commit/clustering/__init__.py +7 -0
- smart_commit/clustering/graph_clustering.py +78 -0
- smart_commit/clustering/heuristic.py +178 -0
- smart_commit/clustering/semantic.py +201 -0
- smart_commit/clustering.py +186 -0
- smart_commit/commit_messages.py +232 -0
- smart_commit/committer.py +91 -0
- smart_commit/config.py +88 -0
- smart_commit/constants.py +92 -0
- smart_commit/diff_parser.py +158 -0
- smart_commit/embeddings/__init__.py +7 -0
- smart_commit/embeddings/generator.py +128 -0
- smart_commit/embeddings/similarity.py +159 -0
- smart_commit/embeddings.py +108 -0
- smart_commit/exceptions.py +102 -0
- smart_commit/execution/__init__.py +6 -0
- smart_commit/execution/committer.py +102 -0
- smart_commit/git/__init__.py +8 -0
- smart_commit/git/safety.py +75 -0
- smart_commit/git/scanner.py +182 -0
- smart_commit/git/service.py +129 -0
- smart_commit/git_service.py +302 -0
- smart_commit/logger.py +99 -0
- smart_commit/models.py +73 -0
- smart_commit/plugins/__init__.py +6 -0
- smart_commit/plugins/base.py +135 -0
- smart_commit/plugins/django.py +40 -0
- smart_commit/plugins/fastapi.py +39 -0
- smart_commit/plugins/react.py +49 -0
- smart_commit/preview/__init__.py +6 -0
- smart_commit/preview/editor.py +224 -0
- smart_commit/preview/renderer.py +94 -0
- smart_commit/preview.py +131 -0
- smart_commit/summarizer.py +93 -0
- smart_commit/utils.py +83 -0
- smart_commit_cli-0.1.0.dist-info/METADATA +804 -0
- smart_commit_cli-0.1.0.dist-info/RECORD +53 -0
- smart_commit_cli-0.1.0.dist-info/WHEEL +4 -0
- smart_commit_cli-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from ..models import FileDiff
|
|
3
|
+
from ..logger import get_logger
|
|
4
|
+
|
|
5
|
+
logger = get_logger("analysis.diff_parser")
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DiffParser:
|
|
9
|
+
"""Parses git diffs with enhanced language awareness."""
|
|
10
|
+
|
|
11
|
+
# Language-specific keywords
|
|
12
|
+
LANGUAGE_PATTERNS = {
|
|
13
|
+
".py": {
|
|
14
|
+
"keywords": ["def ", "class ", "async def ", "import ", "from "],
|
|
15
|
+
"symbols": r"(?:def|class|async def)\s+(\w+)",
|
|
16
|
+
},
|
|
17
|
+
".js": {
|
|
18
|
+
"keywords": ["function ", "class ", "const ", "import ", "export "],
|
|
19
|
+
"symbols": r"(?:function|class)\s+(\w+)|(?:const|let|var)\s+(\w+)",
|
|
20
|
+
},
|
|
21
|
+
".ts": {
|
|
22
|
+
"keywords": ["interface ", "type ", "class ", "function ", "export "],
|
|
23
|
+
"symbols": r"(?:interface|type|class|function)\s+(\w+)",
|
|
24
|
+
},
|
|
25
|
+
".tsx": {
|
|
26
|
+
"keywords": ["interface ", "function ", "const ", "export "],
|
|
27
|
+
"symbols": r"(?:interface|function)\s+(\w+)|(?:const)\s+(\w+)",
|
|
28
|
+
},
|
|
29
|
+
".jsx": {
|
|
30
|
+
"keywords": ["function ", "export ", "const ", "import "],
|
|
31
|
+
"symbols": r"(?:function|const)\s+(\w+)",
|
|
32
|
+
},
|
|
33
|
+
".java": {
|
|
34
|
+
"keywords": ["class ", "public ", "private ", "interface "],
|
|
35
|
+
"symbols": r"(?:class|interface)\s+(\w+)",
|
|
36
|
+
},
|
|
37
|
+
".go": {
|
|
38
|
+
"keywords": ["func ", "type ", "package ", "import "],
|
|
39
|
+
"symbols": r"func\s+(?:\(\w+\s+\*?\w+\))?\s*(\w+)",
|
|
40
|
+
},
|
|
41
|
+
".rs": {
|
|
42
|
+
"keywords": ["fn ", "struct ", "impl ", "pub ", "use "],
|
|
43
|
+
"symbols": r"(?:fn|struct|impl)\s+(\w+)",
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
def parse_diff(self, raw_diff: str, file_path: str) -> list[str]:
|
|
48
|
+
"""Extract meaningful changes from a raw diff.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
raw_diff: Raw diff content.
|
|
52
|
+
file_path: Path to file.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
List of change descriptions.
|
|
56
|
+
"""
|
|
57
|
+
if not raw_diff.strip():
|
|
58
|
+
return []
|
|
59
|
+
|
|
60
|
+
changes = []
|
|
61
|
+
file_ext = self._get_file_extension(file_path)
|
|
62
|
+
|
|
63
|
+
# Parse diff lines
|
|
64
|
+
lines = raw_diff.split('\n')
|
|
65
|
+
added_lines = [l[1:] for l in lines if l.startswith('+') and not l.startswith('+++')]
|
|
66
|
+
removed_lines = [l[1:] for l in lines if l.startswith('-') and not l.startswith('---')]
|
|
67
|
+
|
|
68
|
+
# High-level change type
|
|
69
|
+
if added_lines and not removed_lines:
|
|
70
|
+
changes.append("Added new content")
|
|
71
|
+
elif removed_lines and not added_lines:
|
|
72
|
+
changes.append("Removed content")
|
|
73
|
+
elif added_lines and removed_lines:
|
|
74
|
+
changes.append("Modified content")
|
|
75
|
+
|
|
76
|
+
patterns = self.LANGUAGE_PATTERNS.get(file_ext, {})
|
|
77
|
+
if "symbols" in patterns:
|
|
78
|
+
symbols = self._extract_symbols(added_lines, patterns["symbols"])
|
|
79
|
+
for symbol in symbols:
|
|
80
|
+
changes.append(f"changed {symbol}")
|
|
81
|
+
|
|
82
|
+
# Stats
|
|
83
|
+
num_added = len(added_lines)
|
|
84
|
+
num_removed = len(removed_lines)
|
|
85
|
+
|
|
86
|
+
if num_added > 0 or num_removed > 0:
|
|
87
|
+
changes.append(f"+{num_added}/-{num_removed}")
|
|
88
|
+
|
|
89
|
+
return changes
|
|
90
|
+
|
|
91
|
+
def extract_summary(self, raw_diff: str, file_path: str) -> str:
|
|
92
|
+
"""Extract one-line summary of changes.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
raw_diff: Raw diff content.
|
|
96
|
+
file_path: Path to file.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Summary string.
|
|
100
|
+
"""
|
|
101
|
+
changes = self.parse_diff(raw_diff, file_path)
|
|
102
|
+
|
|
103
|
+
if not changes:
|
|
104
|
+
return "Modified file"
|
|
105
|
+
|
|
106
|
+
significant = [
|
|
107
|
+
c for c in changes
|
|
108
|
+
if c not in ["Added new content", "Modified content", "Removed content"]
|
|
109
|
+
and not c.startswith("+")
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
if significant:
|
|
113
|
+
return significant[0]
|
|
114
|
+
|
|
115
|
+
return changes[0]
|
|
116
|
+
|
|
117
|
+
def enrich_file_diff(self, file_diff: FileDiff) -> FileDiff:
|
|
118
|
+
"""Attach deterministic symbols and imports to a file diff."""
|
|
119
|
+
file_ext = self._get_file_extension(file_diff.file_path)
|
|
120
|
+
patterns = self.LANGUAGE_PATTERNS.get(file_ext, {})
|
|
121
|
+
changed_lines = self._changed_lines(file_diff.raw_diff)
|
|
122
|
+
if "symbols" in patterns:
|
|
123
|
+
file_diff.symbols = self._extract_symbols(changed_lines, patterns["symbols"])
|
|
124
|
+
file_diff.imports = self._extract_imports(changed_lines)
|
|
125
|
+
return file_diff
|
|
126
|
+
|
|
127
|
+
@staticmethod
|
|
128
|
+
def _get_file_extension(file_path: str) -> str:
|
|
129
|
+
"""Get file extension."""
|
|
130
|
+
if '.' not in file_path:
|
|
131
|
+
return ""
|
|
132
|
+
return '.' + file_path.split('.')[-1]
|
|
133
|
+
|
|
134
|
+
@staticmethod
|
|
135
|
+
def _extract_symbols(lines: list[str], pattern: str) -> list[str]:
|
|
136
|
+
"""Extract symbols using regex pattern."""
|
|
137
|
+
symbols = []
|
|
138
|
+
|
|
139
|
+
for line in lines:
|
|
140
|
+
matches = re.findall(pattern, line)
|
|
141
|
+
for match in matches:
|
|
142
|
+
if isinstance(match, tuple):
|
|
143
|
+
symbol = next((m for m in match if m), None)
|
|
144
|
+
else:
|
|
145
|
+
symbol = match
|
|
146
|
+
|
|
147
|
+
if symbol:
|
|
148
|
+
symbols.append(symbol)
|
|
149
|
+
|
|
150
|
+
return list(set(symbols)) # Deduplicate
|
|
151
|
+
|
|
152
|
+
@staticmethod
|
|
153
|
+
def _changed_lines(raw_diff: str) -> list[str]:
|
|
154
|
+
lines = []
|
|
155
|
+
for line in raw_diff.splitlines():
|
|
156
|
+
if line.startswith(("+++", "---")):
|
|
157
|
+
continue
|
|
158
|
+
if line.startswith(("+", "-")):
|
|
159
|
+
lines.append(line[1:])
|
|
160
|
+
return lines
|
|
161
|
+
|
|
162
|
+
@staticmethod
|
|
163
|
+
def _extract_imports(lines: list[str]) -> list[str]:
|
|
164
|
+
imports = []
|
|
165
|
+
for line in lines:
|
|
166
|
+
stripped = line.strip()
|
|
167
|
+
if stripped.startswith(("import ", "from ")):
|
|
168
|
+
imports.append(stripped)
|
|
169
|
+
elif " require(" in stripped:
|
|
170
|
+
imports.append(stripped)
|
|
171
|
+
return sorted(set(imports))
|
|
172
|
+
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Summary generation from diffs and rich semantics."""
|
|
2
|
+
|
|
3
|
+
from ..models import FileDiff
|
|
4
|
+
from ..logger import get_logger
|
|
5
|
+
from .diff_parser import DiffParser
|
|
6
|
+
from ..cache import cache_manager
|
|
7
|
+
|
|
8
|
+
logger = get_logger("analysis.summarizer")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Summarizer:
|
|
12
|
+
"""Converts raw diffs and structured symbols into semantic summaries."""
|
|
13
|
+
|
|
14
|
+
def __init__(self):
|
|
15
|
+
"""Initialize summarizer."""
|
|
16
|
+
self.diff_parser = DiffParser()
|
|
17
|
+
|
|
18
|
+
def summarize_file_diff(self, file_diff: FileDiff) -> str:
|
|
19
|
+
"""Generate summary for a file diff using AST and diff analysis.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
file_diff: FileDiff object.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Summary string (target: 20-80 words).
|
|
26
|
+
"""
|
|
27
|
+
if file_diff.summary:
|
|
28
|
+
return file_diff.summary
|
|
29
|
+
|
|
30
|
+
# Check cache
|
|
31
|
+
cache_key = file_diff.raw_diff + str(file_diff.extracted_symbols)
|
|
32
|
+
cached = cache_manager.get("summaries", cache_key)
|
|
33
|
+
if cached:
|
|
34
|
+
return cached
|
|
35
|
+
|
|
36
|
+
sym = file_diff.extracted_symbols
|
|
37
|
+
sentences = []
|
|
38
|
+
|
|
39
|
+
if sym.added:
|
|
40
|
+
sentences.append(f"Added {', '.join(sym.added[:3])}{' and more' if len(sym.added)>3 else ''}.")
|
|
41
|
+
if sym.modified:
|
|
42
|
+
sentences.append(f"Refactored {', '.join(sym.modified[:3])}.")
|
|
43
|
+
if sym.removed:
|
|
44
|
+
sentences.append(f"Removed {', '.join(sym.removed[:3])}.")
|
|
45
|
+
if sym.imported:
|
|
46
|
+
sentences.append(f"Introduced dependencies on {', '.join(sym.imported[:3])}.")
|
|
47
|
+
|
|
48
|
+
if not sentences:
|
|
49
|
+
# Fallback
|
|
50
|
+
summary = self.diff_parser.extract_summary(
|
|
51
|
+
file_diff.raw_diff,
|
|
52
|
+
file_diff.file_path
|
|
53
|
+
)
|
|
54
|
+
if len(summary) > 80:
|
|
55
|
+
summary = summary[:77] + "..."
|
|
56
|
+
sentences.append(summary)
|
|
57
|
+
|
|
58
|
+
summary = " ".join(sentences)
|
|
59
|
+
if len(summary) > 120:
|
|
60
|
+
summary = summary[:117] + "..."
|
|
61
|
+
|
|
62
|
+
cache_manager.set("summaries", cache_key, summary)
|
|
63
|
+
return summary
|
|
64
|
+
|
|
65
|
+
def summarize_cluster(self, file_diffs: list[FileDiff]) -> str:
|
|
66
|
+
"""Generate summary for a cluster of files."""
|
|
67
|
+
if not file_diffs:
|
|
68
|
+
return "No changes"
|
|
69
|
+
|
|
70
|
+
summaries = [self.summarize_file_diff(diff) for diff in file_diffs]
|
|
71
|
+
unique_summaries = list(dict.fromkeys(summaries))
|
|
72
|
+
|
|
73
|
+
if len(unique_summaries) == 1:
|
|
74
|
+
return unique_summaries[0]
|
|
75
|
+
|
|
76
|
+
combined = " ".join(unique_summaries[:3])
|
|
77
|
+
if len(combined) > 120:
|
|
78
|
+
combined = combined[:117] + "..."
|
|
79
|
+
|
|
80
|
+
return combined
|
|
81
|
+
|
|
82
|
+
def extract_keywords(self, file_diff: FileDiff) -> list[str]:
|
|
83
|
+
"""Extract keywords from a file diff."""
|
|
84
|
+
keywords = []
|
|
85
|
+
sym = file_diff.extracted_symbols
|
|
86
|
+
keywords.extend(sym.added)
|
|
87
|
+
keywords.extend(sym.modified)
|
|
88
|
+
keywords.extend(sym.removed)
|
|
89
|
+
keywords.extend(sym.imported)
|
|
90
|
+
keywords.extend(sym.classes)
|
|
91
|
+
keywords.extend(sym.functions)
|
|
92
|
+
|
|
93
|
+
if not keywords:
|
|
94
|
+
# Fallback
|
|
95
|
+
path_parts = file_diff.file_path.lower().split('/')
|
|
96
|
+
for part in path_parts:
|
|
97
|
+
clean = part.replace('.', '').replace('-', '_')
|
|
98
|
+
if clean and len(clean) > 2 and clean not in ['src', 'lib', 'utils', 'test']:
|
|
99
|
+
keywords.append(clean)
|
|
100
|
+
|
|
101
|
+
return list(set(keywords))
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Symbol extraction bridging diffs and AST."""
|
|
2
|
+
|
|
3
|
+
from typing import List, Dict, Set
|
|
4
|
+
from ..models import FileDiff, SymbolExtraction
|
|
5
|
+
from .ast_parser import ASTParser
|
|
6
|
+
from .diff_parser import DiffParser
|
|
7
|
+
from ..logger import get_logger
|
|
8
|
+
|
|
9
|
+
logger = get_logger("analysis.symbol_extractor")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SymbolExtractor:
|
|
13
|
+
"""Extracts symbols from changed files."""
|
|
14
|
+
|
|
15
|
+
def __init__(self):
|
|
16
|
+
self.diff_parser = DiffParser()
|
|
17
|
+
|
|
18
|
+
def extract_symbols(self, file_diff: FileDiff) -> SymbolExtraction:
|
|
19
|
+
"""Extract rich symbol information from a file diff.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
file_diff: FileDiff object containing raw diff.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
SymbolExtraction with categorized symbols.
|
|
26
|
+
"""
|
|
27
|
+
extraction = SymbolExtraction()
|
|
28
|
+
|
|
29
|
+
if not file_diff.raw_diff.strip():
|
|
30
|
+
return extraction
|
|
31
|
+
|
|
32
|
+
file_path = file_diff.file_path
|
|
33
|
+
ext = self.diff_parser._get_file_extension(file_path).lstrip(".")
|
|
34
|
+
if ext in ["py"]:
|
|
35
|
+
lang = "python"
|
|
36
|
+
elif ext in ["js", "jsx"]:
|
|
37
|
+
lang = "javascript"
|
|
38
|
+
elif ext in ["ts", "tsx"]:
|
|
39
|
+
lang = "typescript"
|
|
40
|
+
else:
|
|
41
|
+
lang = ext
|
|
42
|
+
|
|
43
|
+
# Try to parse full added content for AST if available
|
|
44
|
+
# But we only have diff. So we can extract added/removed lines
|
|
45
|
+
added_lines = [l[1:] for l in file_diff.raw_diff.split('\n') if l.startswith('+') and not l.startswith('+++')]
|
|
46
|
+
removed_lines = [l[1:] for l in file_diff.raw_diff.split('\n') if l.startswith('-') and not l.startswith('---')]
|
|
47
|
+
|
|
48
|
+
added_content = "\n".join(added_lines)
|
|
49
|
+
removed_content = "\n".join(removed_lines)
|
|
50
|
+
|
|
51
|
+
ast_parser = ASTParser(lang)
|
|
52
|
+
|
|
53
|
+
# Parse added content
|
|
54
|
+
if added_content:
|
|
55
|
+
extraction.classes.extend(ast_parser.extract_classes(added_content))
|
|
56
|
+
extraction.functions.extend(ast_parser.extract_functions(added_content))
|
|
57
|
+
extraction.imported.extend(ast_parser.extract_imports(added_content))
|
|
58
|
+
|
|
59
|
+
# Use diff parser for standard symbols as a fallback
|
|
60
|
+
patterns = self.diff_parser.LANGUAGE_PATTERNS.get("." + ext, {})
|
|
61
|
+
if "symbols" in patterns:
|
|
62
|
+
symbols = self.diff_parser._extract_symbols(added_lines, patterns["symbols"])
|
|
63
|
+
extraction.added.extend(symbols)
|
|
64
|
+
|
|
65
|
+
imports = self.diff_parser._extract_imports(added_lines)
|
|
66
|
+
extraction.imported.extend(imports)
|
|
67
|
+
|
|
68
|
+
# Parse removed content
|
|
69
|
+
if removed_content:
|
|
70
|
+
patterns = self.diff_parser.LANGUAGE_PATTERNS.get("." + ext, {})
|
|
71
|
+
if "symbols" in patterns:
|
|
72
|
+
symbols = self.diff_parser._extract_symbols(removed_lines, patterns["symbols"])
|
|
73
|
+
extraction.removed.extend(symbols)
|
|
74
|
+
|
|
75
|
+
# Deduplicate
|
|
76
|
+
extraction.classes = list(set(extraction.classes))
|
|
77
|
+
extraction.functions = list(set(extraction.functions))
|
|
78
|
+
extraction.imported = list(set(extraction.imported))
|
|
79
|
+
extraction.added = list(set(extraction.added))
|
|
80
|
+
extraction.removed = list(set(extraction.removed))
|
|
81
|
+
|
|
82
|
+
# Determine modified (present in both added and removed)
|
|
83
|
+
modified = set(extraction.added).intersection(set(extraction.removed))
|
|
84
|
+
extraction.modified = list(modified)
|
|
85
|
+
extraction.added = list(set(extraction.added) - modified)
|
|
86
|
+
extraction.removed = list(set(extraction.removed) - modified)
|
|
87
|
+
|
|
88
|
+
return extraction
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Benchmarking for Smart Commit pipeline."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from .git import RepositoryScanner
|
|
7
|
+
from .analysis import DiffParser, Summarizer
|
|
8
|
+
from .analysis.symbol_extractor import SymbolExtractor
|
|
9
|
+
from .analysis.dependency_graph import DependencyGraph
|
|
10
|
+
from .clustering import SemanticClustering
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
from rich.table import Table
|
|
15
|
+
except ImportError:
|
|
16
|
+
Console = None
|
|
17
|
+
Table = None
|
|
18
|
+
|
|
19
|
+
class BenchmarkHarness:
|
|
20
|
+
"""Measures performance of the semantic pipeline."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, repo_path: Path):
|
|
23
|
+
self.repo_path = repo_path
|
|
24
|
+
self.console = Console() if Console else None
|
|
25
|
+
|
|
26
|
+
def run(self):
|
|
27
|
+
"""Execute the benchmark."""
|
|
28
|
+
print(f"Starting benchmark on {self.repo_path}...")
|
|
29
|
+
results = []
|
|
30
|
+
|
|
31
|
+
# 1. Scanning
|
|
32
|
+
start = time.time()
|
|
33
|
+
snapshot = RepositoryScanner(self.repo_path).scan()
|
|
34
|
+
results.append(("Scanning", time.time() - start))
|
|
35
|
+
|
|
36
|
+
num_files = snapshot.total_files_changed
|
|
37
|
+
if num_files == 0:
|
|
38
|
+
print("No files changed in repo. Cannot benchmark.")
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
# 2. Parsing & Symbols
|
|
42
|
+
start = time.time()
|
|
43
|
+
parser = DiffParser()
|
|
44
|
+
extractor = SymbolExtractor()
|
|
45
|
+
for file_diff in snapshot.all_files:
|
|
46
|
+
parser.enrich_file_diff(file_diff)
|
|
47
|
+
file_diff.extracted_symbols = extractor.extract_symbols(file_diff)
|
|
48
|
+
results.append(("AST & Symbols", time.time() - start))
|
|
49
|
+
|
|
50
|
+
# 3. Summaries
|
|
51
|
+
start = time.time()
|
|
52
|
+
summarizer = Summarizer()
|
|
53
|
+
for file_diff in snapshot.all_files:
|
|
54
|
+
file_diff.summary = summarizer.summarize_file_diff(file_diff)
|
|
55
|
+
results.append(("Summarization", time.time() - start))
|
|
56
|
+
|
|
57
|
+
# 4. Dependency Graph
|
|
58
|
+
start = time.time()
|
|
59
|
+
dep_graph = DependencyGraph(self.repo_path)
|
|
60
|
+
results.append(("Dependency Graph", time.time() - start))
|
|
61
|
+
|
|
62
|
+
# 5. Clustering (includes Embeddings)
|
|
63
|
+
start = time.time()
|
|
64
|
+
clustering = SemanticClustering()
|
|
65
|
+
clustering.dep_graph = dep_graph
|
|
66
|
+
clusters = clustering.cluster_files(snapshot.all_files, repo_path=self.repo_path)
|
|
67
|
+
results.append(("Clustering & Embeddings", time.time() - start))
|
|
68
|
+
|
|
69
|
+
total_time = sum(t for _, t in results)
|
|
70
|
+
|
|
71
|
+
if self.console and Table:
|
|
72
|
+
table = Table(title=f"Benchmark Results ({num_files} files)")
|
|
73
|
+
table.add_column("Stage", style="cyan")
|
|
74
|
+
table.add_column("Time (s)", justify="right", style="green")
|
|
75
|
+
|
|
76
|
+
for stage, t in results:
|
|
77
|
+
table.add_row(stage, f"{t:.3f}")
|
|
78
|
+
table.add_row("Total", f"{total_time:.3f}", style="bold yellow")
|
|
79
|
+
|
|
80
|
+
self.console.print()
|
|
81
|
+
self.console.print(table)
|
|
82
|
+
else:
|
|
83
|
+
print(f"\nBenchmark Results ({num_files} files)")
|
|
84
|
+
for stage, t in results:
|
|
85
|
+
print(f"{stage}: {t:.3f}s")
|
|
86
|
+
print(f"Total: {total_time:.3f}s")
|
|
87
|
+
|
|
88
|
+
print(f"\nGenerated {len(clusters)} clusters.")
|
smart_commit/cache.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Disk-based caching for AST, summaries, and embeddings."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import hashlib
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Optional, Dict
|
|
7
|
+
|
|
8
|
+
from .logger import get_logger
|
|
9
|
+
|
|
10
|
+
logger = get_logger("cache")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CacheManager:
|
|
14
|
+
"""Manages disk-based caching for expensive operations."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, cache_dir: str = ".smart_commit_cache"):
|
|
17
|
+
self.cache_dir = Path(cache_dir)
|
|
18
|
+
self.cache_file = self.cache_dir / "cache.json"
|
|
19
|
+
self._cache: Dict[str, Any] = {}
|
|
20
|
+
self._load_cache()
|
|
21
|
+
|
|
22
|
+
def _load_cache(self):
|
|
23
|
+
"""Load cache from disk."""
|
|
24
|
+
if not self.cache_file.exists():
|
|
25
|
+
return
|
|
26
|
+
try:
|
|
27
|
+
with open(self.cache_file, "r", encoding="utf-8") as f:
|
|
28
|
+
self._cache = json.load(f)
|
|
29
|
+
except Exception as e:
|
|
30
|
+
logger.debug(f"Failed to load cache: {e}")
|
|
31
|
+
self._cache = {}
|
|
32
|
+
|
|
33
|
+
def _save_cache(self):
|
|
34
|
+
"""Save cache to disk."""
|
|
35
|
+
try:
|
|
36
|
+
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
|
37
|
+
with open(self.cache_file, "w", encoding="utf-8") as f:
|
|
38
|
+
json.dump(self._cache, f)
|
|
39
|
+
except Exception as e:
|
|
40
|
+
logger.debug(f"Failed to save cache: {e}")
|
|
41
|
+
|
|
42
|
+
def _generate_key(self, namespace: str, text: str) -> str:
|
|
43
|
+
"""Generate a stable cache key."""
|
|
44
|
+
text_hash = hashlib.md5(text.encode("utf-8")).hexdigest()
|
|
45
|
+
return f"{namespace}_{text_hash}"
|
|
46
|
+
|
|
47
|
+
def get(self, namespace: str, text: str) -> Optional[Any]:
|
|
48
|
+
"""Retrieve a value from the cache."""
|
|
49
|
+
key = self._generate_key(namespace, text)
|
|
50
|
+
return self._cache.get(key)
|
|
51
|
+
|
|
52
|
+
def set(self, namespace: str, text: str, value: Any):
|
|
53
|
+
"""Set a value in the cache."""
|
|
54
|
+
key = self._generate_key(namespace, text)
|
|
55
|
+
self._cache[key] = value
|
|
56
|
+
self._save_cache()
|
|
57
|
+
|
|
58
|
+
def clear(self):
|
|
59
|
+
"""Clear the cache."""
|
|
60
|
+
self._cache = {}
|
|
61
|
+
if self.cache_file.exists():
|
|
62
|
+
self.cache_file.unlink()
|
|
63
|
+
|
|
64
|
+
# Global cache instance
|
|
65
|
+
cache_manager = CacheManager()
|