Files
2025-09-19 18:51:25 +02:00

264 lines
11 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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()