Migrate all repos into monorepo context folders
Bahn: aisupport, Analyse-O2C-C2S, awesome-bahn-mcp-servers, beam-mcp,
Confluence_Bot, db-planet-mcp-server, O2C-Harness, project-audit,
Projekt-KIQ-HP, teamlandkarte-mcp
Dhive: Jury-Voting
Privat: CV, NoteGraph (NOTE: NoteGraph needs complete redo after consolidation)
Shared: AI-Orchestrator, OrgMyLife, power_skills_and_more
Shared/references: symphony (read-only)
Bahn repos remain available as independent remotes - this monorepo
pulls them in via subtree, the originals are untouched.
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
"""
|
||||
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()
|
||||
Reference in New Issue
Block a user