gitflow-analytics 3.3.0__py3-none-any.whl → 3.5.2__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.
- gitflow_analytics/_version.py +1 -1
- gitflow_analytics/cli.py +517 -15
- gitflow_analytics/cli_wizards/__init__.py +10 -0
- gitflow_analytics/cli_wizards/install_wizard.py +1181 -0
- gitflow_analytics/cli_wizards/run_launcher.py +433 -0
- gitflow_analytics/config/__init__.py +3 -0
- gitflow_analytics/config/aliases.py +306 -0
- gitflow_analytics/config/loader.py +35 -1
- gitflow_analytics/config/schema.py +13 -0
- gitflow_analytics/constants.py +75 -0
- gitflow_analytics/core/cache.py +7 -3
- gitflow_analytics/core/data_fetcher.py +66 -30
- gitflow_analytics/core/git_timeout_wrapper.py +6 -4
- gitflow_analytics/core/progress.py +2 -4
- gitflow_analytics/core/subprocess_git.py +31 -5
- gitflow_analytics/identity_llm/analysis_pass.py +13 -3
- gitflow_analytics/identity_llm/analyzer.py +14 -2
- gitflow_analytics/identity_llm/models.py +7 -1
- gitflow_analytics/qualitative/classifiers/llm/openai_client.py +5 -3
- gitflow_analytics/security/config.py +6 -6
- gitflow_analytics/security/extractors/dependency_checker.py +14 -14
- gitflow_analytics/security/extractors/secret_detector.py +8 -14
- gitflow_analytics/security/extractors/vulnerability_scanner.py +9 -9
- gitflow_analytics/security/llm_analyzer.py +10 -10
- gitflow_analytics/security/security_analyzer.py +17 -17
- gitflow_analytics/tui/screens/analysis_progress_screen.py +1 -1
- gitflow_analytics/ui/progress_display.py +36 -29
- gitflow_analytics/verify_activity.py +23 -26
- {gitflow_analytics-3.3.0.dist-info → gitflow_analytics-3.5.2.dist-info}/METADATA +1 -1
- {gitflow_analytics-3.3.0.dist-info → gitflow_analytics-3.5.2.dist-info}/RECORD +34 -31
- gitflow_analytics/security/reports/__init__.py +0 -5
- gitflow_analytics/security/reports/security_report.py +0 -358
- {gitflow_analytics-3.3.0.dist-info → gitflow_analytics-3.5.2.dist-info}/WHEEL +0 -0
- {gitflow_analytics-3.3.0.dist-info → gitflow_analytics-3.5.2.dist-info}/entry_points.txt +0 -0
- {gitflow_analytics-3.3.0.dist-info → gitflow_analytics-3.5.2.dist-info}/licenses/LICENSE +0 -0
- {gitflow_analytics-3.3.0.dist-info → gitflow_analytics-3.5.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
"""Interactive launcher for GitFlow Analytics with repository selection and preferences.
|
|
2
|
+
|
|
3
|
+
This module provides an interactive workflow for running GitFlow Analytics with:
|
|
4
|
+
- Configuration file selection
|
|
5
|
+
- Repository multi-select
|
|
6
|
+
- Analysis period configuration
|
|
7
|
+
- Cache management
|
|
8
|
+
- Persistent preferences
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
from datetime import datetime, timezone
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any, Optional
|
|
17
|
+
|
|
18
|
+
import click
|
|
19
|
+
import yaml
|
|
20
|
+
|
|
21
|
+
from ..config import Config, ConfigLoader
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class InteractiveLauncher:
|
|
27
|
+
"""Interactive launcher for gitflow-analytics with preferences."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, config_path: Optional[Path] = None):
|
|
30
|
+
"""Initialize the interactive launcher.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
config_path: Optional path to configuration file.
|
|
34
|
+
If not provided, will search for default configs.
|
|
35
|
+
"""
|
|
36
|
+
self.config_path = config_path or self._find_default_config()
|
|
37
|
+
self.config: Optional[Config] = None
|
|
38
|
+
self.preferences: dict[str, Any] = {}
|
|
39
|
+
|
|
40
|
+
def run(self) -> bool:
|
|
41
|
+
"""Execute interactive launcher workflow.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
True if analysis completed successfully, False otherwise.
|
|
45
|
+
"""
|
|
46
|
+
click.echo("🚀 GitFlow Analytics Interactive Launcher\n")
|
|
47
|
+
|
|
48
|
+
# Step 1: Load configuration
|
|
49
|
+
if not self._load_config():
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
# Step 2: Load existing preferences
|
|
53
|
+
self._load_preferences()
|
|
54
|
+
|
|
55
|
+
# Step 3: Repository selection
|
|
56
|
+
selected_repos = self._select_repositories()
|
|
57
|
+
if not selected_repos:
|
|
58
|
+
click.echo("❌ No repositories selected!")
|
|
59
|
+
return False
|
|
60
|
+
|
|
61
|
+
# Step 4: Analysis period
|
|
62
|
+
weeks = self._select_analysis_period()
|
|
63
|
+
|
|
64
|
+
# Step 5: Cache management
|
|
65
|
+
clear_cache = self._confirm_clear_cache()
|
|
66
|
+
|
|
67
|
+
# Step 6: Identity analysis option
|
|
68
|
+
skip_identity = self._confirm_skip_identity()
|
|
69
|
+
|
|
70
|
+
# Step 7: Save preferences
|
|
71
|
+
self._save_preferences(selected_repos, weeks, clear_cache, skip_identity)
|
|
72
|
+
|
|
73
|
+
# Step 8: Run analysis
|
|
74
|
+
return self._run_analysis(selected_repos, weeks, clear_cache, skip_identity)
|
|
75
|
+
|
|
76
|
+
def _find_default_config(self) -> Optional[Path]:
|
|
77
|
+
"""Search for default configuration file.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Path to config file if found, None otherwise.
|
|
81
|
+
"""
|
|
82
|
+
# Common config file names in order of preference
|
|
83
|
+
config_names = [
|
|
84
|
+
"config.yaml",
|
|
85
|
+
"config.yml",
|
|
86
|
+
"gitflow-config.yaml",
|
|
87
|
+
"gitflow-config.yml",
|
|
88
|
+
".gitflow.yaml",
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
cwd = Path.cwd()
|
|
92
|
+
for name in config_names:
|
|
93
|
+
config_path = cwd / name
|
|
94
|
+
if config_path.exists():
|
|
95
|
+
return config_path
|
|
96
|
+
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
def _load_config(self) -> bool:
|
|
100
|
+
"""Load configuration file.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
True if configuration loaded successfully, False otherwise.
|
|
104
|
+
"""
|
|
105
|
+
if not self.config_path:
|
|
106
|
+
click.echo("❌ No configuration file found!")
|
|
107
|
+
click.echo("\nSearched for: config.yaml, config.yml, gitflow-config.yaml")
|
|
108
|
+
click.echo("\n💡 Run 'gitflow-analytics install' to create a configuration")
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
if not self.config_path.exists():
|
|
112
|
+
click.echo(f"❌ Configuration file not found: {self.config_path}")
|
|
113
|
+
return False
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
click.echo(f"📁 Loading configuration from: {self.config_path}")
|
|
117
|
+
self.config = ConfigLoader.load(self.config_path)
|
|
118
|
+
click.echo("✅ Configuration loaded\n")
|
|
119
|
+
return True
|
|
120
|
+
except Exception as e:
|
|
121
|
+
click.echo(f"❌ Error loading configuration: {e}")
|
|
122
|
+
logger.error(f"Config loading error: {type(e).__name__}")
|
|
123
|
+
return False
|
|
124
|
+
|
|
125
|
+
def _load_preferences(self) -> None:
|
|
126
|
+
"""Load existing launcher preferences from configuration."""
|
|
127
|
+
if not self.config:
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
# Load preferences from config YAML if they exist
|
|
131
|
+
try:
|
|
132
|
+
with open(self.config_path) as f:
|
|
133
|
+
config_data = yaml.safe_load(f)
|
|
134
|
+
|
|
135
|
+
if "launcher" in config_data:
|
|
136
|
+
self.preferences = config_data["launcher"]
|
|
137
|
+
logger.info(f"Loaded preferences: {self.preferences}")
|
|
138
|
+
except Exception as e:
|
|
139
|
+
logger.warning(f"Could not load preferences: {e}")
|
|
140
|
+
self.preferences = {}
|
|
141
|
+
|
|
142
|
+
def _get_available_repositories(self) -> list[str]:
|
|
143
|
+
"""Get list of available repositories from config.
|
|
144
|
+
|
|
145
|
+
Supports both explicit repository configuration and organization mode.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
List of repository names
|
|
149
|
+
"""
|
|
150
|
+
repos = []
|
|
151
|
+
|
|
152
|
+
# Check for organization mode
|
|
153
|
+
try:
|
|
154
|
+
with open(self.config_path) as f:
|
|
155
|
+
config_data = yaml.safe_load(f)
|
|
156
|
+
|
|
157
|
+
github_config = config_data.get("github", {})
|
|
158
|
+
|
|
159
|
+
if github_config.get("organization"):
|
|
160
|
+
# Organization mode - fetch repos from GitHub
|
|
161
|
+
token = self._resolve_env_var(github_config.get("token", ""))
|
|
162
|
+
if token:
|
|
163
|
+
from github import Github
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
gh = Github(token)
|
|
167
|
+
org_name = github_config["organization"]
|
|
168
|
+
org = gh.get_organization(org_name)
|
|
169
|
+
|
|
170
|
+
# Get all non-archived repos
|
|
171
|
+
for repo in org.get_repos(type="all"):
|
|
172
|
+
if not repo.archived:
|
|
173
|
+
repos.append(repo.full_name)
|
|
174
|
+
|
|
175
|
+
if repos:
|
|
176
|
+
click.echo(
|
|
177
|
+
f"🔍 Discovered {len(repos)} repositories from organization '{org_name}'\n"
|
|
178
|
+
)
|
|
179
|
+
except Exception as e:
|
|
180
|
+
click.echo(
|
|
181
|
+
f"⚠️ Could not fetch organization repos: {type(e).__name__}", err=True
|
|
182
|
+
)
|
|
183
|
+
logger.error(f"Organization repo fetch error: {e}")
|
|
184
|
+
|
|
185
|
+
# Fall back to explicit repositories
|
|
186
|
+
if not repos and self.config and self.config.repositories:
|
|
187
|
+
repos = [repo.path.name for repo in self.config.repositories]
|
|
188
|
+
|
|
189
|
+
except Exception as e:
|
|
190
|
+
logger.warning(f"Could not determine repositories: {e}")
|
|
191
|
+
# Final fallback
|
|
192
|
+
if self.config and self.config.repositories:
|
|
193
|
+
repos = [repo.path.name for repo in self.config.repositories]
|
|
194
|
+
|
|
195
|
+
return repos
|
|
196
|
+
|
|
197
|
+
def _resolve_env_var(self, value: str) -> str:
|
|
198
|
+
"""Resolve environment variable in config value.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
value: Config value that may contain ${VAR_NAME}
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
Resolved value
|
|
205
|
+
"""
|
|
206
|
+
import os
|
|
207
|
+
import re
|
|
208
|
+
|
|
209
|
+
# Load .env file if it exists next to config
|
|
210
|
+
env_path = self.config_path.parent / ".env"
|
|
211
|
+
if env_path.exists():
|
|
212
|
+
try:
|
|
213
|
+
with open(env_path) as f:
|
|
214
|
+
for line in f:
|
|
215
|
+
line = line.strip()
|
|
216
|
+
if line and not line.startswith("#") and "=" in line:
|
|
217
|
+
key, val = line.split("=", 1)
|
|
218
|
+
os.environ.setdefault(key.strip(), val.strip())
|
|
219
|
+
except Exception as e:
|
|
220
|
+
logger.debug(f"Could not load .env: {e}")
|
|
221
|
+
|
|
222
|
+
# Resolve ${VAR_NAME} patterns
|
|
223
|
+
pattern = r"\$\{([^}]+)\}"
|
|
224
|
+
|
|
225
|
+
def replace_var(match):
|
|
226
|
+
var_name = match.group(1)
|
|
227
|
+
return os.environ.get(var_name, match.group(0))
|
|
228
|
+
|
|
229
|
+
return re.sub(pattern, replace_var, value)
|
|
230
|
+
|
|
231
|
+
def _select_repositories(self) -> list[str]:
|
|
232
|
+
"""Interactive repository selection with multi-select.
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
List of selected repository names.
|
|
236
|
+
"""
|
|
237
|
+
# Check for organization mode first
|
|
238
|
+
repos = self._get_available_repositories()
|
|
239
|
+
if not repos:
|
|
240
|
+
click.echo("❌ No repositories configured!")
|
|
241
|
+
return []
|
|
242
|
+
|
|
243
|
+
click.echo("📂 Available Repositories:\n")
|
|
244
|
+
|
|
245
|
+
# Get last selected repos from preferences
|
|
246
|
+
last_selected = self.preferences.get("last_selected_repos", [])
|
|
247
|
+
|
|
248
|
+
# Display repositories with numbering and selection status
|
|
249
|
+
for i, repo_name in enumerate(repos, 1):
|
|
250
|
+
status = "✓" if repo_name in last_selected else " "
|
|
251
|
+
click.echo(f" [{status}] {i}. {repo_name}")
|
|
252
|
+
|
|
253
|
+
# Get user selection
|
|
254
|
+
click.echo("\n📝 Select repositories:")
|
|
255
|
+
click.echo(" • Enter numbers (comma-separated): 1,3,5")
|
|
256
|
+
click.echo(" • Enter 'all' for all repositories")
|
|
257
|
+
click.echo(" • Press Enter to use previous selection")
|
|
258
|
+
|
|
259
|
+
selection = click.prompt("Selection", default="", show_default=False).strip()
|
|
260
|
+
|
|
261
|
+
if selection.lower() == "all":
|
|
262
|
+
selected = repos
|
|
263
|
+
click.echo(f"✅ Selected all {len(selected)} repositories\n")
|
|
264
|
+
return selected
|
|
265
|
+
elif not selection and last_selected:
|
|
266
|
+
click.echo(f"✅ Using previous selection: {len(last_selected)} repositories\n")
|
|
267
|
+
return last_selected
|
|
268
|
+
elif not selection:
|
|
269
|
+
# Default to all repos if no previous selection
|
|
270
|
+
selected = repos
|
|
271
|
+
click.echo(f"✅ Selected all {len(selected)} repositories (default)\n")
|
|
272
|
+
return selected
|
|
273
|
+
else:
|
|
274
|
+
# Parse comma-separated numbers
|
|
275
|
+
try:
|
|
276
|
+
indices = [int(x.strip()) for x in selection.split(",")]
|
|
277
|
+
selected = []
|
|
278
|
+
for i in indices:
|
|
279
|
+
if 1 <= i <= len(repos):
|
|
280
|
+
selected.append(repos[i - 1])
|
|
281
|
+
else:
|
|
282
|
+
click.echo(f"⚠️ Invalid index: {i} (ignored)")
|
|
283
|
+
|
|
284
|
+
if not selected:
|
|
285
|
+
click.echo("❌ No valid repositories selected!")
|
|
286
|
+
return []
|
|
287
|
+
|
|
288
|
+
click.echo(f"✅ Selected {len(selected)} repositories\n")
|
|
289
|
+
return selected
|
|
290
|
+
except (ValueError, IndexError) as e:
|
|
291
|
+
click.echo(f"❌ Invalid selection: {e}")
|
|
292
|
+
return self._select_repositories() # Retry
|
|
293
|
+
|
|
294
|
+
def _select_analysis_period(self) -> int:
|
|
295
|
+
"""Prompt for analysis period in weeks.
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
Number of weeks to analyze.
|
|
299
|
+
"""
|
|
300
|
+
default_weeks = self.preferences.get("default_weeks", 4)
|
|
301
|
+
weeks = click.prompt(
|
|
302
|
+
"📅 Number of weeks to analyze",
|
|
303
|
+
type=click.IntRange(1, 52),
|
|
304
|
+
default=default_weeks,
|
|
305
|
+
)
|
|
306
|
+
return weeks
|
|
307
|
+
|
|
308
|
+
def _confirm_clear_cache(self) -> bool:
|
|
309
|
+
"""Confirm cache clearing.
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
True if cache should be cleared, False otherwise.
|
|
313
|
+
"""
|
|
314
|
+
default = self.preferences.get("auto_clear_cache", False)
|
|
315
|
+
return click.confirm("🗑️ Clear cache before analysis?", default=default)
|
|
316
|
+
|
|
317
|
+
def _confirm_skip_identity(self) -> bool:
|
|
318
|
+
"""Confirm skipping identity analysis.
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
True if identity analysis should be skipped, False otherwise.
|
|
322
|
+
"""
|
|
323
|
+
default = self.preferences.get("skip_identity_analysis", False)
|
|
324
|
+
return click.confirm("🔍 Skip identity analysis?", default=default)
|
|
325
|
+
|
|
326
|
+
def _save_preferences(
|
|
327
|
+
self,
|
|
328
|
+
repos: list[str],
|
|
329
|
+
weeks: int,
|
|
330
|
+
clear_cache: bool,
|
|
331
|
+
skip_identity: bool,
|
|
332
|
+
) -> None:
|
|
333
|
+
"""Save preferences to config file.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
repos: Selected repository names
|
|
337
|
+
weeks: Analysis period in weeks
|
|
338
|
+
clear_cache: Whether to clear cache
|
|
339
|
+
skip_identity: Whether to skip identity analysis
|
|
340
|
+
"""
|
|
341
|
+
try:
|
|
342
|
+
click.echo("💾 Saving preferences...")
|
|
343
|
+
|
|
344
|
+
# Load existing config YAML
|
|
345
|
+
with open(self.config_path) as f:
|
|
346
|
+
config_data = yaml.safe_load(f)
|
|
347
|
+
|
|
348
|
+
# Update launcher preferences
|
|
349
|
+
config_data["launcher"] = {
|
|
350
|
+
"last_selected_repos": repos,
|
|
351
|
+
"default_weeks": weeks,
|
|
352
|
+
"auto_clear_cache": clear_cache,
|
|
353
|
+
"skip_identity_analysis": skip_identity,
|
|
354
|
+
"last_run": datetime.now(timezone.utc).isoformat(),
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
# Write back to file
|
|
358
|
+
with open(self.config_path, "w") as f:
|
|
359
|
+
yaml.dump(config_data, f, default_flow_style=False, sort_keys=False)
|
|
360
|
+
|
|
361
|
+
click.echo("✅ Preferences saved to config.yaml\n")
|
|
362
|
+
except Exception as e:
|
|
363
|
+
click.echo(f"⚠️ Could not save preferences: {e}")
|
|
364
|
+
logger.error(f"Preference saving error: {type(e).__name__}")
|
|
365
|
+
|
|
366
|
+
def _run_analysis(
|
|
367
|
+
self,
|
|
368
|
+
repos: list[str],
|
|
369
|
+
weeks: int,
|
|
370
|
+
clear_cache: bool,
|
|
371
|
+
skip_identity: bool,
|
|
372
|
+
) -> bool:
|
|
373
|
+
"""Execute analysis with selected options.
|
|
374
|
+
|
|
375
|
+
Args:
|
|
376
|
+
repos: Selected repository names
|
|
377
|
+
weeks: Analysis period in weeks
|
|
378
|
+
clear_cache: Whether to clear cache
|
|
379
|
+
skip_identity: Whether to skip identity analysis
|
|
380
|
+
|
|
381
|
+
Returns:
|
|
382
|
+
True if analysis completed successfully, False otherwise.
|
|
383
|
+
"""
|
|
384
|
+
click.echo("🚀 Starting analysis...")
|
|
385
|
+
click.echo(f" Repositories: {', '.join(repos)}")
|
|
386
|
+
click.echo(f" Period: {weeks} weeks")
|
|
387
|
+
click.echo(f" Clear cache: {'Yes' if clear_cache else 'No'}")
|
|
388
|
+
click.echo(f" Skip identity: {'Yes' if skip_identity else 'No'}\n")
|
|
389
|
+
|
|
390
|
+
# Build command to execute
|
|
391
|
+
cmd = [
|
|
392
|
+
sys.executable,
|
|
393
|
+
"-m",
|
|
394
|
+
"gitflow_analytics.cli",
|
|
395
|
+
"analyze",
|
|
396
|
+
"-c",
|
|
397
|
+
str(self.config_path),
|
|
398
|
+
"--weeks",
|
|
399
|
+
str(weeks),
|
|
400
|
+
]
|
|
401
|
+
|
|
402
|
+
if clear_cache:
|
|
403
|
+
cmd.append("--clear-cache")
|
|
404
|
+
|
|
405
|
+
if skip_identity:
|
|
406
|
+
cmd.append("--skip-identity-analysis")
|
|
407
|
+
|
|
408
|
+
# Execute analysis as subprocess to avoid Click context issues
|
|
409
|
+
try:
|
|
410
|
+
result = subprocess.run(cmd, check=True)
|
|
411
|
+
click.echo("\n✅ Analysis complete!")
|
|
412
|
+
return result.returncode == 0
|
|
413
|
+
except subprocess.CalledProcessError as e:
|
|
414
|
+
click.echo(f"\n❌ Analysis failed with exit code: {e.returncode}")
|
|
415
|
+
logger.error(f"Analysis subprocess error: {e}")
|
|
416
|
+
return False
|
|
417
|
+
except Exception as e:
|
|
418
|
+
click.echo(f"\n❌ Analysis failed: {e}")
|
|
419
|
+
logger.error(f"Analysis error: {type(e).__name__}")
|
|
420
|
+
return False
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def run_interactive_launcher(config_path: Optional[Path] = None) -> bool:
|
|
424
|
+
"""Run the interactive launcher.
|
|
425
|
+
|
|
426
|
+
Args:
|
|
427
|
+
config_path: Optional path to configuration file
|
|
428
|
+
|
|
429
|
+
Returns:
|
|
430
|
+
True if launcher completed successfully, False otherwise
|
|
431
|
+
"""
|
|
432
|
+
launcher = InteractiveLauncher(config_path=config_path)
|
|
433
|
+
return launcher.run()
|
|
@@ -6,6 +6,7 @@ sub-modules while maintaining backward compatibility.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
# Re-export main interfaces for backward compatibility
|
|
9
|
+
from .aliases import AliasesManager, DeveloperAlias
|
|
9
10
|
from .loader import ConfigLoader
|
|
10
11
|
from .schema import (
|
|
11
12
|
AnalysisConfig,
|
|
@@ -40,4 +41,6 @@ __all__ = [
|
|
|
40
41
|
"LLMClassificationConfig",
|
|
41
42
|
"CommitClassificationConfig",
|
|
42
43
|
"BranchAnalysisConfig",
|
|
44
|
+
"AliasesManager",
|
|
45
|
+
"DeveloperAlias",
|
|
43
46
|
]
|