From ee632ee088818442f42e2367a4a49577b82494ae Mon Sep 17 00:00:00 2001 From: Michal Tuszowski Date: Fri, 19 Sep 2025 21:38:42 +0200 Subject: [PATCH] sync: help_scripts v0.1 --- help_scripts/sync_foundry_module.py | 446 +++++++++++++++++++++------- help_scripts/version.nfo | 1 + 2 files changed, 338 insertions(+), 109 deletions(-) create mode 100644 help_scripts/version.nfo diff --git a/help_scripts/sync_foundry_module.py b/help_scripts/sync_foundry_module.py index ecfefee..0bf5761 100644 --- a/help_scripts/sync_foundry_module.py +++ b/help_scripts/sync_foundry_module.py @@ -1,19 +1,27 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ -sync_foundry_module.py (z dry-run, argumentami CLI i preflightem git/module.json) +sync_foundry_module.py +Rozszerzony: +- Tryb AUTO: wykrywanie katalogów ze zmianami (git), obsługa modułów i niemodułów. +- Każdy katalog -> osobny commit i push. +- Root version.nfo: opcjonalny bump globalnej wersji (0.x) jeśli są zmiany i HEAD ma tag z obecną wersją. +- Zipowanie po commit/push, przed podmianą plików (moduły) lub tuż po commicie (niemoduły): + ZIP_MODE = "ALL" | "MODULE_ONLY" | "OFF"; CLI: --zip-mode; --zip-output-dir. -Kroki: - [PRE] W katalogu źródłowym: - - Wczytaj module.json, pobierz 'id' i 'version' - - Zwiększ ostatnią liczbę w 'version' o 1, zapisz module.json - - git add -A; git commit -m "sync: v"; git push - (w dry-run wyłącznie plan, bez modyfikacji i bez gita) +Dwa tryby: + - AUTO (domyślny): wykryj wszystkie ZMIENIONE podkatalogi w SOURCE_BASE_DIR: + * jeśli folder zawiera module.json -> procedura "moduł" + * jeśli nie -> procedura "niemoduł" (version.nfo 0.x, inkrementacja) + - SINGLE: przetwórz wyłącznie katalog wskazany w MODULE_SUBDIR_NAME (jak wcześniej) - [2] Wyczyść zawartość docelowego modułu (z potwierdzeniem lub bez; w dry-run tylko lista) - [3] Skopiuj zawartość źródła do celu (w dry-run: plan) +Dry-run: + - nic NIE modyfikuje (ani plików, ani repo), nie czyści i nie kopiuje – tylko plan. -Wymagania: Python 3.8+, git w PATH, uprawnienia do zapisu. +Wymagania: + - Python 3.8+ + - git w PATH (dla commit/push i wykrywania zmian) + - uprawnienia do zapisu w repo i w katalogu docelowym modułów """ from __future__ import annotations @@ -24,27 +32,38 @@ import argparse import json import subprocess from pathlib import Path -from typing import List, Optional, Tuple +from typing import List, Optional, Tuple, Set # ========================== # KONFIGURACJA (DOMYŚLNA) # ========================== +# Tryb: "auto" (wykryj zmienione katalogi) lub "single" (tylko MODULE_SUBDIR_NAME) +MODE: str = "auto" # "auto" | "single" + TARGET_USER: str = "mtusz" # nazwa folderu w C:\Users\... APPDATA_KIND: Optional[str] = None # None | "Local" | "LocalLow" | "Roaming" TARGET_MODULES_DIR_OVERRIDE: Optional[str] = None # pełna ścieżka do ...\FoundryVTT\Data\modules -MODULE_SUBDIR_NAME: str = "wg-voidships-builder" +MODULE_SUBDIR_NAME: str = "wg-voidships" # używane w trybie SINGLE CONFIRM_DELETE: bool = True SOURCE_BASE_DIR: str = r"C:\vtt_work" RELATIVE_PATTERN: str = r"FoundryVTT\Data\modules" DRY_RUN: bool = False # globalny tryb symulacji (nic nie zmienia) +# --- ZIP settings --- +# ALL -> zipuj WSZYSTKIE katalogi (moduły i niemoduły) +# MODULE_ONLY -> zipuj TYLKO katalogi z module.json +# OFF -> nie twórz zipów +ZIP_MODE: str = "MODULE_ONLY" # "ALL" | "MODULE_ONLY" | "OFF" +# Katalog wyjściowy na archiwa .zip; jeśli None, użyje SOURCE_BASE_DIR/_zips +ZIP_OUTPUT_DIR: Optional[str] = None + # ========================== # KONIEC KONFIGURACJI # ========================== APPDATA_VARIANTS = ("Local", "LocalLow", "Roaming") - +ARGS = None # wypełniane w main() class AmbiguityError(RuntimeError): pass @@ -54,6 +73,56 @@ def norm(p: Path) -> Path: return Path(os.path.normpath(str(p))) +def sh(cmd: List[str], cwd: Path) -> Tuple[int, str]: + """Uruchom polecenie i zwróć (rc, out).""" + r = subprocess.run(cmd, cwd=str(cwd), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) + return r.returncode, r.stdout + + +def git_head_tag(repo_dir: Path) -> Optional[str]: + """Zwraca tag dokładnie na HEAD (jeśli istnieje), inaczej None.""" + rc, out = sh(["git", "describe", "--tags", "--exact-match", "HEAD"], repo_dir) + if rc != 0: + return None + return out.strip() if out.strip() else None + + +def ensure_git_repo(path: Path) -> None: + rc, out = sh(["git", "rev-parse", "--is-inside-work-tree"], path) + if rc != 0 or "true" not in out.strip().lower(): + raise RuntimeError(f"Katalog {norm(path)} nie wygląda na repozytorium git.\n{out}") + + +def git_changed_top_level_dirs(repo_dir: Path) -> List[str]: + """ + Zwraca posortowaną listę unikalnych NAZW PODKATALOGÓW (1. poziom) w repo, + które mają zmiany wg `git status --porcelain` (staged/unstaged/untracked). + Zmiany w plikach w root repo są ignorowane (brak katalogu). + """ + rc, out = sh(["git", "status", "--porcelain"], repo_dir) + if rc != 0: + raise RuntimeError(f"Błąd podczas `git status --porcelain` w {norm(repo_dir)}:\n{out}") + changed: Set[str] = set() + for line in out.splitlines(): + if not line.strip(): + continue + # format: XY lub "?? " + try: + path = line[3:].strip() + except Exception: + continue + # ignoruj rename "R a -> b": wówczas path ma "a -> b" + if "->" in path: + path = path.split("->", 1)[1].strip() + parts = Path(path).parts + if len(parts) >= 1: + top = parts[0] + # bierz tylko katalogi (jeśli top to plik w root, pomiń) + if (repo_dir / top).is_dir(): + changed.add(top) + return sorted(changed) + + def find_modules_dir_for_user(user: str, relative_pattern: str, appdata_kind: Optional[str]) -> Path: @@ -115,19 +184,6 @@ def iter_dir_contents(dir_path: Path) -> List[Path]: return list(dir_path.iterdir()) -def list_recursive(dir_path: Path) -> List[Path]: - items: List[Path] = [] - if not dir_path.exists(): - return items - for root, dirs, files in os.walk(dir_path): - root_p = Path(root) - for d in dirs: - items.append(root_p / d) - for f in files: - items.append(root_p / f) - return items - - def clear_dir_contents(dir_path: Path, dry_run: bool) -> Tuple[int, int]: ensure_dir(dir_path) files = 0 @@ -231,10 +287,6 @@ def read_manifest(manifest_path: Path) -> dict: def bump_version(last_version: str) -> str: - """ - Zwiększa o 1 ostatnią *liczbę* w wersji rozdzielanej kropkami. - Przykłady: 1 -> 2, 1.2 -> 1.3, 1.2.9 -> 1.2.10 - """ parts = last_version.strip().split(".") if not parts: raise ValueError(f"Niepoprawna wersja: '{last_version}'") @@ -246,19 +298,15 @@ def bump_version(last_version: str) -> str: return ".".join(parts) -def write_manifest(manifest_path: Path, data: dict) -> None: - tmp = manifest_path.with_suffix(".json.tmp") +def write_json_atomic(path: Path, data: dict) -> None: + tmp = path.with_suffix(path.suffix + ".tmp") with tmp.open("w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2) f.write("\n") - tmp.replace(manifest_path) # atomowa podmiana na tym samym FS + tmp.replace(path) def run_git_commands(repo_dir: Path, commit_msg: str, dry_run: bool) -> None: - """ - Wykonuje: git add -A; git commit -m ""; git push - W dry_run — tylko wyświetla plan. - """ if dry_run: print("[DRY-RUN] git add -A") print(f"[DRY-RUN] git commit -m {commit_msg!r}") @@ -266,31 +314,25 @@ def run_git_commands(repo_dir: Path, commit_msg: str, dry_run: bool) -> None: return def run(cmd: List[str]) -> None: - r = subprocess.run(cmd, cwd=str(repo_dir), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) - if r.returncode != 0: - raise RuntimeError(f"Błąd podczas uruchamiania: {' '.join(cmd)}\nWyjście:\n{r.stdout}") - else: - if r.stdout.strip(): - print(r.stdout) + rc, out = sh(cmd, repo_dir) + if rc != 0: + raise RuntimeError(f"Błąd podczas uruchamiania: {' '.join(cmd)}\nWyjście:\n{out}") + if out.strip(): + print(out) - # add, commit (może nie utworzyć commita jeśli brak zmian), push run(["git", "add", "-A"]) - # commit powinien się powieść — jeśli nie ma zmian, chcemy FAIL - r = subprocess.run(["git", "commit", "-m", commit_msg], cwd=str(repo_dir), - stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) - if r.returncode != 0: - # typowy komunikat: "nothing to commit, working tree clean" -> traktujemy jako błąd wg wymagań - raise RuntimeError(f"Błąd podczas git commit:\n{r.stdout}") - else: - if r.stdout.strip(): - print(r.stdout) + rc, out = sh(["git", "commit", "-m", commit_msg], repo_dir) + if rc != 0: + raise RuntimeError(f"Błąd podczas git commit:\n{out}") + if out.strip(): + print(out) run(["git", "push"]) -def preflight_source(src_dir: Path, dry_run: bool) -> Tuple[str, str]: +def preflight_module(src_dir: Path, dry_run: bool) -> Tuple[str, str]: """ - Wczytuje module.json, bumpuje wersję, zapisuje (jeśli nie dry_run), - i wykonuje git add/commit/push. Zwraca (module_id, new_version). + Moduł: odczyt module.json, bump, zapis (jeśli nie dry_run), commit i push. + Zwraca (module_id, new_version). """ manifest_path = src_dir / "module.json" manifest = read_manifest(manifest_path) @@ -302,25 +344,140 @@ def preflight_source(src_dir: Path, dry_run: bool) -> Tuple[str, str]: old_version = str(manifest["version"]) new_version = bump_version(old_version) - print(f"[PRE] module.json: id={module_id}, version={old_version} -> {new_version}") + print(f"[PRE][MODULE] {src_dir.name}: id={module_id}, version={old_version} -> {new_version}") if dry_run: print("[DRY-RUN] Nie zapisuję module.json i nie wykonuję git.") else: manifest["version"] = new_version - write_manifest(manifest_path, manifest) - print(f"[PRE] Zapisano zaktualizowany {norm(manifest_path)}") - + write_json_atomic(manifest_path, manifest) commit_msg = f"sync: {module_id} v{new_version}" run_git_commands(src_dir, commit_msg, dry_run=False) return module_id, new_version -# ================= CLI / MAIN ================= +def preflight_non_module(src_dir: Path, dry_run: bool) -> str: + """ + Niemoduł: version.nfo w formacie '0.x' (x int). + - jeśli brak -> '0.1' + - jeśli jest -> inkrementuj x + Zapis (nie dry-run) oraz osobny commit/push. + Zwraca new_version (np. '0.4'). + """ + nfo = src_dir / "version.nfo" + if not nfo.exists(): + new_version = "0.1" + print(f"[PRE][FOLDER] {src_dir.name}: version.nfo brak -> {new_version}") + if not dry_run: + with nfo.open("w", encoding="utf-8") as f: + f.write(new_version + "\n") + commit_msg = f"sync: {src_dir.name} v{new_version}" + run_git_commands(src_dir, commit_msg, dry_run=False) + return new_version + + # istnieje -> wczytaj i bumpnij + content = nfo.read_text(encoding="utf-8").strip() + if not content.startswith("0."): + raise ValueError(f"{norm(nfo)}: oczekiwano formatu '0.x', otrzymano '{content}'") + tail = content.split(".", 1)[1] + try: + x = int(tail) + except ValueError: + raise ValueError(f"{norm(nfo)}: część po '0.' nie jest liczbą: '{tail}'") + new_version = f"0.{x+1}" + print(f"[PRE][FOLDER] {src_dir.name}: version {content} -> {new_version}") + + if not dry_run: + with nfo.open("w", encoding="utf-8") as f: + f.write(new_version + "\n") + commit_msg = f"sync: {src_dir.name} v{new_version}" + run_git_commands(src_dir, commit_msg, dry_run=False) + + return new_version + + +# ====== ROOT version.nfo (globalna wersja repo) ====== +def maybe_bump_root_version(repo_dir: Path, changed_any: bool, dry_run: bool) -> Optional[str]: + """ + Jeśli: + - istnieją zmiany do przetworzenia (changed_any == True) + - HEAD ma tag zawierający aktualną wersję globalną z version.nfo (0.x) + to bumpnij version.nfo w roocie (0.x -> 0.(x+1)), zrób osobny commit i push. + Zwraca nową wersję (str) lub None, jeśli pominięto. + """ + if not changed_any: + print("[ROOT] Pomijam bump wersji globalnej: brak zmian do przetworzenia.") + return None + + nfo = repo_dir / "version.nfo" + if not nfo.exists(): + print("[ROOT] Brak version.nfo w roocie; pomijam bump (wymagany istniejący plik).") + return None + + current = nfo.read_text(encoding="utf-8").strip() + if not current.startswith("0."): + raise ValueError(f"{nfo}: oczekiwano formatu '0.x', otrzymano '{current}'") + # HEAD musi mieć tag zawierający 'current' + tag = git_head_tag(repo_dir) + if not tag or current not in tag: + print(f"[ROOT] Pomijam bump: {'brak taga na HEAD' if not tag else f'tag \"{tag}\" nie zawiera wersji {current}'}") + return None + + # Bump + try: + tail = int(current.split(".", 1)[1]) + except ValueError: + raise ValueError(f"{nfo}: część po '0.' nie jest liczbą: '{current}'") + new_version = f"0.{tail+1}" + print(f"[ROOT] Globalna wersja: {current} -> {new_version}") + + if dry_run: + print("[DRY-RUN][ROOT] Zapis version.nfo i commit/push zostaną pominięte.") + return new_version + + nfo.write_text(new_version + "\n", encoding="utf-8") + run_git_commands(repo_dir, f"sync: root v{new_version}", dry_run=False) + return new_version + + +# ====== ZIP utilities ====== +def should_zip(is_module: bool, zip_mode: str) -> bool: + if zip_mode == "OFF": + return False + if zip_mode == "ALL": + return True + # MODULE_ONLY + return is_module + + +def ensure_zip_output_dir(source_base: Path, override: Optional[str]) -> Path: + out = Path(override) if override else (source_base / "_zips") + out.mkdir(parents=True, exist_ok=True) + return out + + +def make_zip_for_folder(src_dir: Path, version: str, out_dir: Path, dry_run: bool) -> Path: + """ + Tworzy .zip z zawartością src_dir (bez nadkatalogów) o nazwie NAME-vVERSION.zip + Zwraca ścieżkę do archiwum. + """ + base_name = f"{src_dir.name}-v{version}" + target = out_dir / (base_name + ".zip") + if dry_run: + print(f"[DRY-RUN][ZIP] {src_dir} -> {target}") + return target + # shutil.make_archive potrzebuje base_name BEZ .zip i root_dir+base_dir + base_name_path = out_dir / base_name + shutil.make_archive(str(base_name_path), "zip", root_dir=str(src_dir), base_dir=".") + print(f"[ZIP] Utworzono {target}") + return target + def parse_args() -> argparse.Namespace: - p = argparse.ArgumentParser(description="Synchronizacja modułu FoundryVTT (preflight: module.json + git).") + p = argparse.ArgumentParser(description="Synchronizacja FoundryVTT: AUTO wykrywanie zmian + obsługa modułów/niemodułów.") + p.add_argument("--mode", choices=["auto", "single"], default=MODE, + help=f"Tryb działania (domyślnie: {MODE}).") p.add_argument("--user", dest="user", default=TARGET_USER, help=f"Nazwa użytkownika Windows (domyślnie: {TARGET_USER})") p.add_argument("--appdata-kind", dest="appdata_kind", choices=APPDATA_VARIANTS + (None,), @@ -331,85 +488,156 @@ def parse_args() -> argparse.Namespace: help="Pełna ścieżka do ...\\FoundryVTT\\Data\\modules (pomija wyszukiwanie).") p.add_argument("--module-subdir-name", dest="module_subdir_name", default=MODULE_SUBDIR_NAME, - help=f"Nazwa podkatalogu modułu (domyślnie: {MODULE_SUBDIR_NAME})") + help=f"Nazwa podkatalogu modułu (tylko w --mode single). Domyślnie: {MODULE_SUBDIR_NAME}") cd_group = p.add_mutually_exclusive_group() cd_group.add_argument("--confirm-delete", dest="confirm_delete", action="store_true", - help="Włącz potwierdzenie kasowania (jeśli było wyłączone).") + help="Włącz potwierdzenie kasowania.") cd_group.add_argument("--no-confirm-delete", dest="confirm_delete", action="store_false", - help="Wyłącz potwierdzenie kasowania (tryb bez pytania).") + help="Wyłącz potwierdzenie kasowania.") p.set_defaults(confirm_delete=CONFIRM_DELETE) p.add_argument("--source-base-dir", dest="source_base_dir", default=SOURCE_BASE_DIR, - help=f"Katalog źródłowy (domyślnie: {SOURCE_BASE_DIR})") + help=f"Katalog źródłowy repo (domyślnie: {SOURCE_BASE_DIR})") p.add_argument("--relative-pattern", dest="relative_pattern", default=RELATIVE_PATTERN, - help=f"Wzorzec relatywny od AppData (domyślnie: {RELATIVE_PATTERN})") + help=f"Wzorzec relatywny od AppData dla katalogu modules (domyślnie: {RELATIVE_PATTERN})") dr_group = p.add_mutually_exclusive_group() dr_group.add_argument("--dry-run", dest="dry_run", action="store_true", help="Tryb symulacji: tylko plan (bez modyfikacji i bez gita).") dr_group.add_argument("--no-dry-run", dest="dry_run", action="store_false", - help="Tryb wykonawczy: wprowadza zmiany i robi git.") + help="Tryb wykonawczy: modyfikacje + git.") p.set_defaults(dry_run=DRY_RUN) + # ZIP options + p.add_argument("--zip-mode", dest="zip_mode", + choices=["ALL", "MODULE_ONLY", "OFF"], + default=ZIP_MODE, + help=f"Zipowanie: ALL | MODULE_ONLY | OFF (domyślnie: {ZIP_MODE})") + p.add_argument("--zip-output-dir", dest="zip_output_dir", + default=ZIP_OUTPUT_DIR, + help="Katalog wyjściowy na zipy (domyślnie: SOURCE_BASE_DIR/_zips)") + return p.parse_args() -def main() -> int: - args = parse_args() - - user = args.user - appdata_kind = args.appdata_kind if args.appdata_kind != "None" else None - modules_override = args.modules_dir_override - module_subdir = args.module_subdir_name - confirm_delete = args.confirm_delete - source_base = args.source_base_dir - relative_pattern = args.relative_pattern - dry_run = args.dry_run - - # Ścieżki - src_dir = Path(source_base) / module_subdir - - # [PRE] module.json + git w katalogu źródłowym - print(f"[PRE] Katalog źródłowy: {norm(src_dir)}") - preflight_source(src_dir, dry_run=dry_run) - - # 1) Ustal katalog modules (cel) - if modules_override: - modules_dir = Path(modules_override) +def resolve_modules_base(user: str, relative_pattern: str, appdata_kind: Optional[str], + override: Optional[str]) -> Path: + if override: + modules_dir = Path(override) if not modules_dir.is_dir(): raise FileNotFoundError(f"TARGET_MODULES_DIR_OVERRIDE wskazuje na nieistniejący katalog: {norm(modules_dir)}") - else: - modules_dir = find_modules_dir_for_user(user, relative_pattern, appdata_kind) + return modules_dir + return find_modules_dir_for_user(user, relative_pattern, appdata_kind) - print(f"[INFO] Katalog modules: {norm(modules_dir)}") - target_module_dir = modules_dir / module_subdir - print(f"[INFO] Docelowy katalog modułu: {norm(target_module_dir)}") +def process_module(src_dir: Path, modules_base: Path, confirm_delete: bool, dry_run: bool) -> None: + module_id, new_ver = preflight_module(src_dir, dry_run=dry_run) + + target_dir = modules_base / src_dir.name + print(f"[INFO][MODULE] Cel modułu: {norm(target_dir)}") - # 2) Kasowanie zawartości (albo plan w dry-run) if confirm_delete and not dry_run: - ans = input(f"Czy na pewno wyczyścić zawartość '{norm(target_module_dir)}'? [y/N]: ").strip().lower() + ans = input(f"Czy na pewno wyczyścić zawartość '{norm(target_dir)}'? [y/N]: ").strip().lower() if ans not in ("y", "yes", "t", "tak"): - print("[INFO] Przerwano na życzenie użytkownika.") - return 0 - elif confirm_delete and dry_run: - print("[INFO] (dry-run) Pytanie o potwierdzenie pomijane — tryb symulacji.") + raise RuntimeError("Przerwano na życzenie użytkownika.") - print("[AKCJA] Czyszczenie zawartości katalogu modułu..." if not dry_run else "[SYMULACJA] Plan czyszczenia katalogu modułu...") - del_files, del_dirs = clear_dir_contents(target_module_dir, dry_run=dry_run) + if confirm_delete and dry_run: + print("[INFO] (dry-run) Pytanie o potwierdzenie pominięte.") + + # ZIP po commicie, przed podmianą + if should_zip(is_module=True, zip_mode=ARGS.zip_mode): + out_dir = ensure_zip_output_dir(Path(ARGS.source_base_dir), ARGS.zip_output_dir) + try: + make_zip_for_folder(src_dir, new_ver, out_dir, dry_run) + except Exception as e: + raise RuntimeError(f"Błąd zipowania modułu {src_dir.name}: {e}") from e + + print("[AKCJA] Czyszczenie..." if not dry_run else "[SYMULACJA] Plan czyszczenia...") + del_files, del_dirs = clear_dir_contents(target_dir, dry_run=dry_run) print(f"[INFO] {'Do usunięcia' if dry_run else 'Usunięto'}: plików={del_files}, katalogów={del_dirs}") - # 3) Kopiowanie (albo plan kopiowania) - print("[AKCJA] Kopiowanie zawartości..." if not dry_run else "[SYMULACJA] Plan kopiowania...") - cp_files, cp_dirs = copy_dir_contents(src_dir, target_module_dir, dry_run=dry_run) + print("[AKCJA] Kopiowanie..." if not dry_run else "[SYMULACJA] Plan kopiowania...") + cp_files, cp_dirs = copy_dir_contents(src_dir, target_dir, dry_run=dry_run) print(f"[INFO] {'Do skopiowania' if dry_run else 'Skopiowano'}: plików={cp_files}, katalogów={cp_dirs}") - print("[SUKCES] " + ("Symulacja zakończona." if dry_run else "Zakończono synchronizację.")) - print(f" Źródło: {norm(src_dir)}") - print(f" Cel: {norm(target_module_dir)}") + print(f"[DONE][MODULE] {src_dir.name} -> commit 'sync: {module_id} v{new_ver}' wykonany." if not dry_run + else f"[DRY-RUN][MODULE] {src_dir.name} -> commit planowany.") + + +def process_non_module(src_dir: Path, dry_run: bool) -> None: + new_ver = preflight_non_module(src_dir, dry_run=dry_run) + # ZIP po commicie (jeśli ustawienie pozwala) + if should_zip(is_module=False, zip_mode=ARGS.zip_mode): + out_dir = ensure_zip_output_dir(Path(ARGS.source_base_dir), ARGS.zip_output_dir) + try: + make_zip_for_folder(src_dir, new_ver, out_dir, dry_run) + except Exception as e: + raise RuntimeError(f"Błąd zipowania folderu {src_dir.name}: {e}") from e + print(f"[DONE][FOLDER] {src_dir.name} -> version.nfo = {new_ver}; commit wykonany." if not dry_run + else f"[DRY-RUN][FOLDER] {src_dir.name} -> version.nfo będzie {new_ver}; commit planowany.") + + +def main() -> int: + global ARGS + ARGS = parse_args() + + mode = ARGS.mode + user = ARGS.user + appdata_kind = ARGS.appdata_kind if ARGS.appdata_kind != "None" else None + modules_override = ARGS.modules_dir_override + module_subdir_single = ARGS.module_subdir_name + confirm_delete = ARGS.confirm_delete + source_base = Path(ARGS.source_base_dir) + relative_pattern = ARGS.relative_pattern + dry_run = ARGS.dry_run + + if not source_base.is_dir(): + raise FileNotFoundError(f"Katalog źródłowy nie istnieje: {norm(source_base)}") + + ensure_git_repo(source_base) + + # Ustal bazę modules (tylko raz) + modules_base = resolve_modules_base(user, relative_pattern, appdata_kind, modules_override) + print(f"[INFO] Katalog modules base: {norm(modules_base)}") + + if mode == "single": + src_dir = source_base / module_subdir_single + if not src_dir.is_dir(): + raise FileNotFoundError(f"[SINGLE] Brak katalogu: {norm(src_dir)}") + # Root bump (globalna wersja) – tylko jeśli "są zmiany do przetworzenia" (single -> True) + maybe_bump_root_version(source_base, changed_any=True, dry_run=dry_run) + if (src_dir / "module.json").is_file(): + print(f"[MODE=SINGLE][MODULE] {src_dir.name}") + process_module(src_dir, modules_base, confirm_delete, dry_run) + else: + print(f"[MODE=SINGLE][FOLDER] {src_dir.name}") + process_non_module(src_dir, dry_run) + print("[SUKCES] Tryb SINGLE zakończony.") + return 0 + + # AUTO: wykryj zmienione katalogi + changed = git_changed_top_level_dirs(source_base) + print(f"[AUTO] Zmienione katalogi w {norm(source_base)}: {changed if changed else '(brak)'}") + # Root bump (globalna wersja) – tylko jeśli są jakiekolwiek zmiany do przetworzenia + maybe_bump_root_version(source_base, changed_any=bool(changed), dry_run=dry_run) + if not changed: + print("[INFO] Brak zmian do przetworzenia.") + return 0 + + # Przetwarzaj w stałej kolejności + for name in changed: + src_dir = source_base / name + if (src_dir / "module.json").is_file(): + print(f"\n[AUTO][MODULE] {name}") + process_module(src_dir, modules_base, confirm_delete, dry_run) + else: + print(f"\n[AUTO][FOLDER] {name}") + process_non_module(src_dir, dry_run) + + print("\n[SUKCES] Tryb AUTO zakończony.") return 0 diff --git a/help_scripts/version.nfo b/help_scripts/version.nfo new file mode 100644 index 0000000..49d5957 --- /dev/null +++ b/help_scripts/version.nfo @@ -0,0 +1 @@ +0.1