Skip to content

Commit 476ad78

Browse files
committed
fix(blame): Normalize and deduplicate commit links
Normalize stored commit SHAs before resolving blame links and emit one annotation per canonical commit and decision pair.
1 parent 29fa8f6 commit 476ad78

2 files changed

Lines changed: 33 additions & 4 deletions

File tree

src/entirecontext/core/blame_decisions.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,14 +61,15 @@ def _rejected_count(raw: str | None) -> int:
6161

6262

6363
def _resolve_blamed_sha(repo_path: str, stored_sha: str, blamed_shas: set[str]) -> str | None:
64-
if stored_sha in blamed_shas:
65-
return stored_sha
66-
if not re.fullmatch(r"[0-9a-f]{4,63}", stored_sha):
64+
normalized_sha = stored_sha.lower()
65+
if normalized_sha in blamed_shas:
66+
return normalized_sha
67+
if not re.fullmatch(r"[0-9a-f]{4,63}", normalized_sha):
6768
return None
6869

6970
try:
7071
result = subprocess.run(
71-
["git", "rev-parse", "--verify", f"{stored_sha}^{{commit}}"],
72+
["git", "rev-parse", "--verify", f"{normalized_sha}^{{commit}}"],
7273
cwd=repo_path,
7374
capture_output=True,
7475
text=True,
@@ -139,6 +140,7 @@ def annotate_file(
139140
).fetchall()
140141
blamed_sha_set = set(shas)
141142
resolved_links: dict[str, str | None] = {}
143+
annotation_keys: set[tuple[str, str]] = set()
142144
for row in rows:
143145
stored_sha = row["commit_sha"]
144146
if stored_sha not in resolved_links:
@@ -147,6 +149,10 @@ def annotate_file(
147149
if resolved_sha is None:
148150
continue
149151
annotated_shas.add(resolved_sha)
152+
annotation_key = (resolved_sha, row["id"])
153+
if annotation_key in annotation_keys:
154+
continue
155+
annotation_keys.add(annotation_key)
150156
rationale = row["rationale"]
151157
annotations.append(
152158
BlameAnnotation(

tests/test_blame_decisions.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,29 @@ def test_abbreviated_commit_link_resolves_to_full_blame_sha(self, ec_repo, ec_db
7878
assert result["annotations"][0].commit_sha == full_sha
7979
assert result["annotations"][0].decision_id == decision["id"]
8080

81+
def test_uppercase_abbreviated_commit_link_is_normalized(self, ec_repo, ec_db):
82+
full_sha = _commit(ec_repo, "uppercase.py", "line1\n", "commit with uppercase link")
83+
decision = create_decision(ec_db, title="Uppercase SHA decision")
84+
link_decision_to_commit(ec_db, decision["id"], full_sha[:8].upper())
85+
86+
result = annotate_file(ec_db, str(ec_repo), "uppercase.py")
87+
88+
assert result["annotated_sha_count"] == 1
89+
assert result["annotations"][0].commit_sha == full_sha
90+
assert result["annotations"][0].decision_id == decision["id"]
91+
92+
def test_equivalent_full_and_abbreviated_links_are_deduplicated(self, ec_repo, ec_db):
93+
full_sha = _commit(ec_repo, "duplicate.py", "line1\n", "commit with duplicate links")
94+
decision = create_decision(ec_db, title="One decision, two link forms")
95+
link_decision_to_commit(ec_db, decision["id"], full_sha)
96+
link_decision_to_commit(ec_db, decision["id"], full_sha[:8])
97+
98+
result = annotate_file(ec_db, str(ec_repo), "duplicate.py")
99+
100+
assert result["annotated_sha_count"] == 1
101+
assert len(result["annotations"]) == 1
102+
assert result["annotations"][0].decision_id == decision["id"]
103+
81104
def test_happy_single_decision(self, ec_repo, ec_db):
82105
sha1 = _commit(ec_repo, "foo.py", "line1\nline2\n", "commit 1")
83106
_commit(ec_repo, "foo.py", "line1\nline2\nline3\nline4\n", "commit 2")

0 commit comments

Comments
 (0)