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.
- git_therapy/__init__.py +7 -0
- git_therapy/advanced_cli.py +434 -0
- git_therapy/analyzers/__init__.py +0 -0
- git_therapy/analyzers/flow.py +0 -0
- git_therapy/analyzers/hero.py +373 -0
- git_therapy/analyzers/insights.py +497 -0
- git_therapy/analyzers/personality.py +480 -0
- git_therapy/analyzers/rage.py +260 -0
- git_therapy/analyzers/sleep.py +293 -0
- git_therapy/analyzers/team_dynamics.py +347 -0
- git_therapy/cli.py +168 -0
- git_therapy/config/__init__.py +321 -0
- git_therapy/parser/__init__.py +0 -0
- git_therapy/parser/diff_analyzer.py +0 -0
- git_therapy/parser/git_log.py +136 -0
- git_therapy/performance.py +209 -0
- git_therapy/report/__init__.py +0 -0
- git_therapy/report/generator.py +203 -0
- git_therapy/report/templates/report.html +554 -0
- git_therapy/therapy/__init__.py +0 -0
- git_therapy/therapy/narrative.py +0 -0
- git_therapy-1.0.0.dist-info/METADATA +311 -0
- git_therapy-1.0.0.dist-info/RECORD +26 -0
- git_therapy-1.0.0.dist-info/WHEEL +4 -0
- git_therapy-1.0.0.dist-info/entry_points.txt +3 -0
- git_therapy-1.0.0.dist-info/licenses/LICENSE +21 -0
git_therapy/__init__.py
ADDED
|
@@ -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
|