diff --git a/scripts/profile_sync.py b/scripts/profile_sync.py index 70f3b7f2..c4a3247b 100644 --- a/scripts/profile_sync.py +++ b/scripts/profile_sync.py @@ -2819,13 +2819,22 @@ def realign_prose( def emulators_dir_is_dirty(emulators_dir: str) -> bool: - """True when the profile directory carries uncommitted changes.""" + """True when the profile directory carries uncommitted changes. + + Git is asked from inside the directory, so a profile tree that lives in + another repository answers for itself. A directory git cannot read is + never reported clean: the writes have nothing to roll back to. + """ result = subprocess.run( - ["git", "status", "--porcelain", "--", emulators_dir], + ["git", "-C", emulators_dir, "status", "--porcelain", "--", "."], capture_output=True, text=True, check=False, ) + if result.returncode: + raise RuntimeError( + f"cannot read the git status of {emulators_dir}: {result.stderr.strip()}" + ) return bool(result.stdout.strip()) @@ -3218,18 +3227,19 @@ def main() -> None: args.backfill_commits or args.rebase_refs or args.bump_commit or args.realign_prose ) - if ( - writes - and not args.dry_run - and not args.force - and emulators_dir_is_dirty(args.emulators_dir) - ): - print( - f"{args.emulators_dir} carries uncommitted changes. " - "Commit them first or pass --force.", - file=sys.stderr, - ) - raise SystemExit(1) + if writes and not args.dry_run and not args.force: + try: + dirty = emulators_dir_is_dirty(args.emulators_dir) + except RuntimeError as e: + print(f"{e}. Pass --force to write anyway.", file=sys.stderr) + raise SystemExit(1) from e + if dirty: + print( + f"{args.emulators_dir} carries uncommitted changes. " + "Commit them first or pass --force.", + file=sys.stderr, + ) + raise SystemExit(1) if args.realign_prose: for name in selected: diff --git a/tests/test_profile_sync.py b/tests/test_profile_sync.py index d47b7383..d18c2c59 100644 --- a/tests/test_profile_sync.py +++ b/tests/test_profile_sync.py @@ -3511,5 +3511,27 @@ class TestBuildReportProse(TestBuildReport): self.assertEqual(report.entries[0].kind, "prose") +class TestEmulatorsDirStatus(unittest.TestCase): + """git was asked from the current directory and its exit code ignored: + a profile tree in another repository, or in none, read as clean, and + --rebase-refs wrote over uncommitted profiles.""" + + def _git(self, *args: str, cwd: str) -> None: + subprocess.run(["git", *args], cwd=cwd, check=True, capture_output=True) + + def test_a_tree_in_another_repository_answers_for_itself(self): + with tempfile.TemporaryDirectory() as root: + self._git("init", "-q", cwd=root) + Path(root, "a.yml").write_text("emulator: a\n") + self.assertTrue(profile_sync.emulators_dir_is_dirty(root)) + + def test_a_tree_outside_any_repository_is_not_clean(self): + with tempfile.TemporaryDirectory() as root, mock.patch.dict( + os.environ, {"GIT_CEILING_DIRECTORIES": str(Path(root).parent)} + ): + with self.assertRaises(RuntimeError): + profile_sync.emulators_dir_is_dirty(root) + + if __name__ == "__main__": unittest.main()