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.
Files changed (36) hide show
  1. gitflow_analytics/_version.py +1 -1
  2. gitflow_analytics/cli.py +517 -15
  3. gitflow_analytics/cli_wizards/__init__.py +10 -0
  4. gitflow_analytics/cli_wizards/install_wizard.py +1181 -0
  5. gitflow_analytics/cli_wizards/run_launcher.py +433 -0
  6. gitflow_analytics/config/__init__.py +3 -0
  7. gitflow_analytics/config/aliases.py +306 -0
  8. gitflow_analytics/config/loader.py +35 -1
  9. gitflow_analytics/config/schema.py +13 -0
  10. gitflow_analytics/constants.py +75 -0
  11. gitflow_analytics/core/cache.py +7 -3
  12. gitflow_analytics/core/data_fetcher.py +66 -30
  13. gitflow_analytics/core/git_timeout_wrapper.py +6 -4
  14. gitflow_analytics/core/progress.py +2 -4
  15. gitflow_analytics/core/subprocess_git.py +31 -5
  16. gitflow_analytics/identity_llm/analysis_pass.py +13 -3
  17. gitflow_analytics/identity_llm/analyzer.py +14 -2
  18. gitflow_analytics/identity_llm/models.py +7 -1
  19. gitflow_analytics/qualitative/classifiers/llm/openai_client.py +5 -3
  20. gitflow_analytics/security/config.py +6 -6
  21. gitflow_analytics/security/extractors/dependency_checker.py +14 -14
  22. gitflow_analytics/security/extractors/secret_detector.py +8 -14
  23. gitflow_analytics/security/extractors/vulnerability_scanner.py +9 -9
  24. gitflow_analytics/security/llm_analyzer.py +10 -10
  25. gitflow_analytics/security/security_analyzer.py +17 -17
  26. gitflow_analytics/tui/screens/analysis_progress_screen.py +1 -1
  27. gitflow_analytics/ui/progress_display.py +36 -29
  28. gitflow_analytics/verify_activity.py +23 -26
  29. {gitflow_analytics-3.3.0.dist-info → gitflow_analytics-3.5.2.dist-info}/METADATA +1 -1
  30. {gitflow_analytics-3.3.0.dist-info → gitflow_analytics-3.5.2.dist-info}/RECORD +34 -31
  31. gitflow_analytics/security/reports/__init__.py +0 -5
  32. gitflow_analytics/security/reports/security_report.py +0 -358
  33. {gitflow_analytics-3.3.0.dist-info → gitflow_analytics-3.5.2.dist-info}/WHEEL +0 -0
  34. {gitflow_analytics-3.3.0.dist-info → gitflow_analytics-3.5.2.dist-info}/entry_points.txt +0 -0
  35. {gitflow_analytics-3.3.0.dist-info → gitflow_analytics-3.5.2.dist-info}/licenses/LICENSE +0 -0
  36. {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
  ]