git-therapy 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.
@@ -0,0 +1,7 @@
1
+ """
2
+ git-therapy: A CLI tool that psychoanalyzes your Git history.
3
+
4
+ Your repository has a lot to say. Are you ready to listen?
5
+ """
6
+
7
+ __version__ = "0.1.0"
@@ -0,0 +1,434 @@
1
+ """
2
+ Advanced CLI interface for git-therapy with configuration support and team analysis.
3
+ """
4
+
5
+ import json
6
+ import os
7
+ from pathlib import Path
8
+ from typing import Optional
9
+
10
+ import click
11
+ from dateutil.parser import parse as parse_date
12
+
13
+ from git_therapy.analyzers.team_dynamics import TeamDynamicsAnalyzer
14
+ from git_therapy.config import ConfigManager, load_config
15
+ from git_therapy.parser.git_log import GitLogParser
16
+ from git_therapy.report.generator import HtmlReportGenerator
17
+
18
+
19
+ @click.group() # type: ignore[misc]
20
+ @click.option("--config", type=click.Path(), help="Path to configuration file") # type: ignore[misc]
21
+ @click.option("--verbose", "-v", is_flag=True, help="Enable verbose output") # type: ignore[misc]
22
+ @click.pass_context
23
+ def cli(ctx: click.Context, config: Optional[str], verbose: bool) -> None:
24
+ """Advanced Git repository psychological analysis tool.
25
+
26
+ Analyze your development team's behavior patterns, stress indicators,
27
+ and collaboration dynamics through commit history analysis.
28
+ """
29
+ # Ensure context object exists
30
+ ctx.ensure_object(dict)
31
+
32
+ # Load configuration
33
+ if config:
34
+ ctx.obj["config"] = load_config(config)
35
+ else:
36
+ ctx.obj["config"] = load_config()
37
+
38
+ ctx.obj["verbose"] = verbose
39
+
40
+
41
+ @cli.command() # type: ignore[misc]
42
+ @click.option("--path", type=click.Path(exists=True), help="Path to git repository") # type: ignore[misc]
43
+ @click.option("--author", help="Filter commits by author (email or name)") # type: ignore[misc]
44
+ @click.option("--since", help='Only commits after this date (e.g., "2023-01-01")') # type: ignore[misc]
45
+ @click.option("--until", help='Only commits before this date (e.g., "2024-01-01")') # type: ignore[misc]
46
+ @click.option(
47
+ "--max-count", type=int, default=100, help="Maximum number of commits to analyze"
48
+ ) # type: ignore[misc]
49
+ @click.option(
50
+ "--format",
51
+ "output_format",
52
+ type=click.Choice(["json", "summary", "html"]),
53
+ default="summary",
54
+ help="Output format",
55
+ ) # type: ignore[misc]
56
+ @click.option(
57
+ "--output", "output_file", help="Output file path (required for HTML format)"
58
+ ) # type: ignore[misc]
59
+ @click.option("--include-team", is_flag=True, help="Include team dynamics analysis") # type: ignore[misc]
60
+ @click.pass_context
61
+ def analyze(
62
+ ctx: click.Context,
63
+ path: Optional[str],
64
+ author: Optional[str],
65
+ since: Optional[str],
66
+ until: Optional[str],
67
+ max_count: int,
68
+ output_format: str,
69
+ output_file: Optional[str],
70
+ include_team: bool,
71
+ ) -> None:
72
+ """Analyze Git repository with psychological profiling."""
73
+ config = ctx.obj["config"]
74
+ verbose = ctx.obj["verbose"]
75
+
76
+ try:
77
+ # Parse date strings
78
+ since_date = parse_date(since) if since else None
79
+ until_date = parse_date(until) if until else None
80
+
81
+ # Initialize parser
82
+ parser = GitLogParser(path)
83
+
84
+ if verbose:
85
+ click.echo("Loading repository information...")
86
+
87
+ # Get repository info
88
+ repo_info = parser.get_repository_info()
89
+
90
+ # Parse commits
91
+ if verbose:
92
+ click.echo(f"Analyzing up to {max_count} commits...")
93
+
94
+ commits = parser.parse_commits(
95
+ max_count=max_count, since=since_date, until=until_date, author=author
96
+ )
97
+
98
+ # Determine repository name
99
+ repo_name = repo_info.get("path", "Unknown Repository")
100
+ if repo_name != "Unknown Repository":
101
+ repo_name = Path(repo_name).name
102
+
103
+ if output_format == "html":
104
+ # HTML report generation
105
+ if not output_file:
106
+ click.echo("Error: --output is required for HTML format", err=True)
107
+ raise click.Abort()
108
+
109
+ if not commits:
110
+ click.echo("Error: No commits found to analyze", err=True)
111
+ raise click.Abort()
112
+
113
+ click.echo("Generating comprehensive HTML report...")
114
+ generator = HtmlReportGenerator()
115
+
116
+ try:
117
+ output_path = generator.generate_report(
118
+ commits=commits, output_path=output_file, repository_name=repo_name
119
+ )
120
+ click.echo(f"HTML report generated: {output_path}")
121
+ click.echo(f"Open {output_path} in your browser to view the analysis.")
122
+ except Exception as e:
123
+ click.echo(f"Error generating HTML report: {e}", err=True)
124
+ raise click.Abort()
125
+
126
+ elif output_format == "json":
127
+ # JSON output with optional team dynamics
128
+ output_data = {
129
+ "repository": repo_info,
130
+ "commits": [
131
+ {
132
+ "hash": commit.hash,
133
+ "author_name": commit.author_name,
134
+ "author_email": commit.author_email,
135
+ "timestamp": commit.timestamp.isoformat(),
136
+ "message": commit.message,
137
+ "files_changed": commit.files_changed,
138
+ "insertions": commit.insertions,
139
+ "deletions": commit.deletions,
140
+ "is_merge": commit.is_merge,
141
+ }
142
+ for commit in commits
143
+ ],
144
+ }
145
+
146
+ # Add team dynamics if requested
147
+ if include_team and commits:
148
+ if verbose:
149
+ click.echo("Analyzing team dynamics...")
150
+ team_analyzer = TeamDynamicsAnalyzer()
151
+ team_dynamics = team_analyzer.analyze_team_dynamics(commits)
152
+
153
+ output_data["team_dynamics"] = {
154
+ "total_contributors": team_dynamics.total_contributors,
155
+ "communication_health": team_dynamics.communication_health,
156
+ "workflow_efficiency": team_dynamics.workflow_efficiency,
157
+ "knowledge_silos": [
158
+ {"author": author, "exclusive_files": list(files)}
159
+ for author, files in team_dynamics.knowledge_silos
160
+ ],
161
+ "hot_files": [
162
+ {"file": file, "contributor_count": count}
163
+ for file, count in team_dynamics.hot_files[:10]
164
+ ],
165
+ }
166
+
167
+ click.echo(json.dumps(output_data, indent=2))
168
+ else:
169
+ # Enhanced summary output
170
+ click.echo("Git Therapy Analysis")
171
+ click.echo("=" * 50)
172
+ click.echo(f"Repository: {repo_info.get('path', 'Unknown')}")
173
+
174
+ if "error" in repo_info:
175
+ click.echo(f"Warning: {repo_info['error']}")
176
+ return
177
+
178
+ click.echo(f"Branch: {repo_info.get('current_branch', 'Unknown')}")
179
+ click.echo(f"Total commits: {repo_info.get('total_commits', 0)}")
180
+ click.echo(f"Contributors: {repo_info.get('contributors', 0)}")
181
+ click.echo(f"Analyzed commits: {len(commits)}")
182
+ click.echo()
183
+
184
+ if commits:
185
+ # Basic stats
186
+ total_insertions = sum(commit.insertions for commit in commits)
187
+ total_deletions = sum(commit.deletions for commit in commits)
188
+ merge_commits = sum(1 for commit in commits if commit.is_merge)
189
+
190
+ click.echo("Quick Stats:")
191
+ click.echo(f" Lines added: +{total_insertions:,}")
192
+ click.echo(f" Lines removed: -{total_deletions:,}")
193
+ click.echo(f" Merge commits: {merge_commits}")
194
+
195
+ if include_team:
196
+ if verbose:
197
+ click.echo("Analyzing team dynamics...")
198
+ team_analyzer = TeamDynamicsAnalyzer()
199
+ team_dynamics = team_analyzer.analyze_team_dynamics(commits)
200
+
201
+ click.echo()
202
+ click.echo("Team Dynamics:")
203
+ click.echo(
204
+ f" Communication Health: {team_dynamics.communication_health:.1%}"
205
+ )
206
+ click.echo(
207
+ f" Workflow Efficiency: {team_dynamics.workflow_efficiency:.1%}"
208
+ )
209
+ click.echo(
210
+ f" Knowledge Silos: {len(team_dynamics.knowledge_silos)}"
211
+ )
212
+ click.echo(f" Hot Files: {len(team_dynamics.hot_files)}")
213
+
214
+ click.echo()
215
+
216
+ # Most recent commits
217
+ click.echo("Recent Activity:")
218
+ for commit in commits[:5]:
219
+ date_str = commit.timestamp.strftime("%Y-%m-%d %H:%M")
220
+ message_preview = commit.message.split("\n")[0][:60]
221
+ if len(commit.message.split("\n")[0]) > 60:
222
+ message_preview += "..."
223
+ click.echo(f" {date_str} | {message_preview}")
224
+ click.echo(
225
+ f" {commit.author_name} (+{commit.insertions}/-{commit.deletions})"
226
+ )
227
+ click.echo()
228
+
229
+ click.echo("For detailed analysis:")
230
+ click.echo(" git-therapy analyze --format html --output report.html")
231
+ click.echo(
232
+ " git-therapy analyze --include-team # Add team dynamics"
233
+ )
234
+ else:
235
+ click.echo("No commits found matching the criteria.")
236
+
237
+ except Exception as e:
238
+ click.echo(f"Error: {e}", err=True)
239
+ if verbose:
240
+ import traceback
241
+
242
+ traceback.print_exc()
243
+ raise click.Abort()
244
+
245
+
246
+ @cli.command() # type: ignore[misc]
247
+ @click.option("--show", is_flag=True, help="Show current configuration") # type: ignore[misc]
248
+ @click.option("--init", is_flag=True, help="Create default configuration file") # type: ignore[misc]
249
+ @click.option("--path", help="Configuration file path") # type: ignore[misc]
250
+ @click.pass_context
251
+ def config(ctx: click.Context, show: bool, init: bool, path: Optional[str]) -> None:
252
+ """Manage git-therapy configuration."""
253
+ config_manager = ConfigManager()
254
+
255
+ if show:
256
+ # Show current configuration
257
+ current_config = ctx.obj["config"]
258
+ click.echo("Current Configuration:")
259
+ click.echo("=" * 30)
260
+
261
+ click.echo("Rage Detection:")
262
+ click.echo(f" High threshold: {current_config.rage.high_rage_threshold}")
263
+ click.echo(f" Medium threshold: {current_config.rage.medium_rage_threshold}")
264
+ click.echo(
265
+ f" Short message threshold: {current_config.rage.short_message_threshold}"
266
+ )
267
+
268
+ click.echo("Sleep Analysis:")
269
+ click.echo(
270
+ f" Night hours: {current_config.sleep.night_start_hour:02d}:00 - {current_config.sleep.night_end_hour:02d}:00"
271
+ )
272
+ click.echo(
273
+ f" High risk threshold: {current_config.sleep.high_risk_threshold:.1%}"
274
+ )
275
+
276
+ click.echo("Hero Detection:")
277
+ click.echo(f" Weekend days: {current_config.hero.weekend_days}")
278
+ click.echo(
279
+ f" Emergency keywords: {len(current_config.hero.emergency_keywords)} defined"
280
+ )
281
+
282
+ click.echo("Personality Analysis:")
283
+ click.echo(
284
+ f" Min commits threshold: {current_config.personality.min_commits_threshold}"
285
+ )
286
+
287
+ click.echo("Global Settings:")
288
+ click.echo(f" Verbose output: {current_config.verbose_output}")
289
+ click.echo(f" Cache analysis: {current_config.cache_analysis}")
290
+ click.echo(f" Parallel processing: {current_config.parallel_processing}")
291
+
292
+ elif init:
293
+ # Create default configuration file
294
+ config_path = path or ".git-therapy.yaml"
295
+
296
+ if os.path.exists(config_path):
297
+ if not click.confirm(
298
+ f"Configuration file {config_path} already exists. Overwrite?"
299
+ ):
300
+ click.echo("Configuration initialization cancelled.")
301
+ return
302
+
303
+ try:
304
+ saved_path = config_manager.save_config(config_path)
305
+ click.echo(f"Default configuration created: {saved_path}")
306
+ click.echo("Edit this file to customize git-therapy behavior.")
307
+ except Exception as e:
308
+ click.echo(f"Error creating configuration: {e}", err=True)
309
+ raise click.Abort()
310
+ else:
311
+ click.echo(
312
+ "Use --show to view current config or --init to create default config"
313
+ )
314
+
315
+
316
+ @cli.command() # type: ignore[misc]
317
+ @click.option("--path", type=click.Path(exists=True), help="Path to git repository") # type: ignore[misc]
318
+ @click.option("--since", help='Only commits after this date (e.g., "2023-01-01")') # type: ignore[misc]
319
+ @click.option("--until", help='Only commits before this date (e.g., "2024-01-01")') # type: ignore[misc]
320
+ @click.option(
321
+ "--max-count", type=int, default=200, help="Maximum number of commits to analyze"
322
+ ) # type: ignore[misc]
323
+ @click.pass_context
324
+ def team(
325
+ ctx: click.Context,
326
+ path: Optional[str],
327
+ since: Optional[str],
328
+ until: Optional[str],
329
+ max_count: int,
330
+ ) -> None:
331
+ """Analyze team collaboration patterns and dynamics."""
332
+ verbose = ctx.obj["verbose"]
333
+
334
+ try:
335
+ # Parse date strings
336
+ since_date = parse_date(since) if since else None
337
+ until_date = parse_date(until) if until else None
338
+
339
+ # Initialize parser
340
+ parser = GitLogParser(path)
341
+
342
+ if verbose:
343
+ click.echo("Loading repository and analyzing team dynamics...")
344
+
345
+ # Parse commits
346
+ commits = parser.parse_commits(
347
+ max_count=max_count, since=since_date, until=until_date
348
+ )
349
+
350
+ if not commits:
351
+ click.echo("No commits found to analyze.")
352
+ return
353
+
354
+ # Analyze team dynamics
355
+ team_analyzer = TeamDynamicsAnalyzer()
356
+ team_dynamics = team_analyzer.analyze_team_dynamics(commits)
357
+
358
+ # Display results
359
+ click.echo("Team Dynamics Analysis")
360
+ click.echo("=" * 40)
361
+ click.echo(f"Total Contributors: {team_dynamics.total_contributors}")
362
+ click.echo(f"Communication Health: {team_dynamics.communication_health:.1%}")
363
+ click.echo(f"Workflow Efficiency: {team_dynamics.workflow_efficiency:.1%}")
364
+ click.echo()
365
+
366
+ # Knowledge silos
367
+ if team_dynamics.knowledge_silos:
368
+ click.echo("Knowledge Silos (Contributors with exclusive file access):")
369
+ for author, files in team_dynamics.knowledge_silos:
370
+ author_name = author.split("@")[0] # Show just the name part
371
+ click.echo(f" {author_name}: {len(files)} exclusive files")
372
+ if verbose:
373
+ for file in list(files)[:5]: # Show first 5 files
374
+ click.echo(f" - {file}")
375
+ if len(files) > 5:
376
+ click.echo(f" ... and {len(files) - 5} more")
377
+ click.echo()
378
+
379
+ # Hot files
380
+ if team_dynamics.hot_files:
381
+ click.echo("Hot Files (Worked on by multiple contributors):")
382
+ for file, count in team_dynamics.hot_files[:10]:
383
+ click.echo(f" {file}: {count} contributors")
384
+ click.echo()
385
+
386
+ # Collaboration insights
387
+ if team_dynamics.collaboration_matrix:
388
+ click.echo("Top Collaborations:")
389
+ collaborations = [
390
+ (authors, metrics)
391
+ for authors, metrics in team_dynamics.collaboration_matrix.items()
392
+ if metrics.total_collaborations > 0
393
+ ]
394
+ collaborations.sort(key=lambda x: x[1].total_collaborations, reverse=True)
395
+
396
+ for (author1, author2), metrics in collaborations[:5]:
397
+ name1 = author1.split("@")[0]
398
+ name2 = author2.split("@")[0]
399
+ click.echo(
400
+ f" {name1} <-> {name2}: {metrics.total_collaborations} shared files"
401
+ )
402
+ if metrics.merge_conflicts > 0:
403
+ click.echo(
404
+ f" Warning: Estimated conflicts: {metrics.merge_conflicts}"
405
+ )
406
+
407
+ # Recommendations
408
+ click.echo("Recommendations:")
409
+ if team_dynamics.communication_health < 0.6:
410
+ click.echo(
411
+ " - Consider improving commit message quality and reducing conflicts"
412
+ )
413
+ if team_dynamics.workflow_efficiency < 0.6:
414
+ click.echo(" - Review commit sizes and frequency for better workflow")
415
+ if len(team_dynamics.knowledge_silos) > 0:
416
+ click.echo(
417
+ " - Address knowledge silos through code reviews and pair programming"
418
+ )
419
+ if len(team_dynamics.hot_files) > 5:
420
+ click.echo(
421
+ " - Consider refactoring frequently-modified files to reduce conflicts"
422
+ )
423
+
424
+ except Exception as e:
425
+ click.echo(f"Error: {e}", err=True)
426
+ if verbose:
427
+ import traceback
428
+
429
+ traceback.print_exc()
430
+ raise click.Abort()
431
+
432
+
433
+ if __name__ == "__main__":
434
+ cli()
File without changes
File without changes