""" GitLab Branch-Analyse: Aktive Branches der Kern-Services. Nutzt Projekt-IDs direkt (zuverlaessiger als Pfad-Encoding). """ import urllib.request, json, ssl from datetime import datetime, timezone from pathlib import Path from collections import defaultdict GITLAB_URL = "https://git.tech.rz.db.de" API = f"{GITLAB_URL}/api/v4" 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_BESTELLSYSTEM", "") if not TOKEN: print("FEHLER: GITLAB_TOKEN_BESTELLSYSTEM nicht in .secrets!") exit(1) ctx = ssl.create_default_context() ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE def api_get(path): url = f"{API}{path}" req = urllib.request.Request(url) req.add_header("PRIVATE-TOKEN", TOKEN) try: resp = urllib.request.urlopen(req, context=ctx, timeout=30) return json.loads(resp.read().decode("utf-8")) except Exception as e: return None # Kern-Services mit IDs (aus gitlab-find-projects.py) CORE_PROJECTS = { "portal-middleware": 56664, "portal-ui": 51502, "steuerung-vertrieb": 54688, "auftrags-verwaltung-trasse": 63436, "common-interface": 52825, "stammdaten-bereitstellung": 150168, "tadef-connector": 352680, "archivierungsservice": 294401, "ifp-connector": 256070, "kundendaten-bereitstellung": 109956, "vertragsdaten-verteiler": 352409, "rabattnummern-bereitstellung": 216978, "taftap-tdm-konverter": 256243, } print("=== GitLab Branch-Analyse ===") print(f"Datum: {datetime.now().strftime('%Y-%m-%d %H:%M')}") print(f"Projekte: {len(CORE_PROJECTS)}") print() now = datetime.now(timezone.utc) all_branches = [] api_calls = 0 for proj_name, proj_id in CORE_PROJECTS.items(): # Branches laden (paginiert) branches = [] page = 1 while True: data = api_get(f"/projects/{proj_id}/repository/branches?per_page=100&page={page}") api_calls += 1 if not data: break branches.extend(data) if len(data) < 100: break page += 1 feature_branches = [] for b in branches: name = b["name"] if name in ("master", "main", "develop"): continue commit = b.get("commit", {}) committed_date = commit.get("committed_date", "") author = commit.get("author_name", "?") if committed_date: try: dt = datetime.fromisoformat(committed_date.replace("Z", "+00:00")) age_days = (now - dt).days except: age_days = -1 else: age_days = -1 feature_branches.append({ "project": proj_name, "branch": name, "author": author, "age_days": age_days, "date": committed_date[:10] if committed_date else "?", }) all_branches.extend(feature_branches) active = [b for b in feature_branches if 0 <= b["age_days"] <= 30] stale = [b for b in feature_branches if b["age_days"] > 30] print(f" {proj_name:<35} {len(feature_branches):>3} Branches ({len(active)} aktiv, {len(stale)} stale)") print(f"\nAPI-Calls: {api_calls}") print(f"Branches gesamt: {len(all_branches)}") # Aktive Branches (letzte 30 Tage) active_branches = [b for b in all_branches if 0 <= b["age_days"] <= 30] active_branches.sort(key=lambda x: x["age_days"]) print(f"\n--- Aktive Branches (letzte 30 Tage): {len(active_branches)} ---") print(f"{'Projekt':<30} {'Branch':<60} {'Autor':<25} {'Tage'}") print("-" * 125) for b in active_branches[:50]: branch_short = b["branch"][:59] print(f"{b['project']:<30} {branch_short:<60} {b['author']:<25} {b['age_days']}d") # Stale Branches (>60 Tage) stale_branches = [b for b in all_branches if b["age_days"] > 60] stale_branches.sort(key=lambda x: x["age_days"], reverse=True) print(f"\n--- Stale Branches (>60 Tage): {len(stale_branches)} ---") if stale_branches: print(f"{'Projekt':<30} {'Branch':<60} {'Tage'}") print("-" * 95) for b in stale_branches[:20]: branch_short = b["branch"][:59] print(f"{b['project']:<30} {branch_short:<60} {b['age_days']}d") # Analyse: Wer arbeitet an was? print(f"\n--- Aktivitaet pro Autor (letzte 30 Tage) ---") authors = defaultdict(list) for b in active_branches: authors[b["author"]].append(b) for author, branches in sorted(authors.items(), key=lambda x: -len(x[1]))[:20]: projects = set(b["project"] for b in branches) print(f" {author:<30} {len(branches):>2} Branches in: {', '.join(sorted(projects))}") # Spring Boot 4 Branches sb4_branches = [b for b in all_branches if "spring" in b["branch"].lower() or "boot-4" in b["branch"].lower() or "springboot" in b["branch"].lower()] print(f"\n--- Spring Boot 4 Branches: {len(sb4_branches)} ---") for b in sorted(sb4_branches, key=lambda x: x["age_days"]): status = "AKTIV" if b["age_days"] <= 30 else f"STALE ({b['age_days']}d)" print(f" {b['project']:<30} {b['branch']:<55} {status}") # Renovate Branches renovate_branches = [b for b in all_branches if "renovate" in b["branch"].lower()] renovate_active = [b for b in renovate_branches if b["age_days"] <= 7] renovate_stale = [b for b in renovate_branches if b["age_days"] > 30] print(f"\n--- Renovate Branches: {len(renovate_branches)} gesamt ---") print(f" Frisch (<=7d): {len(renovate_active)}") print(f" Stale (>30d): {len(renovate_stale)}") # Ticket-Extraktion aus Branch-Namen print(f"\n--- Jira-Tickets in aktiven Branches ---") import re tickets = defaultdict(list) for b in active_branches: matches = re.findall(r'(O2C[A-Z]+-\d+|TTTI-\d+)', b["branch"].upper()) for ticket in matches: tickets[ticket].append(b) if tickets: for ticket, branches in sorted(tickets.items()): projs = set(b["project"] for b in branches) print(f" {ticket:<15} in {', '.join(sorted(projs))}") # CSV Export csv_path = Path("project-audit/data/gitlab-branches.csv") with open(csv_path, "w", encoding="utf-8") as f: f.write('"Project";"Branch";"Author";"LastCommit";"AgeDays"\n') for b in sorted(all_branches, key=lambda x: (x["project"], x["age_days"])): f.write(f'"{b["project"]}";"{b["branch"]}";"{b["author"]}";"{b["date"]}";"{b["age_days"]}"\n') print(f"\nCSV exportiert: {csv_path}")