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.
Files changed (24) hide show
  1. diffstory-1.0.0/LICENSE +21 -0
  2. {diffstory-0.4.2 → diffstory-1.0.0}/PKG-INFO +34 -1
  3. {diffstory-0.4.2 → diffstory-1.0.0}/README.md +31 -0
  4. {diffstory-0.4.2 → diffstory-1.0.0}/pyproject.toml +1 -1
  5. {diffstory-0.4.2 → diffstory-1.0.0}/src/diffstory/__init__.py +1 -1
  6. {diffstory-0.4.2 → diffstory-1.0.0}/src/diffstory/cli.py +145 -7
  7. diffstory-1.0.0/src/diffstory/generators/__init__.py +2 -0
  8. diffstory-1.0.0/src/diffstory/generators/get_css.py +1537 -0
  9. diffstory-1.0.0/src/diffstory/generators/get_js.py +674 -0
  10. diffstory-1.0.0/src/diffstory/git_utils.py +897 -0
  11. diffstory-1.0.0/src/diffstory/html_generator.py +1326 -0
  12. {diffstory-0.4.2 → diffstory-1.0.0}/src/diffstory.egg-info/PKG-INFO +34 -1
  13. {diffstory-0.4.2 → diffstory-1.0.0}/src/diffstory.egg-info/SOURCES.txt +5 -1
  14. diffstory-0.4.2/src/diffstory/git_utils.py +0 -449
  15. diffstory-0.4.2/src/diffstory/html_generator.py +0 -2523
  16. {diffstory-0.4.2 → diffstory-1.0.0}/setup.cfg +0 -0
  17. {diffstory-0.4.2 → diffstory-1.0.0}/src/diffstory/__main__.py +0 -0
  18. {diffstory-0.4.2 → diffstory-1.0.0}/src/diffstory/diff_parser.py +0 -0
  19. {diffstory-0.4.2 → diffstory-1.0.0}/src/diffstory/loader.py +0 -0
  20. {diffstory-0.4.2 → diffstory-1.0.0}/src/diffstory/syntax.py +0 -0
  21. {diffstory-0.4.2 → diffstory-1.0.0}/src/diffstory.egg-info/dependency_links.txt +0 -0
  22. {diffstory-0.4.2 → diffstory-1.0.0}/src/diffstory.egg-info/entry_points.txt +0 -0
  23. {diffstory-0.4.2 → diffstory-1.0.0}/src/diffstory.egg-info/requires.txt +0 -0
  24. {diffstory-0.4.2 → diffstory-1.0.0}/src/diffstory.egg-info/top_level.txt +0 -0
@@ -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.4.2
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "diffstory"
7
- version = "0.4.2"
7
+ version = "1.0.0"
8
8
  description = "Transform Git diffs into rich, interactive, self-contained HTML reports"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -1,3 +1,3 @@
1
1
  """DiffStory — Transform Git diffs into rich, interactive HTML reports."""
2
2
 
3
- __version__ = "0.4.2"
3
+ __version__ = "1.0.0"
@@ -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
- If the given path is the default and we're inside a git repo,
317
- place it in a 'stories/' directory outside the git working tree
318
- so that git does not track it.
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 path for the default, put it outside the git repo
352
- output_path = _resolve_output_path(args.output)
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(files, output_path=output_path, repo_name="diff", verbose=verbose)
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:
@@ -0,0 +1,2 @@
1
+ from .get_css import _get_css
2
+ from .get_js import _get_javascript