diff --git a/gui/main.py b/gui/main.py index a0628c0..9520733 100644 --- a/gui/main.py +++ b/gui/main.py @@ -131,6 +131,20 @@ class UpdateThread(QThread): return False return True + @classmethod + def should_install_to_root(cls, rel: Path, root: Path) -> bool: + """Return True when *rel* may be copied into the selected install root. + + Git expands ``.git_archival.txt`` while creating a source archive. Keep + that expanded file in archive-only installs, but do not copy it back + into a live checkout where it would dirty the worktree on every update. + """ + if not cls.should_install(rel): + return False + if rel.as_posix() == ".git_archival.txt" and (root / ".git").exists(): + return False + return True + @classmethod def resolve_archive_root(cls, extract_dir: Path) -> Path: """Locate the single top-level folder inside an extracted archive. @@ -281,7 +295,11 @@ class UpdateThread(QThread): install_files = [ src for src in extracted.rglob("*") - if src.is_file() and self.should_install(src.relative_to(extracted)) + if src.is_file() + and self.should_install_to_root( + src.relative_to(extracted), + root, + ) ] if not install_files: raise RuntimeError( diff --git a/tests/test_bundled_updates.py b/tests/test_bundled_updates.py index 3d2f30d..a0c3665 100644 --- a/tests/test_bundled_updates.py +++ b/tests/test_bundled_updates.py @@ -68,6 +68,51 @@ class UpdateThreadInstallFilterTests(unittest.TestCase): with self.subTest(rel=rel): self.assertFalse(UpdateThread.should_install(Path(rel))) + def test_preserves_archive_metadata_in_git_checkout(self): + from gui.main import UpdateThread + + with tempfile.TemporaryDirectory() as raw: + root = Path(raw) + (root / ".git").mkdir() + + self.assertFalse( + UpdateThread.should_install_to_root( + Path(".git_archival.txt"), + root, + ) + ) + self.assertTrue( + UpdateThread.should_install_to_root(Path("gui/main.py"), root) + ) + + def test_preserves_archive_metadata_with_git_file(self): + """Linked worktrees and submodules use a .git file, not a directory.""" + from gui.main import UpdateThread + + with tempfile.TemporaryDirectory() as raw: + root = Path(raw) + (root / ".git").write_text("gitdir: elsewhere\n", encoding="utf-8") + + self.assertFalse( + UpdateThread.should_install_to_root( + Path(".git_archival.txt"), + root, + ) + ) + + def test_installs_archive_metadata_outside_git_checkout(self): + from gui.main import UpdateThread + + with tempfile.TemporaryDirectory() as raw: + root = Path(raw) + + self.assertTrue( + UpdateThread.should_install_to_root( + Path(".git_archival.txt"), + root, + ) + ) + class UpdateThreadArchiveRootTests(unittest.TestCase): """Gitea zips nest files under a single top folder (repo display name)."""