diffstory 0.4.2__tar.gz → 1.0.0__tar.gz
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.
- diffstory-1.0.0/LICENSE +21 -0
- {diffstory-0.4.2 → diffstory-1.0.0}/PKG-INFO +34 -1
- {diffstory-0.4.2 → diffstory-1.0.0}/README.md +31 -0
- {diffstory-0.4.2 → diffstory-1.0.0}/pyproject.toml +1 -1
- {diffstory-0.4.2 → diffstory-1.0.0}/src/diffstory/__init__.py +1 -1
- {diffstory-0.4.2 → diffstory-1.0.0}/src/diffstory/cli.py +145 -7
- diffstory-1.0.0/src/diffstory/generators/__init__.py +2 -0
- diffstory-1.0.0/src/diffstory/generators/get_css.py +1537 -0
- diffstory-1.0.0/src/diffstory/generators/get_js.py +674 -0
- diffstory-1.0.0/src/diffstory/git_utils.py +897 -0
- diffstory-1.0.0/src/diffstory/html_generator.py +1326 -0
- {diffstory-0.4.2 → diffstory-1.0.0}/src/diffstory.egg-info/PKG-INFO +34 -1
- {diffstory-0.4.2 → diffstory-1.0.0}/src/diffstory.egg-info/SOURCES.txt +5 -1
- diffstory-0.4.2/src/diffstory/git_utils.py +0 -449
- diffstory-0.4.2/src/diffstory/html_generator.py +0 -2523
- {diffstory-0.4.2 → diffstory-1.0.0}/setup.cfg +0 -0
- {diffstory-0.4.2 → diffstory-1.0.0}/src/diffstory/__main__.py +0 -0
- {diffstory-0.4.2 → diffstory-1.0.0}/src/diffstory/diff_parser.py +0 -0
- {diffstory-0.4.2 → diffstory-1.0.0}/src/diffstory/loader.py +0 -0
- {diffstory-0.4.2 → diffstory-1.0.0}/src/diffstory/syntax.py +0 -0
- {diffstory-0.4.2 → diffstory-1.0.0}/src/diffstory.egg-info/dependency_links.txt +0 -0
- {diffstory-0.4.2 → diffstory-1.0.0}/src/diffstory.egg-info/entry_points.txt +0 -0
- {diffstory-0.4.2 → diffstory-1.0.0}/src/diffstory.egg-info/requires.txt +0 -0
- {diffstory-0.4.2 → diffstory-1.0.0}/src/diffstory.egg-info/top_level.txt +0 -0
diffstory-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lakshay Jindal
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: diffstory
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.0
|
|
4
4
|
Summary: Transform Git diffs into rich, interactive, self-contained HTML reports
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/lakshayjindal/diffstory
|
|
@@ -13,7 +13,9 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
13
13
|
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
14
14
|
Requires-Python: >=3.10
|
|
15
15
|
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
16
17
|
Requires-Dist: Pygments>=2.10
|
|
18
|
+
Dynamic: license-file
|
|
17
19
|
|
|
18
20
|
# DiffStory
|
|
19
21
|
|
|
@@ -93,6 +95,37 @@ Alongside the HTML report, export structured data:
|
|
|
93
95
|
diffstory --staged --json --md --csv
|
|
94
96
|
```
|
|
95
97
|
|
|
98
|
+
### Analytics & Insights
|
|
99
|
+
|
|
100
|
+
Every report now includes rich analytics computed from git history:
|
|
101
|
+
|
|
102
|
+
- **🔥 Hotspots** — Files modified most frequently in recent history
|
|
103
|
+
- **⚠ Risk Analysis** — Heuristic risk scoring based on LOC, files touched, core modules, and hotspots
|
|
104
|
+
- **👤 Ownership Analysis** — Top contributor per file with suggested reviewer
|
|
105
|
+
- **📈 Change Timeline** — Commit distribution by day of week
|
|
106
|
+
- **📝 Semantic Summary** — Deterministic summary from filenames and commit messages
|
|
107
|
+
- **📦 Dependency Diff** — Auto-detects changes to `requirements.txt`, `package.json`, `Cargo.toml`, `go.mod`, etc.
|
|
108
|
+
- **📌 TODO/FIXME Detection** — Scans added lines for annotations
|
|
109
|
+
- **🧪 Test Impact** — Maps changed files to their likely test files
|
|
110
|
+
- **📂 Folder Heatmap** — Change distribution across directories
|
|
111
|
+
- **🔬 Complexity Delta** — Function-size changes in Python files
|
|
112
|
+
- **⏳ Commit Evolution** — Slider to scrub through commits in a range
|
|
113
|
+
- **✅ Review Mode** — Per-file checkboxes with localStorage persistence (`--review`)
|
|
114
|
+
|
|
115
|
+
### CI Badge Mode
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
diffstory HEAD~1 HEAD --summary-only
|
|
119
|
+
# Files Changed: 14 | LOC: +233/-98 | Risk: Medium | Hotspots: 3
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Output Directory
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
diffstory --output-dir reviews/
|
|
126
|
+
diffstory --output-dir reviews/ -o sprint-17
|
|
127
|
+
```
|
|
128
|
+
|
|
96
129
|
---
|
|
97
130
|
|
|
98
131
|
## Installation
|
|
@@ -76,6 +76,37 @@ Alongside the HTML report, export structured data:
|
|
|
76
76
|
diffstory --staged --json --md --csv
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
+
### Analytics & Insights
|
|
80
|
+
|
|
81
|
+
Every report now includes rich analytics computed from git history:
|
|
82
|
+
|
|
83
|
+
- **🔥 Hotspots** — Files modified most frequently in recent history
|
|
84
|
+
- **⚠ Risk Analysis** — Heuristic risk scoring based on LOC, files touched, core modules, and hotspots
|
|
85
|
+
- **👤 Ownership Analysis** — Top contributor per file with suggested reviewer
|
|
86
|
+
- **📈 Change Timeline** — Commit distribution by day of week
|
|
87
|
+
- **📝 Semantic Summary** — Deterministic summary from filenames and commit messages
|
|
88
|
+
- **📦 Dependency Diff** — Auto-detects changes to `requirements.txt`, `package.json`, `Cargo.toml`, `go.mod`, etc.
|
|
89
|
+
- **📌 TODO/FIXME Detection** — Scans added lines for annotations
|
|
90
|
+
- **🧪 Test Impact** — Maps changed files to their likely test files
|
|
91
|
+
- **📂 Folder Heatmap** — Change distribution across directories
|
|
92
|
+
- **🔬 Complexity Delta** — Function-size changes in Python files
|
|
93
|
+
- **⏳ Commit Evolution** — Slider to scrub through commits in a range
|
|
94
|
+
- **✅ Review Mode** — Per-file checkboxes with localStorage persistence (`--review`)
|
|
95
|
+
|
|
96
|
+
### CI Badge Mode
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
diffstory HEAD~1 HEAD --summary-only
|
|
100
|
+
# Files Changed: 14 | LOC: +233/-98 | Risk: Medium | Hotspots: 3
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Output Directory
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
diffstory --output-dir reviews/
|
|
107
|
+
diffstory --output-dir reviews/ -o sprint-17
|
|
108
|
+
```
|
|
109
|
+
|
|
79
110
|
---
|
|
80
111
|
|
|
81
112
|
## Installation
|
|
@@ -18,6 +18,18 @@ from diffstory.git_utils import (
|
|
|
18
18
|
)
|
|
19
19
|
from diffstory.html_generator import generate_report
|
|
20
20
|
from diffstory.loader import Spinner
|
|
21
|
+
from diffstory.git_utils import (
|
|
22
|
+
get_hotspots,
|
|
23
|
+
get_change_timeline,
|
|
24
|
+
get_folder_stats,
|
|
25
|
+
get_dependency_files_for_diff,
|
|
26
|
+
scan_diff_for_todos,
|
|
27
|
+
map_related_tests,
|
|
28
|
+
get_complexity_delta,
|
|
29
|
+
get_commits_for_evolution,
|
|
30
|
+
compute_file_evolution,
|
|
31
|
+
compute_semantic_summary,
|
|
32
|
+
)
|
|
21
33
|
|
|
22
34
|
|
|
23
35
|
# ---------------------------------------------------------------------------
|
|
@@ -143,6 +155,28 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
143
155
|
help="Generate report from a diff file directly (no git repository needed)",
|
|
144
156
|
)
|
|
145
157
|
|
|
158
|
+
parser.add_argument(
|
|
159
|
+
"--output-dir",
|
|
160
|
+
metavar="DIR",
|
|
161
|
+
default=None,
|
|
162
|
+
help="Output directory for generated files (creates dir if not exists). "
|
|
163
|
+
"All report files go here; when combined with -o, uses the -o name as the stem.",
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
parser.add_argument(
|
|
167
|
+
"--summary-only",
|
|
168
|
+
action="store_true",
|
|
169
|
+
default=False,
|
|
170
|
+
help="Print a one-line summary suitable for CI pipelines and exit",
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
parser.add_argument(
|
|
174
|
+
"--review",
|
|
175
|
+
action="store_true",
|
|
176
|
+
default=False,
|
|
177
|
+
help="Enable review mode with per-file checkboxes and comment annotations",
|
|
178
|
+
)
|
|
179
|
+
|
|
146
180
|
parser.add_argument(
|
|
147
181
|
"--no-open",
|
|
148
182
|
action="store_true",
|
|
@@ -310,14 +344,22 @@ def _read_diff_from_file(path: str) -> str:
|
|
|
310
344
|
sys.exit(1)
|
|
311
345
|
|
|
312
346
|
|
|
313
|
-
def _resolve_output_path(given_path: str) -> str:
|
|
347
|
+
def _resolve_output_path(given_path: str, output_dir: Optional[str] = None) -> str:
|
|
314
348
|
"""Resolve the output file path.
|
|
315
349
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
350
|
+
Priority:
|
|
351
|
+
1. If --output-dir is given, place the file(s) in that directory.
|
|
352
|
+
2. If default path and inside a git repo, use 'stories/' outside the repo.
|
|
353
|
+
3. Otherwise use the given path as-is (resolved).
|
|
319
354
|
"""
|
|
320
355
|
given = Path(given_path)
|
|
356
|
+
|
|
357
|
+
# If --output-dir is provided
|
|
358
|
+
if output_dir:
|
|
359
|
+
out_dir = Path(output_dir).expanduser().resolve()
|
|
360
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
361
|
+
return str(out_dir / given.name)
|
|
362
|
+
|
|
321
363
|
# Only redirect the default path — if the user explicitly passed -o, use as-is
|
|
322
364
|
if given.name != "diffstory-report.html":
|
|
323
365
|
return str(given.resolve())
|
|
@@ -335,6 +377,35 @@ def _resolve_output_path(given_path: str) -> str:
|
|
|
335
377
|
return str(given.resolve())
|
|
336
378
|
|
|
337
379
|
|
|
380
|
+
def _resolve_output_dir(output_dir: Optional[str], given_path: str) -> Optional[str]:
|
|
381
|
+
"""Resolve the output directory path.
|
|
382
|
+
|
|
383
|
+
Returns the resolved output directory string or None if not set.
|
|
384
|
+
"""
|
|
385
|
+
if output_dir:
|
|
386
|
+
out_dir = Path(output_dir).expanduser().resolve()
|
|
387
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
388
|
+
return str(out_dir)
|
|
389
|
+
return None
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def _print_ci_summary(files, stats=None, hotspots=None) -> None:
|
|
393
|
+
"""Print a one-line summary suitable for CI pipelines (--summary-only)."""
|
|
394
|
+
additions = sum(1 for f in files for h in f.hunks for l in h.lines if l.line_type == "addition")
|
|
395
|
+
deletions = sum(1 for f in files for h in f.hunks for l in h.lines if l.line_type == "deletion")
|
|
396
|
+
file_count = len(files)
|
|
397
|
+
|
|
398
|
+
risk_level = "Low"
|
|
399
|
+
if file_count > 15 or additions + deletions > 500:
|
|
400
|
+
risk_level = "High"
|
|
401
|
+
elif file_count > 8 or additions + deletions > 200:
|
|
402
|
+
risk_level = "Medium"
|
|
403
|
+
|
|
404
|
+
hotspot_count = len(hotspots[:3]) if hotspots else 0
|
|
405
|
+
|
|
406
|
+
print(f"Files Changed: {file_count} | LOC: +{additions}/-{deletions} | Risk: {risk_level} | Hotspots: {hotspot_count}")
|
|
407
|
+
|
|
408
|
+
|
|
338
409
|
def main() -> None:
|
|
339
410
|
"""Main entry point for the diffstory CLI."""
|
|
340
411
|
parser = build_parser()
|
|
@@ -348,8 +419,9 @@ def main() -> None:
|
|
|
348
419
|
if debug:
|
|
349
420
|
verbose = True # debug implies verbose
|
|
350
421
|
|
|
351
|
-
# Resolve output
|
|
352
|
-
|
|
422
|
+
# Resolve output directory and path
|
|
423
|
+
output_dir_resolved = _resolve_output_dir(args.output_dir, args.output)
|
|
424
|
+
output_path = _resolve_output_path(args.output, output_dir=output_dir_resolved)
|
|
353
425
|
|
|
354
426
|
# Handle --diff flag (read diff from file, no git needed)
|
|
355
427
|
if args.diff:
|
|
@@ -362,11 +434,20 @@ def main() -> None:
|
|
|
362
434
|
if not files:
|
|
363
435
|
print("No parseable diff files found.")
|
|
364
436
|
sys.exit(0)
|
|
437
|
+
|
|
438
|
+
# --summary-only mode
|
|
439
|
+
if args.summary_only:
|
|
440
|
+
_print_ci_summary(files)
|
|
441
|
+
return
|
|
442
|
+
|
|
365
443
|
has_exports = args.json or args.md or args.csv
|
|
366
444
|
if has_exports:
|
|
367
445
|
generate_exports(files, output_path, args.json, args.md, args.csv)
|
|
368
446
|
try:
|
|
369
|
-
report_path = generate_report(
|
|
447
|
+
report_path = generate_report(
|
|
448
|
+
files, output_path=output_path, repo_name="diff",
|
|
449
|
+
verbose=verbose, review_mode=args.review,
|
|
450
|
+
)
|
|
370
451
|
except Exception as e:
|
|
371
452
|
if debug:
|
|
372
453
|
import traceback
|
|
@@ -413,6 +494,52 @@ def main() -> None:
|
|
|
413
494
|
spinner.fail("No parseable diff files found")
|
|
414
495
|
sys.exit(0)
|
|
415
496
|
|
|
497
|
+
# Collect analytics data for reports
|
|
498
|
+
if verbose:
|
|
499
|
+
spinner.update("Gathering analytics...")
|
|
500
|
+
|
|
501
|
+
# Collect analytics data (protected from failures)
|
|
502
|
+
hotspots = []
|
|
503
|
+
timeline = {}
|
|
504
|
+
folder_stats = {}
|
|
505
|
+
deps = []
|
|
506
|
+
todos = []
|
|
507
|
+
test_impact = []
|
|
508
|
+
complexity = []
|
|
509
|
+
summaries = []
|
|
510
|
+
evolution_commits = []
|
|
511
|
+
evolution_files_data = {}
|
|
512
|
+
|
|
513
|
+
try:
|
|
514
|
+
hotspots = get_hotspots()
|
|
515
|
+
timeline = get_change_timeline()
|
|
516
|
+
folder_stats = get_folder_stats(files)
|
|
517
|
+
deps = get_dependency_files_for_diff(files)
|
|
518
|
+
todos = scan_diff_for_todos(diff_text)
|
|
519
|
+
test_impact = map_related_tests(files)
|
|
520
|
+
complexity = get_complexity_delta(files)
|
|
521
|
+
summaries = compute_semantic_summary(files)
|
|
522
|
+
|
|
523
|
+
# Commit evolution data
|
|
524
|
+
if commit_a is not None or commit_b is not None:
|
|
525
|
+
base = commit_a or "HEAD"
|
|
526
|
+
head = commit_b or "HEAD"
|
|
527
|
+
commits = get_commits_for_evolution(limit=20, base_commit=base, head_commit=head)
|
|
528
|
+
if commits:
|
|
529
|
+
evolution_commits = commits
|
|
530
|
+
for f in files[:3]:
|
|
531
|
+
evo = compute_file_evolution(f.display_path, commits)
|
|
532
|
+
if evo:
|
|
533
|
+
evolution_files_data[f.display_path] = evo
|
|
534
|
+
except Exception as e:
|
|
535
|
+
if verbose:
|
|
536
|
+
print(f" Analytics warning: {e}")
|
|
537
|
+
|
|
538
|
+
# --summary-only mode (CI badge)
|
|
539
|
+
if args.summary_only:
|
|
540
|
+
_print_ci_summary(files, hotspots=hotspots)
|
|
541
|
+
return
|
|
542
|
+
|
|
416
543
|
# Generate exports if requested
|
|
417
544
|
has_exports = args.json or args.md or args.csv
|
|
418
545
|
if has_exports:
|
|
@@ -439,6 +566,17 @@ def main() -> None:
|
|
|
439
566
|
commit_b=commit_b,
|
|
440
567
|
verbose=verbose,
|
|
441
568
|
progress_callback=on_blame_progress if total_files > 1 else None,
|
|
569
|
+
review_mode=args.review,
|
|
570
|
+
hotspots=hotspots,
|
|
571
|
+
timeline=timeline,
|
|
572
|
+
folder_stats=folder_stats,
|
|
573
|
+
dependency_diffs=deps,
|
|
574
|
+
todos=todos,
|
|
575
|
+
test_impact=test_impact,
|
|
576
|
+
complexity_delta=complexity,
|
|
577
|
+
semantic_summaries=summaries,
|
|
578
|
+
evolution_commits=evolution_commits,
|
|
579
|
+
evolution_files=evolution_files_data,
|
|
442
580
|
)
|
|
443
581
|
except Exception as e:
|
|
444
582
|
if debug:
|