From a3c853316a859525a5a5e29fb267fd44e2f03ec4 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Thu, 5 Nov 2026 19:27:12 +0100 Subject: [PATCH] fix: name the dirty profiles profile_sync refuses The refusal said the directory carried uncommitted changes without naming the profile, among about 375. It now lists the files git status reports, and says that --force writes over them. --- scripts/profile_sync.py | 20 +++++++++++++++----- tests/test_profile_sync.py | 18 ++++++++++++++++++ 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/scripts/profile_sync.py b/scripts/profile_sync.py index 4a911a71d..1d2b81f38 100644 --- a/scripts/profile_sync.py +++ b/scripts/profile_sync.py @@ -4154,16 +4154,22 @@ def _refuse_dirty_tree(emulators_dir: str, names: list[str]) -> None: print(f"{e}. Pass --force to write anyway.", file=sys.stderr) raise SystemExit(1) from e if dirty: + # Another session may own the change, so the refusal names the files: + # --force writes over them. + one = len(dirty) == 1 print( - f"{emulators_dir} carries uncommitted changes to the selected " - "profiles. Commit them first or pass --force.", + f"{', '.join(dirty)} {'has' if one else 'have'} uncommitted changes: " + f"commit {'it' if one else 'them'} or pass --force, which writes over " + f"{'it' if one else 'them'}.", file=sys.stderr, ) raise SystemExit(1) -def emulators_dir_is_dirty(emulators_dir: str, names: list[str] | None = None) -> bool: - """True when the profile directory carries uncommitted changes. +def emulators_dir_is_dirty(emulators_dir: str, names: list[str] | None = None) -> list[str]: + """The files of the profile directory that carry uncommitted changes. + + Paths read as the status prints them, from the repository root. ``names`` narrows the question to those profiles. Git is asked from inside the directory, so a profile tree that lives in another @@ -4181,7 +4187,11 @@ def emulators_dir_is_dirty(emulators_dir: str, names: list[str] | None = None) - raise RuntimeError( f"cannot read the git status of {emulators_dir}: {result.stderr.strip()}" ) - return bool(result.stdout.strip()) + return sorted( + line[3:].split(" -> ")[-1].strip('"') + for line in result.stdout.splitlines() + if line.strip() + ) def build_parser() -> argparse.ArgumentParser: diff --git a/tests/test_profile_sync.py b/tests/test_profile_sync.py index 0fef264af..a9d71c062 100644 --- a/tests/test_profile_sync.py +++ b/tests/test_profile_sync.py @@ -5303,6 +5303,24 @@ class TestEmulatorsDirStatus(unittest.TestCase): self.assertTrue(profile_sync.emulators_dir_is_dirty(root, ["a"])) self.assertFalse(profile_sync.emulators_dir_is_dirty(root, ["b"])) + def test_the_refusal_names_the_changed_profile(self): + """One profile changed by another session among ~375 refused the + whole run without saying which.""" + with tempfile.TemporaryDirectory() as root: + self._git("init", "-q", cwd=root) + for name in "abc": + Path(root, f"{name}.yml").write_text(f"emulator: {name}\n") + self._git("add", "a.yml", "b.yml", "c.yml", cwd=root) + self._git("-c", "user.name=t", "-c", "user.email=t@t", "commit", "-qm", "p", cwd=root) + Path(root, "b.yml").write_text("emulator: b2\n") + self.assertEqual(profile_sync.emulators_dir_is_dirty(root, ["a", "b", "c"]), + ["b.yml"]) + err = io.StringIO() + with contextlib.redirect_stderr(err), self.assertRaises(SystemExit): + profile_sync._refuse_dirty_tree(root, ["a", "b", "c"]) + self.assertIn("b.yml has uncommitted changes", err.getvalue()) + self.assertNotIn("a.yml", err.getvalue()) + 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)}