Cleanup part one

This commit is contained in:
2025-09-19 18:51:25 +02:00
parent c2a93cbd86
commit bf6e1050d9
14 changed files with 0 additions and 0 deletions
+263
View File
@@ -0,0 +1,263 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import argparse
import os
import sys
import zipfile
import tempfile
import shutil
import subprocess
from datetime import datetime, timezone
from collections import defaultdict
def eprint(*a, **kw):
print(*a, file=sys.stderr, **kw)
def is_git_repo(path: str) -> bool:
return os.path.isdir(os.path.join(path, ".git"))
def run(cmd, cwd=None, env=None, check=True, capture=True):
result = subprocess.run(
cmd,
cwd=cwd,
env=env,
text=True,
capture_output=capture,
)
if check and result.returncode != 0:
eprint(f"[BŁĄD] Komenda nie powiodła się: {' '.join(cmd)}")
if capture:
eprint(result.stdout)
eprint(result.stderr)
sys.exit(result.returncode)
return result
def list_zip_files(zip_dir: str):
zips = []
for name in os.listdir(zip_dir):
if not name.lower().endswith(".zip"):
continue
path = os.path.join(zip_dir, name)
try:
st = os.stat(path)
except FileNotFoundError:
continue
zips.append((st.st_mtime, path))
zips.sort(key=lambda x: x[0]) # najstarsze -> najnowsze
return [p for _, p in zips]
def find_module_json(root: str):
"""Zwraca listę absolutnych ścieżek do plików module.json (rekurencyjnie), pomija .git."""
matches = []
for dirpath, dirnames, filenames in os.walk(root):
if ".git" in dirnames:
dirnames.remove(".git")
if "module.json" in filenames:
matches.append(os.path.join(dirpath, "module.json"))
return matches
def ensure_subdir(repo_root: str, subdir: str) -> str:
"""Zwraca absolutną ścieżkę docelowego podkatalogu w repo, tworząc go jeśli trzeba.
Zabezpiecza przed wychodzeniem poza repo (..)."""
target = repo_root
if subdir:
target = os.path.abspath(os.path.join(repo_root, subdir))
repo_root_abs = os.path.abspath(repo_root)
if os.path.commonpath([target, repo_root_abs]) != repo_root_abs:
eprint(f"[BŁĄD] Niedozwolona ścieżka podkatalogu: {subdir}")
sys.exit(1)
os.makedirs(target, exist_ok=True)
return target
def build_repo_index(repo_root: str):
"""Mapa: basename -> [abspathy] (bez .git)."""
index = defaultdict(list)
for dirpath, dirnames, filenames in os.walk(repo_root):
if ".git" in dirnames:
dirnames.remove(".git")
for f in filenames:
index[f].append(os.path.join(dirpath, f))
return index
def git_mv_or_rename(repo_root: str, src_abs: str, dest_abs: str):
"""Przenosi plik używając git mv, a w razie potrzeby zwykłego move. Tworzy katalog docelowy."""
if os.path.abspath(src_abs) == os.path.abspath(dest_abs):
return
os.makedirs(os.path.dirname(dest_abs), exist_ok=True)
rel_src = os.path.relpath(src_abs, repo_root)
rel_dest = os.path.relpath(dest_abs, repo_root)
# Spróbuj git mv (utrzymanie historii)
res = subprocess.run(["git", "mv", "-f", rel_src, rel_dest], cwd=repo_root)
if res.returncode != 0:
# Jeśli plik nie był w git lub inny problem — zwykłe przeniesienie
shutil.move(src_abs, dest_abs)
def mirror_zip_into_target(zip_root: str, target_dir: str, repo_root: str, dry_run: bool):
"""Odwzorowuje strukturę zip_root wewnątrz target_dir.
- Jeśli plik o tej samej nazwie istnieje w repo w innej ścieżce i dest nie istnieje -> przenosimy (git mv).
- W przeciwnym razie kopiujemy (nadpisując).
"""
repo_index = build_repo_index(repo_root)
for dirpath, dirnames, filenames in os.walk(zip_root):
rel_dir = os.path.relpath(dirpath, zip_root)
dest_dir = target_dir if rel_dir in (".", "") else os.path.join(target_dir, rel_dir)
if dry_run:
print(f"[PLAN] Utworzę katalog: {os.path.relpath(dest_dir, repo_root)}")
else:
os.makedirs(dest_dir, exist_ok=True)
for fn in filenames:
src = os.path.join(dirpath, fn)
dest = os.path.join(dest_dir, fn)
# 1) Unikaj duplikatów: jeśli dest nie istnieje, a w repo jest dokładnie jeden kandydat o tej samej nazwie -> przenieś
if not os.path.exists(dest):
candidates = [p for p in repo_index.get(fn, []) if os.path.abspath(p) != os.path.abspath(dest)]
# Odfiltruj ewentualne kandydaty wewnątrz .git (i tak ich nie było dodanych)
candidates = [p for p in candidates if "/.git/" not in p.replace("\\", "/")]
if len(candidates) == 1:
prev = candidates[0]
rel_prev = os.path.relpath(prev, repo_root)
rel_new = os.path.relpath(dest, repo_root)
if dry_run:
print(f"[MOVE] {rel_prev} -> {rel_new}")
else:
git_mv_or_rename(repo_root, prev, dest)
# aktualizuj indeks
repo_index[fn] = [dest if p == prev else p for p in repo_index.get(fn, [])]
elif len(candidates) > 1:
# Nie zgadujemy, żeby nie zrobić złej migracji — kopiujemy i ostrzegamy
print(f"[INFO] Wiele kandydatów o nazwie '{fn}' w repo ({len(candidates)}). "
f"Kopiuję do {os.path.relpath(dest, repo_root)} (bez przenoszenia).")
# 2) Nadpisz/utwórz plik zawartością z ZIPa (repo ma odzwierciedlać ZIP)
if dry_run:
print(f"[COPY] {os.path.relpath(src, zip_root)} -> {os.path.relpath(dest, repo_root)}")
else:
os.makedirs(os.path.dirname(dest), exist_ok=True)
shutil.copy2(src, dest)
# 3) Upewnij się, że indeks widzi plik w nowym miejscu (dla kolejnych kolizji)
if dest not in repo_index.get(fn, []):
repo_index[fn].append(dest)
def git_has_staged_changes(repo: str) -> bool:
res = subprocess.run(["git", "diff", "--cached", "--quiet"], cwd=repo)
return res.returncode == 1 # 1 = są zmiany; 0 = brak
def git_is_clean(repo: str) -> bool:
res = run(["git", "status", "--porcelain"], cwd=repo, capture=True, check=False)
return res.stdout.strip() == ""
def main():
parser = argparse.ArgumentParser(
description="Import archiwów ZIP do repozytorium git (od najstarszego), z commitami po każdym ZIPie."
)
parser.add_argument("--zip-dir", required=True, help="Katalog z plikami .zip")
parser.add_argument("--repo", required=True, help="Katalog repozytorium git (cel)")
parser.add_argument(
"--subdir",
default="",
help="Opcjonalny podkatalog w repo, do którego mają trafić pliki (utworzony jeśli nie istnieje).",
)
parser.add_argument(
"--allow-dirty",
action="store_true",
help="Pozwól działać, nawet jeśli repo nie jest czyste (domyślnie: wymagana czystość).",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Tylko pokaż plan (bez rozpakowywania i commitów).",
)
args = parser.parse_args()
zip_dir = os.path.abspath(args.zip_dir)
repo = os.path.abspath(args.repo)
subdir = args.subdir.strip()
if not os.path.isdir(zip_dir):
eprint(f"[BŁĄD] Nie ma katalogu: {zip_dir}")
sys.exit(1)
if not os.path.isdir(repo):
eprint(f"[BŁĄD] Nie ma katalogu repozytorium: {repo}")
sys.exit(1)
if not is_git_repo(repo):
eprint(f"[BŁĄD] {repo} nie wygląda na repozytorium git (brak .git).")
sys.exit(1)
target_dir = ensure_subdir(repo, subdir)
if not args.allow_dirty and not git_is_clean(repo):
eprint("[BŁĄD] Repo nie jest czyste. Użyj --allow-dirty, jeśli chcesz mimo to kontynuować.")
sys.exit(1)
zips = list_zip_files(zip_dir)
if not zips:
print("[INFO] Brak plików .zip do przetworzenia.")
return
rel_target = os.path.relpath(target_dir, repo)
print(f"[INFO] Cel importu: {target_dir} (rel: {rel_target})")
print(f"[INFO] Znaleziono {len(zips)} archiwów .zip do przetworzenia (od najstarszego do najnowszego).")
for i, zip_path in enumerate(zips, start=1):
zip_name = os.path.basename(zip_path)
mtime = datetime.fromtimestamp(os.path.getmtime(zip_path), tz=timezone.utc)
mtime_iso = mtime.strftime("%Y-%m-%dT%H:%M:%SZ")
print(f"\n=== [{i}/{len(zips)}] {zip_name} (mtime: {mtime_iso}) ===")
if args.dry_run:
print(f"[DRY-RUN] Odtworzyłbym strukturę {zip_name} w '{rel_target}', "
f"z przenosinami plików o tej samej nazwie.")
continue
stage_dir = tempfile.mkdtemp(prefix="zipstage_")
try:
with zipfile.ZipFile(zip_path, "r") as zf:
zf.extractall(stage_dir)
except zipfile.BadZipFile:
eprint(f"[BŁĄD] Uszkodzony zip: {zip_name}. Pomijam.")
shutil.rmtree(stage_dir, ignore_errors=True)
continue
# Ustal punkt wejścia importu
matches = find_module_json(stage_dir)
if len(matches) == 1:
zip_root = os.path.dirname(matches[0])
rel = os.path.relpath(zip_root, stage_dir)
print(f"[OK] Wykryto module.json w '{rel}'. Odtwarzam strukturę tego katalogu.")
elif len(matches) == 0:
zip_root = stage_dir
print("[INFO] Brak module.json. Odtwarzam pełną strukturę ZIPa w repo (merge z istniejącymi katalogami).")
else:
# wiele module.json — to ryzykowne; pozostawiamy do ręcznej interwencji
print("[UWAGA] Wykryto wiele plików module.json w archiwum. "
"Ta sytuacja jest niejednoznaczna — pomijam ten ZIP.")
shutil.rmtree(stage_dir, ignore_errors=True)
continue
# Mirror + przenosiny
mirror_zip_into_target(zip_root, target_dir, repo, dry_run=False)
# Git add + commit (jeśli są zmiany) commit z korzenia repo
run(["git", "add", "-A"], cwd=repo)
if git_has_staged_changes(repo):
msg = f"Import {zip_name} do {rel_target} ({mtime_iso})"
env = os.environ.copy()
env["GIT_AUTHOR_DATE"] = mtime_iso
env["GIT_COMMITTER_DATE"] = mtime_iso
run(["git", "commit", "-m", msg], cwd=repo, env=env)
print(f"[OK] Commit: {msg}")
else:
print("[INFO] Brak zmian po rozpakowaniu — pomijam commit.")
shutil.rmtree(stage_dir, ignore_errors=True)
print("\n[ZAKOŃCZONE] Przetwarzanie archiwów .zip zakończone.")
print(f"[INFO] Pliki zostały odzwierciedlone w: {target_dir}")
if __name__ == "__main__":
main()