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.
183 lines
6.4 KiB
Python
183 lines
6.4 KiB
Python
"""
|
|
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}")
|