fix(update): apply Gitea archive under dazedtl root

- Resolve archive root dynamically instead of a stale slug
- Fail when the zip has no installable files
- Install into PROJECT_ROOT and cover archive-root regressions
This commit is contained in:
DazedAnon 2026-07-22 14:02:26 -05:00
parent 4d1b01c870
commit 7f7348a5a6
2 changed files with 95 additions and 5 deletions

View file

@ -78,7 +78,9 @@ class UpdateThread(QThread):
REPO_OWNER = "dazed"
REPO_SLUG = "dazed-mtl-tool"
REPO_BRANCH = "main"
ARCHIVE_ROOT = "dazed-mtl-tool"
# Gitea names the archive top folder after the repo display name (currently
# "dazedtl"). Resolve dynamically so a rename cannot silently no-op again.
ARCHIVE_ROOT = "dazedtl"
SHA_FILE = str(LAST_UPDATE_SHA_PATH)
# Top-level paths that should never be touched during update
@ -117,6 +119,25 @@ class UpdateThread(QThread):
return False
return True
@classmethod
def resolve_archive_root(cls, extract_dir: Path) -> Path:
"""Locate the single top-level folder inside an extracted archive.
Prefers ``ARCHIVE_ROOT`` when present; otherwise accepts exactly one
subdirectory (Gitea zip layout). Raises if the layout is unexpected.
"""
preferred = extract_dir / cls.ARCHIVE_ROOT
if preferred.is_dir():
return preferred
dirs = sorted(p for p in extract_dir.iterdir() if p.is_dir())
if len(dirs) == 1:
return dirs[0]
found = [p.name for p in dirs]
raise FileNotFoundError(
f"Could not find update archive root under {extract_dir} "
f"(expected {cls.ARCHIVE_ROOT!r}; found {found})"
)
@staticmethod
def _fmt_bytes(num: int) -> str:
if num >= 1024 * 1024:
@ -211,16 +232,21 @@ class UpdateThread(QThread):
with zipfile.ZipFile(zip_path, "r") as zf:
zf.extractall(tmp)
extracted = tmp / self.ARCHIVE_ROOT
root = Path(".").resolve()
extracted = self.resolve_archive_root(tmp)
root = PROJECT_ROOT.resolve()
install_files = [
src
for src in extracted.rglob("*")
if src.is_file() and self.should_install(src.relative_to(extracted))
]
total_files = max(len(install_files), 1)
if not install_files:
raise RuntimeError(
"Update archive contained no installable files "
f"(root={extracted.name!r})"
)
total_files = len(install_files)
self.progress.emit("Applying", 85, "Installing updated files…")
for index, src in enumerate(install_files, start=1):
rel = src.relative_to(extracted)

View file

@ -62,6 +62,67 @@ class UpdateThreadInstallFilterTests(unittest.TestCase):
self.assertFalse(UpdateThread.should_install(Path(rel)))
class UpdateThreadArchiveRootTests(unittest.TestCase):
"""Gitea zips nest files under a single top folder (repo display name)."""
def test_prefers_configured_archive_root(self):
from gui.main import UpdateThread
with tempfile.TemporaryDirectory() as raw:
base = Path(raw)
preferred = base / UpdateThread.ARCHIVE_ROOT
preferred.mkdir()
(base / "other").mkdir()
self.assertEqual(UpdateThread.resolve_archive_root(base), preferred)
def test_falls_back_to_single_subdirectory(self):
from gui.main import UpdateThread
with tempfile.TemporaryDirectory() as raw:
base = Path(raw)
only = base / "renamed-repo-folder"
only.mkdir()
(only / "gui").mkdir()
(only / "gui" / "main.py").write_text("# ok", encoding="utf-8")
self.assertEqual(UpdateThread.resolve_archive_root(base), only)
def test_raises_when_archive_root_missing(self):
from gui.main import UpdateThread
with tempfile.TemporaryDirectory() as raw:
base = Path(raw)
(base / "a").mkdir()
(base / "b").mkdir()
with self.assertRaises(FileNotFoundError):
UpdateThread.resolve_archive_root(base)
def test_stale_archive_root_name_would_have_installed_nothing(self):
"""Regression: wrong ARCHIVE_ROOT used to report success with 0 files."""
from gui.main import UpdateThread
with tempfile.TemporaryDirectory() as raw:
base = Path(raw)
real = base / "dazedtl"
real.mkdir()
(real / "gui").mkdir()
(real / "gui" / "main.py").write_text("print('hi')\n", encoding="utf-8")
(real / "data").mkdir()
(real / "data" / "help").mkdir()
(real / "data" / "help" / "00-welcome.md").write_text("# hi\n", encoding="utf-8")
# Old hardcoded slug before the DazedTL rebrand.
stale = base / "dazed-mtl-tool"
self.assertFalse(stale.exists())
self.assertEqual(list(stale.rglob("*")), [])
extracted = UpdateThread.resolve_archive_root(base)
install_files = [
src
for src in extracted.rglob("*")
if src.is_file() and UpdateThread.should_install(src.relative_to(extracted))
]
self.assertGreaterEqual(len(install_files), 2)
class ShippedDataTrackingTests(unittest.TestCase):
"""Guide/help and other shipped data/ files must be git-trackable.
@ -417,8 +478,11 @@ class MaintainerRefreshTests(unittest.TestCase):
with patch.object(forge, "_fetch_plugins", return_value=plugin):
self.assertTrue(forge.refresh_forge_plugins(log_fn=None))
self.assertEqual((base / "Forge_MV.js").read_bytes(), plugin)
# Maintainer refresh updates MZ modern only; MV stays on the legacy bundle.
self.assertEqual((base / "Forge_MZ.js").read_bytes(), plugin)
self.assertEqual((upstream / "Forge_MZ.js").read_bytes(), plugin)
self.assertFalse((base / "Forge_MV.js").exists())
self.assertFalse((upstream / "Forge_MV.js").exists())
if __name__ == "__main__":