gabriel / muse public
fix patch fix/84-blame-merge-commit-misattribution #1 / 1
AI Agent gabriel · 8 hours ago · Oct 21, 2026 · Diff

fix: muse blame no longer attributes lines to commits that never touched the file (muse#84)

Root cause: blame_file() walked iter_ancestors()'s flattened BFS order over the full commit DAG and diffed ADJACENT entries of that list as if they were real parent/child pairs. That's only true for a linear chain -- the moment history has a merge commit, a commit's BFS-neighbor is frequently some unrelated commit reached via a completely different branch, not its real parent. Confirmed exactly: the ticket's blamed commit 659f09b2 has real parent 9a6e44e8 (content-identical -- the commit provably never touched the file), but iter_ancestors's BFS order placed an unrelated commit (6d327a10) immediately after it in the flattened list. blame_file compared 659f09b2 against that unrelated neighbor, found a content difference, and permanently misattributed a line to it -- never discovering that walking 659f09b2's REAL parent would have shown the file unchanged and continued the search further back.

Fix: blame_file() now does a proper DAG walk using each commit's real parent_commit_id / parent2_commit_id, in reverse-topological order (a commit's own outgoing edges are only explored once every real child of it, within the search's ancestor set, has already had a chance to push attribution into it). This guarantees every content comparison uses a genuine commit-graph edge -- never an arbitrary BFS-neighbor pair -- and that each commit is visited exactly once with complete information, regardless of how many branches converge on it. _walk_ancestry (still delegating to iter_ancestors, per the existing test_walk_ancestry_delegates_to_iter_ancestors guard) is now used only to compute the ancestor *set* for DAG-reachability membership checks, not as an ordered adjacency list.

Preserved the existing performance optimization (test_blame_skips_read_ for_unchanged_commits): object_id equality is checked first and moves attribution back without ever reading file bytes; actual line content is only read lazily, exactly when a real content diff against a specific real parent is unavoidable.

New regression test (test_blame_never_attributes_to_commit_that_never_ touched_file) reproduces the ticket's exact DAG shape: base -> feature (the only commit that changes the file) -> merge (base-side unrelated commit + feature, content identical to feature, never touches the file) -> final (also untouched). Confirmed genuine RED: the old algorithm attributed a line to the merge commit itself, exactly mirroring the ticket's symptom of attributing to a commit absent from files_modified with byte-identical content to its parent.

Live-reproduced the ticket's exact repro against musehub's real history: `muse blame src/scss/_layout.scss --range 38-41` previously attributed line 38 to 659f09b2 (files_modified excludes the file; content byte-identical to its real parent). Now attributes line 38 to fbd6a93ed2655 and lines 39-41 to c1d980625041d -- confirmed both commits genuinely differ from their respective real parents for this exact file. Runtime improved slightly (3.45s vs 4.2s original) despite the extra correctness work, likely because the DAG-correct walk no longer wastes time comparing against unrelated BFS neighbors whose diffs can't possibly produce useful attribution.

Targeted regression suite -- test_core_blame.py (15, including the new test), test_cmd_blame.py (the only direct caller of blame_file per muse code impact) -- all pass (78 passed), zero regressions.

Co-Authored-By: Claude Sonnet 5 <[email protected]>

sha256:7763f749a1b2845c66db69db1cc9930ad22e4b470c46416280363181780e8ea8 sha
+3 ~4 symbols
2 changed · 1216 in snapshot files
sha256:81083a751d2a4729bc8fa31e9829691a16729c7cade31e7a76f3b7b53ec93040 snapshot
+3
symbols added
~4
symbols modified
2
files changed
1216
files in snapshot
0
dead code introduced
Semantic Changes 7 symbols
~ muse/core/blame.py .py 2 symbols added, 3 symbols modified
+ CommitRecord import import CommitRecord L40–40
+ deque import import deque L35–35
~ tests/test_core_blame.py .py 1 symbol added, 1 symbol modified
+ test_blame_never_attributes_to_commit_that_never_touched_file function function test_blame_never_attributes_to_commit_that_never_touched_file L211–276
Files Changed
~2
1216 in snapshot
← Older Oldest on fix/84-blame-merge-commit-misattribution
All commits
Newer → Latest on fix/84-blame-merge-commit-misattribution

0 comments

No comments yet. Be the first to start the discussion.

To add a comment, use the Muse CLI: muse hub commit comment sha256:7763f749a1b2845c66db69db1cc9930ad22e4b470c46416280363181780e8ea8 --body "your comment"