Files
management/verfahren/issue-migration/migrate.py
T

108 lines
4.2 KiB
Python

#!/usr/bin/env python3
# Issue-Migration Gitea -> GitLab (gitops#48). Stdlib-only.
#
# Eigenschaften:
# - Dry-Run per Default; --execute schreibt wirklich
# - idempotent: Marker "<!-- gitea-migration: OWNER/REPO#N -->" 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("<!-- gitea-migration: "):
existing.add(line.strip())
page += 1
# Gitea-Issues (alle Zustaende), PRs ausfiltern
issues = []
page = 1
while True:
batch = gitea(f"/repos/{repo}/issues?state=all&type=issues&limit=50&page={page}")
if not batch:
break
issues.extend(batch)
page += 1
issues.sort(key=lambda i: i["number"]) # aufsteigend = chronologische Anlage
print(f"{repo}: {len(issues)} Issues gefunden, Ziel-Projekt {project}, "
f"{'EXECUTE' if execute else 'DRY-RUN'}")
for i in issues:
n = i["number"]
marker = f"<!-- gitea-migration: {repo}#{n} -->"
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()