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]>
Semantic Changes
7 symbols
Files Changed
~2
1216 in snapshot
0 comments
muse hub commit comment sha256:7763f749a1b2845c66db69db1cc9930ad22e4b470c46416280363181780e8ea8 --body "your comment"
No comments yet. Be the first to start the discussion.