Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ Writing a PR description after the coding is finished is easy to postpone. diffn

## Features

### Choose your diff

Summarize tracked working-tree changes, staged changes, a base ref, or a commit range.

Expand Down
12 changes: 10 additions & 2 deletions src/diffnote/analyze.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@

CONV_RE = re.compile(r"^(feat|fix|refactor|perf|chore|docs|test|style|build|ci)"
r"(\([^)]+\))?!?:", re.I)
BREAKING_COMMIT_RE = re.compile(r"^[a-z][a-z0-9-]*(?:\([^)]+\))?!:", re.I)
PUBLIC_API_RE = re.compile(r"^(?:lib/|src/(?:[^/]+/)*api(?:/|$))", re.I)


MINUS_RE = re.compile(r"^--- ")
Expand Down Expand Up @@ -120,9 +122,11 @@ def _join_text(file):
return "\n".join(file["lines"])


def assess_risks(files):
def assess_risks(files, commit_messages=None):
"""Return list of risk dicts: {label, files:[...]}."""
risks = {}
breaking_commit = any(BREAKING_COMMIT_RE.match(message) for message in (commit_messages or []))
breaking_files = []
for f in files:
path = f["new"]
content = _join_text(f)
Expand All @@ -134,6 +138,10 @@ def assess_risks(files):
risks.setdefault("dependency change", []).append(path)
if f["added"] + f["deleted"] >= 500:
risks.setdefault("large batch change (review carefully)", []).append(path)
if breaking_commit or PUBLIC_API_RE.match(path):
breaking_files.append(path)
if breaking_files:
risks["possible breaking change"] = breaking_files
return [{"label": k, "files": v} for k, v in risks.items()]


Expand Down Expand Up @@ -167,7 +175,7 @@ def build_summary(files, commit_messages=None):
for t in _categorize(f["new"]):
areas[t] += 1

risks = assess_risks(files)
risks = assess_risks(files, commit_messages)
title = suggest_title(files, commit_messages)
section = suggest_section(files, commit_messages)

Expand Down
40 changes: 40 additions & 0 deletions tests/test_analyze.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import json
import textwrap
from diffnote import analyze
from diffnote import render

SAMPLE = textwrap.dedent(
"""
Expand Down Expand Up @@ -36,6 +38,20 @@ def f():
).strip()


def _files_for(*paths):
diff = []
for path in paths:
diff.extend([
f"diff --git a/{path} b/{path}",
f"--- a/{path}",
f"+++ b/{path}",
"@@ -1 +1 @@",
"-old value",
"+new value",
])
return analyze.parse_diff("\n".join(diff))


def test_parse_diff_counts():
files = analyze.parse_diff(SAMPLE)
assert len(files) == 4
Expand Down Expand Up @@ -67,3 +83,27 @@ def test_suggest_title_derives_migration():
files = analyze.parse_diff(SAMPLE)
s = analyze.build_summary(files, commit_messages=[])
assert "schema" in s["title"] or s["title"].startswith("feat")


def test_breaking_risk_covers_public_api_paths_only():
files = _files_for("lib/client.py", "src/app/api/routes.py", "src/app/apiary.py")
summary = analyze.build_summary(files)
risk = next(r for r in summary["risks"] if r["label"] == "possible breaking change")
assert risk["files"] == ["lib/client.py", "src/app/api/routes.py"]


def test_breaking_commit_marker_covers_all_changed_files():
files = _files_for("src/app/core.py", "README.md")
summary = analyze.build_summary(files, ["feat(api)!: remove the old response field"])
risk = next(r for r in summary["risks"] if r["label"] == "possible breaking change")
assert risk["files"] == ["src/app/core.py", "README.md"]


def test_breaking_risk_appears_in_terminal_markdown_and_json():
summary = analyze.build_summary(_files_for("src/api/client.py"), ["fix!: change public return type"])
terminal = render.render_terminal(summary, use_color=False)
markdown = render.render_markdown(summary)
machine = json.loads(render.render_json(summary))
assert "possible breaking change" in terminal
assert "possible breaking change" in markdown
assert any(risk["label"] == "possible breaking change" for risk in machine["risks"])
Loading