#!/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()