code-maat-python 0.1.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.
- code_maat_python/__init__.py +12 -0
- code_maat_python/__main__.py +5 -0
- code_maat_python/analyses/__init__.py +39 -0
- code_maat_python/analyses/age.py +101 -0
- code_maat_python/analyses/authors.py +60 -0
- code_maat_python/analyses/churn.py +353 -0
- code_maat_python/analyses/communication.py +151 -0
- code_maat_python/analyses/coupling.py +136 -0
- code_maat_python/analyses/effort.py +210 -0
- code_maat_python/analyses/entities.py +51 -0
- code_maat_python/analyses/revisions.py +56 -0
- code_maat_python/analyses/soc.py +90 -0
- code_maat_python/analyses/summary.py +61 -0
- code_maat_python/cli.py +822 -0
- code_maat_python/output/__init__.py +0 -0
- code_maat_python/parser.py +232 -0
- code_maat_python/pipeline.py +112 -0
- code_maat_python/transformers/__init__.py +0 -0
- code_maat_python/transformers/grouper.py +204 -0
- code_maat_python/transformers/team_mapper.py +132 -0
- code_maat_python/transformers/time_grouper.py +146 -0
- code_maat_python/utils/__init__.py +0 -0
- code_maat_python/utils/math.py +105 -0
- code_maat_python-0.1.0.dist-info/METADATA +545 -0
- code_maat_python-0.1.0.dist-info/RECORD +28 -0
- code_maat_python-0.1.0.dist-info/WHEEL +4 -0
- code_maat_python-0.1.0.dist-info/entry_points.txt +3 -0
- code_maat_python-0.1.0.dist-info/licenses/LICENSE +674 -0
code_maat_python/cli.py
ADDED
|
@@ -0,0 +1,822 @@
|
|
|
1
|
+
"""Command-line interface for code-maat-python.
|
|
2
|
+
|
|
3
|
+
Modern CLI using click framework with subcommands for each analysis type.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from functools import wraps
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import click
|
|
13
|
+
import pandas as pd
|
|
14
|
+
|
|
15
|
+
from code_maat_python.analyses import (
|
|
16
|
+
analyze_authors,
|
|
17
|
+
analyze_coupling,
|
|
18
|
+
analyze_entities,
|
|
19
|
+
analyze_revisions,
|
|
20
|
+
analyze_soc,
|
|
21
|
+
analyze_summary,
|
|
22
|
+
)
|
|
23
|
+
from code_maat_python.analyses.age import code_age
|
|
24
|
+
from code_maat_python.analyses.churn import (
|
|
25
|
+
abs_churn,
|
|
26
|
+
author_churn,
|
|
27
|
+
entity_churn,
|
|
28
|
+
entity_ownership,
|
|
29
|
+
main_dev,
|
|
30
|
+
refactoring_main_dev,
|
|
31
|
+
)
|
|
32
|
+
from code_maat_python.analyses.communication import communication
|
|
33
|
+
from code_maat_python.analyses.effort import (
|
|
34
|
+
entity_effort,
|
|
35
|
+
fragmentation,
|
|
36
|
+
main_dev_by_revs,
|
|
37
|
+
)
|
|
38
|
+
from code_maat_python.parser import parse_git_log
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def validate_logfile(ctx: click.Context, param: click.Parameter, value: str) -> Path:
|
|
42
|
+
"""Validate that the logfile exists and is readable.
|
|
43
|
+
|
|
44
|
+
Supports regular files, stdin, pipes, and process substitution (e.g., <(git log ...)).
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
ctx: Click context
|
|
48
|
+
param: Click parameter
|
|
49
|
+
value: Path string
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Path object if valid
|
|
53
|
+
|
|
54
|
+
Raises:
|
|
55
|
+
click.BadParameter: If file doesn't exist or isn't readable
|
|
56
|
+
"""
|
|
57
|
+
path = Path(value)
|
|
58
|
+
|
|
59
|
+
# Check if path exists (regular files, symlinks, etc.)
|
|
60
|
+
# or if it's a special file (pipes, character devices like /dev/fd/N)
|
|
61
|
+
if not path.exists() and not (path.is_fifo() or path.is_char_device()):
|
|
62
|
+
raise click.BadParameter(f"File not found: {value}")
|
|
63
|
+
|
|
64
|
+
# Try to open and read to verify it's actually readable
|
|
65
|
+
try:
|
|
66
|
+
with open(path, 'r') as f:
|
|
67
|
+
# Just verify we can open it; don't read anything yet
|
|
68
|
+
pass
|
|
69
|
+
except (OSError, IOError) as e:
|
|
70
|
+
raise click.BadParameter(f"Cannot read file: {value} ({e})")
|
|
71
|
+
|
|
72
|
+
return path
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def common_options(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
76
|
+
"""Add common options to all analysis commands.
|
|
77
|
+
|
|
78
|
+
Adds --group, --team-map-file, --rows, and --output options.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
@click.option(
|
|
82
|
+
"--group",
|
|
83
|
+
"-g",
|
|
84
|
+
type=str,
|
|
85
|
+
help="Architectural grouping specification file",
|
|
86
|
+
)
|
|
87
|
+
@click.option(
|
|
88
|
+
"--team-map-file",
|
|
89
|
+
"-p",
|
|
90
|
+
type=str,
|
|
91
|
+
help="Team mapping CSV file",
|
|
92
|
+
)
|
|
93
|
+
@click.option(
|
|
94
|
+
"--rows",
|
|
95
|
+
"-r",
|
|
96
|
+
type=int,
|
|
97
|
+
help="Maximum number of rows to output",
|
|
98
|
+
)
|
|
99
|
+
@click.option(
|
|
100
|
+
"--output",
|
|
101
|
+
"-o",
|
|
102
|
+
type=str,
|
|
103
|
+
help="Output CSV file (default: stdout)",
|
|
104
|
+
)
|
|
105
|
+
@wraps(func)
|
|
106
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
107
|
+
return func(*args, **kwargs)
|
|
108
|
+
|
|
109
|
+
return wrapper
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def apply_transformers(
|
|
113
|
+
df: pd.DataFrame, group_file: str | None, team_map_file: str | None
|
|
114
|
+
) -> pd.DataFrame:
|
|
115
|
+
"""Apply transformers in correct order.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
df: DataFrame with parsed git log data
|
|
119
|
+
group_file: Path to architectural grouping specification file
|
|
120
|
+
team_map_file: Path to team mapping CSV file
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Transformed DataFrame
|
|
124
|
+
"""
|
|
125
|
+
result = df
|
|
126
|
+
|
|
127
|
+
# Apply architectural grouping first (reduces entity granularity)
|
|
128
|
+
if group_file:
|
|
129
|
+
from code_maat_python.transformers.grouper import (
|
|
130
|
+
load_group_specification_file,
|
|
131
|
+
map_entities_to_groups,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
patterns = load_group_specification_file(group_file)
|
|
135
|
+
result = map_entities_to_groups(result, patterns)
|
|
136
|
+
|
|
137
|
+
# Apply team mapping second (reduces author granularity)
|
|
138
|
+
if team_map_file:
|
|
139
|
+
from code_maat_python.transformers.team_mapper import (
|
|
140
|
+
load_team_mapping_file,
|
|
141
|
+
map_authors_to_teams,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
mapping = load_team_mapping_file(team_map_file)
|
|
145
|
+
result = map_authors_to_teams(result, mapping)
|
|
146
|
+
|
|
147
|
+
return result
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def output_results(df: pd.DataFrame, output: str | None, max_rows: int | None = None) -> None:
|
|
151
|
+
"""Output analysis results to stdout or file.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
df: DataFrame with analysis results
|
|
155
|
+
output: Output file path or None for stdout
|
|
156
|
+
max_rows: Maximum number of rows to output (None for all)
|
|
157
|
+
"""
|
|
158
|
+
# Limit rows if specified
|
|
159
|
+
if max_rows and len(df) > max_rows:
|
|
160
|
+
df = df.head(max_rows)
|
|
161
|
+
|
|
162
|
+
if output:
|
|
163
|
+
df.to_csv(output, index=False)
|
|
164
|
+
click.echo(f"Results written to {output}", err=True)
|
|
165
|
+
else:
|
|
166
|
+
# Output to stdout
|
|
167
|
+
click.echo(df.to_csv(index=False))
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def handle_analysis_error(e: Exception) -> None:
|
|
171
|
+
"""Handle analysis errors with helpful messages.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
e: Exception that occurred
|
|
175
|
+
"""
|
|
176
|
+
click.echo(f"Error during analysis: {str(e)}", err=True)
|
|
177
|
+
sys.exit(1)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# Main command group
|
|
181
|
+
@click.group()
|
|
182
|
+
@click.version_option(version="0.1.0", prog_name="code-maat-python")
|
|
183
|
+
def main() -> None:
|
|
184
|
+
"""Code Maat Pandas - Modern Python tool for mining version control data.
|
|
185
|
+
|
|
186
|
+
Analyzes git repository logs to identify patterns, coupling, churn,
|
|
187
|
+
and other metrics useful for understanding code evolution.
|
|
188
|
+
|
|
189
|
+
\b
|
|
190
|
+
Example workflow:
|
|
191
|
+
1. Generate a git log:
|
|
192
|
+
git log --all -M -C --numstat --date=short \\
|
|
193
|
+
--pretty=format:'--%h--%cd--%cn' > git.log
|
|
194
|
+
|
|
195
|
+
2. Run an analysis:
|
|
196
|
+
code-maat-python coupling git.log --min-coupling 50
|
|
197
|
+
|
|
198
|
+
3. Save results to CSV:
|
|
199
|
+
code-maat-python revisions git.log --output results.csv
|
|
200
|
+
"""
|
|
201
|
+
pass
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@main.command()
|
|
205
|
+
@click.argument("logfile", type=str, callback=validate_logfile)
|
|
206
|
+
@common_options
|
|
207
|
+
def authors(
|
|
208
|
+
logfile: Path,
|
|
209
|
+
group: str | None,
|
|
210
|
+
team_map_file: str | None,
|
|
211
|
+
rows: int | None,
|
|
212
|
+
output: str | None,
|
|
213
|
+
) -> None:
|
|
214
|
+
"""Count distinct authors per entity with revision counts.
|
|
215
|
+
|
|
216
|
+
Shows how many authors have worked on each file and the number
|
|
217
|
+
of revisions, useful for identifying knowledge distribution.
|
|
218
|
+
|
|
219
|
+
\b
|
|
220
|
+
Example:
|
|
221
|
+
code-maat-python authors git.log
|
|
222
|
+
code-maat-python authors git.log --output authors.csv
|
|
223
|
+
code-maat-python authors git.log --group layers.txt --rows 20
|
|
224
|
+
"""
|
|
225
|
+
try:
|
|
226
|
+
df = parse_git_log(logfile)
|
|
227
|
+
df = apply_transformers(df, group, team_map_file)
|
|
228
|
+
result = analyze_authors(df)
|
|
229
|
+
output_results(result, output, rows)
|
|
230
|
+
except Exception as e:
|
|
231
|
+
handle_analysis_error(e)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@main.command()
|
|
235
|
+
@click.argument("logfile", type=str, callback=validate_logfile)
|
|
236
|
+
@common_options
|
|
237
|
+
def revisions(
|
|
238
|
+
logfile: Path,
|
|
239
|
+
group: str | None,
|
|
240
|
+
team_map_file: str | None,
|
|
241
|
+
rows: int | None,
|
|
242
|
+
output: str | None,
|
|
243
|
+
) -> None:
|
|
244
|
+
"""Sort entities by revision frequency.
|
|
245
|
+
|
|
246
|
+
Lists all files sorted by number of revisions, useful for
|
|
247
|
+
identifying hotspots and frequently changed code.
|
|
248
|
+
|
|
249
|
+
\b
|
|
250
|
+
Example:
|
|
251
|
+
code-maat-python revisions git.log
|
|
252
|
+
code-maat-python revisions git.log --output revisions.csv
|
|
253
|
+
code-maat-python revisions git.log --group layers.txt --rows 10
|
|
254
|
+
"""
|
|
255
|
+
try:
|
|
256
|
+
df = parse_git_log(logfile)
|
|
257
|
+
df = apply_transformers(df, group, team_map_file)
|
|
258
|
+
result = analyze_revisions(df)
|
|
259
|
+
output_results(result, output, rows)
|
|
260
|
+
except Exception as e:
|
|
261
|
+
handle_analysis_error(e)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
@main.command()
|
|
265
|
+
@click.argument("logfile", type=str, callback=validate_logfile)
|
|
266
|
+
@common_options
|
|
267
|
+
def entities(
|
|
268
|
+
logfile: Path,
|
|
269
|
+
group: str | None,
|
|
270
|
+
team_map_file: str | None,
|
|
271
|
+
rows: int | None,
|
|
272
|
+
output: str | None,
|
|
273
|
+
) -> None:
|
|
274
|
+
"""List all entities with basic statistics.
|
|
275
|
+
|
|
276
|
+
Shows all files in the repository with commit counts,
|
|
277
|
+
useful for understanding the scope of the codebase.
|
|
278
|
+
|
|
279
|
+
\b
|
|
280
|
+
Example:
|
|
281
|
+
code-maat-python entities git.log
|
|
282
|
+
code-maat-python entities git.log --output entities.csv
|
|
283
|
+
code-maat-python entities git.log --group layers.txt
|
|
284
|
+
"""
|
|
285
|
+
try:
|
|
286
|
+
df = parse_git_log(logfile)
|
|
287
|
+
df = apply_transformers(df, group, team_map_file)
|
|
288
|
+
result = analyze_entities(df)
|
|
289
|
+
output_results(result, output, rows)
|
|
290
|
+
except Exception as e:
|
|
291
|
+
handle_analysis_error(e)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
@main.command()
|
|
295
|
+
@click.argument("logfile", type=str, callback=validate_logfile)
|
|
296
|
+
@common_options
|
|
297
|
+
def summary(
|
|
298
|
+
logfile: Path,
|
|
299
|
+
group: str | None,
|
|
300
|
+
team_map_file: str | None,
|
|
301
|
+
rows: int | None,
|
|
302
|
+
output: str | None,
|
|
303
|
+
) -> None:
|
|
304
|
+
"""Generate overview statistics for the repository.
|
|
305
|
+
|
|
306
|
+
Provides high-level statistics including number of commits,
|
|
307
|
+
entities, authors, and date range of the repository history.
|
|
308
|
+
|
|
309
|
+
\b
|
|
310
|
+
Example:
|
|
311
|
+
code-maat-python summary git.log
|
|
312
|
+
code-maat-python summary git.log --output summary.csv
|
|
313
|
+
code-maat-python summary git.log --group layers.txt
|
|
314
|
+
"""
|
|
315
|
+
try:
|
|
316
|
+
df = parse_git_log(logfile)
|
|
317
|
+
df = apply_transformers(df, group, team_map_file)
|
|
318
|
+
result = analyze_summary(df)
|
|
319
|
+
output_results(result, output, rows)
|
|
320
|
+
except Exception as e:
|
|
321
|
+
handle_analysis_error(e)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
@main.command()
|
|
325
|
+
@click.argument("logfile", type=str, callback=validate_logfile)
|
|
326
|
+
@click.option(
|
|
327
|
+
"--min-revs",
|
|
328
|
+
type=int,
|
|
329
|
+
default=5,
|
|
330
|
+
show_default=True,
|
|
331
|
+
help="Minimum number of revisions for a module to be included",
|
|
332
|
+
)
|
|
333
|
+
@click.option(
|
|
334
|
+
"--min-shared-revs",
|
|
335
|
+
type=int,
|
|
336
|
+
default=5,
|
|
337
|
+
show_default=True,
|
|
338
|
+
help="Minimum number of shared revisions between modules",
|
|
339
|
+
)
|
|
340
|
+
@click.option(
|
|
341
|
+
"--min-coupling",
|
|
342
|
+
type=int,
|
|
343
|
+
default=30,
|
|
344
|
+
show_default=True,
|
|
345
|
+
help="Minimum coupling percentage (0-100)",
|
|
346
|
+
)
|
|
347
|
+
@click.option(
|
|
348
|
+
"--max-coupling",
|
|
349
|
+
type=int,
|
|
350
|
+
default=100,
|
|
351
|
+
show_default=True,
|
|
352
|
+
help="Maximum coupling percentage (0-100)",
|
|
353
|
+
)
|
|
354
|
+
@click.option(
|
|
355
|
+
"--max-changeset-size",
|
|
356
|
+
type=int,
|
|
357
|
+
default=30,
|
|
358
|
+
show_default=True,
|
|
359
|
+
help="Maximum number of files in a commit to consider",
|
|
360
|
+
)
|
|
361
|
+
@common_options
|
|
362
|
+
def coupling(
|
|
363
|
+
logfile: Path,
|
|
364
|
+
min_revs: int,
|
|
365
|
+
min_shared_revs: int,
|
|
366
|
+
min_coupling: int,
|
|
367
|
+
max_coupling: int,
|
|
368
|
+
max_changeset_size: int,
|
|
369
|
+
group: str | None,
|
|
370
|
+
team_map_file: str | None,
|
|
371
|
+
rows: int | None,
|
|
372
|
+
output: str | None,
|
|
373
|
+
) -> None:
|
|
374
|
+
"""Calculate logical coupling between files.
|
|
375
|
+
|
|
376
|
+
Identifies files that frequently change together, which may
|
|
377
|
+
indicate hidden dependencies or architectural issues.
|
|
378
|
+
|
|
379
|
+
Logical coupling is calculated as:
|
|
380
|
+
(shared_revisions / average_revisions) * 100
|
|
381
|
+
|
|
382
|
+
Large commits (> max-changeset-size) are filtered out to avoid
|
|
383
|
+
noise from bulk refactorings or automated changes.
|
|
384
|
+
|
|
385
|
+
\b
|
|
386
|
+
Example:
|
|
387
|
+
code-maat-python coupling git.log
|
|
388
|
+
code-maat-python coupling git.log --min-coupling 50
|
|
389
|
+
code-maat-python coupling git.log --min-revs 10 --min-shared-revs 5
|
|
390
|
+
code-maat-python coupling git.log --group layers.txt --rows 20
|
|
391
|
+
code-maat-python coupling git.log --output coupling.csv
|
|
392
|
+
"""
|
|
393
|
+
try:
|
|
394
|
+
# Validate parameters
|
|
395
|
+
if not (0 <= min_coupling <= 100):
|
|
396
|
+
raise click.BadParameter("min-coupling must be between 0 and 100")
|
|
397
|
+
if not (0 <= max_coupling <= 100):
|
|
398
|
+
raise click.BadParameter("max-coupling must be between 0 and 100")
|
|
399
|
+
if min_coupling > max_coupling:
|
|
400
|
+
raise click.BadParameter("min-coupling must be <= max-coupling")
|
|
401
|
+
|
|
402
|
+
df = parse_git_log(logfile)
|
|
403
|
+
df = apply_transformers(df, group, team_map_file)
|
|
404
|
+
result = analyze_coupling(
|
|
405
|
+
df,
|
|
406
|
+
min_revs=min_revs,
|
|
407
|
+
min_shared_revs=min_shared_revs,
|
|
408
|
+
min_coupling=min_coupling,
|
|
409
|
+
max_coupling=max_coupling,
|
|
410
|
+
max_changeset_size=max_changeset_size,
|
|
411
|
+
)
|
|
412
|
+
output_results(result, output, rows)
|
|
413
|
+
except Exception as e:
|
|
414
|
+
handle_analysis_error(e)
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
@main.command()
|
|
418
|
+
@click.argument("logfile", type=str, callback=validate_logfile)
|
|
419
|
+
@click.option(
|
|
420
|
+
"--max-changeset-size",
|
|
421
|
+
type=int,
|
|
422
|
+
default=30,
|
|
423
|
+
show_default=True,
|
|
424
|
+
help="Maximum number of files in a commit to consider",
|
|
425
|
+
)
|
|
426
|
+
@common_options
|
|
427
|
+
def soc(
|
|
428
|
+
logfile: Path,
|
|
429
|
+
max_changeset_size: int,
|
|
430
|
+
group: str | None,
|
|
431
|
+
team_map_file: str | None,
|
|
432
|
+
rows: int | None,
|
|
433
|
+
output: str | None,
|
|
434
|
+
) -> None:
|
|
435
|
+
"""Calculate sum of coupling (SOC) for each entity.
|
|
436
|
+
|
|
437
|
+
A simpler metric than full logical coupling. For each commit
|
|
438
|
+
with m files, each file gets a SOC score of (m-1).
|
|
439
|
+
|
|
440
|
+
This provides a quick way to identify files that are often
|
|
441
|
+
changed together with other files.
|
|
442
|
+
|
|
443
|
+
\b
|
|
444
|
+
Example:
|
|
445
|
+
code-maat-python soc git.log
|
|
446
|
+
code-maat-python soc git.log --max-changeset-size 50
|
|
447
|
+
code-maat-python soc git.log --group layers.txt --rows 10
|
|
448
|
+
code-maat-python soc git.log --output soc.csv
|
|
449
|
+
"""
|
|
450
|
+
try:
|
|
451
|
+
df = parse_git_log(logfile)
|
|
452
|
+
df = apply_transformers(df, group, team_map_file)
|
|
453
|
+
result = analyze_soc(df, max_changeset_size=max_changeset_size)
|
|
454
|
+
output_results(result, output, rows)
|
|
455
|
+
except Exception as e:
|
|
456
|
+
handle_analysis_error(e)
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
@main.command()
|
|
460
|
+
@click.argument("logfile", type=str, callback=validate_logfile)
|
|
461
|
+
@common_options
|
|
462
|
+
def abs_churn_cmd(
|
|
463
|
+
logfile: Path,
|
|
464
|
+
group: str | None,
|
|
465
|
+
team_map_file: str | None,
|
|
466
|
+
rows: int | None,
|
|
467
|
+
output: str | None,
|
|
468
|
+
) -> None:
|
|
469
|
+
"""Calculate absolute code churn per date.
|
|
470
|
+
|
|
471
|
+
Shows total lines added and deleted for each date in the
|
|
472
|
+
commit history, useful for identifying periods of high activity.
|
|
473
|
+
|
|
474
|
+
\b
|
|
475
|
+
Example:
|
|
476
|
+
code-maat-python abs-churn git.log
|
|
477
|
+
code-maat-python abs-churn git.log --output churn.csv
|
|
478
|
+
code-maat-python abs-churn git.log --rows 30
|
|
479
|
+
"""
|
|
480
|
+
try:
|
|
481
|
+
df = parse_git_log(logfile)
|
|
482
|
+
df = apply_transformers(df, group, team_map_file)
|
|
483
|
+
result = abs_churn(df)
|
|
484
|
+
output_results(result, output, rows)
|
|
485
|
+
except Exception as e:
|
|
486
|
+
handle_analysis_error(e)
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
@main.command()
|
|
490
|
+
@click.argument("logfile", type=str, callback=validate_logfile)
|
|
491
|
+
@common_options
|
|
492
|
+
def author_churn_cmd(
|
|
493
|
+
logfile: Path,
|
|
494
|
+
group: str | None,
|
|
495
|
+
team_map_file: str | None,
|
|
496
|
+
rows: int | None,
|
|
497
|
+
output: str | None,
|
|
498
|
+
) -> None:
|
|
499
|
+
"""Calculate total churn per author.
|
|
500
|
+
|
|
501
|
+
Shows total lines added and deleted by each author across
|
|
502
|
+
all entities, useful for understanding individual contributions.
|
|
503
|
+
|
|
504
|
+
\b
|
|
505
|
+
Example:
|
|
506
|
+
code-maat-python author-churn git.log
|
|
507
|
+
code-maat-python author-churn git.log --team-map-file teams.csv
|
|
508
|
+
code-maat-python author-churn git.log --output author-churn.csv
|
|
509
|
+
"""
|
|
510
|
+
try:
|
|
511
|
+
df = parse_git_log(logfile)
|
|
512
|
+
df = apply_transformers(df, group, team_map_file)
|
|
513
|
+
result = author_churn(df)
|
|
514
|
+
output_results(result, output, rows)
|
|
515
|
+
except Exception as e:
|
|
516
|
+
handle_analysis_error(e)
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
@main.command()
|
|
520
|
+
@click.argument("logfile", type=str, callback=validate_logfile)
|
|
521
|
+
@common_options
|
|
522
|
+
def entity_churn_cmd(
|
|
523
|
+
logfile: Path,
|
|
524
|
+
group: str | None,
|
|
525
|
+
team_map_file: str | None,
|
|
526
|
+
rows: int | None,
|
|
527
|
+
output: str | None,
|
|
528
|
+
) -> None:
|
|
529
|
+
"""Calculate absolute churn per entity.
|
|
530
|
+
|
|
531
|
+
Shows total lines added and deleted for each file, sorted by
|
|
532
|
+
lines added (a better predictor of post-release defects).
|
|
533
|
+
|
|
534
|
+
\b
|
|
535
|
+
Example:
|
|
536
|
+
code-maat-python entity-churn git.log
|
|
537
|
+
code-maat-python entity-churn git.log --group layers.txt --rows 20
|
|
538
|
+
code-maat-python entity-churn git.log --output entity-churn.csv
|
|
539
|
+
"""
|
|
540
|
+
try:
|
|
541
|
+
df = parse_git_log(logfile)
|
|
542
|
+
df = apply_transformers(df, group, team_map_file)
|
|
543
|
+
result = entity_churn(df)
|
|
544
|
+
output_results(result, output, rows)
|
|
545
|
+
except Exception as e:
|
|
546
|
+
handle_analysis_error(e)
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
@main.command()
|
|
550
|
+
@click.argument("logfile", type=str, callback=validate_logfile)
|
|
551
|
+
@common_options
|
|
552
|
+
def entity_ownership_cmd(
|
|
553
|
+
logfile: Path,
|
|
554
|
+
group: str | None,
|
|
555
|
+
team_map_file: str | None,
|
|
556
|
+
rows: int | None,
|
|
557
|
+
output: str | None,
|
|
558
|
+
) -> None:
|
|
559
|
+
"""Calculate ownership of each entity by author based on churn.
|
|
560
|
+
|
|
561
|
+
Shows how much each author has contributed to each entity
|
|
562
|
+
in terms of lines added and deleted, identifying code ownership.
|
|
563
|
+
|
|
564
|
+
\b
|
|
565
|
+
Example:
|
|
566
|
+
code-maat-python entity-ownership git.log
|
|
567
|
+
code-maat-python entity-ownership git.log --group layers.txt
|
|
568
|
+
code-maat-python entity-ownership git.log --output ownership.csv
|
|
569
|
+
"""
|
|
570
|
+
try:
|
|
571
|
+
df = parse_git_log(logfile)
|
|
572
|
+
df = apply_transformers(df, group, team_map_file)
|
|
573
|
+
result = entity_ownership(df)
|
|
574
|
+
output_results(result, output, rows)
|
|
575
|
+
except Exception as e:
|
|
576
|
+
handle_analysis_error(e)
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
@main.command()
|
|
580
|
+
@click.argument("logfile", type=str, callback=validate_logfile)
|
|
581
|
+
@common_options
|
|
582
|
+
def main_dev_cmd(
|
|
583
|
+
logfile: Path,
|
|
584
|
+
group: str | None,
|
|
585
|
+
team_map_file: str | None,
|
|
586
|
+
rows: int | None,
|
|
587
|
+
output: str | None,
|
|
588
|
+
) -> None:
|
|
589
|
+
"""Identify the main developer of each entity by lines added.
|
|
590
|
+
|
|
591
|
+
The main developer is the author who contributed the most lines
|
|
592
|
+
of code to each entity. Returns ownership percentage.
|
|
593
|
+
|
|
594
|
+
\b
|
|
595
|
+
Example:
|
|
596
|
+
code-maat-python main-dev git.log
|
|
597
|
+
code-maat-python main-dev git.log --group layers.txt --rows 15
|
|
598
|
+
code-maat-python main-dev git.log --output main-dev.csv
|
|
599
|
+
"""
|
|
600
|
+
try:
|
|
601
|
+
df = parse_git_log(logfile)
|
|
602
|
+
df = apply_transformers(df, group, team_map_file)
|
|
603
|
+
result = main_dev(df)
|
|
604
|
+
output_results(result, output, rows)
|
|
605
|
+
except Exception as e:
|
|
606
|
+
handle_analysis_error(e)
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
@main.command()
|
|
610
|
+
@click.argument("logfile", type=str, callback=validate_logfile)
|
|
611
|
+
@common_options
|
|
612
|
+
def refactoring_main_dev_cmd(
|
|
613
|
+
logfile: Path,
|
|
614
|
+
group: str | None,
|
|
615
|
+
team_map_file: str | None,
|
|
616
|
+
rows: int | None,
|
|
617
|
+
output: str | None,
|
|
618
|
+
) -> None:
|
|
619
|
+
"""Identify the main developer of each entity by lines removed.
|
|
620
|
+
|
|
621
|
+
Alternative calculation identifying main developer as the author
|
|
622
|
+
who removed the most lines (representing refactoring effort).
|
|
623
|
+
|
|
624
|
+
\b
|
|
625
|
+
Example:
|
|
626
|
+
code-maat-python refactoring-main-dev git.log
|
|
627
|
+
code-maat-python refactoring-main-dev git.log --rows 10
|
|
628
|
+
code-maat-python refactoring-main-dev git.log --output refactoring.csv
|
|
629
|
+
"""
|
|
630
|
+
try:
|
|
631
|
+
df = parse_git_log(logfile)
|
|
632
|
+
df = apply_transformers(df, group, team_map_file)
|
|
633
|
+
result = refactoring_main_dev(df)
|
|
634
|
+
output_results(result, output, rows)
|
|
635
|
+
except Exception as e:
|
|
636
|
+
handle_analysis_error(e)
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
@main.command()
|
|
640
|
+
@click.argument("logfile", type=str, callback=validate_logfile)
|
|
641
|
+
@common_options
|
|
642
|
+
def entity_effort_cmd(
|
|
643
|
+
logfile: Path,
|
|
644
|
+
group: str | None,
|
|
645
|
+
team_map_file: str | None,
|
|
646
|
+
rows: int | None,
|
|
647
|
+
output: str | None,
|
|
648
|
+
) -> None:
|
|
649
|
+
"""Calculate author contribution to each entity by revision count.
|
|
650
|
+
|
|
651
|
+
Identifies how many revisions each author contributed to each
|
|
652
|
+
entity, providing a measure of effort.
|
|
653
|
+
|
|
654
|
+
\b
|
|
655
|
+
Example:
|
|
656
|
+
code-maat-python entity-effort git.log
|
|
657
|
+
code-maat-python entity-effort git.log --group layers.txt
|
|
658
|
+
code-maat-python entity-effort git.log --output effort.csv
|
|
659
|
+
"""
|
|
660
|
+
try:
|
|
661
|
+
df = parse_git_log(logfile)
|
|
662
|
+
df = apply_transformers(df, group, team_map_file)
|
|
663
|
+
result = entity_effort(df)
|
|
664
|
+
output_results(result, output, rows)
|
|
665
|
+
except Exception as e:
|
|
666
|
+
handle_analysis_error(e)
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
@main.command()
|
|
670
|
+
@click.argument("logfile", type=str, callback=validate_logfile)
|
|
671
|
+
@common_options
|
|
672
|
+
def main_dev_by_revs_cmd(
|
|
673
|
+
logfile: Path,
|
|
674
|
+
group: str | None,
|
|
675
|
+
team_map_file: str | None,
|
|
676
|
+
rows: int | None,
|
|
677
|
+
output: str | None,
|
|
678
|
+
) -> None:
|
|
679
|
+
"""Identify the main developer of each entity by revision count.
|
|
680
|
+
|
|
681
|
+
The main developer is the author who contributed the most
|
|
682
|
+
revisions to each entity. Returns ownership percentage.
|
|
683
|
+
|
|
684
|
+
\b
|
|
685
|
+
Example:
|
|
686
|
+
code-maat-python main-dev-by-revs git.log
|
|
687
|
+
code-maat-python main-dev-by-revs git.log --rows 15
|
|
688
|
+
code-maat-python main-dev-by-revs git.log --output main-dev-revs.csv
|
|
689
|
+
"""
|
|
690
|
+
try:
|
|
691
|
+
df = parse_git_log(logfile)
|
|
692
|
+
df = apply_transformers(df, group, team_map_file)
|
|
693
|
+
result = main_dev_by_revs(df)
|
|
694
|
+
output_results(result, output, rows)
|
|
695
|
+
except Exception as e:
|
|
696
|
+
handle_analysis_error(e)
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
@main.command()
|
|
700
|
+
@click.argument("logfile", type=str, callback=validate_logfile)
|
|
701
|
+
@common_options
|
|
702
|
+
def fragmentation_cmd(
|
|
703
|
+
logfile: Path,
|
|
704
|
+
group: str | None,
|
|
705
|
+
team_map_file: str | None,
|
|
706
|
+
rows: int | None,
|
|
707
|
+
output: str | None,
|
|
708
|
+
) -> None:
|
|
709
|
+
"""Calculate fragmentation for each entity using fractal value.
|
|
710
|
+
|
|
711
|
+
The fractal value measures how fragmented contributions are
|
|
712
|
+
across authors. 0 = single author, approaching 1 = highly fragmented.
|
|
713
|
+
|
|
714
|
+
\b
|
|
715
|
+
Example:
|
|
716
|
+
code-maat-python fragmentation git.log
|
|
717
|
+
code-maat-python fragmentation git.log --group layers.txt --rows 20
|
|
718
|
+
code-maat-python fragmentation git.log --output fragmentation.csv
|
|
719
|
+
"""
|
|
720
|
+
try:
|
|
721
|
+
df = parse_git_log(logfile)
|
|
722
|
+
df = apply_transformers(df, group, team_map_file)
|
|
723
|
+
result = fragmentation(df)
|
|
724
|
+
output_results(result, output, rows)
|
|
725
|
+
except Exception as e:
|
|
726
|
+
handle_analysis_error(e)
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
@main.command()
|
|
730
|
+
@click.argument("logfile", type=str, callback=validate_logfile)
|
|
731
|
+
@click.option(
|
|
732
|
+
"--reference-date",
|
|
733
|
+
"-d",
|
|
734
|
+
type=str,
|
|
735
|
+
help="Reference date for age calculation (default: today, format: YYYY-MM-DD)",
|
|
736
|
+
)
|
|
737
|
+
@common_options
|
|
738
|
+
def age(
|
|
739
|
+
logfile: Path,
|
|
740
|
+
reference_date: str | None,
|
|
741
|
+
group: str | None,
|
|
742
|
+
team_map_file: str | None,
|
|
743
|
+
rows: int | None,
|
|
744
|
+
output: str | None,
|
|
745
|
+
) -> None:
|
|
746
|
+
"""Calculate age of entities in months since last modification.
|
|
747
|
+
|
|
748
|
+
Identifies stale code that hasn't been modified recently, which
|
|
749
|
+
may indicate technical debt, abandoned features, or stable components.
|
|
750
|
+
|
|
751
|
+
\b
|
|
752
|
+
Example:
|
|
753
|
+
code-maat-python age git.log
|
|
754
|
+
code-maat-python age git.log --reference-date 2023-12-31
|
|
755
|
+
code-maat-python age git.log --group layers.txt --rows 25
|
|
756
|
+
code-maat-python age git.log --output age.csv
|
|
757
|
+
"""
|
|
758
|
+
try:
|
|
759
|
+
df = parse_git_log(logfile)
|
|
760
|
+
df = apply_transformers(df, group, team_map_file)
|
|
761
|
+
|
|
762
|
+
# Parse reference date if provided
|
|
763
|
+
ref_date = None
|
|
764
|
+
if reference_date:
|
|
765
|
+
ref_date = pd.Timestamp(reference_date)
|
|
766
|
+
|
|
767
|
+
result = code_age(df, reference_date=ref_date)
|
|
768
|
+
output_results(result, output, rows)
|
|
769
|
+
except Exception as e:
|
|
770
|
+
handle_analysis_error(e)
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
@main.command()
|
|
774
|
+
@click.argument("logfile", type=str, callback=validate_logfile)
|
|
775
|
+
@click.option(
|
|
776
|
+
"--min-shared",
|
|
777
|
+
type=int,
|
|
778
|
+
default=5,
|
|
779
|
+
show_default=True,
|
|
780
|
+
help="Minimum number of shared entities between developers",
|
|
781
|
+
)
|
|
782
|
+
@click.option(
|
|
783
|
+
"--min-coupling",
|
|
784
|
+
type=int,
|
|
785
|
+
default=30,
|
|
786
|
+
show_default=True,
|
|
787
|
+
help="Minimum coupling strength percentage (0-100)",
|
|
788
|
+
)
|
|
789
|
+
@common_options
|
|
790
|
+
def communication_cmd(
|
|
791
|
+
logfile: Path,
|
|
792
|
+
min_shared: int,
|
|
793
|
+
min_coupling: int,
|
|
794
|
+
group: str | None,
|
|
795
|
+
team_map_file: str | None,
|
|
796
|
+
rows: int | None,
|
|
797
|
+
output: str | None,
|
|
798
|
+
) -> None:
|
|
799
|
+
"""Calculate communication needs between developers.
|
|
800
|
+
|
|
801
|
+
Identifies developers who work on the same code files, indicating
|
|
802
|
+
a need for communication and coordination. Strength is normalized
|
|
803
|
+
by each developer's total workload.
|
|
804
|
+
|
|
805
|
+
\b
|
|
806
|
+
Example:
|
|
807
|
+
code-maat-python communication git.log
|
|
808
|
+
code-maat-python communication git.log --min-shared 10 --min-coupling 50
|
|
809
|
+
code-maat-python communication git.log --team-map-file teams.csv --rows 15
|
|
810
|
+
code-maat-python communication git.log --output communication.csv
|
|
811
|
+
"""
|
|
812
|
+
try:
|
|
813
|
+
df = parse_git_log(logfile)
|
|
814
|
+
df = apply_transformers(df, group, team_map_file)
|
|
815
|
+
result = communication(df, min_shared=min_shared, min_coupling=min_coupling)
|
|
816
|
+
output_results(result, output, rows)
|
|
817
|
+
except Exception as e:
|
|
818
|
+
handle_analysis_error(e)
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
if __name__ == "__main__":
|
|
822
|
+
main()
|