diffstory 0.3.0__tar.gz → 0.4.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: diffstory
3
- Version: 0.3.0
3
+ Version: 0.4.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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "diffstory"
7
- version = "0.3.0"
7
+ version = "0.4.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.3.0"
3
+ __version__ = "0.4.0"
@@ -243,6 +243,9 @@ def get_commit_info(commit_hash: str, cwd: Optional[Path] = None) -> dict:
243
243
  Returns dict with: hash, author, author_email, author_date,
244
244
  committer, committer_email, committer_date, subject, body,
245
245
  parents, files_changed, insertions, deletions.
246
+
247
+ Handles root commits (no parent), merge commits, and
248
+ the all-zero hash (uncommitted/staged changes).
246
249
  """
247
250
  # Handle all-zero hash (uncommitted/staged changes)
248
251
  if all(c == "0" for c in commit_hash):
@@ -278,16 +281,49 @@ def get_commit_info(commit_hash: str, cwd: Optional[Path] = None) -> dict:
278
281
  if len(parts) < 9:
279
282
  return {"hash": commit_hash, "subject": "unknown"}
280
283
 
284
+ parents_list = parts[7].split() if parts[7] else []
285
+
281
286
  # Count files changed, insertions, deletions (skip for all-zero/uncommitted)
282
287
  files_changed = 0
283
288
  insertions = 0
284
289
  deletions = 0
285
290
  if not all(c == "0" for c in commit_hash):
286
291
  try:
287
- stat_output = _run_git(
288
- ["diff", "--stat", f"{commit_hash}~1..{commit_hash}", "--"],
289
- cwd=cwd,
290
- )
292
+ if parents_list:
293
+ # Has parents — use diff with first parent
294
+ parent_ref = parents_list[0]
295
+ stat_output = _run_git(
296
+ ["diff", "--stat", f"{parent_ref}..{commit_hash}", "--"],
297
+ cwd=cwd,
298
+ )
299
+ else:
300
+ # Root commit — count files via log --name-status
301
+ stat_output = _run_git(
302
+ ["log", "-1", "--format=", "--name-status", commit_hash],
303
+ cwd=cwd,
304
+ )
305
+ # Parse added files from root commit
306
+ for line in stat_output.strip().splitlines():
307
+ if line.strip():
308
+ files_changed += 1
309
+ if line.startswith("A"):
310
+ insertions += 1 # approximate
311
+ return {
312
+ "hash": parts[0],
313
+ "author": parts[1],
314
+ "author_email": parts[2],
315
+ "author_date": parts[3],
316
+ "committer": parts[4],
317
+ "committer_email": parts[5],
318
+ "committer_date": parts[6],
319
+ "parents": parents_list,
320
+ "subject": parts[8],
321
+ "body": body.strip(),
322
+ "files_changed": files_changed,
323
+ "insertions": insertions,
324
+ "deletions": deletions,
325
+ }
326
+
291
327
  stat_lines = stat_output.strip().split("\n") if stat_output.strip() else []
292
328
  for line in stat_lines:
293
329
  m = re.search(r"(\d+) file[s]? changed", line)
@@ -310,7 +346,7 @@ def get_commit_info(commit_hash: str, cwd: Optional[Path] = None) -> dict:
310
346
  "committer": parts[4],
311
347
  "committer_email": parts[5],
312
348
  "committer_date": parts[6],
313
- "parents": parts[7].split() if parts[7] else [],
349
+ "parents": parents_list,
314
350
  "subject": parts[8],
315
351
  "body": body.strip(),
316
352
  "files_changed": files_changed,
@@ -356,6 +356,7 @@ def _collect_blame_data(
356
356
  staged: bool = False,
357
357
  commit_a: Optional[str] = None,
358
358
  commit_b: Optional[str] = None,
359
+ verbose: bool = False,
359
360
  ) -> dict:
360
361
  """Collect blame and commit metadata for all changed lines.
361
362
 
@@ -364,15 +365,12 @@ def _collect_blame_data(
364
365
  - commits: dict mapping commit_hash to commit metadata
365
366
 
366
367
  Handles renamed files by using the old path for deletion blame
367
- and the new path for addition/context blame. Skips blame when
368
- no revision info is available (e.g. --diff mode).
368
+ and the new path for addition/context blame. Falls back to
369
+ blaming the working tree when no revision info is available.
369
370
  """
370
- # If no commit info at all, skip blame entirely (e.g. --diff mode)
371
- if not staged and commit_a is None and commit_b is None:
372
- return {"line_blame": {}, "commits": {}}
373
-
374
371
  line_blame: dict = {}
375
372
  all_commits: set = set()
373
+ blame_attempted = False
376
374
 
377
375
  for fi, file in enumerate(files):
378
376
  new_filepath = file.display_path
@@ -394,55 +392,100 @@ def _collect_blame_data(
394
392
  elif staged:
395
393
  old_revision = "HEAD"
396
394
 
395
+ # File exists — attempt blame. For working tree (all None), get_blame
396
+ # runs on the working tree. For --diff mode (no git repo), the per-file
397
+ # try/except catches the GitError gracefully.
398
+ blame_attempted = True
399
+
400
+ if verbose:
401
+ new_rev_str = str(new_revision) if new_revision else "working tree"
402
+ old_rev_str = str(old_revision) if old_revision else "working tree"
403
+ print(f" Blaming {file.display_path} (new: {new_rev_str}, old: {old_rev_str})...")
404
+
397
405
  # Get blame for current (new) version — skip if file doesn't exist at revision
398
406
  blame_new: dict = {}
407
+ new_blame_ok = False
399
408
  if new_filepath != "/dev/null":
400
409
  try:
401
410
  blame_new = get_blame_for_revision(new_filepath, revision=new_revision)
402
- except Exception:
403
- pass
404
-
405
- # Get blame for old version if different from new (e.g. renames)
411
+ new_blame_ok = True
412
+ if verbose:
413
+ print(f" Got {len(blame_new)} blame entries for new version")
414
+ except Exception as e:
415
+ if verbose:
416
+ print(f" Warning: could not get blame for {new_filepath} at {new_revision}: {e}")
417
+
418
+ # Get blame for old version if different from new (e.g. renames, different revision)
406
419
  blame_old: dict = blame_new
420
+ old_blame_ok = new_blame_ok
407
421
  if old_revision and old_filepath != new_filepath:
408
422
  try:
409
423
  blame_old = get_blame_for_revision(old_filepath, revision=old_revision)
410
- except Exception:
424
+ old_blame_ok = True
425
+ if verbose:
426
+ print(f" Got {len(blame_old)} blame entries for old version (renamed path)")
427
+ except Exception as e:
411
428
  blame_old = {}
429
+ old_blame_ok = False
430
+ if verbose:
431
+ print(f" Warning: could not get blame for {old_filepath} at {old_revision}: {e}")
412
432
  elif old_revision and old_filepath == new_filepath and old_revision != new_revision:
413
433
  # Same path but different revision — re-blame
414
434
  try:
415
435
  blame_old = get_blame_for_revision(old_filepath, revision=old_revision)
416
- except Exception:
417
- pass
436
+ old_blame_ok = True
437
+ if verbose:
438
+ print(f" Got {len(blame_old)} blame entries for old version (different revision)")
439
+ except Exception as e:
440
+ old_blame_ok = new_blame_ok
441
+ if verbose:
442
+ print(f" Warning: could not get blame for {old_filepath} at {old_revision}: {e}")
418
443
 
419
444
  # Map blame by line number for additions and context
445
+ mapped_count = 0
420
446
  for hunk in file.hunks:
421
447
  for line in hunk.lines:
422
448
  if line.line_type in ("addition", "context") and line.new_lineno:
423
- entry = blame_new.get(line.new_lineno)
449
+ entry = blame_new.get(line.new_lineno) if new_blame_ok else None
424
450
  if entry:
425
451
  key = str(fi) + ":" + str(line.new_lineno)
426
452
  line_blame[key] = entry
427
453
  all_commits.add(entry["commit"])
454
+ mapped_count += 1
428
455
 
429
456
  elif line.line_type == "deletion" and line.old_lineno:
430
- if blame_old:
457
+ if old_blame_ok:
431
458
  entry = blame_old.get(line.old_lineno)
432
459
  if entry:
433
460
  key = str(fi) + ":" + str(line.old_lineno)
434
461
  line_blame[key] = entry
435
462
  all_commits.add(entry["commit"])
463
+ mapped_count += 1
464
+
465
+ if verbose:
466
+ print(f" Mapped {mapped_count} lines to blame data")
436
467
 
437
468
  # Collect commit metadata for all unique commits
438
469
  commits: dict = {}
439
- for chash in all_commits:
440
- if chash and len(chash) == 40:
441
- try:
442
- info = get_commit_info(chash)
443
- commits[chash] = info
444
- except Exception:
445
- pass
470
+ if blame_attempted and all_commits:
471
+ if verbose:
472
+ print(f" Collecting metadata for {len(all_commits)} unique commits...")
473
+ for chash in all_commits:
474
+ if chash and len(chash) == 40:
475
+ try:
476
+ info = get_commit_info(chash)
477
+ commits[chash] = info
478
+ except Exception as e:
479
+ if verbose:
480
+ print(f" Warning: could not get commit info for {chash[:8]}: {e}")
481
+
482
+ # Warn if blame was attempted but produced no data at all
483
+ if blame_attempted and not line_blame:
484
+ import sys as _sys
485
+ _sys.stderr.write(
486
+ "Warning: git blame could not collect data for any files. "
487
+ "Try --verbose for details.\n"
488
+ )
446
489
 
447
490
  return {
448
491
  "line_blame": line_blame,
@@ -500,10 +543,19 @@ def generate_report(
500
543
 
501
544
  # Collect blame data
502
545
  try:
503
- blame_data_dict = _collect_blame_data(files, staged=staged, commit_a=commit_a, commit_b=commit_b)
504
- except Exception:
546
+ blame_data_dict = _collect_blame_data(
547
+ files, staged=staged, commit_a=commit_a, commit_b=commit_b, verbose=verbose,
548
+ )
549
+ except Exception as e:
550
+ if verbose:
551
+ print(f" Warning: blame collection failed: {e}")
505
552
  blame_data_dict = {"line_blame": {}, "commits": {}}
506
553
 
554
+ blame_line_count = len(blame_data_dict["line_blame"])
555
+ blame_commit_count = len(blame_data_dict["commits"])
556
+ if verbose:
557
+ print(f" Blame data: {blame_line_count} lines mapped, {blame_commit_count} commits")
558
+
507
559
  stats = _compute_stats(files, blame_data=blame_data_dict["line_blame"], commits_data=blame_data_dict["commits"])
508
560
 
509
561
  # Collect search data
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: diffstory
3
- Version: 0.3.0
3
+ Version: 0.4.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
File without changes
File without changes