ref-agents 1.0.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.
- ref_agents/__init__.py +9 -0
- ref_agents/api_keys.json.example +8 -0
- ref_agents/auth.py +129 -0
- ref_agents/codemap/..md +62 -0
- ref_agents/codemap/CODE_MAP.md +37 -0
- ref_agents/codemap/core.md +43 -0
- ref_agents/codemap/models.md +43 -0
- ref_agents/codemap/prompts.md +40 -0
- ref_agents/codemap/security.md +45 -0
- ref_agents/codemap/tools.md +94 -0
- ref_agents/codemap/tools_browser.md +44 -0
- ref_agents/codemap/utils.md +42 -0
- ref_agents/codemap/workflow.md +42 -0
- ref_agents/config/ai_patterns.yaml +101 -0
- ref_agents/config/frameworks/angular.yaml +104 -0
- ref_agents/config/frameworks/aspnet.yaml +84 -0
- ref_agents/config/frameworks/ef_core.yaml +81 -0
- ref_agents/config/frameworks/react.yaml +111 -0
- ref_agents/config/frameworks/spring_boot.yaml +117 -0
- ref_agents/config/languages/csharp.yaml +153 -0
- ref_agents/config/languages/java.yaml +188 -0
- ref_agents/config/languages/javascript.yaml +172 -0
- ref_agents/config/languages/python.yaml +153 -0
- ref_agents/config/languages/typescript.yaml +193 -0
- ref_agents/constants.py +553 -0
- ref_agents/core/__init__.py +15 -0
- ref_agents/core/config_loader.py +160 -0
- ref_agents/core/config_models.py +167 -0
- ref_agents/core/config_parsing.py +84 -0
- ref_agents/core/language_detector.py +388 -0
- ref_agents/core/validation_models.py +66 -0
- ref_agents/core/validation_primitives.py +176 -0
- ref_agents/errors.py +34 -0
- ref_agents/license_client.py +247 -0
- ref_agents/models/__init__.py +22 -0
- ref_agents/models/gherkin.py +45 -0
- ref_agents/models/hierarchy.py +80 -0
- ref_agents/models/invest.py +59 -0
- ref_agents/models/version.py +49 -0
- ref_agents/prompts/__init__.py +9 -0
- ref_agents/prompts/start_agent.py +772 -0
- ref_agents/rules/architecture/backend_patterns.md +43 -0
- ref_agents/rules/architecture/diagramming.md +100 -0
- ref_agents/rules/architecture/frontend_patterns.md +40 -0
- ref_agents/rules/architecture/impact_analysis.md +129 -0
- ref_agents/rules/architecture/migration_strategy.md +208 -0
- ref_agents/rules/architecture/regression_protocol.md +77 -0
- ref_agents/rules/architecture/system_design.md +97 -0
- ref_agents/rules/common/codemap_standard.md +97 -0
- ref_agents/rules/common/core_protocol.md +59 -0
- ref_agents/rules/common/prompt_engineering.md +294 -0
- ref_agents/rules/development/debugging.md +32 -0
- ref_agents/rules/development/implementation.md +205 -0
- ref_agents/rules/operations/completion.md +119 -0
- ref_agents/rules/operations/cutover_protocol.md +218 -0
- ref_agents/rules/operations/discovery.md +179 -0
- ref_agents/rules/operations/fix_workflow.md +87 -0
- ref_agents/rules/operations/forensics.md +278 -0
- ref_agents/rules/operations/platform.md +263 -0
- ref_agents/rules/operations/synchronous_flow.md +25 -0
- ref_agents/rules/product/ac_validation.md +25 -0
- ref_agents/rules/product/brainstorming.md +27 -0
- ref_agents/rules/product/ref_flow.md +101 -0
- ref_agents/rules/product/requirements_std.md +114 -0
- ref_agents/rules/product/spec_writing.md +235 -0
- ref_agents/rules/product/strategy.md +96 -0
- ref_agents/rules/quality/documentation_standards.md +46 -0
- ref_agents/rules/quality/parity_testing.md +234 -0
- ref_agents/rules/quality/project_documentation.md +56 -0
- ref_agents/rules/quality/qa_lead.md +111 -0
- ref_agents/rules/quality/test_design.md +146 -0
- ref_agents/rules/quality/testing_standards.md +293 -0
- ref_agents/rules/review/pr_review.md +116 -0
- ref_agents/rules/security/security_audit.md +83 -0
- ref_agents/security/__init__.py +22 -0
- ref_agents/security/dependency_audit.py +188 -0
- ref_agents/security/file_audit.py +208 -0
- ref_agents/security/network_scan.py +179 -0
- ref_agents/security/report_generator.py +313 -0
- ref_agents/security/secret_scan.py +252 -0
- ref_agents/security/url_scan.py +240 -0
- ref_agents/security_scan.py +236 -0
- ref_agents/server.py +1586 -0
- ref_agents/session.py +100 -0
- ref_agents/tool_names.py +55 -0
- ref_agents/tools/__init__.py +8 -0
- ref_agents/tools/agents_generator.py +315 -0
- ref_agents/tools/ai_pattern_detector.py +815 -0
- ref_agents/tools/brownfield_populator.py +529 -0
- ref_agents/tools/browser/__init__.py +50 -0
- ref_agents/tools/browser/evidence_verifier.py +302 -0
- ref_agents/tools/browser/execution_logger.py +249 -0
- ref_agents/tools/browser/playwright_mcp_client.py +259 -0
- ref_agents/tools/browser/screenshot_utils.py +184 -0
- ref_agents/tools/browser/test_executor.py +537 -0
- ref_agents/tools/code_quality_scanner.py +629 -0
- ref_agents/tools/codemap/..md +93 -0
- ref_agents/tools/codemap/CODE_MAP.md +30 -0
- ref_agents/tools/codemap/browser.md +44 -0
- ref_agents/tools/codemap.py +403 -0
- ref_agents/tools/codemap_freshness.py +234 -0
- ref_agents/tools/comment_smell_scanner.py +346 -0
- ref_agents/tools/complexity.py +436 -0
- ref_agents/tools/complexity_ast.py +333 -0
- ref_agents/tools/compliance.py +246 -0
- ref_agents/tools/compliance_remediation.py +846 -0
- ref_agents/tools/context_graph.py +839 -0
- ref_agents/tools/context_manager.py +550 -0
- ref_agents/tools/context_tools.py +121 -0
- ref_agents/tools/cross_repo_linker.py +393 -0
- ref_agents/tools/dead_code_scanner.py +637 -0
- ref_agents/tools/debt_scanner.py +1092 -0
- ref_agents/tools/dependency_graph.py +272 -0
- ref_agents/tools/discovery_audit.py +372 -0
- ref_agents/tools/docs_scanner.py +600 -0
- ref_agents/tools/evaluate_gate.py +119 -0
- ref_agents/tools/external_detector.py +524 -0
- ref_agents/tools/features_generator.py +282 -0
- ref_agents/tools/flow_gap_detector.py +373 -0
- ref_agents/tools/flow_mapper.py +327 -0
- ref_agents/tools/full_suite_runner.py +740 -0
- ref_agents/tools/gherkin_parser.py +227 -0
- ref_agents/tools/guard_tools.py +139 -0
- ref_agents/tools/handoff_tools.py +282 -0
- ref_agents/tools/health_scanner.py +1211 -0
- ref_agents/tools/hierarchy_manager.py +289 -0
- ref_agents/tools/invest_scorer.py +249 -0
- ref_agents/tools/jira_confluence_export.py +306 -0
- ref_agents/tools/json_output.py +76 -0
- ref_agents/tools/migration_mapper.py +946 -0
- ref_agents/tools/migration_readiness_scanner.py +209 -0
- ref_agents/tools/pattern_learner.py +522 -0
- ref_agents/tools/report_utils.py +155 -0
- ref_agents/tools/requirements_serializer.py +225 -0
- ref_agents/tools/security_audit_tool.py +106 -0
- ref_agents/tools/sequencing_engine.py +288 -0
- ref_agents/tools/summary_generator.py +275 -0
- ref_agents/tools/symbol_resolver.py +306 -0
- ref_agents/tools/symbol_smoke_runner.py +336 -0
- ref_agents/tools/test_plan_validator.py +189 -0
- ref_agents/tools/test_smell_walker.py +902 -0
- ref_agents/tools/tier1_fixer.py +502 -0
- ref_agents/tools/validators/__init__.py +419 -0
- ref_agents/tools/validators/architect.py +268 -0
- ref_agents/tools/validators/cutover_engineer.py +167 -0
- ref_agents/tools/validators/developer.py +180 -0
- ref_agents/tools/validators/discovery.py +150 -0
- ref_agents/tools/validators/forensic_engineer.py +191 -0
- ref_agents/tools/validators/impact_architect.py +181 -0
- ref_agents/tools/validators/migration_planner.py +166 -0
- ref_agents/tools/validators/parity_tester.py +155 -0
- ref_agents/tools/validators/platform_engineer.py +134 -0
- ref_agents/tools/validators/pr_reviewer.py +129 -0
- ref_agents/tools/validators/product_manager.py +291 -0
- ref_agents/tools/validators/qa_lead.py +172 -0
- ref_agents/tools/validators/scrum_master.py +212 -0
- ref_agents/tools/validators/security_owner.py +162 -0
- ref_agents/tools/validators/specifier.py +134 -0
- ref_agents/tools/validators/strategist.py +149 -0
- ref_agents/tools/validators/tester.py +121 -0
- ref_agents/tools/version_manager.py +202 -0
- ref_agents/tools/workflow_tools.py +1549 -0
- ref_agents/utils/__init__.py +21 -0
- ref_agents/utils/git_utils.py +351 -0
- ref_agents/utils/handoff_logger.py +368 -0
- ref_agents/utils/ignore_matcher.py +270 -0
- ref_agents/workflow/__init__.py +19 -0
- ref_agents/workflow/capabilities.py +328 -0
- ref_agents/workflow/state_machine.py +708 -0
- ref_agents/workflow/transitions.py +658 -0
- ref_agents-1.0.0.dist-info/METADATA +365 -0
- ref_agents-1.0.0.dist-info/RECORD +175 -0
- ref_agents-1.0.0.dist-info/WHEEL +4 -0
- ref_agents-1.0.0.dist-info/entry_points.txt +2 -0
- ref_agents-1.0.0.dist-info/licenses/LICENSE +115 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""REF Agents utilities package."""
|
|
2
|
+
|
|
3
|
+
from ref_agents.utils.git_utils import (
|
|
4
|
+
GitUser,
|
|
5
|
+
REF_CACHE_DIR,
|
|
6
|
+
find_project_root,
|
|
7
|
+
get_current_user_id,
|
|
8
|
+
get_git_root,
|
|
9
|
+
get_git_user,
|
|
10
|
+
get_ref_cache_dir,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"GitUser",
|
|
15
|
+
"REF_CACHE_DIR",
|
|
16
|
+
"find_project_root",
|
|
17
|
+
"get_current_user_id",
|
|
18
|
+
"get_git_root",
|
|
19
|
+
"get_git_user",
|
|
20
|
+
"get_ref_cache_dir",
|
|
21
|
+
]
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
"""Git utilities for REF Agents.
|
|
2
|
+
|
|
3
|
+
Provides user identification, repository context, and project root detection.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import subprocess
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
import structlog
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
logger = structlog.get_logger(__name__)
|
|
17
|
+
|
|
18
|
+
# Standard marker files for project root detection
|
|
19
|
+
PROJECT_MARKERS = (
|
|
20
|
+
"pyproject.toml",
|
|
21
|
+
"CODE_MAP.md",
|
|
22
|
+
"package.json",
|
|
23
|
+
"setup.py",
|
|
24
|
+
"Cargo.toml",
|
|
25
|
+
"go.mod",
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Cache directory name (consistent with .pytest_cache, .mypy_cache, .ruff_cache)
|
|
29
|
+
REF_CACHE_DIR = ".ref_cache"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class GitUser:
|
|
34
|
+
"""Git user identity."""
|
|
35
|
+
|
|
36
|
+
email: str
|
|
37
|
+
name: str
|
|
38
|
+
|
|
39
|
+
def __str__(self) -> str:
|
|
40
|
+
"""Return formatted user string."""
|
|
41
|
+
return f"{self.name} <{self.email}>"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def is_git_repo(directory: str | Path | None = None) -> bool:
|
|
45
|
+
"""Check if directory is inside a git repository.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
directory: Path to check. Defaults to current working directory.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
True if inside a git repo, False otherwise.
|
|
52
|
+
"""
|
|
53
|
+
try:
|
|
54
|
+
cmd = ["git", "rev-parse", "--git-dir"]
|
|
55
|
+
subprocess.run(
|
|
56
|
+
cmd,
|
|
57
|
+
capture_output=True,
|
|
58
|
+
check=True,
|
|
59
|
+
cwd=str(directory) if directory else None,
|
|
60
|
+
)
|
|
61
|
+
return True
|
|
62
|
+
except subprocess.CalledProcessError:
|
|
63
|
+
return False
|
|
64
|
+
except FileNotFoundError:
|
|
65
|
+
logger.warning("git_not_found", msg="Git is not installed")
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def get_git_root(directory: str | Path | None = None) -> Path | None:
|
|
70
|
+
"""Get git repository root directory.
|
|
71
|
+
|
|
72
|
+
Uses `git rev-parse --show-toplevel` - the industry standard approach.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
directory: Directory to start search from.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Path to git root, or None if not in a git repo.
|
|
79
|
+
"""
|
|
80
|
+
try:
|
|
81
|
+
result = subprocess.run(
|
|
82
|
+
["git", "rev-parse", "--show-toplevel"],
|
|
83
|
+
capture_output=True,
|
|
84
|
+
text=True,
|
|
85
|
+
check=True,
|
|
86
|
+
cwd=str(directory) if directory else None,
|
|
87
|
+
)
|
|
88
|
+
return Path(result.stdout.strip())
|
|
89
|
+
except subprocess.CalledProcessError:
|
|
90
|
+
return None
|
|
91
|
+
except FileNotFoundError:
|
|
92
|
+
logger.debug("git_not_installed")
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def find_project_root(start_dir: Path | None = None) -> Path | None:
|
|
97
|
+
"""Find project root using marker file detection.
|
|
98
|
+
|
|
99
|
+
Priority (industry best practice):
|
|
100
|
+
1. Session project_root (explicit user setting via set_project_root)
|
|
101
|
+
2. Git root (most reliable - used by pytest, mypy, ruff)
|
|
102
|
+
3. Marker files (pyproject.toml, CODE_MAP.md, package.json, etc.)
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
start_dir: Directory to start search from. Defaults to cwd.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Project root path, or None if not found.
|
|
109
|
+
"""
|
|
110
|
+
# Avoid circular import
|
|
111
|
+
from ref_agents.session import SessionManager
|
|
112
|
+
|
|
113
|
+
# Priority 1: Explicit session setting (highest priority)
|
|
114
|
+
session_root = SessionManager.get().project_root
|
|
115
|
+
if session_root and session_root.exists():
|
|
116
|
+
logger.debug("project_root_found", source="session", path=str(session_root))
|
|
117
|
+
return session_root
|
|
118
|
+
|
|
119
|
+
# Priority 2: Git root (most reliable)
|
|
120
|
+
git_root = get_git_root(start_dir)
|
|
121
|
+
if git_root:
|
|
122
|
+
logger.debug("project_root_found", source="git", path=str(git_root))
|
|
123
|
+
return git_root
|
|
124
|
+
|
|
125
|
+
# Priority 3: Marker file traversal
|
|
126
|
+
current = start_dir or Path.cwd()
|
|
127
|
+
for path in [current, *current.parents]:
|
|
128
|
+
if any((path / marker).exists() for marker in PROJECT_MARKERS):
|
|
129
|
+
logger.debug("project_root_found", source="marker", path=str(path))
|
|
130
|
+
return path
|
|
131
|
+
|
|
132
|
+
logger.debug("project_root_not_found")
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def get_ref_reports_dir(project_root: Path | None = None) -> Path:
|
|
137
|
+
"""Get REF reports directory path (durable — not gitignored content).
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
project_root: Project root. If None, uses find_project_root().
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
Path to ref-reports directory.
|
|
144
|
+
|
|
145
|
+
Raises:
|
|
146
|
+
RuntimeError: If project root cannot be determined.
|
|
147
|
+
"""
|
|
148
|
+
if project_root is None:
|
|
149
|
+
project_root = find_project_root()
|
|
150
|
+
if project_root is None:
|
|
151
|
+
raise RuntimeError(
|
|
152
|
+
"Cannot determine project root. "
|
|
153
|
+
"Use set_project_root() or run from a git repository."
|
|
154
|
+
)
|
|
155
|
+
return project_root / "ref-reports"
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def get_ref_cache_dir(project_root: Path | None = None) -> Path:
|
|
159
|
+
"""Get REF cache directory path.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
project_root: Project root. If None, uses find_project_root().
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
Path to .ref_cache directory.
|
|
166
|
+
|
|
167
|
+
Raises:
|
|
168
|
+
RuntimeError: If project root cannot be determined.
|
|
169
|
+
"""
|
|
170
|
+
if project_root is None:
|
|
171
|
+
project_root = find_project_root()
|
|
172
|
+
if project_root is None:
|
|
173
|
+
raise RuntimeError(
|
|
174
|
+
"Cannot determine project root. "
|
|
175
|
+
"Use set_project_root() or run from a git repository."
|
|
176
|
+
)
|
|
177
|
+
return project_root / REF_CACHE_DIR
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def get_git_user(directory: str | Path | None = None) -> GitUser | None:
|
|
181
|
+
"""Get git user from configuration.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
directory: Repository directory. Defaults to current working directory.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
GitUser with email and name, or None if not in git repo or not configured.
|
|
188
|
+
"""
|
|
189
|
+
if not is_git_repo(directory):
|
|
190
|
+
logger.debug("not_git_repo", directory=str(directory))
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
cwd = str(directory) if directory else None
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
email_result = subprocess.run(
|
|
197
|
+
["git", "config", "user.email"],
|
|
198
|
+
capture_output=True,
|
|
199
|
+
text=True,
|
|
200
|
+
check=True,
|
|
201
|
+
cwd=cwd,
|
|
202
|
+
)
|
|
203
|
+
name_result = subprocess.run(
|
|
204
|
+
["git", "config", "user.name"],
|
|
205
|
+
capture_output=True,
|
|
206
|
+
text=True,
|
|
207
|
+
check=True,
|
|
208
|
+
cwd=cwd,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
email = email_result.stdout.strip()
|
|
212
|
+
name = name_result.stdout.strip()
|
|
213
|
+
|
|
214
|
+
if not email:
|
|
215
|
+
logger.warning("git_email_not_configured")
|
|
216
|
+
return None
|
|
217
|
+
|
|
218
|
+
return GitUser(email=email, name=name or "Unknown")
|
|
219
|
+
|
|
220
|
+
except subprocess.CalledProcessError as e:
|
|
221
|
+
logger.warning("git_config_error", error=str(e))
|
|
222
|
+
return None
|
|
223
|
+
except FileNotFoundError:
|
|
224
|
+
logger.warning("git_not_found")
|
|
225
|
+
return None
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def get_current_user_id(directory: str | Path | None = None) -> str:
|
|
229
|
+
"""Get current user identifier for logging.
|
|
230
|
+
|
|
231
|
+
Falls back to OS user if git not available.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
directory: Repository directory.
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
User identifier string (email or OS username).
|
|
238
|
+
"""
|
|
239
|
+
import os
|
|
240
|
+
|
|
241
|
+
git_user = get_git_user(directory)
|
|
242
|
+
if git_user:
|
|
243
|
+
return git_user.email
|
|
244
|
+
|
|
245
|
+
# Fallback to OS user
|
|
246
|
+
return os.environ.get("USER", "unknown")
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def get_changed_files(
|
|
250
|
+
directory: str | Path | None = None,
|
|
251
|
+
extensions: set[str] | None = None,
|
|
252
|
+
) -> list[Path]:
|
|
253
|
+
"""Get list of files changed since last commit.
|
|
254
|
+
|
|
255
|
+
Includes: modified, added, untracked files.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
directory: Repository directory.
|
|
259
|
+
extensions: Filter by file extensions (e.g., {'.py', '.ts'}).
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
List of changed file paths relative to repo root.
|
|
263
|
+
"""
|
|
264
|
+
if not is_git_repo(directory):
|
|
265
|
+
return []
|
|
266
|
+
|
|
267
|
+
dir_path = Path(directory) if directory else Path.cwd()
|
|
268
|
+
changed: list[Path] = []
|
|
269
|
+
|
|
270
|
+
try:
|
|
271
|
+
result = subprocess.run(
|
|
272
|
+
["git", "diff", "--name-only", "HEAD"],
|
|
273
|
+
capture_output=True,
|
|
274
|
+
text=True,
|
|
275
|
+
cwd=str(dir_path),
|
|
276
|
+
check=False,
|
|
277
|
+
)
|
|
278
|
+
if result.returncode == 0:
|
|
279
|
+
for line in result.stdout.strip().splitlines():
|
|
280
|
+
if line:
|
|
281
|
+
changed.append(Path(line))
|
|
282
|
+
|
|
283
|
+
result = subprocess.run(
|
|
284
|
+
["git", "ls-files", "--others", "--exclude-standard"],
|
|
285
|
+
capture_output=True,
|
|
286
|
+
text=True,
|
|
287
|
+
cwd=str(dir_path),
|
|
288
|
+
check=False,
|
|
289
|
+
)
|
|
290
|
+
if result.returncode == 0:
|
|
291
|
+
for line in result.stdout.strip().splitlines():
|
|
292
|
+
if line:
|
|
293
|
+
changed.append(Path(line))
|
|
294
|
+
|
|
295
|
+
except FileNotFoundError:
|
|
296
|
+
logger.warning("git_not_found")
|
|
297
|
+
return []
|
|
298
|
+
|
|
299
|
+
# Filter by extension if specified
|
|
300
|
+
if extensions:
|
|
301
|
+
changed = [f for f in changed if f.suffix in extensions]
|
|
302
|
+
|
|
303
|
+
return list(set(changed))
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def get_git_file_stats(file_path: Path) -> dict[str, int]:
|
|
307
|
+
"""Get commit statistics for a file (churn analysis).
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
file_path: Path to the file.
|
|
311
|
+
|
|
312
|
+
Returns:
|
|
313
|
+
Dict with keys: commit_count, author_count.
|
|
314
|
+
"""
|
|
315
|
+
stats = {"commit_count": 0, "author_count": 0}
|
|
316
|
+
|
|
317
|
+
if not is_git_repo(file_path.parent):
|
|
318
|
+
return stats
|
|
319
|
+
|
|
320
|
+
try:
|
|
321
|
+
# Get commit count
|
|
322
|
+
cmd_commits = ["git", "rev-list", "--count", "HEAD", "--", str(file_path)]
|
|
323
|
+
result_commits = subprocess.run(
|
|
324
|
+
cmd_commits,
|
|
325
|
+
capture_output=True,
|
|
326
|
+
text=True,
|
|
327
|
+
check=False,
|
|
328
|
+
cwd=str(file_path.parent),
|
|
329
|
+
)
|
|
330
|
+
if result_commits.returncode == 0 and result_commits.stdout.strip():
|
|
331
|
+
stats["commit_count"] = int(result_commits.stdout.strip())
|
|
332
|
+
|
|
333
|
+
# Get unique authors
|
|
334
|
+
cmd_authors = ["git", "log", "--format=%aE", "--", str(file_path)]
|
|
335
|
+
result_authors = subprocess.run(
|
|
336
|
+
cmd_authors,
|
|
337
|
+
capture_output=True,
|
|
338
|
+
text=True,
|
|
339
|
+
check=False,
|
|
340
|
+
cwd=str(file_path.parent),
|
|
341
|
+
)
|
|
342
|
+
if result_authors.returncode == 0:
|
|
343
|
+
authors = set(result_authors.stdout.strip().splitlines())
|
|
344
|
+
# Filter empty lines
|
|
345
|
+
authors = {a for a in authors if a}
|
|
346
|
+
stats["author_count"] = len(authors)
|
|
347
|
+
|
|
348
|
+
except (subprocess.SubprocessError, ValueError, OSError) as e:
|
|
349
|
+
logger.debug("git_stats_failed", file=str(file_path), error=str(e))
|
|
350
|
+
|
|
351
|
+
return stats
|