#!/usr/bin/env python3 # Issue-Migration Gitea -> GitLab (gitops#48). Stdlib-only. # # Eigenschaften: # - Dry-Run per Default; --execute schreibt wirklich # - idempotent: Marker "" in der # GitLab-Beschreibung; bereits migrierte Issues werden uebersprungen # - Kommentare werden als Notes uebernommen (Original-Autor + -Zeit im Text, # da die API-Autorschaft auf den Token-User laeuft — bekannter, akzeptierter # Verlust, siehe gitops#48) # - Zeitstempel bleiben erhalten: der Admin-Token darf created_at setzen # - geschlossene Issues werden nach Anlage geschlossen # - Labels werden namensgleich uebernommen (Gruppen-Labels muessen existieren) # - PRs werden ausgelassen (Gitea listet sie im Issue-Endpunkt mit) # # Aufruf: # python3 migrate.py sorb/thread-net-git 18 # Dry-Run # python3 migrate.py sorb/thread-net-git 18 --execute import json import sys import urllib.parse import urllib.request GITEA = "https://rohana.axion1337.de" GITLAB = "https://git.lab" GITEA_TOKEN = open("/Users/thore/.config/gitea-rohana/token").read().strip() GITLAB_TOKEN = open("/Users/thore/.config/gitlab-lab/token").read().strip() def gitea(path): req = urllib.request.Request(f"{GITEA}/api/v1{path}", headers={"Authorization": f"token {GITEA_TOKEN}"}) return json.loads(urllib.request.urlopen(req).read()) def gitlab(method, path, payload=None): data = urllib.parse.urlencode(payload).encode() if payload else None req = urllib.request.Request(f"{GITLAB}/api/v4{path}", data=data, headers={"PRIVATE-TOKEN": GITLAB_TOKEN}, method=method) return json.loads(urllib.request.urlopen(req).read()) def main(): repo = sys.argv[1] project = sys.argv[2] execute = "--execute" in sys.argv # Bestehende Migrations-Marker im Zielprojekt einsammeln (Idempotenz) existing = set() page = 1 while True: batch = gitlab("GET", f"/projects/{project}/issues?per_page=100&page={page}&scope=all&state=all") if not batch: break for i in batch: desc = i.get("description") or "" for line in desc.splitlines(): if line.startswith("" if marker in existing: print(f" #{n} bereits migriert - uebersprungen") continue labels = ",".join(l["name"] for l in i.get("labels", [])) comments = gitea(f"/repos/{repo}/issues/{n}/comments") if i.get("comments") else [] footer = (f"\n\n---\n*Migriert aus Gitea [{repo}#{n}]({GITEA}/{repo}/issues/{n}) " f"— dort erstellt am {i['created_at'][:10]} von {i['user']['login']}.*\n{marker}") print(f" #{n} [{i['state']}] {i['title'][:60]} | labels: {labels or '-'} | " f"{len(comments)} kommentare") if not execute: continue new = gitlab("POST", f"/projects/{project}/issues", { "title": i["title"], "description": (i.get("body") or "") + footer, "labels": labels, "created_at": i["created_at"], }) for c in comments: gitlab("POST", f"/projects/{project}/issues/{new['iid']}/notes", { "body": f"**{c['user']['login']} am {c['created_at'][:16].replace('T', ' ')} (Gitea):**\n\n{c['body']}", "created_at": c["created_at"], }) if i["state"] == "closed": gitlab("PUT", f"/projects/{project}/issues/{new['iid']}", {"state_event": "close"}) print(f" -> GitLab #{new['iid']} angelegt ({i['state']})") if __name__ == "__main__": main()