""" Git Push via GitLab Commits API (REST). Umgeht das Git-HTTP-Protokoll komplett. Nutzt POST /api/v4/projects/:id/repository/commits Strategie: Vergleiche lokale Dateien mit Remote, pushe nur Aenderungen. """ import urllib.request, json, ssl, base64, os from pathlib import Path REPO_PATH = Path(__file__).parent.parent # project-audit/ secrets = {} with open("project-audit/.secrets", "r", encoding="utf-8-sig") as f: for line in f: if "=" in line and not line.startswith("#"): k, v = line.strip().split("=", 1) secrets[k.strip()] = v.strip() TOKEN = secrets.get("GITLAB_TOKEN_AUDIT", "") PROJECT = "AndreKnie%2Fproject-audit" BASE = f"https://git.tech.rz.db.de/api/v4/projects/{PROJECT}" BRANCH = "master" ctx = ssl.create_default_context() ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE def api_get(path): url = BASE + path req = urllib.request.Request(url) req.add_header("PRIVATE-TOKEN", TOKEN) resp = urllib.request.urlopen(req, context=ctx, timeout=30) return json.loads(resp.read().decode("utf-8")) def api_post(path, data): url = BASE + path body = json.dumps(data).encode("utf-8") req = urllib.request.Request(url, data=body, method="POST") req.add_header("PRIVATE-TOKEN", TOKEN) req.add_header("Content-Type", "application/json") resp = urllib.request.urlopen(req, context=ctx, timeout=60) return json.loads(resp.read().decode("utf-8")) def get_remote_tree(path="", ref=BRANCH): """Holt den Dateibaum vom Remote rekursiv.""" files = {} page = 1 while True: try: items = api_get(f"/repository/tree?ref={ref}&path={path}&per_page=100&page={page}&recursive=true") if not items: break for item in items: if item["type"] == "blob": files[item["path"]] = item["id"] # SHA des Blobs page += 1 except Exception as e: print(f" Tree-Fehler (Seite {page}): {e}") break return files def get_local_files(): """Sammelt alle lokalen Dateien (respektiert .gitignore).""" patterns = [] gitignore = REPO_PATH / ".gitignore" if gitignore.exists(): with open(gitignore) as f: patterns = [l.strip().strip("/") for l in f if l.strip() and not l.startswith("#")] files = {} for root, dirs, fnames in os.walk(REPO_PATH): if ".git" in root: continue for fname in fnames: full = Path(root) / fname rel = str(full.relative_to(REPO_PATH)).replace("\\", "/") if any(p in rel for p in patterns): continue files[rel] = full return files def main(): print(f"=== Git Push via API ===") print(f" Token: ...{TOKEN[-4:]}") print(f" Branch: {BRANCH}") print() # 1. Remote-Tree holen print(" Remote-Tree laden...") remote_files = get_remote_tree() print(f" Remote: {len(remote_files)} Dateien") # 2. Lokale Dateien sammeln print(" Lokale Dateien sammeln...") local_files = get_local_files() print(f" Lokal: {len(local_files)} Dateien") # 3. Diff berechnen actions = [] new_files = set(local_files.keys()) - set(remote_files.keys()) deleted_files = set(remote_files.keys()) - set(local_files.keys()) # Neue Dateien for f in sorted(new_files): path = local_files[f] try: content = path.read_bytes() actions.append({ "action": "create", "file_path": f, "content": base64.b64encode(content).decode(), "encoding": "base64" }) except Exception: pass # Geaenderte Dateien (vereinfacht: alle existierenden neu hochladen) # Nur wenn es neue/geloeschte gibt, sonst nichts zu tun changed = [] for f in sorted(set(local_files.keys()) & set(remote_files.keys())): path = local_files[f] try: content = path.read_bytes() # Einfacher Check: Dateigroesse oder Inhalt hat sich geaendert # Wir koennen den SHA nicht lokal berechnen ohne git, also updaten wir alles # was in den letzten Tagen geaendert wurde mtime = os.path.getmtime(path) import time if time.time() - mtime < 7 * 86400: # Letzte 7 Tage changed.append(f) actions.append({ "action": "update", "file_path": f, "content": base64.b64encode(content).decode(), "encoding": "base64" }) except Exception: pass # Geloeschte Dateien for f in sorted(deleted_files): actions.append({ "action": "delete", "file_path": f }) print(f" Neu: {len(new_files)}, Geaendert: {len(changed)}, Geloescht: {len(deleted_files)}") if not actions: print(" Keine Aenderungen zu pushen.") return # GitLab API hat ein Limit von ~100 Actions pro Commit # Aufteilen in Batches BATCH_SIZE = 50 total_batches = (len(actions) + BATCH_SIZE - 1) // BATCH_SIZE print(f" Push in {total_batches} Batch(es) a {BATCH_SIZE} Dateien...") for i in range(0, len(actions), BATCH_SIZE): batch = actions[i:i+BATCH_SIZE] batch_num = i // BATCH_SIZE + 1 commit_msg = f"Audit Update (Batch {batch_num}/{total_batches})" if total_batches > 1 else "Audit Update" print(f" Batch {batch_num}: {len(batch)} Aktionen...") try: result = api_post("/repository/commits", { "branch": BRANCH, "commit_message": commit_msg, "actions": batch }) print(f" OK: {result.get('short_id', '?')} - {result.get('title', '?')}") except urllib.error.HTTPError as e: body = e.read().decode("utf-8", errors="replace") print(f" FEHLER {e.code}: {body[:300]}") if e.code == 400 and "already exists" in body: print(" Tipp: Datei existiert bereits, versuche 'update' statt 'create'") break except Exception as e: print(f" FEHLER: {e}") break print() print(" Fertig!") if __name__ == "__main__": main()