metrics: handle distinct refs resolving to the same SHA in diff (#10429) - #11038
metrics: handle distinct refs resolving to the same SHA in diff (#10429)#11038jbbqqf wants to merge 1 commit into
Conversation
|
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #11038 +/- ##
==========================================
+ Coverage 90.68% 90.97% +0.29%
==========================================
Files 504 505 +1
Lines 39795 41152 +1357
Branches 3141 3266 +125
==========================================
+ Hits 36087 37440 +1353
- Misses 3042 3072 +30
+ Partials 666 640 -26 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…verse#10429) `dvc metrics diff --all <a> <b>` returned `{}` when `a` and `b` were distinct ref names that pointed at the same commit (e.g. `main` and `HEAD`). The brancher groups same-SHA revisions under a single comma-joined composite key (e.g. `"main,HEAD"`), but `_diff()` looked each rev up by exact key, missed the composite, and produced an empty result instead of an all-zero diff. Fall back to membership lookup when the direct key isn't present. This also implicitly fixes `dvc params diff --all <a> <b>`, which delegates to the same `_diff` helper.
117f6e7 to
299f06c
Compare
eeshsaxena
left a comment
There was a problem hiding this comment.
The diagnosis is exactly right, and I confirmed it at the source. dvc/repo/brancher.py yields the composite key here:
for sha, names in found_revs.items():
...
yield sha if sha_only else ",".join(names)so when two refs resolve to the same commit they land under a single "main,HEAD" key, and the old result.get(old_rev, {}) / result.get(new_rev, {}) both miss it and silently produce an empty diff. Falling back to a membership check against the split key restores the lookup, and the {} default preserves the previous behaviour for a genuine miss. Nice regression test too.
One edge case worth being aware of (and it's inherited rather than introduced by this PR): git allows commas in ref names. I checked:
git check-ref-format --branch 'a,b' -> accepted
git check-ref-format 'refs/heads/feat,x' -> accepted
So a branch literally named a,b is emitted by brancher as the key "a,b" - indistinguishable from the composite key for two refs named a and b. With this change, _resolve_rev(result, "a") would split "a,b" and match, returning metrics for the unrelated a,b branch.
That ambiguity really lives in brancher's ",".join(names) (the key format is lossy), so I don't think it blocks this PR - the fix is strictly better than the current silent-empty-diff. But it might be worth either a short comment noting the assumption, or, if you want it airtight, having brancher carry the names as structured data (a tuple/list) rather than a joined string, so consumers don't have to re-parse it. That would also let _resolve_rev disappear.
Deferring to the maintainers.
Summary
dvc metrics diff --all <a> <b>returned{}when<a>and<b>were distinct ref names that resolved to the same commit (e.g.mainandHEAD). The reporter on #10429 traced this to the interaction betweendvc/repo/brancher.py(groups same-SHA revisions under a single comma-joined key like"main,HEAD") anddvc/repo/metrics/diff.py:_diff(looks each rev up by exact key, misses the composite, returns empty).This PR adds a tiny
_resolve_revhelper that falls back to membership lookup when the direct key isn't present. The same helper transparently fixesdvc params diff --all, which delegates to the same_diff.Fixes #10429 — dvc metrics diff --all: on same branch is empty
Context
dvc metrics diff --all $(git rev-parse --abbrev-ref HEAD) HEAD— both refs point at the same SHA, expected behavior is "all metrics with diff = 0.0".dvc/repo/brancher.py:101joins names of revs that resolve to the same SHA with,so the file-system swap is performed once per SHA. The metrics show result is therefore keyed by"main,HEAD". The reporter pinned this exact line in the issue body.Changes
dvc/repo/metrics/diff.py— extract a_resolve_rev(result, rev)helper that first triesresult[rev], then falls back to scanning composite keys for the rev as a comma-separated member. The 5-line in-code comment explains why the second branch is needed (brancher group-by-SHA), so a reviewer reading the diff cold doesn't have to derive the reasoning._diffswitches to_resolve_rev.tests/func/metrics/test_diff.py—test_metrics_diff_same_commit_different_refsregression: builds a single-commit repo, asks formetrics.diff(a_rev=branch, b_rev="HEAD", all=True), asserts the all-zero diff comes back instead of{}.dvc params diffis fixed transparently because it imports and calls the same_diffhelper fromdvc/repo/metrics/diff.py.Reproduce BEFORE/AFTER yourself (copy-paste)
What I ran locally
pytest tests/func/metrics/ tests/func/params/→ 78 passed, 2 skipped (pre-existing skips). 21.85s. Includes the new regression and all existing diff tests on bothmetrics.diffandparams.diff.git stash -- dvc/repo/metrics/diff.pythen rerun the new test → fails with the assertion error confirming the bug is exercised.ruff check dvc/repo/metrics/diff.py tests/func/metrics/test_diff.py→ all checks passed.ruff format→ applied to test file (formatter combined wrapped args).Edge cases tested
a_rev=branch_name,b_rev="HEAD",all=Truetest_metrics_diff_same_commit_different_refs(new)a_rev="HEAD~2",b_rev=workspacetest_metrics_diff_simple,test_metrics_diff_yaml,test_metrics_diff_json(existing, unchanged)a_rev=b_rev=HEADtest_metrics_diff_with_unchangedstyleparams.diffparams.diffcalls the same_diff; existing params tests pass{}test_no_commits,test_metrics_diff_no_metrics(existing, unchanged)Risk / blast radius
result[rev]is absent — i.e. when the brancher emitted a composite key. For all "normal" diffs (different SHAs) the direct lookup hits and behavior is byte-identical to before.rev in key.split(","), notrev in key— so a rev string containing commas (which is invalid as a git ref anyway) won't accidentally match.metrics.diffandparams.diffbenefit (params delegates to the same_diff). No other callers of_diff.Release note
PR drafted with assistance from Claude Code. The reporter on #10429 had already pinpointed the exact lines (
brancher.py:101group-by-SHA,metrics/diff.pyexact-key lookup) — this PR ships the minimal fix at the lookup site rather than touching the brancher contract. The reproducer block was used during development.