diff --git a/scripts/profile_sync.py b/scripts/profile_sync.py index 4a4bcf376..f0e3bf745 100644 --- a/scripts/profile_sync.py +++ b/scripts/profile_sync.py @@ -4226,6 +4226,8 @@ def _apply_writes( name, path, args, known ): return + if _gained_since_pin(args, name, profile, report): + return with tempfile.TemporaryDirectory() as scratch: # Always work on a copy. A dry run reports what it left there, a real # run promotes it. A recale rewrites the refs for HEAD, so the pin @@ -4284,6 +4286,36 @@ def _apply_writes( print(message) +def _gained_since_pin( + args: argparse.Namespace, name: str, profile: dict, report: ProfileReport +) -> bool: + """Whether the cited files gained a file name or a hash literal since the pin. + + Refs that still anchor say nothing of what the code added around them: + mame2003_plus gained two Taito G-NET BIOS sets in a zn.c whose cited + lines only shifted. A bump past that would document a file set the code + no longer has. --accept-changed says the additions were read. + """ + if args.accept_changed or not report.head or report.pin == report.head: + return False + if report.new_files is None and report.new_hashes is None: + probe = argparse.Namespace(**{**vars(args), "detect_new_files": True, "watch_hashes": True}) + _attach_detection(probe, profile, report) + gained = sorted({item["name"] for item in report.new_files or () if item.get("since_pin")}) + added = report.new_hashes or [] + if not (gained or added or report.detection_skipped): + return False + detail = ", ".join(gained[:5]) + (f", {len(added)} hash literal(s)" if added else "") + if report.detection_skipped: + detail = f"cited files unread ({report.detection_skipped})" + print( + f"{name}: the cited files changed beyond the refs since the pin ({detail.strip(', ')}), " + "nothing was written. Read them, then rerun with --accept-changed.", + file=sys.stderr, + ) + return True + + def _cited_paths(report: ProfileReport) -> list[str]: return sorted( {part.new_path or part.part.path for e in report.entries for part in e.parts} diff --git a/tests/test_profile_sync.py b/tests/test_profile_sync.py index 00ddc8c05..7b98f624c 100644 --- a/tests/test_profile_sync.py +++ b/tests/test_profile_sync.py @@ -2526,10 +2526,40 @@ class TestWriteDryRun(unittest.TestCase): # Profiles live in a repository: git answers "no history" for an # untracked file, where outside one it answers nothing at all. subprocess.run(["git", "init", "-q"], cwd=self.dir, check=True) + # The cited files gained nothing since the pin in these fixtures. + from unittest import mock # noqa: PLC0415 + + def nothing_gained(args, profile, report): + report.new_files, report.new_hashes = [], [] + + self._detection = mock.patch.object( + profile_sync, "_attach_detection", side_effect=nothing_gained + ) + self._detection.start() def tearDown(self): + self._detection.stop() self.tmp.cleanup() + def test_a_file_gained_since_the_pin_holds_the_bump(self): + # mame2003_plus's zn.c gained two G-NET BIOS sets around refs that + # only shifted. + from unittest import mock # noqa: PLC0415 + + def gained(args, profile, report): + report.new_files = [{"path": "zn.c", "name": "coh3002t.zip", "since_pin": True}] + report.new_hashes = [] + + before = self.path.read_text() + with mock.patch.object(profile_sync, "_attach_detection", side_effect=gained), \ + contextlib.redirect_stderr(io.StringIO()) as err: + self._run(self._args(rebase_refs=True, dry_run=False), self._shifted()) + self.assertEqual(self.path.read_text(), before) + self.assertIn("coh3002t.zip", err.getvalue()) + with mock.patch.object(profile_sync, "_attach_detection", side_effect=gained): + self._run(self._args(rebase_refs=True, dry_run=False, accept_changed=True), self._shifted()) + self.assertIn('source_commit: "newhead"', self.path.read_text()) + def _args(self, **over): base = dict( emulators_dir=str(self.dir), backfill_commits=False,