crackerjack 0.32.0__py3-none-any.whl â 0.33.1__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 crackerjack might be problematic. Click here for more details.
- crackerjack/__main__.py +1350 -34
- crackerjack/adapters/__init__.py +17 -0
- crackerjack/adapters/lsp_client.py +358 -0
- crackerjack/adapters/rust_tool_adapter.py +194 -0
- crackerjack/adapters/rust_tool_manager.py +193 -0
- crackerjack/adapters/skylos_adapter.py +231 -0
- crackerjack/adapters/zuban_adapter.py +560 -0
- crackerjack/agents/base.py +7 -3
- crackerjack/agents/coordinator.py +271 -33
- crackerjack/agents/documentation_agent.py +9 -15
- crackerjack/agents/dry_agent.py +3 -15
- crackerjack/agents/formatting_agent.py +1 -1
- crackerjack/agents/import_optimization_agent.py +36 -180
- crackerjack/agents/performance_agent.py +17 -98
- crackerjack/agents/performance_helpers.py +7 -31
- crackerjack/agents/proactive_agent.py +1 -3
- crackerjack/agents/refactoring_agent.py +16 -85
- crackerjack/agents/refactoring_helpers.py +7 -42
- crackerjack/agents/security_agent.py +9 -48
- crackerjack/agents/test_creation_agent.py +356 -513
- crackerjack/agents/test_specialist_agent.py +0 -4
- crackerjack/api.py +6 -25
- crackerjack/cli/cache_handlers.py +204 -0
- crackerjack/cli/cache_handlers_enhanced.py +683 -0
- crackerjack/cli/facade.py +100 -0
- crackerjack/cli/handlers.py +224 -9
- crackerjack/cli/interactive.py +6 -4
- crackerjack/cli/options.py +642 -55
- crackerjack/cli/utils.py +2 -1
- crackerjack/code_cleaner.py +58 -117
- crackerjack/config/global_lock_config.py +8 -48
- crackerjack/config/hooks.py +53 -62
- crackerjack/core/async_workflow_orchestrator.py +24 -34
- crackerjack/core/autofix_coordinator.py +3 -17
- crackerjack/core/enhanced_container.py +64 -6
- crackerjack/core/file_lifecycle.py +12 -89
- crackerjack/core/performance.py +2 -2
- crackerjack/core/performance_monitor.py +15 -55
- crackerjack/core/phase_coordinator.py +257 -218
- crackerjack/core/resource_manager.py +14 -90
- crackerjack/core/service_watchdog.py +62 -95
- crackerjack/core/session_coordinator.py +149 -0
- crackerjack/core/timeout_manager.py +14 -72
- crackerjack/core/websocket_lifecycle.py +13 -78
- crackerjack/core/workflow_orchestrator.py +558 -240
- crackerjack/docs/INDEX.md +11 -0
- crackerjack/docs/generated/api/API_REFERENCE.md +10895 -0
- crackerjack/docs/generated/api/CLI_REFERENCE.md +109 -0
- crackerjack/docs/generated/api/CROSS_REFERENCES.md +1755 -0
- crackerjack/docs/generated/api/PROTOCOLS.md +3 -0
- crackerjack/docs/generated/api/SERVICES.md +1252 -0
- crackerjack/documentation/__init__.py +31 -0
- crackerjack/documentation/ai_templates.py +756 -0
- crackerjack/documentation/dual_output_generator.py +765 -0
- crackerjack/documentation/mkdocs_integration.py +518 -0
- crackerjack/documentation/reference_generator.py +977 -0
- crackerjack/dynamic_config.py +55 -50
- crackerjack/executors/async_hook_executor.py +10 -15
- crackerjack/executors/cached_hook_executor.py +117 -43
- crackerjack/executors/hook_executor.py +8 -34
- crackerjack/executors/hook_lock_manager.py +26 -183
- crackerjack/executors/individual_hook_executor.py +13 -11
- crackerjack/executors/lsp_aware_hook_executor.py +270 -0
- crackerjack/executors/tool_proxy.py +417 -0
- crackerjack/hooks/lsp_hook.py +79 -0
- crackerjack/intelligence/adaptive_learning.py +25 -10
- crackerjack/intelligence/agent_orchestrator.py +2 -5
- crackerjack/intelligence/agent_registry.py +34 -24
- crackerjack/intelligence/agent_selector.py +5 -7
- crackerjack/interactive.py +17 -6
- crackerjack/managers/async_hook_manager.py +0 -1
- crackerjack/managers/hook_manager.py +79 -1
- crackerjack/managers/publish_manager.py +66 -13
- crackerjack/managers/test_command_builder.py +5 -17
- crackerjack/managers/test_executor.py +1 -3
- crackerjack/managers/test_manager.py +109 -7
- crackerjack/managers/test_manager_backup.py +10 -9
- crackerjack/mcp/cache.py +2 -2
- crackerjack/mcp/client_runner.py +1 -1
- crackerjack/mcp/context.py +191 -68
- crackerjack/mcp/dashboard.py +7 -5
- crackerjack/mcp/enhanced_progress_monitor.py +31 -28
- crackerjack/mcp/file_monitor.py +30 -23
- crackerjack/mcp/progress_components.py +31 -21
- crackerjack/mcp/progress_monitor.py +50 -53
- crackerjack/mcp/rate_limiter.py +6 -6
- crackerjack/mcp/server_core.py +161 -32
- crackerjack/mcp/service_watchdog.py +2 -1
- crackerjack/mcp/state.py +4 -7
- crackerjack/mcp/task_manager.py +11 -9
- crackerjack/mcp/tools/core_tools.py +174 -33
- crackerjack/mcp/tools/error_analyzer.py +3 -2
- crackerjack/mcp/tools/execution_tools.py +15 -12
- crackerjack/mcp/tools/execution_tools_backup.py +42 -30
- crackerjack/mcp/tools/intelligence_tool_registry.py +7 -5
- crackerjack/mcp/tools/intelligence_tools.py +5 -2
- crackerjack/mcp/tools/monitoring_tools.py +33 -70
- crackerjack/mcp/tools/proactive_tools.py +24 -11
- crackerjack/mcp/tools/progress_tools.py +5 -8
- crackerjack/mcp/tools/utility_tools.py +20 -14
- crackerjack/mcp/tools/workflow_executor.py +62 -40
- crackerjack/mcp/websocket/app.py +8 -0
- crackerjack/mcp/websocket/endpoints.py +352 -357
- crackerjack/mcp/websocket/jobs.py +40 -57
- crackerjack/mcp/websocket/monitoring_endpoints.py +2935 -0
- crackerjack/mcp/websocket/server.py +7 -25
- crackerjack/mcp/websocket/websocket_handler.py +6 -17
- crackerjack/mixins/__init__.py +3 -0
- crackerjack/mixins/error_handling.py +145 -0
- crackerjack/models/config.py +21 -1
- crackerjack/models/config_adapter.py +49 -1
- crackerjack/models/protocols.py +176 -107
- crackerjack/models/resource_protocols.py +55 -210
- crackerjack/models/task.py +3 -0
- crackerjack/monitoring/ai_agent_watchdog.py +13 -13
- crackerjack/monitoring/metrics_collector.py +426 -0
- crackerjack/monitoring/regression_prevention.py +8 -8
- crackerjack/monitoring/websocket_server.py +643 -0
- crackerjack/orchestration/advanced_orchestrator.py +11 -6
- crackerjack/orchestration/coverage_improvement.py +3 -3
- crackerjack/orchestration/execution_strategies.py +26 -6
- crackerjack/orchestration/test_progress_streamer.py +8 -5
- crackerjack/plugins/base.py +2 -2
- crackerjack/plugins/hooks.py +7 -0
- crackerjack/plugins/managers.py +11 -8
- crackerjack/security/__init__.py +0 -1
- crackerjack/security/audit.py +90 -105
- crackerjack/services/anomaly_detector.py +392 -0
- crackerjack/services/api_extractor.py +615 -0
- crackerjack/services/backup_service.py +2 -2
- crackerjack/services/bounded_status_operations.py +15 -152
- crackerjack/services/cache.py +127 -1
- crackerjack/services/changelog_automation.py +395 -0
- crackerjack/services/config.py +18 -11
- crackerjack/services/config_merge.py +30 -85
- crackerjack/services/config_template.py +506 -0
- crackerjack/services/contextual_ai_assistant.py +48 -22
- crackerjack/services/coverage_badge_service.py +171 -0
- crackerjack/services/coverage_ratchet.py +41 -17
- crackerjack/services/debug.py +3 -3
- crackerjack/services/dependency_analyzer.py +460 -0
- crackerjack/services/dependency_monitor.py +14 -11
- crackerjack/services/documentation_generator.py +491 -0
- crackerjack/services/documentation_service.py +675 -0
- crackerjack/services/enhanced_filesystem.py +6 -5
- crackerjack/services/enterprise_optimizer.py +865 -0
- crackerjack/services/error_pattern_analyzer.py +676 -0
- crackerjack/services/file_hasher.py +1 -1
- crackerjack/services/git.py +41 -45
- crackerjack/services/health_metrics.py +10 -8
- crackerjack/services/heatmap_generator.py +735 -0
- crackerjack/services/initialization.py +30 -33
- crackerjack/services/input_validator.py +5 -97
- crackerjack/services/intelligent_commit.py +327 -0
- crackerjack/services/log_manager.py +15 -12
- crackerjack/services/logging.py +4 -3
- crackerjack/services/lsp_client.py +628 -0
- crackerjack/services/memory_optimizer.py +409 -0
- crackerjack/services/metrics.py +42 -33
- crackerjack/services/parallel_executor.py +416 -0
- crackerjack/services/pattern_cache.py +1 -1
- crackerjack/services/pattern_detector.py +6 -6
- crackerjack/services/performance_benchmarks.py +250 -576
- crackerjack/services/performance_cache.py +382 -0
- crackerjack/services/performance_monitor.py +565 -0
- crackerjack/services/predictive_analytics.py +510 -0
- crackerjack/services/quality_baseline.py +234 -0
- crackerjack/services/quality_baseline_enhanced.py +646 -0
- crackerjack/services/quality_intelligence.py +785 -0
- crackerjack/services/regex_patterns.py +605 -524
- crackerjack/services/regex_utils.py +43 -123
- crackerjack/services/secure_path_utils.py +5 -164
- crackerjack/services/secure_status_formatter.py +30 -141
- crackerjack/services/secure_subprocess.py +11 -92
- crackerjack/services/security.py +61 -30
- crackerjack/services/security_logger.py +18 -22
- crackerjack/services/server_manager.py +124 -16
- crackerjack/services/status_authentication.py +16 -159
- crackerjack/services/status_security_manager.py +4 -131
- crackerjack/services/terminal_utils.py +0 -0
- crackerjack/services/thread_safe_status_collector.py +19 -125
- crackerjack/services/unified_config.py +21 -13
- crackerjack/services/validation_rate_limiter.py +5 -54
- crackerjack/services/version_analyzer.py +459 -0
- crackerjack/services/version_checker.py +1 -1
- crackerjack/services/websocket_resource_limiter.py +10 -144
- crackerjack/services/zuban_lsp_service.py +390 -0
- crackerjack/slash_commands/__init__.py +2 -7
- crackerjack/slash_commands/run.md +2 -2
- crackerjack/tools/validate_input_validator_patterns.py +14 -40
- crackerjack/tools/validate_regex_patterns.py +19 -48
- {crackerjack-0.32.0.dist-info â crackerjack-0.33.1.dist-info}/METADATA +197 -26
- crackerjack-0.33.1.dist-info/RECORD +229 -0
- crackerjack/CLAUDE.md +0 -207
- crackerjack/RULES.md +0 -380
- crackerjack/py313.py +0 -234
- crackerjack-0.32.0.dist-info/RECORD +0 -180
- {crackerjack-0.32.0.dist-info â crackerjack-0.33.1.dist-info}/WHEEL +0 -0
- {crackerjack-0.32.0.dist-info â crackerjack-0.33.1.dist-info}/entry_points.txt +0 -0
- {crackerjack-0.32.0.dist-info â crackerjack-0.33.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
"""Automatic changelog generation and updates service."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
|
|
9
|
+
from .git import GitService
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ChangelogEntry:
|
|
13
|
+
"""Represents a single changelog entry."""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
entry_type: str,
|
|
18
|
+
description: str,
|
|
19
|
+
commit_hash: str = "",
|
|
20
|
+
breaking_change: bool = False,
|
|
21
|
+
) -> None:
|
|
22
|
+
self.type = entry_type
|
|
23
|
+
self.description = description
|
|
24
|
+
self.commit_hash = commit_hash
|
|
25
|
+
self.breaking_change = breaking_change
|
|
26
|
+
|
|
27
|
+
def to_markdown(self) -> str:
|
|
28
|
+
"""Convert entry to markdown format."""
|
|
29
|
+
prefix = "**BREAKING:** " if self.breaking_change else ""
|
|
30
|
+
return f"- {prefix}{self.description}"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ChangelogGenerator:
|
|
34
|
+
"""Generate and update changelogs based on git commits."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, console: Console, git_service: GitService) -> None:
|
|
37
|
+
self.console = console
|
|
38
|
+
self.git = git_service
|
|
39
|
+
|
|
40
|
+
# Conventional commit type mappings to changelog sections
|
|
41
|
+
self.type_mappings = {
|
|
42
|
+
"feat": "Added",
|
|
43
|
+
"fix": "Fixed",
|
|
44
|
+
"docs": "Documentation",
|
|
45
|
+
"style": "Changed",
|
|
46
|
+
"refactor": "Changed",
|
|
47
|
+
"test": "Testing",
|
|
48
|
+
"chore": "Internal",
|
|
49
|
+
"perf": "Performance",
|
|
50
|
+
"build": "Build",
|
|
51
|
+
"ci": "CI/CD",
|
|
52
|
+
"revert": "Reverted",
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# Regex patterns for parsing commit messages
|
|
56
|
+
self.conventional_commit_pattern = re.compile( # REGEX OK: conventional commit parsing
|
|
57
|
+
r"^(?P<type>\w+)(?:\((?P<scope>[^)]+)\))?(?P<breaking>!)?:\s*(?P<description>.+)$"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
self.breaking_change_pattern = (
|
|
61
|
+
re.compile( # REGEX OK: breaking change detection
|
|
62
|
+
r"BREAKING\s*CHANGE[:]\s*(.+)", re.IGNORECASE | re.MULTILINE
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def parse_commit_message(
|
|
67
|
+
self, commit_message: str, commit_hash: str = ""
|
|
68
|
+
) -> ChangelogEntry | None:
|
|
69
|
+
"""Parse a commit message into a changelog entry."""
|
|
70
|
+
# Split commit message into header and body
|
|
71
|
+
lines = commit_message.strip().split("\n")
|
|
72
|
+
header = lines[0].strip()
|
|
73
|
+
body = "\n".join(lines[1:]).strip() if len(lines) > 1 else ""
|
|
74
|
+
|
|
75
|
+
# Try to match conventional commit format
|
|
76
|
+
match = self.conventional_commit_pattern.match(header)
|
|
77
|
+
if not match:
|
|
78
|
+
# Fallback for non-conventional commits
|
|
79
|
+
return self._parse_non_conventional_commit(header, body, commit_hash)
|
|
80
|
+
|
|
81
|
+
commit_type = match.group("type").lower()
|
|
82
|
+
scope = match.group("scope") or ""
|
|
83
|
+
breaking_marker = match.group("breaking") == "!"
|
|
84
|
+
description = match.group("description").strip()
|
|
85
|
+
|
|
86
|
+
# Check for breaking changes in body
|
|
87
|
+
breaking_in_body = bool(self.breaking_change_pattern.search(body))
|
|
88
|
+
breaking_change = breaking_marker or breaking_in_body
|
|
89
|
+
|
|
90
|
+
# Map commit type to changelog section
|
|
91
|
+
changelog_section = self.type_mappings.get(commit_type, "Changed")
|
|
92
|
+
|
|
93
|
+
# Format description
|
|
94
|
+
formatted_description = self._format_description(
|
|
95
|
+
description, scope, commit_type
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
return ChangelogEntry(
|
|
99
|
+
entry_type=changelog_section,
|
|
100
|
+
description=formatted_description,
|
|
101
|
+
commit_hash=commit_hash,
|
|
102
|
+
breaking_change=breaking_change,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
def _parse_non_conventional_commit(
|
|
106
|
+
self, header: str, body: str, commit_hash: str
|
|
107
|
+
) -> ChangelogEntry | None:
|
|
108
|
+
"""Parse non-conventional commit messages."""
|
|
109
|
+
# Simple heuristics for non-conventional commits
|
|
110
|
+
header_lower = header.lower()
|
|
111
|
+
|
|
112
|
+
if any(
|
|
113
|
+
keyword in header_lower for keyword in ("add", "new", "create", "implement")
|
|
114
|
+
):
|
|
115
|
+
entry_type = "Added"
|
|
116
|
+
elif any(
|
|
117
|
+
keyword in header_lower for keyword in ("fix", "bug", "resolve", "correct")
|
|
118
|
+
):
|
|
119
|
+
entry_type = "Fixed"
|
|
120
|
+
elif any(
|
|
121
|
+
keyword in header_lower
|
|
122
|
+
for keyword in ("update", "change", "modify", "improve")
|
|
123
|
+
):
|
|
124
|
+
entry_type = "Changed"
|
|
125
|
+
elif any(keyword in header_lower for keyword in ("remove", "delete", "drop")):
|
|
126
|
+
entry_type = "Removed"
|
|
127
|
+
elif any(keyword in header_lower for keyword in ("doc", "readme", "comment")):
|
|
128
|
+
entry_type = "Documentation"
|
|
129
|
+
else:
|
|
130
|
+
entry_type = "Changed"
|
|
131
|
+
|
|
132
|
+
# Check for breaking changes
|
|
133
|
+
breaking_change = bool(self.breaking_change_pattern.search(body))
|
|
134
|
+
|
|
135
|
+
return ChangelogEntry(
|
|
136
|
+
entry_type=entry_type,
|
|
137
|
+
description=header,
|
|
138
|
+
commit_hash=commit_hash,
|
|
139
|
+
breaking_change=breaking_change,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
def _format_description(
|
|
143
|
+
self, description: str, scope: str, commit_type: str
|
|
144
|
+
) -> str:
|
|
145
|
+
"""Format the changelog description."""
|
|
146
|
+
# Capitalize first letter
|
|
147
|
+
description = description[0].upper() + description[1:] if description else ""
|
|
148
|
+
|
|
149
|
+
# Add scope context if present
|
|
150
|
+
if scope:
|
|
151
|
+
# Only add scope if it's not already mentioned in description
|
|
152
|
+
if scope.lower() not in description.lower():
|
|
153
|
+
description = f"{scope}: {description}"
|
|
154
|
+
|
|
155
|
+
return description
|
|
156
|
+
|
|
157
|
+
def generate_changelog_entries(
|
|
158
|
+
self, since_version: str | None = None, target_file: Path | None = None
|
|
159
|
+
) -> dict[str, list[ChangelogEntry]]:
|
|
160
|
+
"""Generate changelog entries from git commits."""
|
|
161
|
+
try:
|
|
162
|
+
# Get git commits
|
|
163
|
+
git_result = self._get_git_commits(since_version)
|
|
164
|
+
if not git_result:
|
|
165
|
+
return {}
|
|
166
|
+
|
|
167
|
+
# Parse commits into entries
|
|
168
|
+
return self._parse_commits_to_entries(git_result)
|
|
169
|
+
|
|
170
|
+
except Exception as e:
|
|
171
|
+
self.console.print(f"[red]â[/red] Error generating changelog entries: {e}")
|
|
172
|
+
return {}
|
|
173
|
+
|
|
174
|
+
def _get_git_commits(self, since_version: str | None = None) -> str | None:
|
|
175
|
+
"""Get git commit log output."""
|
|
176
|
+
# Build git command
|
|
177
|
+
git_command = self._build_git_log_command(since_version)
|
|
178
|
+
|
|
179
|
+
# Execute git command
|
|
180
|
+
result = self.git._run_git_command(git_command)
|
|
181
|
+
if result.returncode != 0:
|
|
182
|
+
self.console.print(
|
|
183
|
+
f"[yellow]â ī¸[/yellow] Failed to get git log: {result.stderr}"
|
|
184
|
+
)
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
return result.stdout
|
|
188
|
+
|
|
189
|
+
def _build_git_log_command(self, since_version: str | None = None) -> list[str]:
|
|
190
|
+
"""Build the git log command based on parameters."""
|
|
191
|
+
if since_version:
|
|
192
|
+
return [
|
|
193
|
+
"log",
|
|
194
|
+
f"{since_version}..HEAD",
|
|
195
|
+
"--oneline",
|
|
196
|
+
"--no-merges",
|
|
197
|
+
]
|
|
198
|
+
# Get commits since last release tag or last 50 commits
|
|
199
|
+
return ["log", "-50", "--oneline", "--no-merges"]
|
|
200
|
+
|
|
201
|
+
def _parse_commits_to_entries(
|
|
202
|
+
self, git_output: str
|
|
203
|
+
) -> dict[str, list[ChangelogEntry]]:
|
|
204
|
+
"""Parse git commit output into changelog entries."""
|
|
205
|
+
entries_by_type: dict[str, list[ChangelogEntry]] = {}
|
|
206
|
+
|
|
207
|
+
for line in git_output.strip().split("\n"):
|
|
208
|
+
if not line.strip():
|
|
209
|
+
continue
|
|
210
|
+
|
|
211
|
+
entry = self._process_commit_line(line)
|
|
212
|
+
if entry:
|
|
213
|
+
self._add_entry_to_collection(entry, entries_by_type)
|
|
214
|
+
|
|
215
|
+
return entries_by_type
|
|
216
|
+
|
|
217
|
+
def _process_commit_line(self, line: str) -> ChangelogEntry | None:
|
|
218
|
+
"""Process a single commit line into a changelog entry."""
|
|
219
|
+
# Parse commit hash and message
|
|
220
|
+
parts = line.strip().split(" ", 1)
|
|
221
|
+
if len(parts) < 2:
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
commit_hash = parts[0]
|
|
225
|
+
commit_message = parts[1]
|
|
226
|
+
|
|
227
|
+
# Get full commit message
|
|
228
|
+
full_message = self._get_full_commit_message(commit_hash, commit_message)
|
|
229
|
+
|
|
230
|
+
# Parse into changelog entry
|
|
231
|
+
return self.parse_commit_message(full_message, commit_hash)
|
|
232
|
+
|
|
233
|
+
def _get_full_commit_message(self, commit_hash: str, fallback_message: str) -> str:
|
|
234
|
+
"""Get the full commit message for detailed parsing."""
|
|
235
|
+
full_commit_result = self.git._run_git_command(
|
|
236
|
+
["show", "--format=%B", "--no-patch", commit_hash]
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
return (
|
|
240
|
+
full_commit_result.stdout
|
|
241
|
+
if full_commit_result.returncode == 0
|
|
242
|
+
else fallback_message
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
def _add_entry_to_collection(
|
|
246
|
+
self, entry: ChangelogEntry, entries_by_type: dict[str, list[ChangelogEntry]]
|
|
247
|
+
) -> None:
|
|
248
|
+
"""Add a changelog entry to the appropriate type collection."""
|
|
249
|
+
if entry.type not in entries_by_type:
|
|
250
|
+
entries_by_type[entry.type] = []
|
|
251
|
+
entries_by_type[entry.type].append(entry)
|
|
252
|
+
|
|
253
|
+
def update_changelog(
|
|
254
|
+
self,
|
|
255
|
+
changelog_path: Path,
|
|
256
|
+
new_version: str,
|
|
257
|
+
entries_by_type: dict[str, list[ChangelogEntry]] | None = None,
|
|
258
|
+
) -> bool:
|
|
259
|
+
"""Update the changelog file with new entries."""
|
|
260
|
+
try:
|
|
261
|
+
if entries_by_type is None:
|
|
262
|
+
entries_by_type = self.generate_changelog_entries()
|
|
263
|
+
|
|
264
|
+
if not entries_by_type:
|
|
265
|
+
self.console.print("[yellow]âšī¸[/yellow] No new changelog entries to add")
|
|
266
|
+
return True
|
|
267
|
+
|
|
268
|
+
# Read existing changelog
|
|
269
|
+
existing_content = ""
|
|
270
|
+
if changelog_path.exists():
|
|
271
|
+
existing_content = changelog_path.read_text(encoding="utf-8")
|
|
272
|
+
|
|
273
|
+
# Generate new section
|
|
274
|
+
new_section = self._generate_changelog_section(new_version, entries_by_type)
|
|
275
|
+
|
|
276
|
+
# Insert new section
|
|
277
|
+
updated_content = self._insert_new_section(existing_content, new_section)
|
|
278
|
+
|
|
279
|
+
# Write updated changelog
|
|
280
|
+
changelog_path.write_text(updated_content, encoding="utf-8")
|
|
281
|
+
|
|
282
|
+
self.console.print(
|
|
283
|
+
f"[green]â
[/green] Updated {changelog_path.name} with {len(entries_by_type)} sections"
|
|
284
|
+
)
|
|
285
|
+
return True
|
|
286
|
+
|
|
287
|
+
except Exception as e:
|
|
288
|
+
self.console.print(f"[red]â[/red] Failed to update changelog: {e}")
|
|
289
|
+
return False
|
|
290
|
+
|
|
291
|
+
def _generate_changelog_section(
|
|
292
|
+
self, version: str, entries_by_type: dict[str, list[ChangelogEntry]]
|
|
293
|
+
) -> str:
|
|
294
|
+
"""Generate a new changelog section."""
|
|
295
|
+
today = datetime.now().strftime("%Y-%m-%d")
|
|
296
|
+
section_lines = [f"## [{version}] - {today}", ""]
|
|
297
|
+
|
|
298
|
+
# Order sections by importance
|
|
299
|
+
section_order = [
|
|
300
|
+
"Added",
|
|
301
|
+
"Changed",
|
|
302
|
+
"Fixed",
|
|
303
|
+
"Removed",
|
|
304
|
+
"Performance",
|
|
305
|
+
"Security",
|
|
306
|
+
"Deprecated",
|
|
307
|
+
"Documentation",
|
|
308
|
+
"Testing",
|
|
309
|
+
"Build",
|
|
310
|
+
"CI/CD",
|
|
311
|
+
"Internal",
|
|
312
|
+
]
|
|
313
|
+
|
|
314
|
+
for section_name in section_order:
|
|
315
|
+
if section_name in entries_by_type:
|
|
316
|
+
entries = entries_by_type[section_name]
|
|
317
|
+
if entries:
|
|
318
|
+
section_lines.extend((f"### {section_name}", ""))
|
|
319
|
+
|
|
320
|
+
# Sort entries: breaking changes first, then alphabetically
|
|
321
|
+
entries.sort(
|
|
322
|
+
key=lambda e: (not e.breaking_change, e.description.lower())
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
for entry in entries:
|
|
326
|
+
section_lines.append(entry.to_markdown())
|
|
327
|
+
section_lines.append("")
|
|
328
|
+
|
|
329
|
+
return "\n".join(section_lines)
|
|
330
|
+
|
|
331
|
+
def _insert_new_section(self, existing_content: str, new_section: str) -> str:
|
|
332
|
+
"""Insert new section into existing changelog content."""
|
|
333
|
+
if not existing_content.strip():
|
|
334
|
+
# Create new changelog
|
|
335
|
+
header = """# Changelog
|
|
336
|
+
|
|
337
|
+
All notable changes to this project will be documented in this file.
|
|
338
|
+
|
|
339
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
340
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
341
|
+
|
|
342
|
+
"""
|
|
343
|
+
return header + new_section
|
|
344
|
+
|
|
345
|
+
# Find where to insert (after header, before first existing version)
|
|
346
|
+
lines = existing_content.split("\n")
|
|
347
|
+
insert_index = 0
|
|
348
|
+
|
|
349
|
+
# Find the insertion point (after the header, before first version)
|
|
350
|
+
for i, line in enumerate(lines):
|
|
351
|
+
if line.strip().startswith("## ["):
|
|
352
|
+
insert_index = i
|
|
353
|
+
break
|
|
354
|
+
else:
|
|
355
|
+
# No existing version sections found, insert at end
|
|
356
|
+
insert_index = len(lines)
|
|
357
|
+
|
|
358
|
+
# Insert new section
|
|
359
|
+
new_lines = (
|
|
360
|
+
lines[:insert_index] + new_section.split("\n") + lines[insert_index:]
|
|
361
|
+
)
|
|
362
|
+
return "\n".join(new_lines)
|
|
363
|
+
|
|
364
|
+
def generate_changelog_from_commits(
|
|
365
|
+
self, changelog_path: Path, version: str, since_version: str | None = None
|
|
366
|
+
) -> bool:
|
|
367
|
+
"""Generate and update changelog from git commits."""
|
|
368
|
+
self.console.print(
|
|
369
|
+
f"[cyan]đ[/cyan] Generating changelog entries for version {version}..."
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
entries = self.generate_changelog_entries(since_version)
|
|
373
|
+
if not entries:
|
|
374
|
+
self.console.print("[yellow]âšī¸[/yellow] No changelog entries generated")
|
|
375
|
+
return True
|
|
376
|
+
|
|
377
|
+
# Display preview
|
|
378
|
+
self._display_changelog_preview(entries)
|
|
379
|
+
|
|
380
|
+
return self.update_changelog(changelog_path, version, entries)
|
|
381
|
+
|
|
382
|
+
def _display_changelog_preview(
|
|
383
|
+
self, entries_by_type: dict[str, list[ChangelogEntry]]
|
|
384
|
+
) -> None:
|
|
385
|
+
"""Display a preview of generated changelog entries."""
|
|
386
|
+
self.console.print("[cyan]đ[/cyan] Changelog preview:")
|
|
387
|
+
|
|
388
|
+
for section_name, entries in entries_by_type.items():
|
|
389
|
+
if entries:
|
|
390
|
+
self.console.print(f"[bold]{section_name}:[/bold]")
|
|
391
|
+
for entry in entries[:3]: # Show first 3 entries
|
|
392
|
+
self.console.print(f" {entry.to_markdown()}")
|
|
393
|
+
if len(entries) > 3:
|
|
394
|
+
self.console.print(f" [dim]... and {len(entries) - 3} more[/dim]")
|
|
395
|
+
self.console.print()
|
crackerjack/services/config.py
CHANGED
|
@@ -53,8 +53,9 @@ class ConfigurationService:
|
|
|
53
53
|
)
|
|
54
54
|
return False
|
|
55
55
|
|
|
56
|
-
def get_temp_config_path(self) ->
|
|
57
|
-
|
|
56
|
+
def get_temp_config_path(self) -> str | None:
|
|
57
|
+
path = getattr(self, "_temp_config_path", None)
|
|
58
|
+
return str(path) if path else None
|
|
58
59
|
|
|
59
60
|
def _determine_config_mode(self, options: OptionsProtocol) -> str:
|
|
60
61
|
if options.experimental_hooks:
|
|
@@ -132,16 +133,14 @@ class ConfigurationService:
|
|
|
132
133
|
|
|
133
134
|
with config_file.open() as f:
|
|
134
135
|
yaml_result = yaml.safe_load(f)
|
|
135
|
-
config_data = (
|
|
136
|
-
t.cast("dict[str, t.Any]", yaml_result)
|
|
137
|
-
if isinstance(yaml_result, dict)
|
|
138
|
-
else {}
|
|
139
|
-
)
|
|
136
|
+
config_data = yaml_result if isinstance(yaml_result, dict) else {}
|
|
140
137
|
repos = config_data.get("repos", [])
|
|
141
138
|
if not isinstance(repos, list):
|
|
142
139
|
repos = []
|
|
143
140
|
hook_count = sum(
|
|
144
|
-
len(repo.get("hooks", []))
|
|
141
|
+
len(repo.get("hooks", []))
|
|
142
|
+
for repo in t.cast(list[dict[str, t.Any]], repos)
|
|
143
|
+
if isinstance(repo, dict)
|
|
145
144
|
)
|
|
146
145
|
stat = config_file.stat()
|
|
147
146
|
|
|
@@ -149,7 +148,13 @@ class ConfigurationService:
|
|
|
149
148
|
"exists": True,
|
|
150
149
|
"file_size": stat.st_size,
|
|
151
150
|
"modified_time": stat.st_mtime,
|
|
152
|
-
"repo_count": len(
|
|
151
|
+
"repo_count": len(
|
|
152
|
+
[
|
|
153
|
+
r
|
|
154
|
+
for r in t.cast(list[dict[str, t.Any]], repos)
|
|
155
|
+
if isinstance(r, dict)
|
|
156
|
+
]
|
|
157
|
+
),
|
|
153
158
|
"hook_count": hook_count,
|
|
154
159
|
"repos": [
|
|
155
160
|
{
|
|
@@ -157,7 +162,7 @@ class ConfigurationService:
|
|
|
157
162
|
"rev": repo.get("rev", "unknown"),
|
|
158
163
|
"hooks": len(repo.get("hooks", [])),
|
|
159
164
|
}
|
|
160
|
-
for repo in repos
|
|
165
|
+
for repo in t.cast(list[dict[str, t.Any]], repos)
|
|
161
166
|
if isinstance(repo, dict)
|
|
162
167
|
],
|
|
163
168
|
}
|
|
@@ -301,7 +306,9 @@ class ConfigurationService:
|
|
|
301
306
|
return {}
|
|
302
307
|
|
|
303
308
|
version_updates = {}
|
|
304
|
-
repos
|
|
309
|
+
repos: list[dict[str, t.Any]] = (
|
|
310
|
+
config.get("repos", []) if isinstance(config, dict) else []
|
|
311
|
+
)
|
|
305
312
|
for repo in repos:
|
|
306
313
|
repo_url = repo.get("repo", "")
|
|
307
314
|
rev = repo.get("rev", "")
|