mirror of
https://github.com/migatu/vtt_work.git
synced 2026-07-14 13:34:42 +00:00
sync: help_scripts v0.1
This commit is contained in:
+337
-109
@@ -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: <id> v<version>"; 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 <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,
|
||||
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 "<msg>"; 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
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
0.1
|
||||
Reference in New Issue
Block a user