a96308ff2c
Two failures that compounded in the last round of ElvUI breakage:
1. The sweep script only lived in /tmp during a session — re-derived
from scratch each time, so the EXCLUDE_FORKS knowledge wasn't
anywhere reviewable.
2. The old filename-only filter ('if -ElvUI in name skip') missed
ElvUI's customizations inside otherwise-stock-named files AND
rsync --delete killed -ElvUI suffixed widgets that exist only in
ElvUI's bundle (e.g. AceGUIWidget-Button-ElvUI.lua).
This tool fixes both:
- Lives in the repo (tools/sweep.py), so the exclusion list is
visible in version control and reviewable.
- EXCLUDED_FORKS = {'coa-elvui'} hardcoded with an in-source comment
explaining why.
- --exclude='*-ElvUI*' passed to every rsync as belt-and-braces, so
even if a future fork accidentally carries an ElvUI-namespaced
file we never wanted to overwrite, the sweep won't touch it.
- Refuses to add new lib dirs — only updates ones already present
in the fork.
- --dry-run flag for safe verification.
README updated with a 'Forks excluded from sweep' section documenting
the same.
164 lines
5.7 KiB
Python
Executable File
164 lines
5.7 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Sweep this coa-ace3 bundle into every Exiles fork that embeds Ace3 libs.
|
|
|
|
Run from anywhere; the script locates itself and walks sibling repos at
|
|
$REPOS_ROOT (default /home/sub/repos/coa). For each fork that bundles
|
|
any AceXxx-3.0 library, the matching directory is rsynced from this
|
|
bundle with --delete so the fork stays byte-identical to canonical.
|
|
|
|
USAGE
|
|
tools/sweep.py # apply sweep
|
|
tools/sweep.py --dry-run # show what would change without writing
|
|
|
|
WHY THIS SCRIPT EXISTS
|
|
Previous sweeps were ad-hoc scripts living in /tmp during a session
|
|
and re-derived by hand each time. That made it easy to forget the
|
|
ElvUI exclusion (see EXCLUDED_FORKS below) and re-clobber ElvUI's
|
|
customized lib stack on every sync. Committing the canonical script
|
|
here keeps the exclusion list visible and reviewable.
|
|
|
|
EXCLUDED_FORKS
|
|
coa-elvui ships its OWN bundled Ace3 with ElvUI-specific patches
|
|
inside otherwise-stock-named files (AceLocale, AceConfigDialog, the
|
|
AceGUI widgets) PLUS files that don't exist in canonical at all
|
|
(AceGUIWidget-Button-ElvUI.lua). rsync --delete obliterates the
|
|
latter; an in-place sync overwrites the former. Either failure
|
|
breaks /ec and floods locale errors. ElvUI maintains its own bundle
|
|
on its own cadence and must never be swept by this tool.
|
|
"""
|
|
import argparse
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
# --- configuration -----------------------------------------------------------
|
|
|
|
REPOS_ROOT = Path(os.environ.get("REPOS_ROOT", "/home/sub/repos/coa"))
|
|
BUNDLE = Path(__file__).resolve().parent.parent # the coa-ace3 repo root
|
|
|
|
# Forks that this sweep MUST NOT touch. Document the reason inline.
|
|
EXCLUDED_FORKS = {
|
|
"coa-elvui", # ships its own ElvUI-patched Ace3 stack; sweep would clobber it
|
|
}
|
|
|
|
# Lib names whose directories we sync from the bundle.
|
|
LIB_NAMES = re.compile(r"^(LibStub|CallbackHandler-1\.0|Ace\w*-3\.0)$")
|
|
|
|
# rsync excludes — repo metadata never deployed, plus a belt-and-braces
|
|
# guard against deleting any -ElvUI file that might exist in destinations we
|
|
# didn't intend to touch (e.g. a future fork that pulls in some ElvUI widget).
|
|
RSYNC_EXCLUDES = [
|
|
".git", ".gitattributes", ".gitignore", ".github", ".idea",
|
|
".editorconfig", ".luacheckrc", ".pkgmeta",
|
|
"*-ElvUI*", # never delete or overwrite ElvUI-namespaced files
|
|
]
|
|
|
|
|
|
# --- discovery ---------------------------------------------------------------
|
|
|
|
def bundle_lib_sources():
|
|
"""Return {libname: absolute path inside bundle} for every shippable lib.
|
|
|
|
Handles the nested AceConfig children (AceConfigCmd/Dialog/Registry).
|
|
"""
|
|
out = {}
|
|
for path in BUNDLE.rglob("*"):
|
|
if not path.is_dir():
|
|
continue
|
|
if path.name in {".git", "tools"}:
|
|
continue
|
|
if LIB_NAMES.match(path.name):
|
|
out.setdefault(path.name, path)
|
|
return out
|
|
|
|
|
|
def fork_lib_targets(fork: Path, src_names):
|
|
"""Return {libname: absolute path inside fork} for each lib the fork bundles.
|
|
|
|
A lib is "bundled" when there's a top-level directory containing
|
|
<libname>/<libname>.lua. We don't add new libs to forks that didn't
|
|
already ship them.
|
|
"""
|
|
targets = {}
|
|
for path in fork.rglob("*.lua"):
|
|
if "/.git/" in str(path):
|
|
continue
|
|
if "-ElvUI" in path.name:
|
|
continue
|
|
name = path.stem
|
|
if name not in src_names:
|
|
continue
|
|
parent = path.parent
|
|
if parent.name != name:
|
|
continue
|
|
targets.setdefault(name, parent)
|
|
return targets
|
|
|
|
|
|
# --- sweep -------------------------------------------------------------------
|
|
|
|
def sweep(dry_run: bool) -> int:
|
|
src_map = bundle_lib_sources()
|
|
if not src_map:
|
|
print(f"no libs found inside {BUNDLE}; nothing to sweep", file=sys.stderr)
|
|
return 2
|
|
|
|
forks = sorted(p for p in REPOS_ROOT.iterdir()
|
|
if p.is_dir() and p.name.startswith("coa-")
|
|
and p.name != "coa-ace3"
|
|
and (p / ".git").exists())
|
|
|
|
print(f"bundle: {BUNDLE}")
|
|
print(f"forks scan: {REPOS_ROOT}")
|
|
print(f"excluded: {sorted(EXCLUDED_FORKS)}")
|
|
print(f"libs: {sorted(src_map)}")
|
|
print()
|
|
|
|
total_synced = total_skipped = 0
|
|
for fork in forks:
|
|
if fork.name in EXCLUDED_FORKS:
|
|
print(f" skip {fork.name} (excluded — ships its own customized Ace3)")
|
|
total_skipped += 1
|
|
continue
|
|
|
|
targets = fork_lib_targets(fork, src_map.keys())
|
|
if not targets:
|
|
print(f" no-libs {fork.name}")
|
|
continue
|
|
|
|
for libname in sorted(targets):
|
|
src = src_map[libname]
|
|
dst = targets[libname]
|
|
cmd = ["rsync", "-a", "--delete"]
|
|
if dry_run:
|
|
cmd += ["--dry-run", "--itemize-changes"]
|
|
for exc in RSYNC_EXCLUDES:
|
|
cmd += ["--exclude", exc]
|
|
cmd += [f"{src}/", f"{dst}/"]
|
|
res = subprocess.run(cmd, capture_output=True, text=True)
|
|
if res.returncode != 0:
|
|
print(f" ! rsync failed for {fork.name}/{libname}: {res.stderr.strip()}")
|
|
continue
|
|
tag = "would-sync" if dry_run else "synced"
|
|
print(f" {tag:<9} {fork.name}/{dst.relative_to(fork)}")
|
|
total_synced += 1
|
|
|
|
print()
|
|
print(f"summary: {total_synced} lib dirs {'would be synced' if dry_run else 'synced'}, "
|
|
f"{total_skipped} forks excluded")
|
|
return 0
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(description=__doc__)
|
|
ap.add_argument("--dry-run", action="store_true",
|
|
help="show what would change without writing")
|
|
args = ap.parse_args()
|
|
sys.exit(sweep(args.dry_run))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|