mirror of
https://github.com/migatu/vtt_work.git
synced 2026-07-14 21:38:39 +00:00
sync: help_scripts v0.1
This commit is contained in:
+337
-109
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
0.1
|
||||||
Reference in New Issue
Block a user