feat: settle ambiguity by file line shift

This commit is contained in:
Abdessamad Derraz committed 2026-08-08 12:21:13 +02:00
1 parent 9982748e49
commit 6b0306b43d
2 files changed
+112 -5

No files matched your search

+61 -4
View File
@@ -36,7 +36,7 @@ STATUS_ORDER = ("ANCHORED", "SHIFTED", "RENAMED", "AMBIGUOUS", "CHANGED", "GONE"
REVIEW_STATUSES = ("CHANGED", "GONE", "AMBIGUOUS")
REBASE_STATUSES = ("SHIFTED", "RENAMED")
WIDEN_STEPS = (0, 3, 6, 12)
WIDEN_STEPS = (0, 3, 6, 12, 25, 50)
MAX_MATCH_LINES = 20000
PIN = "pin"
@@ -381,6 +381,57 @@ def anchor_part(part: RefPart, fetch, rename_getter, describe=None) -> PartResul
)
SHIFT_SUPPORT = 3
def dominant_shifts(parts, support: int = SHIFT_SUPPORT) -> dict[str, int]:
"""Most common line shift per file, taken from the unambiguous anchors.
A file whose refs mostly moved by the same amount gives strong evidence
for the ones that stayed ambiguous: repetitive tables, as in the NeoGeo
driver, match a cited line in several places, and only one of them sits
where the rest of the file moved to.
"""
by_file: dict[str, dict[int, int]] = {}
for part in parts:
if part.status not in ("ANCHORED", "SHIFTED"):
continue
if part.start is None or part.part.start is None:
continue
counts = by_file.setdefault(part.part.path, {})
delta = part.start - part.part.start
counts[delta] = counts.get(delta, 0) + 1
shifts = {}
for path, counts in by_file.items():
delta, seen = max(counts.items(), key=lambda kv: kv[1])
if seen >= support:
shifts[path] = delta
return shifts
def resolve_by_shift(part: PartResult, shifts: dict[str, int]) -> PartResult:
"""Settle an ambiguous part when one candidate matches the file's shift."""
if part.status != "AMBIGUOUS" or not part.candidates:
return part
if part.part.start is None or part.part.path not in shifts:
return part
wanted = part.part.start + shifts[part.part.path]
if part.candidates.count(wanted) != 1:
return part
span = (part.part.end or part.part.start) - part.part.start
return PartResult(
part.part,
"ANCHORED" if wanted == part.part.start else "SHIFTED",
part.new_path,
wanted,
wanted + span,
[],
f"settled by the {shifts[part.part.path]:+d} line shift of its file",
part.repo,
part.head_url,
)
@dataclass
class EntryReport:
name: str
@@ -576,11 +627,17 @@ def build_report(
slug = view.repo.slug if view is not primary else None
return slug, upstream.raw_url(view.repo, view.head, actual), actual
for entry_name, ref in refs:
parts = [
staged = [
(entry_name, ref, [
anchor_part(part, fetch, rename_getter, describe)
for part in split_source_ref(ref)
]
])
for entry_name, ref in refs
]
shifts = dominant_shifts([p for _, _, parts in staged for p in parts])
for entry_name, ref, parts in staged:
parts = [resolve_by_shift(p, shifts) for p in parts]
status = worst_status([p.status for p in parts])
report.entries.append(EntryReport(entry_name, ref, status, parts))
report.counts[status] = report.counts.get(status, 0) + 1
+51 -1
View File
@@ -244,7 +244,7 @@ class TestAnchorBlock(unittest.TestCase):
self.assertEqual((result.status, result.start), ("SHIFTED", 6))
def test_ambiguous_when_widening_never_resolves(self):
result = anchor_block(["dup"] * 40, ["dup"] * 40, 5, 5)
result = anchor_block(["dup"] * 400, ["dup"] * 400, 5, 5)
self.assertEqual(result.status, "AMBIGUOUS")
self.assertGreater(len(result.candidates), 1)
@@ -275,6 +275,56 @@ class TestAnchorBlock(unittest.TestCase):
self.assertIsNone(result.start)
def _part(path, old, new, status):
return PartResult(RefPart(path, old, old), status, None, new, new, [])
def _ambiguous(path, old, candidates):
return PartResult(RefPart(path, old, old), "AMBIGUOUS", None, None, None, candidates)
class TestDominantShift(unittest.TestCase):
def test_shift_needs_enough_support(self):
parts = [_part("a.c", 10, 30, "SHIFTED"), _part("a.c", 20, 40, "SHIFTED")]
self.assertEqual(profile_sync.dominant_shifts(parts), {})
def test_shift_is_taken_from_resolved_anchors(self):
parts = [_part("a.c", i, i + 20, "SHIFTED") for i in (10, 20, 30)]
self.assertEqual(profile_sync.dominant_shifts(parts), {"a.c": 20})
def test_ambiguous_parts_do_not_vote(self):
parts = [_part("a.c", i, i + 20, "SHIFTED") for i in (10, 20, 30)]
parts.append(_ambiguous("a.c", 40, [99, 500]))
self.assertEqual(profile_sync.dominant_shifts(parts), {"a.c": 20})
def test_candidate_matching_the_shift_is_selected(self):
part = _ambiguous("a.c", 1624, [1644, 1790, 1834])
settled = profile_sync.resolve_by_shift(part, {"a.c": 20})
self.assertEqual(settled.status, "SHIFTED")
self.assertEqual(settled.start, 1644)
self.assertIn("+20", settled.reason)
def test_no_candidate_matches_the_shift(self):
part = _ambiguous("a.c", 1624, [1790, 1834])
self.assertEqual(profile_sync.resolve_by_shift(part, {"a.c": 20}).status,
"AMBIGUOUS")
def test_two_candidates_on_the_shift_stay_ambiguous(self):
part = _ambiguous("a.c", 1624, [1644, 1644])
self.assertEqual(profile_sync.resolve_by_shift(part, {"a.c": 20}).status,
"AMBIGUOUS")
def test_range_length_is_preserved(self):
part = PartResult(RefPart("a.c", 100, 104), "AMBIGUOUS", None, None, None, [120])
settled = profile_sync.resolve_by_shift(part, {"a.c": 20})
self.assertEqual((settled.start, settled.end), (120, 124))
def test_file_without_a_known_shift_is_untouched(self):
part = _ambiguous("b.c", 10, [30])
self.assertEqual(profile_sync.resolve_by_shift(part, {"a.c": 20}).status,
"AMBIGUOUS")
PIN = profile_sync.PIN
HEAD = profile_sync.HEAD