sync: help_scripts v0.1

This commit is contained in:
2025-09-19 21:38:42 +02:00
parent bf6e1050d9
commit ee632ee088
2 changed files with 338 additions and 109 deletions
+337 -109
View File
@@ -1,19 +1,27 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- 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: Dwa tryby:
[PRE] W katalogu źródłowym: - AUTO (domyślny): wykryj wszystkie ZMIENIONE podkatalogi w SOURCE_BASE_DIR:
- Wczytaj module.json, pobierz 'id' i 'version' * jeśli folder zawiera module.json -> procedura "moduł"
- Zwiększ ostatnią liczbę w 'version' o 1, zapisz module.json * jeśli nie -> procedura "niemoduł" (version.nfo 0.x, inkrementacja)
- git add -A; git commit -m "sync: <id> v<version>"; git push - SINGLE: przetwórz wyłącznie katalog wskazany w MODULE_SUBDIR_NAME (jak wcześniej)
(w dry-run wyłącznie plan, bez modyfikacji i bez gita)
[2] Wyczyść zawartość docelowego modułu (z potwierdzeniem lub bez; w dry-run tylko lista) Dry-run:
[3] Skopiuj zawartość źródła do celu (w dry-run: plan) - 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 from __future__ import annotations
@@ -24,27 +32,38 @@ import argparse
import json import json
import subprocess import subprocess
from pathlib import Path from pathlib import Path
from typing import List, Optional, Tuple from typing import List, Optional, Tuple, Set
# ========================== # ==========================
# KONFIGURACJA (DOMYŚLNA) # 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\... TARGET_USER: str = "mtusz" # nazwa folderu w C:\Users\...
APPDATA_KIND: Optional[str] = None # None | "Local" | "LocalLow" | "Roaming" APPDATA_KIND: Optional[str] = None # None | "Local" | "LocalLow" | "Roaming"
TARGET_MODULES_DIR_OVERRIDE: Optional[str] = None # pełna ścieżka do ...\FoundryVTT\Data\modules 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 CONFIRM_DELETE: bool = True
SOURCE_BASE_DIR: str = r"C:\vtt_work" SOURCE_BASE_DIR: str = r"C:\vtt_work"
RELATIVE_PATTERN: str = r"FoundryVTT\Data\modules" RELATIVE_PATTERN: str = r"FoundryVTT\Data\modules"
DRY_RUN: bool = False # globalny tryb symulacji (nic nie zmienia) 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 # KONIEC KONFIGURACJI
# ========================== # ==========================
APPDATA_VARIANTS = ("Local", "LocalLow", "Roaming") APPDATA_VARIANTS = ("Local", "LocalLow", "Roaming")
ARGS = None # wypełniane w main()
class AmbiguityError(RuntimeError): class AmbiguityError(RuntimeError):
pass pass
@@ -54,6 +73,56 @@ def norm(p: Path) -> Path:
return Path(os.path.normpath(str(p))) 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 <path> lub "?? <path>"
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, def find_modules_dir_for_user(user: str,
relative_pattern: str, relative_pattern: str,
appdata_kind: Optional[str]) -> Path: appdata_kind: Optional[str]) -> Path:
@@ -115,19 +184,6 @@ def iter_dir_contents(dir_path: Path) -> List[Path]:
return list(dir_path.iterdir()) 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]: def clear_dir_contents(dir_path: Path, dry_run: bool) -> Tuple[int, int]:
ensure_dir(dir_path) ensure_dir(dir_path)
files = 0 files = 0
@@ -231,10 +287,6 @@ def read_manifest(manifest_path: Path) -> dict:
def bump_version(last_version: str) -> str: 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(".") parts = last_version.strip().split(".")
if not parts: if not parts:
raise ValueError(f"Niepoprawna wersja: '{last_version}'") raise ValueError(f"Niepoprawna wersja: '{last_version}'")
@@ -246,19 +298,15 @@ def bump_version(last_version: str) -> str:
return ".".join(parts) return ".".join(parts)
def write_manifest(manifest_path: Path, data: dict) -> None: def write_json_atomic(path: Path, data: dict) -> None:
tmp = manifest_path.with_suffix(".json.tmp") tmp = path.with_suffix(path.suffix + ".tmp")
with tmp.open("w", encoding="utf-8") as f: with tmp.open("w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2) json.dump(data, f, ensure_ascii=False, indent=2)
f.write("\n") 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: def run_git_commands(repo_dir: Path, commit_msg: str, dry_run: bool) -> None:
"""
Wykonuje: git add -A; git commit -m "<msg>"; git push
W dry_run — tylko wyświetla plan.
"""
if dry_run: if dry_run:
print("[DRY-RUN] git add -A") print("[DRY-RUN] git add -A")
print(f"[DRY-RUN] git commit -m {commit_msg!r}") 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 return
def run(cmd: List[str]) -> None: def run(cmd: List[str]) -> None:
r = subprocess.run(cmd, cwd=str(repo_dir), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) rc, out = sh(cmd, repo_dir)
if r.returncode != 0: if rc != 0:
raise RuntimeError(f"Błąd podczas uruchamiania: {' '.join(cmd)}\nWyjście:\n{r.stdout}") raise RuntimeError(f"Błąd podczas uruchamiania: {' '.join(cmd)}\nWyjście:\n{out}")
else: if out.strip():
if r.stdout.strip(): print(out)
print(r.stdout)
# add, commit (może nie utworzyć commita jeśli brak zmian), push
run(["git", "add", "-A"]) run(["git", "add", "-A"])
# commit powinien się powieść — jeśli nie ma zmian, chcemy FAIL rc, out = sh(["git", "commit", "-m", commit_msg], repo_dir)
r = subprocess.run(["git", "commit", "-m", commit_msg], cwd=str(repo_dir), if rc != 0:
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) raise RuntimeError(f"Błąd podczas git commit:\n{out}")
if r.returncode != 0: if out.strip():
# typowy komunikat: "nothing to commit, working tree clean" -> traktujemy jako błąd wg wymagań print(out)
raise RuntimeError(f"Błąd podczas git commit:\n{r.stdout}")
else:
if r.stdout.strip():
print(r.stdout)
run(["git", "push"]) 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), Moduł: odczyt module.json, bump, zapis (jeśli nie dry_run), commit i push.
i wykonuje git add/commit/push. Zwraca (module_id, new_version). Zwraca (module_id, new_version).
""" """
manifest_path = src_dir / "module.json" manifest_path = src_dir / "module.json"
manifest = read_manifest(manifest_path) 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"]) old_version = str(manifest["version"])
new_version = bump_version(old_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: if dry_run:
print("[DRY-RUN] Nie zapisuję module.json i nie wykonuję git.") print("[DRY-RUN] Nie zapisuję module.json i nie wykonuję git.")
else: else:
manifest["version"] = new_version manifest["version"] = new_version
write_manifest(manifest_path, manifest) write_json_atomic(manifest_path, manifest)
print(f"[PRE] Zapisano zaktualizowany {norm(manifest_path)}")
commit_msg = f"sync: {module_id} v{new_version}" commit_msg = f"sync: {module_id} v{new_version}"
run_git_commands(src_dir, commit_msg, dry_run=False) run_git_commands(src_dir, commit_msg, dry_run=False)
return module_id, new_version 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: 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, p.add_argument("--user", dest="user", default=TARGET_USER,
help=f"Nazwa użytkownika Windows (domyślnie: {TARGET_USER})") help=f"Nazwa użytkownika Windows (domyślnie: {TARGET_USER})")
p.add_argument("--appdata-kind", dest="appdata_kind", choices=APPDATA_VARIANTS + (None,), 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).") help="Pełna ścieżka do ...\\FoundryVTT\\Data\\modules (pomija wyszukiwanie).")
p.add_argument("--module-subdir-name", dest="module_subdir_name", p.add_argument("--module-subdir-name", dest="module_subdir_name",
default=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 = p.add_mutually_exclusive_group()
cd_group.add_argument("--confirm-delete", dest="confirm_delete", action="store_true", 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", 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.set_defaults(confirm_delete=CONFIRM_DELETE)
p.add_argument("--source-base-dir", dest="source_base_dir", p.add_argument("--source-base-dir", dest="source_base_dir",
default=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", p.add_argument("--relative-pattern", dest="relative_pattern",
default=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 = p.add_mutually_exclusive_group()
dr_group.add_argument("--dry-run", dest="dry_run", action="store_true", dr_group.add_argument("--dry-run", dest="dry_run", action="store_true",
help="Tryb symulacji: tylko plan (bez modyfikacji i bez gita).") help="Tryb symulacji: tylko plan (bez modyfikacji i bez gita).")
dr_group.add_argument("--no-dry-run", dest="dry_run", action="store_false", 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) 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() return p.parse_args()
def main() -> int: def resolve_modules_base(user: str, relative_pattern: str, appdata_kind: Optional[str],
args = parse_args() override: Optional[str]) -> Path:
if override:
user = args.user modules_dir = Path(override)
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)
if not modules_dir.is_dir(): if not modules_dir.is_dir():
raise FileNotFoundError(f"TARGET_MODULES_DIR_OVERRIDE wskazuje na nieistniejący katalog: {norm(modules_dir)}") raise FileNotFoundError(f"TARGET_MODULES_DIR_OVERRIDE wskazuje na nieistniejący katalog: {norm(modules_dir)}")
else: return modules_dir
modules_dir = find_modules_dir_for_user(user, relative_pattern, appdata_kind) 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 def process_module(src_dir: Path, modules_base: Path, confirm_delete: bool, dry_run: bool) -> None:
print(f"[INFO] Docelowy katalog modułu: {norm(target_module_dir)}") 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: 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"): if ans not in ("y", "yes", "t", "tak"):
print("[INFO] Przerwano na życzenie użytkownika.") raise RuntimeError("Przerwano na życzenie użytkownika.")
return 0
elif confirm_delete and dry_run:
print("[INFO] (dry-run) Pytanie o potwierdzenie pomijane — tryb symulacji.")
print("[AKCJA] Czyszczenie zawartości katalogu modułu..." if not dry_run else "[SYMULACJA] Plan czyszczenia katalogu modułu...") if confirm_delete and dry_run:
del_files, del_dirs = clear_dir_contents(target_module_dir, dry_run=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}") 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..." if not dry_run else "[SYMULACJA] 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_dir, dry_run=dry_run)
cp_files, cp_dirs = copy_dir_contents(src_dir, target_module_dir, dry_run=dry_run)
print(f"[INFO] {'Do skopiowania' if dry_run else 'Skopiowano'}: plików={cp_files}, katalogów={cp_dirs}") 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"[DONE][MODULE] {src_dir.name} -> commit 'sync: {module_id} v{new_ver}' wykonany." if not dry_run
print(f" Źródło: {norm(src_dir)}") else f"[DRY-RUN][MODULE] {src_dir.name} -> commit planowany.")
print(f" Cel: {norm(target_module_dir)}")
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 return 0
+1
View File
@@ -0,0 +1 @@
0.1