diff --git a/scripts/profile_sync.py b/scripts/profile_sync.py index 385ca2a4..402fab8e 100644 --- a/scripts/profile_sync.py +++ b/scripts/profile_sync.py @@ -1230,6 +1230,7 @@ def build_report( owners: dict[tuple[str, int | None], tuple[RepoView, str]] = {} context: dict[str, object] = {} + pin_trees: dict[str, list[str] | None] = {} def _locate( path: str, start: int | None = None, tokens: tuple = () @@ -1251,20 +1252,27 @@ def build_report( if tail and any(head == v.repo.name for v in views): candidates.append(tail) if "/" not in path: - # A bare filename, the way prose cites files. The HEAD tree of - # each repository resolves it when exactly one path carries it. + # A bare filename, the way prose cites files. Resolved against the + # pin before HEAD: the ref was written at the pin, and a file that + # moved since would otherwise be resolved to a HEAD path that does + # not exist at the pin, reporting GONE for the one reason it never + # should. The rename search then carries the pin path to HEAD. for view in views: - _, tree = _context_for(view) - matches = [ - p for p in tree or [] - if posixpath.basename(p) == path - ] - if len(matches) > 1: - matches = narrow_by_cited(matches, cited_dirs) - # A few survivors are told apart by the cited line below: a - # 266-line Windows configure.ac cannot carry line 754. - if len(matches) <= 4: - candidates.extend(m for m in matches if m not in candidates) + for tree in (_pin_tree_for(view), _context_for(view)[1]): + matches = [ + p for p in tree or [] + if posixpath.basename(p) == path + ] + if len(matches) > 1: + matches = narrow_by_cited(matches, cited_dirs) + # A few survivors are told apart by the cited line below: a + # 266-line Windows configure.ac cannot carry line 754. + if len(matches) <= 4: + candidates.extend( + m for m in matches if m not in candidates + ) + if matches: + break elif not any( upstream.fetch_file(v.repo, sha, path, cache_dir, offline) is not None for v in views for sha in (v.pin, v.head) @@ -1339,6 +1347,14 @@ def build_report( owners[key] = _locate(path, start, tokens) return owners[key] + def _pin_tree_for(view: RepoView): + """Tree at the revision the refs were written against, memoised.""" + key = view.repo.slug + if key not in pin_trees: + tree, _ = upstream.list_tree(view.repo, view.pin, cache_dir, offline) + pin_trees[key] = tree + return pin_trees[key] + def _context_for(view: RepoView): key = view.repo.slug if key not in context: diff --git a/tests/test_profile_sync.py b/tests/test_profile_sync.py index e69c6079..58dd5405 100644 --- a/tests/test_profile_sync.py +++ b/tests/test_profile_sync.py @@ -1367,6 +1367,47 @@ def _sample_report(): ) +class TestBareNameResolvedAtThePin(TestBuildReport): + """A bare filename belongs to the revision the ref was written against. + + Resolution searched the HEAD tree only, so a file that moved between the + pin and HEAD was resolved to its HEAD path and then read at the pin, + where that path does not exist yet: the ref reported GONE for the one + reason it should never report, its own success at HEAD. linapple cites + Memory.cpp, src/Memory.cpp at its pin and src/apple2/Memory.cpp today. + """ + + def _tree_per_revision(self): + def list_tree(repo, sha, cache_dir, offline=False): + return sorted({path for rev, path in self.files if rev == sha}), False + + profile_sync.upstream.list_tree = list_tree + + def test_a_bare_name_follows_the_file_that_moved_after_the_pin(self): + self._tree_per_revision() + self.files[("pinsha", "src/Memory.cpp")] = ["x", "the cited line", "y"] + self.files[("headsha", "src/apple2/Memory.cpp")] = [ + "pad", "x", "the cited line", "y", + ] + report = build_report("test", self._profile(["Memory.cpp:2"]), self.dir) + part = report.entries[0].parts[0] + self.assertNotEqual( + part.status, "GONE", + "the bare name resolved at HEAD and was then read at the pin", + ) + self.assertEqual(part.new_path, "src/apple2/Memory.cpp") + + def test_a_bare_name_present_at_both_revisions_resolves_once(self): + """A resolved bare name reads RENAMED so the recale writes the path.""" + self._tree_per_revision() + self.files[("pinsha", "src/New.cpp")] = ["x", "the cited line"] + self.files[("headsha", "src/New.cpp")] = ["x", "the cited line"] + report = build_report("test", self._profile(["New.cpp:2"]), self.dir) + part = report.entries[0].parts[0] + self.assertEqual(part.status, "RENAMED") + self.assertEqual(part.new_path, "src/New.cpp") + + class TestFormatReport(unittest.TestCase): def test_header_shows_both_revisions(self): text = format_report(_sample_report())